aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/addon_management/install_uninstall.js65
-rw-r--r--lib/addon_management/prefchange.js124
-rw-r--r--lib/html_script_finder/bug_fix.js23
-rw-r--r--lib/html_script_finder/dom_handler.js571
-rw-r--r--lib/html_script_finder/dom_handler/attributes.js137
-rw-r--r--lib/html_script_finder/dom_handler/dom_checker.js478
-rw-r--r--lib/html_script_finder/dom_handler/dom_gatherer.js281
-rw-r--r--lib/html_script_finder/dom_handler/request.js115
-rw-r--r--lib/html_script_finder/dom_handler/script_object.js208
-rw-r--r--lib/html_script_finder/dom_handler/script_properties.js43
-rw-r--r--lib/html_script_finder/html_parser.js158
-rw-r--r--lib/html_script_finder/url_seen_tester.js78
-rw-r--r--lib/html_script_finder/web_labels/find_js_labels.js131
-rw-r--r--lib/html_script_finder/web_labels/js_web_labels.js279
-rw-r--r--lib/html_script_finder/web_labels/script_hash_worker.js62
-rw-r--r--lib/http_observer/allowed_referrers.js67
-rw-r--r--lib/http_observer/caching.js30
-rw-r--r--lib/http_observer/http_request_observer.js146
-rw-r--r--lib/http_observer/process_response.js429
-rw-r--r--lib/http_observer/stream_loader.js111
-rw-r--r--lib/js_checker/constant_types.js190
-rw-r--r--lib/js_checker/free_checker.js231
-rw-r--r--lib/js_checker/js_checker.js518
-rw-r--r--lib/js_checker/license_definitions.js249
-rw-r--r--lib/js_checker/nontrivial_checker.js374
-rw-r--r--lib/js_checker/pattern_utils.js40
-rw-r--r--lib/js_checker/privacy_checker.js44
-rw-r--r--lib/js_checker/privacy_threat_definitions.js52
-rw-r--r--lib/js_checker/relation_checker.js289
-rw-r--r--lib/js_load_observer/js_load_observer.js143
-rw-r--r--lib/main.js70
-rw-r--r--lib/parser/narcissus_worker.js79
-rw-r--r--lib/pref_observer/pref_observer.js71
-rw-r--r--lib/preferences/preferences.js72
-rw-r--r--lib/script_entries/accepted_scripts.js67
-rw-r--r--lib/script_entries/all_scripts.js108
-rw-r--r--lib/script_entries/crypto.js60
-rw-r--r--lib/script_entries/dryrun_scripts.js76
-rw-r--r--lib/script_entries/free_libraries.js65
-rw-r--r--lib/script_entries/removed_scripts.js71
-rw-r--r--lib/script_entries/scripts_cache.js189
-rw-r--r--lib/settings/settings.js0
-rw-r--r--lib/settings/settings_tab.js78
-rw-r--r--lib/settings/storage.js175
-rw-r--r--lib/ui.js186
-rw-r--r--lib/ui/notification.js76
-rw-r--r--lib/ui/script_panel.js73
-rw-r--r--lib/ui/ui_info.js202
-rw-r--r--lib/url_handler/node_punycode.js510
-rw-r--r--lib/url_handler/node_querystring.js213
-rw-r--r--lib/url_handler/node_url.js691
-rw-r--r--lib/url_handler/url_handler.js114
52 files changed, 8912 insertions, 0 deletions
diff --git a/lib/addon_management/install_uninstall.js b/lib/addon_management/install_uninstall.js
new file mode 100644
index 0000000..8ef9cbf
--- /dev/null
+++ b/lib/addon_management/install_uninstall.js
@@ -0,0 +1,65 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * The following module is intended to perform tasks when the
+ * add-on is enabled and disabled.
+ */
+
+const {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+const httpObserver = require("http_observer/http_request_observer");
+var narcissusWorker = require("parser/narcissus_worker")
+ .narcissusWorker;
+const caching = require("http_observer/caching");
+
+const prompt = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService);
+
+
+const tabs = require('sdk/tabs');
+
+/**
+ * Stop the httpObserver when the add-on is disabled or removed.
+ */
+exports.onUnload = function(reason) {
+ if (reason == "disable" ||
+ reason == "shutdown" ||
+ reason == "upgrade" ||
+ reason == "downgrade") {
+ require("settings/storage").librejsStorage.writeCacheToDB();
+ // remove all http notifications
+ httpObserver.removeHttpObserver();
+ // remove worker.
+ narcissusWorker.stopWorker();
+ }
+
+};
+
+exports.onLoad = function () {
+ try {
+ var clearCache = prompt.dialog(null, "LibreJS installation", "If you have tabs and windows opened prior to installing LibreJS, you will have to refresh them for their JavaScript to be analyzed and blocked. Press OK to clear the browser cache.");
+ if (clearCache) {
+ caching.clearAllCache();
+ }
+ } catch (e) {
+ console.debug(e);
+ }
+};
diff --git a/lib/addon_management/prefchange.js b/lib/addon_management/prefchange.js
new file mode 100644
index 0000000..6894be4
--- /dev/null
+++ b/lib/addon_management/prefchange.js
@@ -0,0 +1,124 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var whitelist = [];
+
+var prefSet = require("sdk/simple-prefs");
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+var prompt = Cc['@mozilla.org/embedcomp/prompt-service;1'].
+ getService(Ci.nsIPromptService);
+var scriptsCached = require("script_entries/scripts_cache").scriptsCached;
+
+var setWhitelist = function () {
+ whitelist = [];
+
+ var str;
+ var whitelistString;
+ if (typeof prefSet.prefs.whitelist === 'undefined') {
+ whitelistString = '';
+ } else {
+ whitelistString = prefSet.prefs.whitelist.split(',');
+ }
+
+ for (var i = 0; i < whitelistString.length; i++) {
+ // remove space, trailing slash, escape any nonalpha except *,
+ // replace * with .*
+ str = whitelistString[i]
+ .replace(" ", "").replace(/\/$/, "")
+ .replace(/[^a-z0-9\*]/ig, "\\$&").replace("*", ".*");
+
+ if (str !== '') {
+ whitelist.push(
+ new RegExp('^https?:\/\/(www\\.)?' + str + '/', 'i'));
+ }
+ }
+};
+
+exports.getWhitelist = function () {
+ return whitelist;
+};
+
+exports.init = function () {
+ setWhitelist();
+};
+
+prefSet.on("whitelist", setWhitelist);
+
+/*var setDryRun = function () {
+ var dryRun = prefSet.prefs.dryrun;
+ if (dryRun === true) {
+ prompt.alert(null, "LibreJS Dry Run Mode", "Is Dry Run Mode really what you want? LibreJS will still analyze JavaScript on a page, but it will not block any of it. As a result, ALL of the JavaScript on a page will run as is, whether it is free and trivial or not. You will not be warned again. Uncheck that box if you are unsure.");
+ scriptsCached.resetCache();
+ } else {
+ prompt.alert(null, "LibreJS Dry Run Mode", "LibreJS Dry Run Mode is now disabled");
+ }
+};*/
+
+//prefSet.on("dryrun", setDryRun);
+
+/*exports.isDryRun = function () {
+ // Returns true if dryRun mode is enabled. False otherwise.
+ //return prefSet.prefs.dryrun;
+ return false;
+};*/
+
+var setComplaintTab = function () {
+ var complaintTab = prefSet.prefs.complaint_tab;
+ if (complaintTab === true) {
+ prompt.alert(null, "Turning on complaint tab", "A complaint tab will be displayed on pages where nonfree nontrivial JavaScript is found and contact information is found as well.");
+ } else {
+ prompt.alert(null, "Turning off complaint tab", "No complaint tab will appear on pages, even when nonfree nontrivial JavaScript is found.");
+ }
+};
+prefSet.on("complaint_tab", setComplaintTab);
+
+exports.isComplaintTab = function () {
+ /**
+ * Returns true if complaint tab mode is enabled. False otherwise.
+ */
+ return prefSet.prefs.complaint_tab;
+};
+
+var setDisplayNotifications = function () {
+ var displayNotifications = prefSet.prefs.display_notifications;
+ if (displayNotifications === true) {
+ prompt.alert(null, "Turning on notifications", "Notifications with code snippets will now appear while LibreJS is analyzing JavaScript on a page.");
+ } else {
+ prompt.alert(null, "Turning off notifications", "Notifications of code being analyzed will not be displayed.");
+ }
+};
+
+prefSet.on("display_notifications", setDisplayNotifications);
+
+exports.isDisplayNotifications = function () {
+ /**
+ * Returns true if complaint tab mode is enabled. False otherwise.
+ */
+ return prefSet.prefs.display_notifications;
+};
+
+exports.complaintEmailSubject = function() {
+ return prefSet.prefs.complaint_email_subject;
+};
+
+exports.complaintEmailBody = function() {
+ return prefSet.prefs.complaint_email_body;
+};
diff --git a/lib/html_script_finder/bug_fix.js b/lib/html_script_finder/bug_fix.js
new file mode 100644
index 0000000..bba7653
--- /dev/null
+++ b/lib/html_script_finder/bug_fix.js
@@ -0,0 +1,23 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+exports.END_OF_SCRIPT = 'this.narcissusBugFixLibreJS'; // value from parse tree without \n\n
+exports.narcissusBugFixLibreJS = '\n\n' + exports.END_OF_SCRIPT; // real value from source.
+
diff --git a/lib/html_script_finder/dom_handler.js b/lib/html_script_finder/dom_handler.js
new file mode 100644
index 0000000..0f461d3
--- /dev/null
+++ b/lib/html_script_finder/dom_handler.js
@@ -0,0 +1,571 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ *
+ * dom_handler.js
+ *
+ * After the HTML DOM has been parsed, domHandler finds all the scripts
+ * on a page (including inline, on-page, and external files), and triggers the
+ * JavaScript analysis for each of them.
+ *
+ */
+
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+var scriptProperties =
+ require("html_script_finder/dom_handler/script_properties");
+
+const scriptTypes = scriptProperties.scriptTypes;
+const statusTypes = scriptProperties.statusTypes;
+const reasons = scriptProperties.reasons;
+
+var urlHandler = require("url_handler/url_handler");
+
+var WebLabelFinder =
+ require("html_script_finder/web_labels/js_web_labels").WebLabelFinder;
+
+// object model for script entries.
+var scriptObject = require("html_script_finder/dom_handler/script_object");
+
+var privacyChecker = require("js_checker/privacy_checker").privacyCheck;
+var jsChecker = require("js_checker/js_checker");
+const types = require("js_checker/constant_types");
+
+var checkTypes = types.checkTypes;
+
+var stripCDATAOpen = /<\!\[CDATA\[/gi;
+var stripCDATAClose = /]]>/g;
+
+var isDryRun = require("addon_management/prefchange").isDryRun;
+var allowedRef = require('http_observer/allowed_referrers').allowedReferrers;
+var attributeHelpers = require("html_script_finder/dom_handler/attributes");
+
+// javascript:*
+var jsInAttrRe = attributeHelpers.jsInAttrRe;
+
+// the list of all available event attributes
+var intrinsicEvents = attributeHelpers.intrinsicEvents;
+
+var domGatherer =
+ require("html_script_finder/dom_handler/dom_gatherer").domGatherer;
+var domChecker =
+ require("html_script_finder/dom_handler/dom_checker").domChecker;
+
+/**
+ * The DomHandler object takes a whole document,
+ * finds script elements within that DOM, analyzes them
+ * using the js_checker module and finally returns a cleaned
+ * DOM depending on the result.
+ */
+var DomHandler = function() {
+ // external object with methods used
+ // in DomHandler
+ this.domGatherer = null;
+
+ // external object with methods used
+ // in DomHandler
+ this.domChecker = null;
+
+ this.dom = null;
+ this.pageURL = null;
+
+ // fragment found in url.
+ this.fragment = null;
+
+ // array containing all scripts on a page.
+ this.domScripts = [];
+
+ // array containing all scripts on a page,
+ // data related to them, such as parse tree, ...
+ this.inlineScripts = [];
+
+ this.externalScripts = [];
+
+ // all scripts.
+ this.scripts = [];
+
+ // keeps track of the number of scripts.
+ this.numScripts = 0;
+
+ // store the reference to the callback method
+ // presumably from htmlParser.
+ this.callback = function() {};
+
+ // boolean set to true if external scripts are loaded
+ // from the html page.
+ this.loadsHtmlExternalScripts = false;
+
+ this.jsCheckString = null;
+
+ /* object containing boolean property set to false if trivialness
+ is not allowed anymore (if another script defines ajax requests,
+ ...) */
+ this.allowTrivial = null;
+
+ // boolean set to true if inline JavaScript
+ // is found to be free.
+ this.inlineJsFree = null;
+
+ // boolean set to true when at least one script
+ // has been removed.
+ this.hasRemovedScripts = null;
+
+ // boolean to check if scripts were removed
+ // prevents removeAllJs from running multiple times.
+ this.removedAllScripts = null;
+
+ // will eventually contain an array of data
+ // for the js web labels licenses.
+ this.licenseList = [];
+
+ // the response status for the page (200, 404, ...)
+ this.responseStatus = null;
+
+ // number of scripts fully tested.
+ this.scriptsTested = 0;
+
+ // number of external scripts to be tested.
+ this.numExternalScripts = null;
+
+ // number of inline/inattribute scripts
+ this.numInlineScripts = null;
+};
+
+/**
+ * Initialize properties of the object
+ *
+ * @param {domObject} obj A reference of the DOM object being
+ * analyzed.
+ *
+ * @param {pageURL} string The formatted URL (with fragment
+ * removed) of the corresponding page for this DOM
+ *
+ * @param {fragment} the #fragment from the url if applicable.
+ *
+ * @param {callback} the callback function.
+ *
+ */
+DomHandler.prototype.init = function(
+ domObject, pageURL, fragment, responseStatus, callback
+) {
+ // initialize object properties.
+
+ console.debug('init', pageURL);
+ var that = this;
+
+ this.reset();
+
+ // arguments passed.
+ this.dom = domObject;
+ this.pageURL = pageURL;
+ this.fragment = fragment;
+ this.responseStatus = responseStatus;
+
+ console.debug('in dom handler, responseStatus is', this.responseStatus);
+
+ // make callback function available
+ // for the entire object.
+ this.callback = function (dom) {
+ callback(dom);
+ that.destroy();
+ };
+};
+
+DomHandler.prototype.reset = function () {
+
+ this.dom = null;
+ // arrays.
+ this.onEventElement = [];
+ this.scriptStatus = [];
+ this.inlineScripts = [];
+ this.externalScripts = [];
+ this.scripts = [];
+
+ // booleans
+ this.allowTrivial = true;
+ this.inlineJsFree = false;
+ this.hasRemovedScripts = false;
+ this.removedAllScripts = false;
+
+ // we start with 0, and will increment in
+ // dom_checker.
+ this.numExternalScripts = 0;
+
+ this.numInlineScripts = 0;
+
+ this.scriptsTested = 0;
+
+};
+
+DomHandler.prototype.destroy = function () {
+ this.domGatherer = null;
+ this.domChecker = null;
+ /* destroy callback so that it can't be called multiple times. */
+ this.callback = function() {};
+ //this.reset();
+};
+
+DomHandler.prototype.scriptHasBeenTested = function() {
+ this.scriptsTested++;
+ console.debug('incremented DomHandler.scriptsTested to',
+ this.scriptsTested);
+};
+
+/**
+ * scriptHasJsWebLabel
+ *
+ * Checks if a script was found earlier in a Js License Web Label
+ * table. See http://www.gnu.org/licenses/javascript-labels.html
+ * for more information.
+ *
+ */
+DomHandler.prototype.scriptHasJsWebLabel = function(script) {
+ if (this.licenseList) {
+
+ var url = urlHandler.resolve(this.pageURL, script.src),
+ i = 0,
+ len = this.licenseList.length;
+
+ console.debug('looking for web label');
+
+ for (; i < len; i++) {
+ if (this.licenseList[i].fileUrl === url &&
+ this.licenseList[i].free === true
+ ) {
+ console.debug('found something true');
+ console.debug(
+ this.licenseList[i].fileUrl, ' is found');
+ return true;
+ }
+ }
+ }
+ return false;
+};
+
+/**
+ * processScripts.
+ * Starts by looking for a js web labels page
+ * then calls the complete function, which runs
+ * the rest of the check.
+ */
+DomHandler.prototype.processScripts = function () {
+ var that = this;
+
+ // check for the existence of the
+ // js web labels first.
+ this.lookForJsWebLabels(function () {
+
+ // gather and check all script elements on
+ // page.
+ console.debug("Calling checkAllScripts");
+ that.checkAllScripts();
+
+ });
+
+};
+
+/**
+ * jsWebLabelsComplete
+ *
+ */
+DomHandler.prototype.checkAllScripts = function () {
+ try {
+ console.debug(
+ 'found in', this.pageURL, JSON.stringify(this.licenseList));
+ console.debug('checkAllScripts triggered async');
+
+ // use domGatherer to gather scripts.
+ this.domGatherer.findScripts();
+ this.domGatherer.gatherScriptsContent();
+ this.domGatherer.gatherIntrinsicEvents();
+
+ console.debug('fragment is', this.fragment);
+
+ if (
+ this.fragment === undefined ||
+ this.fragment === null ||
+ this.fragment.indexOf('librejs=true') < 0
+ ) {
+ try {
+
+ // use domChecker to check scripts.
+ console.debug("Calling checkAllInlineScripts");
+ this.domChecker.checkAllInlineScripts();
+ } catch (x) {
+ console.debug('error in domChecker:', x, x.lineNumber);
+ this.removeAllJs();
+ }
+ } else {
+ console.debug('This is a pageworker, removing all js');
+ // this is the Page Worker to find contact link
+ // just remove all the JS since we don't need it.
+ console.debug('fragment found, remove js');
+ this.removeAllJs();
+ }
+ } catch (x) {
+ console.debug('error', x, x.lineNumber, x.fileName);
+ }
+};
+
+/**
+ * lookForJsWebLabels
+ *
+ * Checks if a link to a js web label table exists.
+ * If it does, return an array of objects with the data
+ * gathered (script name, path, license name, url, ...)
+ *
+ */
+DomHandler.prototype.lookForJsWebLabels = function (completed) {
+ var that = this;
+ console.debug("calling lookForJsWebLabels");
+ if (this.fragment !== '#librejs=true') {
+ var webLabelFinder = new WebLabelFinder();
+ webLabelFinder.init(
+ this.dom,
+ this.pageURL,
+ function (licenseList) {
+ // assign array returned to property.
+ that.licenseList = licenseList;
+ console.debug("calling completed");
+ completed();
+ });
+ } else {
+ completed();
+ }
+};
+
+DomHandler.prototype.checkScriptForJsWebLabels = function(script) {
+ var scriptEntry;
+
+ if (this.hasSrc(script) && this.scriptHasJsWebLabel(script)) {
+ // This script is in the list of allowed scripts (through web labels)
+ scriptEntry = scriptObject.Script({
+ 'type': scriptTypes.EXTERNAL,
+ 'status': statusTypes.ACCEPTED,
+ 'element': script,
+ 'url': urlHandler.resolve(this.pageURL, script.src)
+ });
+
+ scriptEntry.tagAsAccepted(this.pageURL, reasons.FREE);
+ return true;
+ }
+};
+
+/**
+ * hasSrc
+ * Check the given script has an src attribute.
+ * @param script obj The script element.
+ * @return a string with the value of the src attribute.
+ */
+DomHandler.prototype.hasSrc = function(script) {
+ if (script.src) {
+ return script.src;
+ }
+ return false;
+};
+
+/**
+ * Uses relationChecker to guess whether the script only uses
+ * predefined functions/variables or interacts with other scripts
+ * (this is still very experimental and needs improvement.)
+ *
+ */
+DomHandler.prototype.removeScriptIfDependent = function (script) {
+ var nonWindowProps = script.tree.relationChecker.nonWindowProperties;
+
+ for (var entry in nonWindowProps) {
+ if (nonWindowProps[entry]) {
+ console.debug('script has non window properties.');
+ this.removeGivenJs(script, reasons.TRIVIAL_NOT_ALLOWED);
+ return true;
+ }
+ }
+};
+
+/**
+ * removeGivenJs
+ * Remove a single script from the DOM.
+ * @param script Obj The script element to be removed from the
+ * DOM.
+ *
+ */
+DomHandler.prototype.removeGivenJs = function (script, reason, singleton, hash) {
+ var commentedOut;
+ var isAllowed = allowedRef.urlInAllowedReferrers(this.pageURL);
+ console.debug("removing given js hash", hash);
+
+ if (script.status != statusTypes.REJECTED &&
+ script.status != statusTypes.JSWEBLABEL
+ ) {
+ console.debug('removing a', script.type);
+ if (script.type === scriptTypes.ATTRIBUTE &&
+ !isAllowed
+ ) {
+ this.removeGivenAttribute(script, reason);
+ return;
+ }
+ if (!isAllowed) {
+ // set invalid type if dry run off.
+ script.element.setAttribute('type', 'librejs/blocked');
+ // add entry as removed.
+ console.debug('removeGivenJs hash is', hash);
+ script.tagAsRemoved(this.pageURL, reason, hash);
+ } else {
+ script.element.setAttribute(
+ 'data-librejs-dryrun', 'librejs/blocked');
+ script.tagAsDryRun(this.pageURL, reason, hash);
+ }
+
+ if (singleton === true) {
+ // flag singletons.
+ script.element.setAttribute('data-singleton', 'true');
+ }
+
+ // remove src if dry run off.
+ if (script.element.getAttribute('src') !== undefined) {
+ script.element.setAttribute(
+ 'data-librejs-blocked-src',
+ script.element.getAttribute('src')
+ );
+ if (!isAllowed) {
+ script.element.removeAttribute('src');
+ }
+ }
+ if (isAllowed) {
+ comment_str = 'LibreJS: Script should be blocked, but page is whitelisted.';
+ script.status = statusTypes.ACCEPTED;
+ } else {
+ comment_str = 'LibreJS: script blocked.';
+ script.status = statusTypes.REJECTED;
+ }
+
+ commentedOut = this.dom.createComment(comment_str);
+ // add a comment for curious source readers.
+ script.element.parentNode.appendChild(commentedOut);
+ script.element.parentNode.insertBefore(commentedOut, script.element);
+ this.hasRemovedScripts = true;
+ }
+};
+
+DomHandler.prototype.removeGivenAttribute = function (script, reason) {
+ var i = 0,
+ le = script.jsAttributes.length;
+
+ console.debug('removing given attribute', script, reason);
+ script.element.setAttribute('data-librejs-blocked-event',
+ JSON.stringify(script.jsAttributes));
+
+ script.tagAsRemoved(this.pageURL, reason, script.hash || script.tree.hash);
+
+ // might need to be removed.
+ script.element.setAttribute('data-librejs-blocked-value', '');
+
+ if (!allowedRef.urlInAllowedReferrers(this.pageURL)) {
+ // only run if not in dry run mode.
+ for (; i < le; i++) {
+ console.debug('removing attribute', JSON.stringify(script.jsAttributes));
+ script.element.removeAttribute(script.jsAttributes[i].attribute);
+ }
+ } else {
+
+ }
+ this.hasRemovedScripts = true;
+};
+
+/**
+ * removeAllJs
+ * Loop through all scripts from top to bottom and add a type
+ * attribute 'librejs/blocked' to prevent their interpretation
+ * by the browser.
+ *
+ */
+DomHandler.prototype.removeAllJs = function (reason) {
+ // remove all js is useless from now on.
+ console.debug('removeAllJs');
+ this.hasRemovedScripts = true;
+
+ // removeAllJs needs not be run next time.
+ this.removedAllScripts = true;
+
+ try {
+ this.removeAllArray(this.scripts, reason);
+ this.callback(this.dom);
+ } catch (x) {
+ console.debug(
+ 'in removeAllJs method: ',
+ x,
+ 'number of scripts is',
+ this.numScripts
+ );
+ this.callback(this.dom);
+ }
+
+};
+
+DomHandler.prototype.removeAllArray = function(scriptArray, reason) {
+ var script, i = 0, le;
+ console.debug('removeAllArray');
+ try {
+ le = scriptArray.length;
+ // loop through all scripts.
+
+ for (; i < le; i++) {
+ script = scriptArray[i];
+ if (script.type === scriptTypes.INLINE ||
+ script.type === scriptTypes.EXTERNAL
+ ) {
+ this.removeGivenJs(script, reason);
+ }
+ else if (script.type === scriptTypes.ATTRIBUTE) {
+ this.removeGivenAttribute(script, reason);
+ }
+ }
+ } catch (e) {
+ this.callback("");
+ }
+
+};
+
+exports.DomHandler = DomHandler;
+
+/**
+ * exports.domHandler
+ * Instantiates a DomHandler and checks the DOM
+ * @param dom obj The given dom for analysis.
+ * @param pageURL string the URL for the page.
+ * @param callback function callback when all the work has been performed.
+ */
+exports.domHandler = function(
+ dom, pageURL, fragment, responseStatus, callback) {
+ console.debug("Creating domHandler");
+ var domHandler = new DomHandler();
+ domHandler.init(dom, pageURL, fragment, responseStatus, callback);
+
+ // use domGatherer methods.
+ domHandler.domGatherer = domGatherer(domHandler);
+
+ // use domChecker methods.
+ domHandler.domChecker = domChecker(domHandler);
+
+ // launch the whole process.
+ console.debug("Calling processScripts");
+ domHandler.processScripts();
+};
diff --git a/lib/html_script_finder/dom_handler/attributes.js b/lib/html_script_finder/dom_handler/attributes.js
new file mode 100644
index 0000000..3e95bab
--- /dev/null
+++ b/lib/html_script_finder/dom_handler/attributes.js
@@ -0,0 +1,137 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// object model for script entries.
+var scriptObject = require("html_script_finder/dom_handler/script_object");
+
+var scriptProperties = require("html_script_finder/dom_handler/script_properties");
+
+const scriptTypes = scriptProperties.scriptTypes;
+
+const statusTypes = scriptProperties.statusTypes;
+
+var jsInAttrRe = /javascript:/ig;
+
+// the list of all available event attributes
+var intrinsicEvents = [
+ "onload",
+ "onunload",
+ "onclick",
+ "ondblclick",
+ "onmousedown",
+ "onmouseup",
+ "onmouseover",
+ "onmousemove",
+ "onmouseout",
+ "onfocus",
+ "onblur",
+ "onkeypress",
+ "onkeydown",
+ "onkeyup",
+ "onsubmit",
+ "onreset",
+ "onselect",
+ "onchange"];
+
+exports.jsInAttrRe = jsInAttrRe;
+exports.intrinsicEvents = intrinsicEvents;
+
+
+/**
+ * findJSinAttribute
+ *
+ * Looks for attributes containing 'javascript:'
+ *
+ */
+exports.findJSinAttribute = function (elem, callback) {
+ var i = 0, attrLen = elem.attributes.length;
+
+ var attribPairs = [];
+
+ for (; i < attrLen; i++) {
+
+ //looping through all attributes in elem to look for "javascript:"
+ attrib = elem.attributes[i];
+
+ if (attrib.value.match(jsInAttrRe)) {
+ str = attrib.value.replace(jsInAttrRe, '');
+ attribPairs.push({attribute: attrib.name, value: str});
+ }
+
+ }
+
+ if (attribPairs.length > 0) {
+ // contains in attribute javascript.
+ scriptEntry = scriptObject.Script({'type': scriptTypes.ATTRIBUTE,
+ 'status': statusTypes.UNCHECKED,
+ 'element': elem,
+ 'jsAttributes': attribPairs
+ });
+
+ // push back to DOMHandler
+ callback(scriptEntry);
+
+ } else {
+ callback(false);
+ }
+
+};
+
+/**
+ * findOnJSAttribute.
+ *
+ * Look for attributes in on*
+ *
+ */
+exports.findOnJSAttribute = function (elem, callback) {
+
+ var i = 0, eventsLen = intrinsicEvents.length;
+
+ var attribPairs = [];
+
+ for (; i < eventsLen; i++) {
+
+ // looping through all on* attributes
+ if (elem.hasAttribute(intrinsicEvents[i])) {
+
+ attribPairs.push({
+ attribute: intrinsicEvents[i],
+ value: elem.getAttribute(intrinsicEvents[i])
+ });
+
+ }
+
+ }
+ if (attribPairs.length > 0) {
+
+ console.debug('found an attribute', scriptTypes.ATTRIBUTE);
+ scriptEntry = scriptObject.Script({'type': scriptTypes.ATTRIBUTE,
+ 'status': statusTypes.UNCHECKED,
+ 'element':elem,
+ 'jsAttributes': attribPairs
+ });
+ // Push back to DOMHandler.
+ // push back to DOMHandler
+ callback(scriptEntry);
+
+ } else {
+ callback(false);
+ }
+};
diff --git a/lib/html_script_finder/dom_handler/dom_checker.js b/lib/html_script_finder/dom_handler/dom_checker.js
new file mode 100644
index 0000000..1a0f30e
--- /dev/null
+++ b/lib/html_script_finder/dom_handler/dom_checker.js
@@ -0,0 +1,478 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * dom_checker.js
+ *
+ * checks scripts for nonfree/nontrivial.
+ *
+ */
+
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+var timer = require("sdk/timers");
+
+var scriptProperties = require("html_script_finder/dom_handler/script_properties");
+const scriptTypes = scriptProperties.scriptTypes;
+const statusTypes = scriptProperties.statusTypes;
+const reasons = scriptProperties.reasons;
+
+// ensure xhr won't create an infinite loop
+// with html content.
+var urlTester = require("html_script_finder/url_seen_tester").urlSeenTester;
+var urlHandler = require("url_handler/url_handler");
+
+var privacyChecker = require("js_checker/privacy_checker").privacyCheck;
+var jsChecker = require("js_checker/js_checker");
+
+const types = require("js_checker/constant_types");
+var checkTypes = types.checkTypes;
+var stripCDATAOpen = /<\!\[CDATA\[/gi;
+var stripCDATAClose = /]]>/g;
+
+const getHash = require("script_entries/scripts_cache").scriptsCached.getHash;
+
+var DomChecker = function() {
+ // reference to domHandler instance
+ // using this object.
+ this.d = null;
+};
+
+/**
+ * init
+ *
+ * assign a reference domHandler object
+ * to access/updates its properties.
+ *
+ */
+DomChecker.prototype.init = function(domHandler) {
+ "use strict";
+
+ this.d = domHandler;
+};
+
+DomChecker.prototype.destroy = function() {
+ "use strict";
+
+ this.d = null;
+};
+
+/**
+ * checkAllInlineScripts
+ *
+ * Sends all the inline/onpage scripts as a whole for a check and
+ * removes all scripts if nonfree nontrivial is found.
+ *
+ */
+DomChecker.prototype.checkAllInlineScripts = function() {
+ "use strict";
+
+ try {
+ var i = 0, len, script;
+
+ if (typeof this.d.inlineScripts !== 'undefined' &&
+ this.d.inlineScripts.length > 0
+ ) {
+ script = this.d.inlineScripts.shift();
+ console.debug("checking script for page",
+ this.d.pageURL
+ /*, JSON.stringify(script)*/);
+ if (this.d.removedAllScripts) {
+ // all js has already been removed.
+ // stop check.
+ console.debug("removed all");
+ return;
+ }
+
+ if (this.d.inlineJsFree === true) {
+ // add entry as accepted.
+ try {
+ hash = getHash(script.text);
+ script.tagAsAccepted(this.d.pageURL, reasons.FREE, hash);
+ } catch (e) {
+ console.debug(e);
+ }
+ }
+
+ // even if page is free we need to check for allow trivial.
+ if (script.type === scriptTypes.INLINE) {
+ console.debug("analyzing script", script);
+ this.analyzeJs(script,
+ script.text,
+ this.checkSingleInlineScript.bind(this));
+ } else if (script.type === scriptTypes.ATTRIBUTE) {
+ console.debug("analyzing inline script", script);
+ this.analyzeJs(script,
+ this.concatAttributes(script),
+ this.checkSingleElementAttributes.bind(this));
+ }
+ } else {
+ // no more inline scripts. Switch to external scripts.
+ this.readyForExternal();
+ }
+ } catch (x) {
+ console.debug('checkAllInlineScripts error',
+ x, x.lineNumber, x.fileName);
+ this.readyForExternal();
+ }
+};
+
+DomChecker.prototype.concatAttributes = function(script) {
+ "use strict";
+ var i = 0,
+ le = script.jsAttributes.length,
+ text = "";
+
+ // we concatenate all js in multiple attributes.
+ // because it's too much of a hassle to keep track
+ // otherwise.
+ for (; i < le; i++) {
+ text += script.jsAttributes[i].value + '\n';
+ }
+
+ return text;
+};
+
+/**
+ *
+ * check a single element with attributes
+ */
+DomChecker.prototype.checkSingleElementAttributes = function(
+ script, loadedScript, checker) {
+ "use strict";
+ var check, value,
+ i = 0,
+ le = script.jsAttributes.length,
+ text = "";
+
+ try {
+ check = checker.parseTree.freeTrivialCheck;
+ script.tree = checker;
+ script.result = check;
+ script.status = statusTypes.CHECKED;
+ } catch (e) {
+ console.debug('problem checking inline scripts', e, e.lineNumber);
+ this.d.removeGivenJs(script);
+ }
+
+ this.processInlineCheckResult(script, check, checker);
+};
+
+DomChecker.prototype.processInlineCheckResult = function(
+ script, check, checker) {
+ "use strict";
+ console.debug("check.reason is", check.reason, "and type", check.type);
+ var hash = checker.hash;
+
+ if (this.d.inlineJsFree === true) {
+ console.debug('tagging', script.text, 'as accepted', "with reason", check.reason);
+ script.tagAsAccepted(this.d.pageURL, this.d.freeReason + " -- " + check.reason, hash);
+ }
+
+ // process the result.
+ if (check.type === checkTypes.FREE) {
+ // this is free.
+ console.debug('tagging', script.text, 'as accepted with reason', check.reason);
+ this.d.inlineJsFree = true;
+ this.d.freeReason = check.reason;
+ // add entry as accepted.
+ script.tagAsAccepted(this.d.pageURL, check.reason, hash);
+ } else if (check.type === checkTypes.FREE_SINGLE_ITEM) {
+ // accept this script.
+ console.debug("free single item, ", check.reason);
+ script.tagAsAccepted(this.d.pageURL, check.reason, hash);
+ } else if (check.type === checkTypes.NONTRIVIAL) {
+ console.debug("nontrivial hash is", hash);
+ if (this.d.inlineJsFree) {
+ // inline is free. So accept.
+ console.debug('tagging', script.text, 'as accepted');
+ script.tagAsAccepted(
+ this.d.pageURL,
+ this.d.freeReason + ' -- ' + check.reason,
+ hash);
+ } else {
+ console.debug('tagging', script.text, 'as removed');
+ this.d.removeGivenJs(script, check.reason, false, hash);
+ }
+ } else if (!this.d.inlineJsFree &&
+ this.d.loadsHtmlExternalScripts &&
+ check.type === checkTypes.TRIVIAL_DEFINES_FUNCTION
+ ) {
+ // nontrivial, because defines function and loads
+ // external scripts
+ console.debug('tagging', script.text, 'as removed');
+ this.d.removeGivenJs(script, reasons.FUNCTIONS_INLINE, false, hash);
+ } else if (!this.d.loadsHtmlExternalScripts &&
+ check === checkTypes.TRIVIAL_DEFINES_FUNCTION
+ ) {
+ console.debug("Tag as accepted doesn't load another external script");
+ script.tagAsAccepted(this.d.pageURL, check.reason, hash);
+ } else if (check.type === checkTypes.TRIVIAL ||
+ check.type === checkTypes.TRIVIAL_DEFINES_FUNCTION ||
+ check.type === checkTypes.WHITELISTED
+ ) {
+ // add entry as accepted.
+ console.debug("Trivial accepted");
+ script.tagAsAccepted(this.d.pageURL, check.reason, hash);
+ }
+
+ // next inline script, if applicable.
+ this.checkAllInlineScripts();
+};
+
+DomChecker.prototype.readyForExternal = function() {
+ "use strict";
+
+ console.debug('DomChecker.readyForExternal');
+ // done with those inline scripts, continue with
+ // the rest.
+ this.checkExternalScripts();
+};
+
+/**
+ * check a single inline script.
+ */
+DomChecker.prototype.checkSingleInlineScript = function(
+ script, loadedScript, checker) {
+ "use strict";
+ var check, text;
+
+ console.debug('DomChecker.checkSingleInlineScript');
+
+ try {
+
+ check = checker.parseTree.freeTrivialCheck;
+
+ // update status.
+ script.tree = checker;
+ script.result = check;
+ console.debug("script result is", check.type);
+ script.status = statusTypes.CHECKED;
+
+ } catch (e) {
+ console.debug('problem checking inline scripts', e, e.lineNumber);
+ this.d.removeGivenJs(script, '', false, checker.hash);
+ }
+
+ this.processInlineCheckResult(script, check, checker);
+
+};
+
+/**
+ * checkExternalScripts
+ * Loop through series of external scripts,
+ * perform xhr to get their data, and check them
+ * to see whether they are free/nontrivial
+ *
+ */
+DomChecker.prototype.checkExternalScripts = function() {
+ "use strict";
+
+ console.debug('DomChecker.checkExternalScripts');
+
+ var i = 0;
+ var len = this.d.externalScripts.length;
+ var that = this;
+
+ console.debug("externalScripts length", len);
+ if (this.d.removedAllScripts || len === 0) {
+ // all js has already been removed.
+ // stop check.
+ this.wrapUpBeforeLeaving();
+ return;
+ }
+
+ for (; i < len; i++) {
+ this.xhr(
+ this.d.externalScripts[i],
+ function(script, scriptText) {
+ console.debug("In xhr callback for script url:", script.url);
+ if (scriptText === false) {
+ that.d.removeGivenJs(script);
+ that.d.scriptHasBeenTested();
+ that.externalCheckIsDone();
+ return;
+ }
+
+ console.debug('about to analyzeJS for script:', script.url);
+ that.analyzeJs(
+ script,
+ scriptText,
+ that.checkSingleExternalScript.bind(that));
+ }
+ );
+ }
+};
+
+DomChecker.prototype.wrapUpBeforeLeaving = function() {
+ "use strict";
+
+ console.debug("wrap up before leaving triggered");
+ console.debug('wrapping up');
+ this.d.callback(this.d.dom);
+
+};
+
+DomChecker.prototype.analyzeJs = function(script, scriptText, callback) {
+ "use strict";
+ console.debug('DomChecker.analyzeJs for script:', script.url);
+ try {
+ var checker = jsChecker.jsChecker();
+ var url = "";
+ if (typeof script.url !== "undefined") {
+ url = script.url;
+ } else {
+ url = this.pageURL;
+ }
+ checker.searchJs(scriptText, function() {
+ console.debug("Analyze JS"/*, JSON.stringify(checker)*/);
+ timer.setTimeout(function() {
+ callback(script, scriptText, checker);
+ }, 0);
+ }, url);
+ } catch (x) {
+ console.debug('error', x, x.lineNumber, x.fileName);
+ }
+};
+
+/**
+ * Check a single external script.
+ */
+DomChecker.prototype.checkSingleExternalScript = function(
+ script, loadedScript, checker
+) {
+ "use strict";
+ var check;
+
+ console.debug('DomChecker.checkSingleExternalScript()');
+ try {
+ check = checker.parseTree.freeTrivialCheck;
+
+ script.tree = checker;
+ script.result = check;
+ console.debug('in checkSingleExternalScript, checker.hash is',
+ checker.hash);
+ if (script.status != statusTypes.JSWEBLABEL) {
+ script.status = statusTypes.CHECKED;
+ }
+
+ if (check.type === checkTypes.FREE ||
+ check.type === checkTypes.FREE_SINGLE_ITEM
+ ) {
+ // add entry as accepted.
+ script.tagAsAccepted(this.d.pageURL, check.reason, checker.hash);
+ }
+
+ else if (check.type === checkTypes.NONTRIVIAL) {
+ console.debug("Removing given js", check.reason);
+ this.d.removeGivenJs(script, check.reason, false, checker.hash);
+ }
+
+ else if (check.type === checkTypes.TRIVIAL ||
+ check.type === checkTypes.WHITELISTED
+ ) {
+ // if it's accepted, allow.
+ script.tagAsAccepted(this.d.pageURL, check.reason, checker.hash);
+ } else {
+ // anything else is nontrivial. Including TRIVIAL_DEFINES_FUNCTION.
+ console.debug("checker hash for remove is ", checker.hash);
+ this.d.removeGivenJs(
+ script, reasons.FUNCTIONS_EXTERNAL, false, checker.hash);
+ }
+
+ } catch (e) {
+ console.debug('error in checkExternalScript',
+ e, e.lineNumber, 'for script', script.url);
+
+ this.d.removeAllJs();
+ this.destroy();
+ return;
+ }
+ console.debug('script url is', script.url, 'result is', script.result);
+ this.d.scriptHasBeenTested();
+ this.externalCheckIsDone();
+};
+
+DomChecker.prototype.externalCheckIsDone = function() {
+ "use strict";
+ console.debug('DomChecker.externalCheckIsDone');
+
+ console.debug('scriptsTested is', this.d.scriptsTested);
+ console.debug('num external', this.d.numExternalScripts);
+
+ if (this.d.scriptsTested >= this.d.numExternalScripts) {
+ console.debug('wrapping up external');
+ this.wrapUpBeforeLeaving();
+ } else {
+ var scriptsToCheck = this.d.numExternalScripts - this.d.scriptsTested;
+ console.debug('Not wrapping up! Waiting to check ' + scriptsToCheck +
+ ' more script(s)');
+
+ if (this.d.externalScripts[0]) {
+ console.debug('script 0 is', this.d.externalScripts[0]);
+ }
+ if (this.d.externalScripts[1]) {
+ console.debug('script 1 is', this.d.externalScripts[1]);
+ }
+ }
+};
+
+/**
+ * xhr
+ * Perform a XMLHttpRequest on the url given.
+ * @param url string A URL.
+ * @return The response text.
+ */
+DomChecker.prototype.xhr = function(script, responseCallback) {
+ "use strict";
+
+ var regex = /^text\/html/i;
+ var url = script.url;
+
+ try {
+ // add url to whitelist.
+ urlTester.addUrl(url);
+
+ // request module. Compatible with Https-Everywhere.
+ require('html_script_finder/dom_handler/request')
+ .request(script, responseCallback).request();
+ } catch (x) {
+ console.debug('error', x, x.lineNumber, x.fileName);
+ responseCallback(script, false);
+ }
+};
+
+/**
+ * exports.domChecker
+ * Instantiate a brand new clone of the domChecker.
+ * @param dom obj The given dom for analysis.
+ * @param pageURL string the URL for the page.
+ * @param callback function callback when all the work has been performed.
+ */
+exports.domChecker = function(domHandler) {
+ "use strict";
+
+ var domChecker = new DomChecker();
+
+ domChecker.init(domHandler);
+
+ return domChecker;
+};
+
+exports.xhr = new DomChecker().xhr;
diff --git a/lib/html_script_finder/dom_handler/dom_gatherer.js b/lib/html_script_finder/dom_handler/dom_gatherer.js
new file mode 100644
index 0000000..4fcee88
--- /dev/null
+++ b/lib/html_script_finder/dom_handler/dom_gatherer.js
@@ -0,0 +1,281 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var scriptProperties = require("html_script_finder/dom_handler/script_properties");
+
+const scriptTypes = scriptProperties.scriptTypes;
+const scriptsCached = require("script_entries/scripts_cache").scriptsCached;
+
+const statusTypes = scriptProperties.statusTypes;
+// object model for script entries.
+var scriptObject = require("html_script_finder/dom_handler/script_object");
+
+var urlHandler = require("url_handler/url_handler");
+
+var attributeHelpers = require("html_script_finder/dom_handler/attributes");
+
+// javascript:*
+var jsInAttrRe = attributeHelpers.jsInAttrRe;
+
+// the list of all available event attributes
+var intrinsicEvents = attributeHelpers.intrinsicEvents;
+
+var privacyChecker = require("js_checker/privacy_checker").privacyCheck;
+
+const types = require("js_checker/constant_types");
+
+var checkTypes = types.checkTypes;
+
+// array reflex valid types as listed in
+// http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsScriptLoader.cpp#437
+// anything appended to end of strings is considered valid:
+var jsValidTypes = [
+ /^text\/javascript/i,
+ /^text\/ecmascript/i,
+ /^application\/javascript/i,
+ /^application\/ecmascript/i,
+ /^application\/x-javascript/i
+];
+
+var stripCDATAOpen = /<\!\[CDATA\[/gi;
+var stripCDATAClose = /]]>/g;
+
+var stripHtmlCommentsInScript = function (s) {
+ s = s.replace(stripCDATAOpen, '');
+ s = s.replace(stripCDATAClose, '');
+ return s;
+};
+
+
+// gather scripts and javascript in attributes across a dom object.
+var DomGatherer = function() {
+ // domHandler object.
+ this.d = null;
+};
+
+/**
+ * init
+ *
+ * assign a reference domHandler object
+ * to access/updates its properties.
+ *
+ */
+DomGatherer.prototype.init = function (domHandler) {
+ this.d = domHandler;
+};
+
+/**
+ * scriptHasInvalidType
+ *
+ * Checks that a script does not have a js "template" type.
+ * Normally any script that has a type attribute other than the
+ * few allowed ones is not interpreted. But by security, we only
+ * discard a few of them.
+ *
+ * @param script obj The script element.
+ * @return returns true if it matches a template type.
+ *
+ */
+DomGatherer.prototype.scriptHasInvalidType = function (script) {
+ var i = 0,
+ le = jsValidTypes.length;
+
+ var type = script.getAttribute('type');
+
+ if (type === 'librejs/blocked') {
+ // js has already been blocked.
+ return true;
+ }
+
+ if (!type) {
+ // type isn't set, don't look further.
+ return false;
+ }
+
+ for (; i < le; i++) {
+ if (jsValidTypes[i].test(type)) {
+ return false;
+ }
+ }
+
+ // type is invalid and
+ // hence cannot be executed.
+ return true;
+};
+
+/**
+ * findScripts
+ *
+ * Assigns the array of scripts in the dom to a property
+ * as well as a number of scripts present for looping purposing.
+ */
+DomGatherer.prototype.findScripts = function() {
+ this.d.domScripts = this.d.dom.getElementsByTagName('script');
+ this.d.numScripts = this.d.domScripts.length;
+};
+
+/**
+ * gatherIntrinsicEvents
+ *
+ * Fetches all the event attributes that might contain JavaScript
+ * as well as all element attributes that start with
+ * "javascript:".
+ *
+ */
+DomGatherer.prototype.gatherIntrinsicEvents = function() {
+ var i = 0, j, k,
+ all = this.d.dom.getElementsByTagName('*'),
+ max = all.length,
+ that = this,
+ attrLen, attrib, str, scriptEntry;
+
+ for (; i < max; i++) {
+ // look for attributes with value javascript:*
+ attributeHelpers.findJSinAttribute(
+ all[i],
+ function (scriptEntry) {
+ if (scriptEntry !== false) {
+
+ that.d.inlineScripts.push(scriptEntry);
+ that.d.scripts.push(scriptEntry);
+
+ // add inline script in the count.
+ that.d.numInlineScripts++;
+ }
+ });
+
+ // look for attributes of on* (onLoad, ...)
+ attributeHelpers.findOnJSAttribute(
+ all[i],
+ function (scriptEntry) {
+ if (scriptEntry !== false) {
+ that.d.inlineScripts.push(scriptEntry);
+ that.d.scripts.push(scriptEntry);
+
+ // add inline script in the count.
+ that.d.numInlineScripts++;
+ }
+ });
+ }
+
+};
+
+/**
+ * gatherScriptsContent
+ *
+ * Aggregate all content within on-page JavaScript code.
+ * Keep a list of all absolute urls to external scripts.
+ *
+ */
+DomGatherer.prototype.gatherScriptsContent = function() {
+ var i = 0, currentScript = '', absolutePath, scriptEntry,
+ that = this;
+ try {
+ for (; i < this.d.numScripts; i++) {
+ if (this.d.checkScriptForJsWebLabels(this.d.domScripts[i])) {
+ //break;
+ absolutePath = urlHandler.resolve(
+ this.d.pageURL, this.d.domScripts[i].src);
+ scriptEntry = scriptObject.Script(
+ {'type': scriptTypes.EXTERNAL,
+ 'status': statusTypes.JSWEBLABEL,
+ 'element': this.d.domScripts[i],
+ 'url': absolutePath});
+ scriptEntry.tree = {};
+
+ this.d.externalScripts.push(scriptEntry);
+ that.d.scripts.push(scriptEntry);
+
+ this.d.loadsHtmlExternalScripts = true;
+
+ // increment number of scripts found.
+ this.d.numExternalScripts++;
+ }
+
+ // check that script has valid type
+ else if (!this.scriptHasInvalidType(this.d.domScripts[i])) {
+
+
+ if (this.d.hasSrc(this.d.domScripts[i]) &&
+ !this.d.scriptHasJsWebLabel(this.d.domScripts[i])) {
+
+ console.debug('an external script', this.d.domScripts[i]);
+
+ absolutePath = urlHandler.resolve(
+ this.d.pageURL, this.d.domScripts[i].src);
+ scriptEntry = scriptObject.Script(
+ {'type': scriptTypes.EXTERNAL,
+ 'status': statusTypes.UNCHECKED,
+ 'element': this.d.domScripts[i],
+ 'url': absolutePath});
+ this.d.externalScripts.push(scriptEntry);
+ that.d.scripts.push(scriptEntry);
+
+ this.d.loadsHtmlExternalScripts = true;
+
+ // increment number of scripts found.
+ this.d.numExternalScripts++;
+
+ } else if (privacyChecker.checkScriptPrivacyThreat(this.d.domScripts[i].text)) {
+ this.d.removeGivenJs(scriptObject.Script(
+ {'type': scriptTypes.SINGLETON,
+ 'status': statusTypes.UNCHECKED,
+ 'element': this.d.domScripts[i],
+ 'text': this.d.domScripts[i].text
+ }), '', true);
+ } else if (this.d.domScripts[i].text !== '') {
+ // using else if since script text is
+ // ignored if src attribute is set.
+ // adding this.narcissusBugFixLibreJS to fix comment bug.
+ var bugfix = require('html_script_finder/bug_fix').narcissusBugFixLibreJS;
+ currentScript = stripHtmlCommentsInScript(this.d.domScripts[i].text + bugfix);
+
+ scriptEntry = scriptObject.Script(
+ {'type': scriptTypes.INLINE,
+ 'status': statusTypes.UNCHECKED,
+ 'element': this.d.domScripts[i],
+ 'text': currentScript});
+ this.d.inlineScripts.push(scriptEntry);
+ this.d.scripts.push(scriptEntry);
+
+ // add inline script in the count.
+ this.d.numInlineScripts++;
+ }
+ }
+ }
+ } catch (e) {
+ // Any problem arising, we remove the script.
+ console.debug('problem gathering scripts', e, e.lineNumber);
+ this.d.removeAllJs();
+ }
+};
+
+/*
+ * exports.domGatherer
+ * Instantiate a brand new clone of the domGatherer.
+ * @param dom obj The given dom for analysis.
+ * @param pageURL string the URL for the page.
+ * @param callback function callback when all the work has been performed.
+ */
+exports.domGatherer = function (domHandler) {
+ var dg = new DomGatherer();
+ dg.init(domHandler);
+ return dg;
+};
diff --git a/lib/html_script_finder/dom_handler/request.js b/lib/html_script_finder/dom_handler/request.js
new file mode 100644
index 0000000..7f217ef
--- /dev/null
+++ b/lib/html_script_finder/dom_handler/request.js
@@ -0,0 +1,115 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var timer = require("sdk/timers");
+
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+var {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+
+// ensure xhr won't create an infinite loop
+// with html content.
+var urlTester = require("html_script_finder/url_seen_tester").urlSeenTester;
+var urlHandler = require("url_handler/url_handler");
+const scriptsCached = require("script_entries/scripts_cache").scriptsCached;
+
+var Request = function() {
+ this.url = null;
+ this.channel = null;
+ this.script = null;
+ this.responseCallback = null;
+};
+
+/**
+ * init
+ */
+Request.prototype.init = function(script, callback) {
+ this.script = script;
+ // set initial url
+ this.url = this.script.url;
+
+ console.debug('In Request.init() for url:', this.url);
+
+ this.responseCallback = callback;
+
+ var iOService = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+
+ this.channel = iOService.newChannel(this.url, null, null);
+};
+
+Request.prototype.request = function() {
+ var that = this;
+ var responseReceived = function (data) {
+ that.responseCallback(that.script, data);
+ };
+ try {
+ this.channel.asyncOpen({
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIRequestObserver, Ci.nsIStreamListener]),
+ data: "",
+ charset: null,
+
+ onStartRequest: function(request, context) {
+ this.charset = request.contentCharset || "UTF-8";
+ },
+
+ onDataAvailable: function (request, context, stream, offset, count) {
+ try {
+ var binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Ci.nsIBinaryInputStream);
+ binaryInputStream.setInputStream(stream);
+ var data = binaryInputStream.readBytes(count);
+ this.data += data;
+ } catch (x) {
+ console.debug('error in request', x, x.lineNumber);
+ responseReceived("");
+ }
+ },
+
+ onStopRequest: function (request, context, result) {
+ try {
+ if (this.charset.toLowerCase() != "utf-8") {
+ var uConv = Cc["@mozilla.org/intl/utf8converterservice;1"]
+ .createInstance(Ci.nsIUTF8ConverterService);
+
+ this.data = uConv.convertStringToUTF8(
+ this.data, this.charset, true);
+ }
+ } catch (e) {
+ console.debug("Issue with nsIUTF8ConverterService", e);
+ console.debug("Charset was", this.charset);
+ responseReceived("");
+ }
+ responseReceived(this.data);
+ }
+ }, null);
+ } catch(e) {
+ console.debug("asyncOpen exception", e);
+ responseReceived("");
+ }
+};
+
+// Instantiate a Request
+exports.request = function(script, callback) {
+ var obj = new Request();
+ obj.init(script, callback);
+ return obj;
+};
diff --git a/lib/html_script_finder/dom_handler/script_object.js b/lib/html_script_finder/dom_handler/script_object.js
new file mode 100644
index 0000000..5431cf6
--- /dev/null
+++ b/lib/html_script_finder/dom_handler/script_object.js
@@ -0,0 +1,208 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var removedScripts = require("script_entries/removed_scripts").removedScripts;
+
+var acceptedScripts = require("script_entries/accepted_scripts")
+ .acceptedScripts;
+var dryRunScripts = require("script_entries/dryrun_scripts").dryRunScripts;
+
+var Script = function(props) {
+ // can be an attribute, an inline script,
+ // or an external script.
+ this.type = null;
+
+ /*
+ * Script.status - The script's current status.
+ *
+ * Possible values are:
+ *
+ * 0 - unchecked
+ * 1 - checked
+ * 2 - accepted
+ * 3 - rejected
+ * 4 - jsweblabel
+ *
+ * See script_properties.js for definitions.
+ */
+ this.status = null;
+
+ // contains the dom element
+ this.element = null;
+
+ // the attribute name, if applicable.
+ this.attribute = null;
+
+ // the script text as a string.
+ this.value = null;
+
+ // the src url if external.
+ this.url = null;
+
+ // the script text if inline.
+ this.text = null;
+
+ this.init(props);
+};
+
+Script.prototype.init = function(props) {
+ // check the required elements are present.
+ if (typeof props === 'undefined') {
+ throw "Error, missing script entry value in script_object.js";
+ }
+
+ // required properties
+ if (typeof props.type !== 'undefined') {
+ this.type = props.type;
+ } else {
+ throw "type is missing";
+ }
+
+ if (typeof props.status !== 'undefined') {
+ this.status = props.status;
+ } else {
+ throw "status is missing";
+ }
+
+ if (typeof props.element !== 'undefined') {
+ this.element = props.element;
+ } else {
+ throw "element is missing";
+ }
+
+ // conditional properties.
+ this.url = (props.url) ? props.url : null;
+ this.text = (props.text) ? props.text : null;
+ this.jsAttributes = (props.jsAttributes) ? props.jsAttributes : null;
+
+ if (typeof this.text !== 'string' &&
+ this.tree !== null &&
+ typeof this.tree === 'object' &&
+ this.tree.hasOwnProperty('jsCode')
+ ) {
+ this.text = this.tree.jsCode;
+ }
+};
+
+Script.prototype.tagAsDryRun = function(pageURL, reason, hash) {
+ var content = this.findContentType();
+ var inline = (this.url != undefined) ? false : true;
+ var url = (inline == false ? this.url : null);
+ console.debug("url is", url);
+ this.element.setAttribute('data-librejs', 'dryrun');
+ this.element.setAttribute('data-librejs-reason', reason);
+
+ dryRunScripts.addAScript(
+ pageURL,
+ {'inline': inline,
+ 'contents': content,
+ 'reason': reason,
+ 'url': url,
+ 'hash': hash
+ });
+};
+
+Script.prototype.tagAsAccepted = function(pageURL, reason, hash) {
+ var content = this.findContentType();
+ var inline = (this.url != undefined) ? false : true;
+ var url = (inline == false ? this.url : null);
+ console.debug("url is", url);
+ this.element.setAttribute('data-librejs', 'accepted');
+ this.element.setAttribute('data-librejs-reason', reason);
+
+ acceptedScripts.addAScript(
+ pageURL,
+ {'inline': inline,
+ 'contents': content,
+ 'reason': reason,
+ 'url': url,
+ 'hash': hash
+ });
+
+};
+
+Script.prototype.tagAsRemoved = function(pageURL, reason, hash) {
+ var content = this.findContentType();
+ var inline = (this.url != undefined) ? false : true;
+ var url = (inline == false ? this.url : null);
+ this.element.setAttribute('data-librejs', 'rejected');
+ this.element.setAttribute('data-librejs-reason', reason);
+ console.debug("tagAsRemoved hash is", hash);
+ removedScripts.addAScript(pageURL, {
+ 'inline': inline,
+ 'contents': content,
+ 'reason': reason,
+ 'url': url,
+ 'hash': hash
+ });
+
+};
+
+Script.prototype.tagAsDryRun = function(pageURL, reason, hash) {
+ var content = this.findContentType();
+ var inline = (this.url != undefined) ? false : true;
+ var url = (inline == false ? this.url : null);
+ this.element.setAttribute('data-librejs', 'dryrun');
+ this.element.setAttribute('data-librejs-reason', reason);
+
+ dryRunScripts.addAScript(
+ pageURL,
+ {'inline': inline,
+ 'contents': content,
+ 'reason': reason,
+ 'url': url,
+ 'hash': hash
+ });
+};
+
+/**
+ * removeNarcissusBugLine
+ *
+ * Removes the line that is appended to all
+ * inline scripts and prevent the bug that prevent
+ * script tags with comments only from being checked.
+ *
+ */
+Script.prototype.removeNarcissusBugLine = function(str) {
+ return str.replace('\n\nthis.narcissusBugFixLibreJS', '');
+};
+
+/**
+ * findContentType
+ *
+ * Figure out whether it's an external script,
+ * an inline script, or an attribute from the property
+ * that has been set, rather than blindly trusting the given
+ * constant.
+ */
+Script.prototype.findContentType = function() {
+ if (this.url != undefined) {
+ return "";
+ } else if (this.text != undefined) {
+ return this.element.text;
+ } else if (this.jsAttributes != undefined) {
+ // return the array.
+ return JSON.stringify(this.jsAttributes);
+ }
+};
+
+exports.Script = function(props) {
+ return new Script(props);
+};
diff --git a/lib/html_script_finder/dom_handler/script_properties.js b/lib/html_script_finder/dom_handler/script_properties.js
new file mode 100644
index 0000000..2eeeedb
--- /dev/null
+++ b/lib/html_script_finder/dom_handler/script_properties.js
@@ -0,0 +1,43 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+exports.scriptTypes = {
+ INLINE: 0,
+ EXTERNAL: 1,
+ ATTRIBUTE: 2,
+ SINGLETON: 3
+};
+
+exports.statusTypes = {
+ UNCHECKED: 0,
+ CHECKED: 1,
+ ACCEPTED: 2,
+ REJECTED: 3,
+ JSWEBLABEL: 4
+};
+
+exports.reasons = {
+ 'FUNCTIONS_INLINE': 'This script is detected as inline, nonfree, defining functions or methods, and the rest of the page as loading external scripts',
+ 'FUNCTIONS_EXTERNAL': 'This script is detected as nonfree, external, and as defining functions or methods',
+ 'CONSTRUCT': 'This script is detected as nonfree and as defining nontrivial constructs',
+ 'FREE': 'This script is detected as free',
+ 'TRIVIAL': 'This script is detected as trivial',
+ 'TRIVIAL_NOT_ALLOWED': 'This script is detected as trivial, but trivial is not allowed here because of other scripts'
+};
diff --git a/lib/html_script_finder/html_parser.js b/lib/html_script_finder/html_parser.js
new file mode 100644
index 0000000..69b2acc
--- /dev/null
+++ b/lib/html_script_finder/html_parser.js
@@ -0,0 +1,158 @@
+/*
+ # ***** BEGIN LICENSE BLOCK *****
+ # Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ #
+ # The contents of this file are subject to the Mozilla Public License Version
+ # 1.1 (the "License"); you may not use this file except in compliance with
+ # the License. You may obtain a copy of the License at
+ # http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS IS" basis,
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ # for the specific language governing rights and limitations under the
+ # License.
+ #
+ # The Original Code is Microsummarizer.
+ #
+ # The Initial Developer of the Original Code is Mozilla.
+ # Portions created by the Initial Developer are Copyright (C) 2006
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Myk Melez <myk@mozilla.org> (Original Author)
+ # Simon Bünzli <zeniko@gmail.com>
+ # Asaf Romano <mano@mozilla.com>
+ # Dan Mills <thunder@mozilla.com>
+ # Ryan Flint <rflint@dslr.net>
+ #
+ # Alternatively, the contents of this file may be used under the terms of
+ # either the GNU General Public License Version 2 or later (the "GPL"), or
+ # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ # in which case the provisions of the GPL or the LGPL are applicable instead
+ # of those above. If you wish to allow use of your version of this file only
+ # under the terms of either the GPL or the LGPL, and not to allow others to
+ # use your version of this file under the terms of the MPL, indicate your
+ # decision by deleting the provisions above and replace them with the notice
+ # and other provisions required by the GPL or the LGPL. If you do not delete
+ # the provisions above, a recipient may use your version of this file under
+ # the terms of any one of the MPL, the GPL or the LGPL.
+ #
+ # ***** END LICENSE BLOCK *****
+ */
+
+/*
+ * The original file is located here:
+ * http://mxr.mozilla.org/mozilla/source/browser/components/microsummaries/src/nsMicrosummaryService.js?raw=1
+ *
+ */
+
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * html_parser
+ *
+ * Takes in an http response (string), loads it into a secured iframe
+ * so that it can be manipulated as a DOM object. It then returns a
+ * modified string to be passed along as a replacement of the original
+ * response.
+ *
+ */
+
+var {Cc, Ci, Cu} = require("chrome");
+
+var domHandlerModule = require("html_script_finder/dom_handler");
+
+const PR_UINT32_MAX = 2147483647;
+
+
+exports.htmlParser = function () {
+
+ return {
+ charset: null,
+ htmlText: null,
+ pageURL: null,
+ fragment: null,
+ contentType: null,
+ responseStatus: null,
+
+ parse: function (htmlText, charset, contentType, url, fragment,
+ responseStatus, parseResult) {
+
+ // DOMParser still has too many issues.
+ this.htmlText = htmlText;
+ this.charset = charset;
+
+ if (this.charset === "" || this.charset === undefined) {
+ this.charset = "utf-8";
+ }
+ this.contentType = contentType;
+ this.pageURL = url;
+ this.fragment = fragment;
+ this.responseStatus = responseStatus;
+ var that = this;
+
+ var domParser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+
+ var dom = domParser.parseFromString(this.htmlText, this.contentType);
+ // console.debug(dom.getElementsByTagName('body')[0].innerHTML);
+ domHandlerModule.domHandler(dom, this.pageURL, this.fragment, this.responseStatus, function (newDom) {
+ parseResult(that.serializeToStream(newDom, that));
+ });
+
+ },
+
+ /**
+ * serializeToStream
+ * Serializes an HTML DOM into a binary stream. Uses
+ * nsIDOMSerializer only as a backup to when the
+ * reconstituteHtmlString method fails (not sure if/when it
+ * happens).
+ * @param dom obj Reference to the dom object
+ * @param that obj Reference to the object returned by htmlParser.
+ * This allows to give access to the iframe.
+ * @return a binary stream.
+ */
+ serializeToStream: function (dom, that) {
+
+ var newData, len;
+
+ try {
+ var storageStream = Cc["@mozilla.org/storagestream;1"].createInstance(Ci.nsIStorageStream);
+ var binaryOutputStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream);
+ var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].createInstance(Ci.nsIDOMSerializer);
+ var encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=" + this.contentType]
+ .createInstance(Ci.nsIDocumentEncoder);
+
+ encoder.setCharset(this.charset);
+ encoder.init(dom, this.contentType, 0);
+ storageStream.init(8192, PR_UINT32_MAX, null);
+
+ binaryOutputStream.setOutputStream(storageStream.getOutputStream(0));
+ encoder.encodeToStream(binaryOutputStream);
+ return storageStream;
+ } catch (e) {
+ console.debug('issue with serializer', e, e.lineNumber);
+ }
+ }
+
+ };
+};
diff --git a/lib/html_script_finder/url_seen_tester.js b/lib/html_script_finder/url_seen_tester.js
new file mode 100644
index 0000000..d2f38a9
--- /dev/null
+++ b/lib/html_script_finder/url_seen_tester.js
@@ -0,0 +1,78 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var httpRe = /^(http:)/i;
+var httpsRe = /^(https:)/i;
+
+exports.urlSeenTester = {
+ whitelist: {},
+
+ httpToHttps: function (url) {
+ try {
+
+ if (httpRe.test(url)) {
+
+ return url.replace(httpRe, 'https:');
+
+ } else if (httpsRe.test(url)) {
+
+ return url.replace(httpsRe, 'http:');
+
+ } else {
+
+ return url;
+
+ }
+ } catch (x) {
+ console.debug('error', x);
+ }
+ },
+
+ clearUrls: function () {
+ this.whitelist = {};
+ },
+
+ clearUrl: function (url) {
+ if (this.whitelist[url]) {
+
+ // console.debug('disallowing', url);
+ delete this.whitelist[url];
+ }
+ },
+
+ addUrl: function (url) {
+ console.debug('adding', url);
+
+ if (!this.isWhitelisted(url)) {
+
+ console.debug('allowing', url);
+ this.whitelist[url] = true;
+ }
+ },
+
+ isWhitelisted: function (url) {
+ if (this.whitelist[url] || this.whitelist[this.httpToHttps(url)]) {
+ console.debug('found to be whitelisted', url);
+ return true;
+ }
+ return false;
+ }
+
+};
diff --git a/lib/html_script_finder/web_labels/find_js_labels.js b/lib/html_script_finder/web_labels/find_js_labels.js
new file mode 100644
index 0000000..6404c49
--- /dev/null
+++ b/lib/html_script_finder/web_labels/find_js_labels.js
@@ -0,0 +1,131 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * This file works in conjunction with lib/html_script_finder/js_web_labels.js
+ * to find mentions of external JavaScript files and their license information.
+ * This allows the dom_handler to allow them by default.
+ */
+
+/**
+ * @param {Array} licenses - An array of html nodes.
+ *
+ * @return {Array} - An array of simple license objects.
+ */
+function getLicensesArrayFromElements(licenses) {
+ var a = [];
+ // Convert the html node into a simpler object
+ for (var i = 0; i < licenses.length; i++) {
+ a.push({
+ licenseName: licenses[i].textContent,
+ licenseUrl: licenses[i].href
+ });
+ }
+ return a;
+}
+
+/**
+ * @param {Array} sources - An array of html nodes.
+ *
+ * @return {Array} - An array of simple source objects.
+ */
+function getSourcesArrayFromElements(sources) {
+ var a = [];
+ for (var i = 0; i < sources.length; i++) {
+ a.push({
+ sourceName: sources[i].textContent,
+ sourceUrl: sources[i].href
+ });
+ }
+ return a;
+}
+
+// find table.
+exports.getLicenseList = function(document) {
+ var tbl = document.getElementById('jslicense-labels1');
+ var jsList = [];
+ var i = 0;
+ var le;
+ var rows;
+ var link;
+ var fileCell;
+ var licenseCell;
+ var sourceCell;
+ var row;
+
+ if (tbl) {
+ try {
+ rows = tbl.getElementsByTagName('tr');
+ le = rows.length;
+ var mockElem = {textContent: 'Unknown', href: 'Unknown'};
+ // loop through rows, and add each valid element to
+ // the array.
+ for (; i < le; i++) {
+ row = rows[i].getElementsByTagName('td');
+
+ // Find script url
+ if (row[0] && row[0].getElementsByTagName('a')[0]) {
+ fileCell = row[0].getElementsByTagName('a')[0];
+ } else {
+ fileCell = mockElem;
+ }
+
+ // 'licenses' and 'sources' will, for normal cases, just
+ // contain one element. If the fileCell is pointing to a
+ // combined JS file with multiple licenses, though, these
+ // arrays will contain multiple elements.
+
+ // Find license info
+ var licenses = [mockElem];
+ if (row[1] && row[1].getElementsByTagName('a').length > 0) {
+ licenses = getLicensesArrayFromElements(
+ row[1].getElementsByTagName('a'));
+ }
+
+ // Find original source info
+ var sources = [mockElem];
+ if (row[2] && row[2].getElementsByTagName('a').length > 0) {
+ sources = getSourcesArrayFromElements(
+ row[2].getElementsByTagName('a'));
+ }
+
+ if (fileCell.href !== 'Unknown') {
+ jsList.push({
+ 'fileName': fileCell.textContent,
+ 'fileUrl': fileCell.href,
+
+ // we'll fill this with value when needed to compare
+ // script.
+ 'fileHash': null,
+
+ 'licenses': licenses,
+ 'sources': sources
+ });
+ }
+ }
+ } catch (e) {
+ console.debug(
+ 'Error fetching JS Web Label licenses',
+ e, e.lineNumber, e.fileName, 'index is', i);
+ }
+ }
+
+ return jsList;
+};
diff --git a/lib/html_script_finder/web_labels/js_web_labels.js b/lib/html_script_finder/web_labels/js_web_labels.js
new file mode 100644
index 0000000..b3fe063
--- /dev/null
+++ b/lib/html_script_finder/web_labels/js_web_labels.js
@@ -0,0 +1,279 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// node.js url module. Makes it easier to resolve
+// urls in that datauri loaded dom
+var urlHandler = require("url_handler/url_handler");
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+var data = require("sdk/self").data;
+
+// license definitions, we are using canonical urls and license
+// identifiers.
+var licenses = require('js_checker/license_definitions').licenses;
+
+var getLicenseList = require('html_script_finder/web_labels/find_js_labels')
+ .getLicenseList;
+const types = require("js_checker/constant_types");
+
+const addToCache = require("html_script_finder/web_labels/script_hash_worker")
+ .addToCache;
+
+// keep web labels in memory so that they can be checked even when they
+// are embedded dynamically.
+var jsWebLabelEntries = {};
+
+// store the url to js web labels already visited during this session
+var jsLabelsPagesVisited = {};
+
+var WebLabelFinder = function () {
+ this.dom = null;
+ this.pageURL = null;
+ this.jslicenseURL = null;
+ this.pageContent = null;
+ this.licenseList = null;
+ this.callback = null;
+};
+
+WebLabelFinder.prototype.init = function(dom, pageURL, callback) {
+ var that = this;
+ this.pageURL = pageURL;
+ this.dom = dom;
+ this.callback = function (a) {
+ if (typeof a === 'undefined') {
+ a = null;
+ }
+
+ // rewrite callback as soon as it is triggered once.
+ that.callback = function () {
+ console.debug("Callback already called");
+ };
+
+ callback(a);
+ };
+ this.findJavaScriptLicenses();
+ this.pageContent = '';
+ this.jslicenseURL = '';
+};
+
+WebLabelFinder.prototype.findJavaScriptLicenses = function () {
+ this.searchForJsLink();
+
+ if (this.jslicenseURL && !(jsLabelsPagesVisited[this.jslicenseURL])) {
+ // get content from license page.
+ console.debug('called fetch license page for', this.jslicenseURL);
+ this.pageContent = this.fetchLicensePage();
+ } else {
+ console.debug(this.jslicenseURL, "already visited");
+ this.callback();
+ }
+};
+
+WebLabelFinder.prototype.searchForJsLink = function() {
+ console.debug('triggered searchForJsLink');
+ if (this.dom) {
+ var linkTags = this.dom.getElementsByTagName('a'),
+ i = 0,
+ len = linkTags.length,
+ path;
+
+ // loop through all a tags.
+ for (; i < len; i++) {
+ if (
+ (linkTags[i].hasAttribute('rel') &&
+ linkTags[i].getAttribute('rel') === 'jslicense') ||
+ (linkTags[i].hasAttribute('data-jslicense') &&
+ linkTags[i].getAttribute('data-jslicense') === '1')
+ ) {
+ // This page has a web labels link
+ return this.formatURL(linkTags[i]);
+ }
+ }
+ }
+
+ // no js web labels were found. call back.
+ this.callback();
+ return false;
+};
+
+WebLabelFinder.prototype.formatURL = function(link) {
+ this.jslicenseURL = urlHandler.resolve(this.pageURL, link.href);
+ this.jslicenseURL = urlHandler.addFragment(this.jslicenseURL, 'librejs=true');
+ console.debug('license URL found', this.jslicenseURL);
+ return this.jslicenseURL;
+};
+
+WebLabelFinder.prototype.fetchLicensePage = function() {
+ var that = this;
+ try {
+ var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
+
+ req.onload = function() {
+ console.debug("Fetching License!");
+ console.debug("URL is ", this._url);
+
+ that.licenseList = getLicenseList(this.responseXML);
+ console.debug("the license list", that.licenseList);
+ that.matchListWithDefs(this._url);
+
+ // add these entries to the global
+ // object for dynamically embedded scripts.
+ jsWebLabelEntries[that.pageURL] = that.licenseList;
+ jsLabelsPagesVisited[req._url] = 1;
+ };
+ console.debug(this.jslicenseURL);
+ req.open('GET', this.jslicenseURL, true);
+ req._url = this.jslicenseURL;
+ req.responseType = "document";
+ req.send();
+ } catch (e) {
+ console.debug(e, e.lineNumber, e.fileName, this.jslicenseURL);
+ this.callback({});
+ }
+};
+
+/**
+ * @method isLicenseFree
+ * Returns true if the given web labels row refers to a script that
+ * can be executed by LibreJS.
+ *
+ * This method has some side effects :-/
+ *
+ * @param {Object} lic - A license node from a JS web labels page. It's
+ * expected to contain one or more licenses.
+ * @return {Boolean}
+ */
+WebLabelFinder.prototype.isLicenseFree = function(
+ lic, jslicenseURL, callback
+) {
+ // For each license that this license row contains.
+ var isFree = false;
+ // licenseStatuses is later used to determine isFree.
+ var licenseStatuses = [];
+
+ for (var i = 0; i < lic.licenses.length; i++) {
+ var license;
+ var found = false;
+
+ // For each license from the internal license definitions
+ for (license in licenses) {
+ if (found === true) {
+ break;
+ }
+ var licDef = licenses[license];
+ var licArray = [];
+ if (!licDef.canonicalUrl) {
+ continue;
+ }
+ if (typeof licDef.canonicalUrl === 'string') {
+ licArray = [licDef.canonicalUrl];
+ } else {
+ licArray = licDef.canonicalUrl;
+ }
+
+ // For each of the canonical URLs recognized by this license
+ // definition
+ for (var j = 0; j < licArray.length; j++) {
+ if (urlHandler.removeFragment(licArray[j]) ===
+ urlHandler.removeFragment(lic.licenses[i].licenseUrl)
+ ) {
+ if (!require("sdk/url").isValidURI(lic.fileUrl)) {
+ console.debug(lic.fileUrl, " is not a valid URL");
+ callback();
+ }
+
+ // This license was recognized, and it was free. Add it
+ // to the array of license status, which we'll look at
+ // when we're done with this web label row.
+ licenseStatuses.push(true);
+
+ console.debug("about TO ADD TO XHR: ", lic.fileUrl);
+ this.listCheck[lic.fileUrl] = 0;
+ addToCache(lic, 0, jslicenseURL, callback);
+
+ // Break out of the nearest two loops cause we found
+ // a matching license
+ found = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // Tally up the licenses we were able to match.
+ if (licenseStatuses.length > 0 &&
+ // If the number of licenses we matched is at least one, and
+ // it's the same number as the number of licenses in this Web
+ // Label column, only then can we recognize this script as free.
+ // licenseStatus.length should never be larger than
+ // lic.licenses.length.
+ licenseStatuses.length >= lic.licenses.length
+ ) {
+ isFree = true;
+ }
+
+ return isFree;
+};
+
+WebLabelFinder.prototype.matchListWithDefs = function(jslicenseURL) {
+ var that = this;
+ var licDef,
+ license, script;
+ var cacheCalls = 0;
+ this.listCheck = {};
+
+ // nested loop.
+ cacheCalls = 0;
+ var callback = function (url) {
+ cacheCalls++;
+ that.listCheck[url] = 1;
+ if (cacheCalls === Object.keys(that.listCheck).length) {
+ console.debug("triggering callback duh");
+ // return array to requester object
+ callback = false;
+ that.callback(that.licenseList);
+ }
+ };
+ require("sdk/timers").setTimeout(function () {
+ // callback after 60 seconds if it's still not returned.
+ // using this as a safeguard.
+ // return array to requester object
+ if (callback !== false) {
+ that.callback(that.licenseList);
+ console.debug(that.listCheck);
+ }
+ }, 15000);
+
+
+
+ for (var i = 0; i < this.licenseList.length; i++) {
+ // this.licenseList[i] is the web labels license column
+ var lic = this.licenseList[i];
+ if (this.isLicenseFree(lic, jslicenseURL, callback)) {
+ lic.free = true;
+ }
+ }
+};
+
+exports.WebLabelFinder = WebLabelFinder;
+
+// Store the web labels harvested across webpages (only in this session).
+exports.jsWebLabelEntries = jsWebLabelEntries;
+
+exports.jsLabelsPagesVisited = jsLabelsPagesVisited;
diff --git a/lib/html_script_finder/web_labels/script_hash_worker.js b/lib/html_script_finder/web_labels/script_hash_worker.js
new file mode 100644
index 0000000..6d8d837
--- /dev/null
+++ b/lib/html_script_finder/web_labels/script_hash_worker.js
@@ -0,0 +1,62 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+const types = require("js_checker/constant_types");
+const scriptsCached = require("script_entries/scripts_cache").scriptsCached;
+const xhr = require('html_script_finder/dom_handler/dom_checker').xhr;
+const timers = require("sdk/timers");
+
+exports.addToCache = function (lic, delay, jsWebLabelsURL, callback) {
+ console.debug("jslicenseURL is", jsWebLabelsURL);
+ if (typeof delay === 'undefined') {
+ delay = 0;
+ }
+
+ // get file hash and store as cached.
+ console.debug('performing xhr for', lic.fileUrl);
+ timers.setTimeout(function() {
+ var cb = function (script, contents) {
+ try {
+ // add a cache entry.
+ var hash = scriptsCached.addEntryIfNotCached(
+ contents,
+ types.freeWithComment(
+ 'This script is free according to a JS Web Labels ' +
+ 'page visited recently (at ' +
+ jsWebLabelsURL.replace("librejs=true", "") + ' )'
+ ),
+ {},
+ true,
+ lic.fileUrl
+ );
+ console.debug('returning xhr from', lic.fileUrl);
+ callback(lic.fileUrl);
+ } catch (e) {
+ callback(lic.fileUrl);
+ }
+ };
+ // just callback after 5 seconds if we don't get the answer yet.
+ timers.setTimeout(function() {
+ cb = function() {};
+ callback(lic.fileUrl); }, 20000);
+
+ xhr({'url': lic.fileUrl}, cb);
+ }, delay);
+};
diff --git a/lib/http_observer/allowed_referrers.js b/lib/http_observer/allowed_referrers.js
new file mode 100644
index 0000000..797284b
--- /dev/null
+++ b/lib/http_observer/allowed_referrers.js
@@ -0,0 +1,67 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var prefChange = require("addon_management/prefchange");
+
+var allowed = {};
+
+/**
+ * Contains a list of pages that are allowed
+ * to execute JavaScript regardless of whether it is
+ * nonfree and nontrivial.
+ */
+var AllowedReferrers = function() {
+};
+
+AllowedReferrers.prototype.addPage = function(url) {
+ allowed[url] = 1;
+};
+
+AllowedReferrers.prototype.urlInAllowedReferrers = function (url) {
+ if (allowed[url] === 1) {
+ return true;
+ }
+ // check if whitelisted.
+ return this.urlInWhitelist(url);
+};
+
+AllowedReferrers.prototype.urlInWhitelist = function(url) {
+ var whitelist = prefChange.getWhitelist();
+ var i = 0, le = whitelist.length;
+ for (; i < le; i++) {
+ if (whitelist[i].test(url)) {
+ return true;
+ }
+ }
+};
+
+AllowedReferrers.prototype.clearSinglePageEntry = function(url) {
+ var index = allowed[url];
+
+ if (allowed[url] === 1) {
+ delete allowed[url];
+ }
+};
+
+AllowedReferrers.prototype.clearAllEntries = function() {
+ allowed = {};
+};
+
+exports.allowedReferrers = new AllowedReferrers();
diff --git a/lib/http_observer/caching.js b/lib/http_observer/caching.js
new file mode 100644
index 0000000..1463b1c
--- /dev/null
+++ b/lib/http_observer/caching.js
@@ -0,0 +1,30 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+const nsICacheService = Ci.nsICacheService;
+const cacheService = Cc["@mozilla.org/network/cache-service;1"].getService(nsICacheService);
+
+exports.clearAllCache = function () {
+ cacheService.evictEntries(Ci.nsICache.STORE_ON_DISK);
+ cacheService.evictEntries(Ci.nsICache.STORE_IN_MEMORY);
+};
+
diff --git a/lib/http_observer/http_request_observer.js b/lib/http_observer/http_request_observer.js
new file mode 100644
index 0000000..3b24aed
--- /dev/null
+++ b/lib/http_observer/http_request_observer.js
@@ -0,0 +1,146 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+var observerService = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+
+// these are our target mime types for response interception.
+var targetMimeTypes = /.*(javascript|ecmascript|html).*/i;
+//var targetMimeTypes = /.*(html).*/i;
+
+// ensure xhr won't create an infinite loop
+// with html content.
+var urlTester = require("html_script_finder/url_seen_tester").urlSeenTester;
+var streamLoader = require("http_observer/stream_loader").streamLoader;
+
+var httpRequestObserver = {
+ observe: function(request, aTopic, aData) {
+ console.debug('atopic is', aTopic);
+ var url, newListener, status;
+
+ if (aTopic === "http-on-examine-response" ||
+ aTopic === "http-on-examine-cached-response" ||
+ aTopic === "http-on-examine-merged-response"
+ ) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+
+ if (request.URI.scheme !== 'chrome' &&
+ (request.responseStatus < 300 ||
+ request.responseStatus > 399) &&
+ (targetMimeTypes.test(request.contentType) ||
+ request.contentType === undefined) &&
+ (!urlTester.isWhitelisted(request.URI.spec) &&
+ !urlTester.isWhitelisted(request.originalURI.spec))
+ ) {
+ newListener = new TracingListener();
+ request.QueryInterface(Ci.nsITraceableChannel);
+ newListener.originalListener = request.setNewListener(newListener);
+ } else if (urlTester.isWhitelisted(request.URI.spec) ||
+ urlTester.isWhitelisted(request.originalURI.spec)
+ ) {
+ urlTester.clearUrl(request.URI.spec);
+ urlTester.clearUrl(request.originalURI.spec);
+ }
+
+ }
+ },
+
+ QueryInterface: function (aIID) {
+ if (aIID.equals(Ci.nsIObserver) ||
+ aIID.equals(Ci.nsISupports)
+ ) {
+ return this;
+ }
+ throw Cr.NS_NOINTERFACE;
+ }
+};
+
+// Copy response listener implementation.
+function TracingListener() {
+ this.originalListener = null;
+ this.streamLoader = streamLoader();
+}
+
+TracingListener.prototype = {
+ onDataAvailable: function(request, context, inputStream, offset, count) {
+ try {
+ this.streamLoader.loader.onDataAvailable(
+ request, context, inputStream, offset, count);
+ } catch (x) {
+ console.debug(x, x.lineNumber, x.fileName, "In this case, charset is");
+ }
+ },
+
+ onStartRequest: function(request, context) {
+ this.streamLoader.setOriginalListener(this.originalListener);
+ this.streamLoader.loader.onStartRequest(request, context);
+ this.originalListener.onStartRequest(request, context);
+ },
+
+ onStopRequest: function(request, context, statusCode) {
+ try {
+ this.streamLoader.loader.onStopRequest(request, context, statusCode);
+ } catch (e) {
+ console.debug('error in onStopRequest', e, e.lineNumber);
+ }
+ },
+
+ QueryInterface: function (aIID) {
+ if (aIID.equals(Ci.nsIStreamListener) ||
+ aIID.equals(Ci.nsISupports)
+ ) {
+ return this;
+ }
+ throw Cr.NS_NOINTERFACE;
+ },
+};
+
+exports.startHttpObserver = function() {
+ try {
+ observerService.addObserver(httpRequestObserver,
+ "http-on-examine-response", false);
+ observerService.addObserver(httpRequestObserver,
+ "http-on-examine-cached-response", false);
+ observerService.addObserver(httpRequestObserver,
+ "http-on-examine-merged-response", false);
+ console.debug('turned on http observer');
+ } catch (e) {
+ console.debug(e);
+ }
+};
+
+exports.startHttpObserver();
+
+/* remove observer */
+exports.removeHttpObserver = function() {
+ try {
+ observerService.removeObserver(httpRequestObserver,
+ "http-on-examine-response");
+ observerService.removeObserver(httpRequestObserver,
+ "http-on-examine-cached-response");
+ observerService.removeObserver(httpRequestObserver,
+ "http-on-examine-merged-response");
+ console.debug('turned off http observer');
+ } catch (e) {
+ console.debug(e);
+ }
+};
diff --git a/lib/http_observer/process_response.js b/lib/http_observer/process_response.js
new file mode 100644
index 0000000..b407ef6
--- /dev/null
+++ b/lib/http_observer/process_response.js
@@ -0,0 +1,429 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * This module checks http responses by mime type and returns a
+ * modified response.
+ */
+
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+var jsChecker = require("js_checker/js_checker");
+
+const types = require("js_checker/constant_types");
+var checkTypes = types.checkTypes;
+
+// check if scripts embedded dynamically have a jsWebLabel entry indexed by referrer.
+var jsWebLabelEntries = require("html_script_finder/web_labels/js_web_labels").jsWebLabelEntries;
+
+var htmlParser = require("html_script_finder/html_parser");
+
+var removedScripts = require("script_entries/removed_scripts").removedScripts;
+var allowedRef = require('http_observer/allowed_referrers').allowedReferrers;
+
+var acceptedScripts = require("script_entries/accepted_scripts").acceptedScripts;
+
+// used to display info when a url is whitelisted.
+var dryRunScripts = require("script_entries/dryrun_scripts").dryRunScripts;
+
+// node.js url module. Makes it easier to resolve
+// urls in that datauri loaded dom
+var urlHandler = require("url_handler/url_handler");
+var isDryRun = require("addon_management/prefchange").isDryRun;
+
+var jsMimeTypeRe = /.*(javascript|ecmascript).*/i;
+var htmlMimeTypeRe = /.*(xhtml\+xml|html|multipart\/x-mixed-replace).*/i;
+
+
+var processResponseObject = {
+ data: null,
+ myParser: null,
+ url: null,
+ scriptFinder: null,
+ jsCheckString: null,
+ referrer: null,
+ contentType: null,
+ resInfo: null,
+ listener: null,
+ req: null,
+
+ /**
+ * starts the handling of a new response.
+ */
+ init: function (listener, resInfo) {
+ this.resInfo = resInfo;
+ this.req = resInfo.request;
+ /* needed for this.req.referrer */
+ this.req.QueryInterface(Ci.nsIHttpChannel);
+ this.listener = listener;
+ this.setData();
+ this.setContentType();
+ this.setUrls();
+ },
+
+ /**
+ * genBinaryOutput
+ * Set or reset binaryOutputStream and storageStream.
+ */
+ genBinaryOutput: function () {
+ this.storageStream = Cc["@mozilla.org/storagestream;1"]
+ .createInstance(Ci.nsIStorageStream);
+ this.binaryOutputStream = Cc["@mozilla.org/binaryoutputstream;1"]
+ .createInstance(Ci.nsIBinaryOutputStream);
+ },
+
+ /**
+ * Gather the data gathered from onDataAvailable.
+ */
+ setData: function () {
+ this.data = this.resInfo.receivedData;
+ //console.debug("\n\nDump of whole data:\n\n", this.data, "\n\n end of dump");
+ // Prevents the http response body from being empty,
+ // which would throw an error.
+ if (this.data == '' || this.data == undefined) {
+ this.data = " ";
+ }
+ },
+
+ /**
+ * Set a standardized lowercase mime type.
+ */
+ setContentType: function() {
+ if (this.req.contentType != undefined) {
+ this.contentType = String(this.req.contentType).toLowerCase();
+ }
+ },
+
+ /**
+ * setUrls
+ * Set the current URL of the response, and
+ * set referrer if applicable.
+ */
+ setUrls: function() {
+
+ if (this.req.URI != undefined) {
+ this.fragment = urlHandler.getFragment(this.req.URI.spec);
+ console.debug('fragment is', this.fragment);
+ this.url = urlHandler.removeFragment(this.req.URI.spec);
+ }
+ if (this.req.referrer != undefined) {
+ this.referrerFragment = urlHandler.getFragment(this.req.referrer.spec);
+ this.referrer = urlHandler.removeFragment(this.req.referrer.spec);
+ }
+ },
+
+ /**
+ * processHTML
+ * Modifies a string of html
+ */
+ processHTML: function() {
+ var charset = this.req.contentCharset, myParser;
+
+ if (this.req.contentCharset != undefined && this.req.contentCharset != "") {
+ charset = this.req.contentCharset;
+ } else {
+ charset = "";
+ }
+ acceptedScripts.clearScripts(this.req.URI.spec);
+ removedScripts.clearScripts(this.req.URI.spec);
+ dryRunScripts.clearScripts(this.req.URI.spec);
+
+ console.debug('charset is', charset);
+ console.debug(
+ 'responseStatus for', this.url, 'is', this.req.responseStatus);
+
+ // send data to htmlParser, and pass on modified data to
+ // originalListener.
+
+ myParser = htmlParser.htmlParser().parse(
+ this.data,
+ charset,
+ this.contentType,
+ this.url,
+ this.fragment,
+ this.req.responseStatus,
+ this.htmlParseCallback.bind(this));
+ },
+
+ /**
+ *
+ * htmlParseCallback
+ *
+ * Passed on the callback result to
+ * the originalListener.
+ *
+ */
+ htmlParseCallback: function(result) {
+
+ var len = result.length;
+
+ try {
+
+ this.listener.onDataAvailable(
+ this.req,
+ this.resInfo.context,
+ result.newInputStream(0), 0, len);
+
+
+ } catch (e) {
+
+ this.req.cancel(this.req.NS_BINDING_ABORTED);
+
+ }
+
+ this.listener.onStopRequest(
+ this.req,
+ this.resInfo.context, this.resInfo.statusCode);
+
+ },
+
+ /**
+ * processJS
+ * Process and modify a string of JavaScript.
+ */
+ processJS: function() {
+ var checker, check, jsCheckString,
+ that = this;
+ //var start = Date.now(), end;
+
+ try {
+ // make sure script isn't already listed as free
+ // in a JS web labels table.
+ if (this.checkJsWebLabelsForScript()) {
+ // this is free. we are done.
+ this.jsListenerCallback();
+ return;
+
+ }
+
+ // analyze javascript in response.
+ checker = jsChecker.jsChecker();
+ check = checker.searchJs(this.data, function () {
+ //console.debug("Has been analyzing", that.data);
+ that.processJsCallback(checker);
+ }, that.url);
+
+
+
+ } catch(e) {
+
+ // any error is considered nontrivial.
+ console.debug('js error in js app, removing script', e);
+ console.debug("error", e, e.lineNumber);
+ // modify data that will be sent to the browser.
+ this.data = '// LibreJS: Script contents were removed when it was loaded from a page, because another script attempted to load this one dynamically. Please place your cursor in the url bar and press the enter key to see the source.';
+ this.jsListenerCallback();
+ }
+
+ },
+
+ /**
+ * checkJsWebLabelsForScript
+ *
+ * check whether script that's been received has an entry
+ * in a js web labels table (lookup referrer.)
+ *
+ */
+ checkJsWebLabelsForScript: function () {
+
+ console.debug('checking script', this.url);
+ console.debug('current list is', JSON.stringify(jsWebLabelEntries));
+ if (jsWebLabelEntries[this.referrer] != undefined) {
+
+ var scriptList = jsWebLabelEntries[this.referrer],
+ i = 0,
+ len = scriptList.length;
+
+ for (; i < len; i++) {
+
+ if (scriptList[i].fileUrl === this.url &&
+ scriptList[i].free === true) {
+
+ console.debug(this.url, "is free and dynamic!");
+
+ var scriptObj = {
+ inline: false,
+ url: this.url,
+ contents: this.url,
+ reason: "This script is free (see JS Web Labels page for detail)"
+ };
+
+ acceptedScripts.addAScript(
+ this.req.referrer.spec, scriptObj, "Script is free");
+
+ return true;
+
+ }
+
+ }
+ }
+ },
+
+ processJsCallback: function(checker) {
+ try {
+ var scriptObj;
+
+ var jsCheckString = checker.parseTree.freeTrivialCheck;
+ console.debug("analyzing js callback for", this.url);
+ // for testing only.
+ //var jsCheckString = {'type': checkTypes.FREE_SINGLE_ITEM };
+ console.debug('jscheckstring is', jsCheckString.type);
+
+ if (jsCheckString.type === checkTypes.NONTRIVIAL) {
+ if (!allowedRef.urlInAllowedReferrers(this.req.referrer.spec)) {
+ //if (true) {
+ console.debug(
+ "url",
+ this.url,
+ " is found nontrivial",
+ "with reason",
+ jsCheckString.reason);
+ scriptObj = {
+ inline: false,
+ contents: '',
+ removalReason: 'nontrivial',
+ reason: jsCheckString.reason,
+ url: this.url,
+ hash: checker.hash
+ };
+ removedScripts.addAScript(this.req.referrer.spec, scriptObj);
+
+ // modify data that will be sent to the browser.
+ this.data = '// LibreJS: Script contents were removed when it was loaded from a page, because another script attempted to load this one dynamically and its contents appear to be nonfree/nontrivial. Please hit enter in the location bar to see the actual source.';
+ } else {
+ console.debug("writing to dry run", this.url);
+ scriptObj = {
+ inline:false,
+ contents: '',
+ removalReason: 'nontrivial',
+ reason: jsCheckString.reason,
+ url: this.url,
+ hash:checker.hash
+ };
+ dryRunScripts.addAScript(this.req.referrer.spec, scriptObj);
+ }
+
+ this.jsListenerCallback();
+
+ } else if (jsCheckString.type === checkTypes.FREE ||
+ jsCheckString.type === checkTypes.FREE_SINGLE_ITEM ||
+ jsCheckString.type === checkTypes.TRIVIAL ||
+ jsCheckString.type === checkTypes.TRIVIAL_DEFINES_FUNCTION ||
+ jsCheckString.type === checkTypes.WHITELISTED) {
+ console.debug(
+ "found a free script for",
+ this.url,
+ this.req.referrer.spec,
+ jsCheckString.reason);
+ console.debug('found a free script', this.req.referrer.spec);
+
+ scriptObj = {inline: false,
+ contents: '',
+ reason: jsCheckString.reason,
+ url: this.url,
+ hash:checker.hash};
+
+ acceptedScripts.addAScript(this.req.referrer.spec, scriptObj);
+ this.jsListenerCallback();
+ }
+
+ //var end = Date.now();
+ console.debug('exec time', this.url, ' -- ', end - start);
+ } catch (x) {
+ console.debug('error', x);
+ }
+ },
+
+ /**
+ * ProcessAllTypes
+ * Calls processHTML or JS if it finds an appropriate content
+ * type. For everything else it just passes on the data to the
+ * original listener.
+ */
+ processAllTypes: function() {
+ // toggle xlibrejs if X-LibreJS is set.
+
+ // process HTML
+ if ((htmlMimeTypeRe.test(this.contentType) ||
+ this.req.contentType === undefined)) {
+ this.processHTML();
+ return;
+ }
+
+ else {
+ // process external JS files that are called from another
+ // file (and hence have a referrer).
+
+ if (this.referrer != undefined &&
+ jsMimeTypeRe.test(this.contentType) &&
+ !(acceptedScripts.isFound(this.referrer, {
+ inline: false, contents: this.url})) &&
+ !(acceptedScripts.isFound(this.referrer, {
+ inline:false, contents:this.req.originalURI.spec}))) {
+
+ // console.debug('process js triggered for', this.url);
+ this.processJS();
+
+ } else {
+ this.jsListenerCallback();
+ }
+
+ }
+
+ },
+
+ jsListenerCallback: function () {
+
+ var len = this.data.length;
+
+ this.genBinaryOutput();
+
+ this.storageStream.init(8192, len, null);
+ this.binaryOutputStream.setOutputStream(
+ this.storageStream.getOutputStream(0));
+ this.binaryOutputStream.writeBytes(this.data, len);
+
+ try {
+ this.listener.onDataAvailable(
+ this.req,
+ this.resInfo.context,
+ this.storageStream.newInputStream(0),
+ 0, len);
+ } catch (e) {
+ this.req.cancel(this.req.NS_BINDING_ABORTED);
+ }
+
+ this.listener.onStopRequest(
+ this.req,
+ this.resInfo.context,
+ this.resInfo.statusCode);
+
+ }
+
+
+};
+
+// creates an instance of processResponseObject.
+exports.ProcessResponse = function (listener, resInfo) {
+ console.debug('triggered');
+ var procResponse = Object.create(processResponseObject);
+ procResponse.init(listener, resInfo);
+ return procResponse;
+};
diff --git a/lib/http_observer/stream_loader.js b/lib/http_observer/stream_loader.js
new file mode 100644
index 0000000..db136db
--- /dev/null
+++ b/lib/http_observer/stream_loader.js
@@ -0,0 +1,111 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+const processResponse = require('http_observer/process_response');
+
+var StreamLoader = function() {
+ this.loader = null;
+ this.listener = null;
+ this.originalListener = null;
+};
+
+StreamLoader.prototype.setOriginalListener = function(listener) {
+ this.originalListener = listener;
+};
+
+StreamLoader.prototype.init = function() {
+ try {
+ var that = this;
+ this.listener = new StreamListener();
+
+ this.listener.callback = function (loader, context, status, data) {
+ //console.debug("here is the data", data);
+ var responseInfo = {'request': loader.channel,
+ 'context': context,
+ 'statusCode': status,
+ 'receivedData': data};
+ var responseHandler = processResponse.ProcessResponse(that.originalListener, responseInfo);
+ responseHandler.processAllTypes();
+
+ that.destroy();
+ };
+
+ this.loader = Cc["@mozilla.org/network/unichar-stream-loader;1"].
+ createInstance(Ci.nsIUnicharStreamLoader);
+
+ this.loader.init(this.listener);
+ } catch (e) {
+ console.debug(e);
+ }
+};
+
+StreamLoader.prototype.destroy = function () {
+ this.loader = null;
+ this.listener = null;
+};
+
+var getRegexForContentType = function (contentType) {
+ if (/xhtml/i.test(contentType)) {
+ return /<\?[^>]*?encoding=(?:["']*)([^"'\s\?>]+)(?:["']*)/i;
+ }
+
+ // return the regular html regexp for anything else.
+ return /<meta[^>]*?charset=(?:["']*)([^"'\s>]+)(?:["']*)/i;
+};
+
+var StreamListener = function() {};
+
+StreamListener.prototype.QueryInterface = function listener_qi(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIUnicharStreamLoaderObserver)) {
+ return this;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+};
+
+StreamListener.prototype.onStreamComplete = function onStreamComplete(
+ loader, context, status, data) {
+ this.callback(loader, context, status, data);
+};
+
+StreamListener.prototype.onDetermineCharset = function onDetermineCharset(
+ loader, context, data) {
+ var match, regex;
+ if (loader.channel.contentCharset !== undefined &&
+ loader.channel.contentCharset !== ""
+ ) {
+ return loader.channel.contentCharset;
+ } else {
+ match = getRegexForContentType(loader.channel.contentType).exec(data);
+ if (match) {
+ loader.channel.contentCharset = match[1];
+ return match[1];
+ } else {
+ return "UTF-8";
+ }
+ }
+};
+
+exports.streamLoader = function () {
+ var l = new StreamLoader();
+ l.init();
+ return l;
+};
diff --git a/lib/js_checker/constant_types.js b/lib/js_checker/constant_types.js
new file mode 100644
index 0000000..59e7489
--- /dev/null
+++ b/lib/js_checker/constant_types.js
@@ -0,0 +1,190 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+//var debug = require("debug/debug");
+
+// token list from exports.init();
+exports.token = {
+ END: 0,
+ NEWLINE: 1,
+ SEMICOLON: 2,
+ COMMA: 3,
+ ASSIGN: 4,
+ HOOK: 5,
+ COLON: 6,
+ CONDITIONAL: 7,
+ OR: 8,
+ AND: 9,
+ BITWISE_OR: 10,
+ BITWISE_XOR: 11,
+ BITWISE_AND: 12,
+ EQ: 13,
+ NE: 14,
+ STRICT_EQ: 15,
+ STRICT_NE: 16,
+ LT: 17,
+ LE: 18,
+ GE: 19,
+ GT: 20,
+ LSH: 21,
+ RSH: 22,
+ URSH: 23,
+ PLUS: 24,
+ MINUS: 25,
+ MUL: 26,
+ DIV: 27,
+ MOD: 28,
+ NOT: 29,
+ BITWISE_NOT: 30,
+ UNARY_PLUS: 31,
+ UNARY_MINUS: 32,
+ INCREMENT: 33,
+ DECREMENT: 34,
+ DOT: 35,
+ LEFT_BRACKET: 36,
+ RIGHT_BRACKET: 37,
+ LEFT_CURLY: 38,
+ RIGHT_CURLY: 39,
+ LEFT_PAREN: 40,
+ RIGHT_PAREN: 41,
+ SCRIPT: 42,
+ BLOCK: 43,
+ LABEL: 44,
+ FOR_IN: 45,
+ CALL: 46,
+ NEW_WITH_ARGS: 47,
+ INDEX: 48,
+ ARRAY_INIT: 49,
+ OBJECT_INIT: 50,
+ PROPERTY_INIT: 51,
+ GETTER: 52,
+ SETTER: 53,
+ GROUP: 54,
+ LIST: 55,
+ LET_BLOCK: 56,
+ ARRAY_COMP: 57,
+ GENERATOR: 58,
+ COMP_TAIL: 59,
+ IDENTIFIER: 60,
+ NUMBER: 61,
+ STRING: 62,
+ REGEXP: 63,
+ BREAK: 64,
+ CASE: 65,
+ CATCH: 66,
+ CONST: 67,
+ CONTINUE: 68,
+ DEBUGGER: 69,
+ DEFAULT: 70,
+ DELETE: 71,
+ DO: 72,
+ ELSE: 73,
+ EXPORT: 74,
+ FALSE: 75,
+ FINALLY: 76,
+ FOR: 77,
+ FUNCTION: 78,
+ IF: 79,
+ IMPORT: 80,
+ IN: 81,
+ INSTANCEOF: 82,
+ LET: 83,
+ MODULE: 84,
+ NEW: 85,
+ NULL: 86,
+ RETURN: 87,
+ SWITCH: 88,
+ THIS: 89,
+ THROW: 90,
+ TRUE: 91,
+ TRY: 92,
+ TYPEOF: 93,
+ VAR: 94,
+ VOID: 95,
+ YIELD: 96,
+ WHILE: 97,
+ WITH: 98
+};
+
+var checkTypes = {
+ // trivial.
+ TRIVIAL: 1,
+
+ // defines functions, and so might or might not
+ // be trivial in the end.
+ TRIVIAL_DEFINES_FUNCTION: 2,
+
+ NONTRIVIAL: 3,
+
+ // Free
+ FREE: 4,
+ FREE_SINGLE_ITEM: 5,
+ WHITELISTED: 6
+};
+
+exports.checkTypes = checkTypes;
+
+exports.emptyTypeObj = function() {
+ return {
+ 'type': null,
+ 'reason': null
+ };
+};
+
+exports.nontrivialWithComment = function(comment) {
+ return {
+ 'type': checkTypes.NONTRIVIAL,
+ 'reason': comment
+ };
+};
+
+exports.trivialWithComment = function(comment) {
+ return {
+ 'type': checkTypes.TRIVIAL,
+ 'reason': comment
+ };
+};
+
+exports.trivialFuncWithComment = function(comment) {
+ return {
+ 'type': checkTypes.TRIVIAL_DEFINES_FUNCTION,
+ 'reason': comment
+ };
+};
+
+exports.freeWithComment = function(comment) {
+ return {
+ 'type': checkTypes.FREE,
+ 'reason': comment
+ };
+};
+
+exports.singleFreeWithComment = function(comment) {
+ return {
+ 'type': checkTypes.FREE_SINGLE_ITEM,
+ 'reason': comment
+ };
+};
+
+exports.whitelisted = function(comment) {
+ return {
+ 'type': checkTypes.WHITELISTED,
+ 'reason': comment + ' -- whitelisted by user'
+ };
+};
diff --git a/lib/js_checker/free_checker.js b/lib/js_checker/free_checker.js
new file mode 100644
index 0000000..9aaa262
--- /dev/null
+++ b/lib/js_checker/free_checker.js
@@ -0,0 +1,231 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var licenses = require('js_checker/license_definitions');
+var simpleStorage = require("sdk/simple-storage");
+const LAZY = licenses.types.LAZY;
+var licenseRegex = [];
+const END_OF_SCRIPT = require('html_script_finder/bug_fix').END_OF_SCRIPT;
+const types = require("js_checker/constant_types");
+
+const token = types.token;
+
+var patternUtils = require('js_checker/pattern_utils').patternUtils;
+
+var licStartLicEndRe = /@licstartThefollowingistheentirelicensenoticefortheJavaScriptcodeinthis(?:page|file)(.*)?@licendTheaboveistheentirelicensenoticefortheJavaScriptcodeinthis(?:page|file)/mi;
+var licenseMagnet = /.*@license ?(magnet\:\?xt=urn\:btih\:[0-9A-Za-z]+).*/;
+var licenseEndMagnet = /.*@license-end.*/i;
+exports.freeCheck = {
+ initLicenses: function (licenses) {
+ for (var item in licenses) {
+ this.stripLicenseToRegexp(licenses[item]);
+ }
+ },
+
+ /**
+ * stripLicenseToRegexp
+ *
+ * Removes all non-alphanumeric characters except for the
+ * special tokens, and replace the text values that are
+ * hardcoded in license_definitions.js
+ *
+ */
+ stripLicenseToRegexp: function (license) {
+ var i = 0,
+ max = license.licenseFragments.length,
+ item;
+
+ for (; i < max; i++) {
+ item = license.licenseFragments[i];
+ item.regex = patternUtils.removeNonalpha(item.text);
+
+ if (license.licenseFragments[i].type === LAZY) {
+
+ // do not permit words before. Since "Not" could be added
+ // and make it nonfree. e.g.: Not licensed under the GPLv3.
+ item.regex = '^(?!.*not).*' + item.regex;
+
+ }
+
+ item.regex = new RegExp(
+ patternUtils.replaceTokens(item.regex), 'i');
+ }
+
+ return license;
+ },
+
+ /**
+ * checkNodeFreeLicense
+ *
+ * Check if the node mentions a free license
+ * in one of its comments.
+ *
+ */
+ checkNodeFreeLicense: function (n, queue) {
+ var strippedComment,
+ magnetLink,
+ comment = this.getComment(n),
+ list = licenses.licenses,
+ i, j,
+ max,
+ regex,
+ frag,
+ matchLicStart,
+ matchMagnet,
+ license,
+ isMagnetValid = false;
+
+ if (n.counter === 2 &&
+ n.parent != undefined &&
+ n.parent.type === token.SCRIPT &&
+ comment != undefined &&
+ comment != " "
+ ) {
+ strippedComment = patternUtils.removeNonalpha(comment);
+ matchLicStart = strippedComment.match(licStartLicEndRe);
+ console.debug("matchMagnet is", matchMagnet);
+ if (matchLicStart) {
+ strippedComment = matchLicStart[1];
+ for (license in list) {
+ frag = list[license].licenseFragments;
+ max = list[license].licenseFragments.length;
+ for (i = 0; i < max; i++) {
+ if (frag[i].regex.test(strippedComment)) {
+ return {
+ licenseName: list[license].licenseName,
+ type: types.checkTypes.FREE
+ };
+
+ }
+ }
+ }
+ }
+ // check for @license -- @license-end notation.
+ return this.matchMagnet(comment, queue);
+ }
+ },
+
+ /**
+ * matchMagnet
+ * Attempts to find valid @license [magnet]
+ * and @license-end notation.
+ */
+ matchMagnet: function (comment, queue) {
+ let matchMagnet = comment.match(licenseMagnet);
+ if (matchMagnet) {
+ let magnetLinkRe = new RegExp(
+ matchMagnet[1].replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
+ );
+ let list = licenses.licenses;
+ let queue_end = queue.length;
+
+ for (var license in list) {
+ frag = list[license].canonicalUrl;
+ console.debug("frag is ", frag);
+ if (frag != undefined) {
+ max = list[license].canonicalUrl.length;
+ console.debug("max is", max);
+ for (i = 0;i < max; i++) {
+ console.debug("current frag is", frag[i]);
+ if (frag[i].match(magnetLinkRe)) {
+ for (let i = 0; i < queue_end; i++) {
+ console.debug(queue[i]);
+ let n = queue[i];
+ comment = this.getComment(n);
+ if (comment != undefined &&
+ comment.match(licenseEndMagnet) &&
+ this.checkIsLastNode(n)
+ ) {
+ // found a closing match. Just accept this script.
+ return {
+ licenseName: list[license].licenseName,
+ type: types.checkTypes.FREE_SINGLE_ITEM
+ };
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return;
+ },
+
+ /**
+ * checkIsLastJsNode.
+ * returns true if n is the last node.
+ * Or if nodes before it are only comments etc (not valid code.)
+ * A special LibreJS node is appended at the end of a script tree to
+ * check if this is the last (and also for narcissus to keep the last comment
+ * in the tree.)
+ * TODO: Refactor LibreJS so that END nodes can have a comment.
+ */
+ checkIsLastNode: function (n) {
+ // first check if the comment is part of the very last statement.
+ if (n.value == "this" && n.next == undefined) {
+ // just make sure the last node is indeed our harmless bit of
+ // js.
+ if (n.tokenizer) {
+ let source = n.tokenizer.source;
+ let substring = source.substr(n.start, n.end+23);
+ if (substring == END_OF_SCRIPT) {
+ return true;
+ }
+ else {
+ console.debug("substring is ", substring);
+ return false;
+ }
+ }
+ console.debug("Hurra! This is the end of our script");
+ return true;
+ }
+
+ // isn't our last node.
+ return false;
+ },
+
+ /**
+ * getComment
+ *
+ * Grab the comment(s) from the node. Concatenates
+ * multiple comments.
+ *
+ */
+ getComment: function (n) {
+ var i = 0, length, comment = "";
+
+ if (n.blockComments == undefined || n.blockComments == " ") {
+ return;
+ }
+
+ length = n.blockComments.length;
+ if (length > 0) {
+ for (; i < length; i++) {
+ comment += n.blockComments[i];
+ }
+ }
+ if (comment == "") {
+ return;
+ }
+ return comment;
+ }
+};
+
+exports.freeCheck.initLicenses(licenses.licenses);
diff --git a/lib/js_checker/js_checker.js b/lib/js_checker/js_checker.js
new file mode 100644
index 0000000..0cf06e3
--- /dev/null
+++ b/lib/js_checker/js_checker.js
@@ -0,0 +1,518 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+var narcissusWorker = require("parser/narcissus_worker")
+ .narcissusWorker;
+
+const nonTrivialModule = require("js_checker/nontrivial_checker");
+const freeChecker = require("js_checker/free_checker");
+const relationChecker = require("js_checker/relation_checker");
+const types = require("js_checker/constant_types");
+
+const scriptsCached = require("script_entries/scripts_cache").scriptsCached;
+var isDryRun = require("addon_management/prefchange").isDryRun;
+
+var checkTypes = types.checkTypes;
+
+const token = types.token;
+
+// for setTimeout.
+const timer = require("sdk/timers");
+
+var callbackMap = {};
+
+/**
+ *
+ * Pairs a hash with a given callback
+ * method from an object.
+ *
+ */
+var setHashCallback = function(hash, callback, notification) {
+ console.debug('setHashCallback', hash);
+ if (hash in callbackMap && isDryRun()) {
+ // workaround for issue with dryrun after checking box.
+ // do nothing.
+ callbackMap[hash] = callback;
+ } else if (hash in callbackMap) {
+ console.debug("callback", callbackMap[hash]);
+ if (notification && typeof notification.close === 'function') {
+ notification.close();
+ }
+ throw Error("already being checked.");
+ } else {
+ console.debug('setting callbackMap for', hash, 'to', callback);
+ callbackMap[hash] = callback;
+ }
+ console.debug("callback is type: ", callback.constructor);
+ //callbackMap[hash] = callback;
+};
+
+var removeHashCallback = function(hash) {
+ if (hash in callbackMap) {
+ delete callbackMap[hash];
+ }
+};
+
+/**
+ * find callback and return result (parse tree).
+ *
+ */
+exports.callbackHashResult = function(hash, result) {
+ console.debug('typeof callbackMap function:', typeof callbackMap[hash]);
+ console.debug('for hash', hash);
+ try {
+ callbackMap[hash](result, hash);
+ } catch (x) {
+ console.debug('error in jsChecker', x, 'hash:', hash);
+ // return tree as false.
+ console.debug("Error with", x);
+ if (typeof callbackMap[hash] === 'function') {
+ callbackMap[hash](false, hash);
+ } else {
+ console.debug('callbackHashResult Error', x);
+ }
+ }
+ // remove callback after it's been called.
+ console.debug('JsChecker.callbackHashResult: calling removeHashCallback');
+ removeHashCallback(hash);
+};
+
+var JsChecker = function() {
+ this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this.nonTrivialChecker = null;
+ this.freeToken = false;
+ this.nontrivialness = false;
+ this.parseTree = null;
+ this.relationChecker = null;
+ this.jsCode = null;
+ this.resultReady = null;
+ this.notification = null;
+ this.walkTreeCancelled = false;
+ this.shortText = null;
+ this.hash = null;
+ this.queue = null; // will contain the nodes of the script.
+};
+
+/**
+ * searchJs
+ *
+ * Takes in some javascript code (as string).
+ * Uses Narcissus parser to build an abstract syntax tree.
+ * Checks for trivialness.
+ *
+ */
+JsChecker.prototype.searchJs = function(jsCode, resultReady, url) {
+ var that = this;
+ var bugfix = require('html_script_finder/bug_fix').narcissusBugFixLibreJS;
+ console.debug('JsChecker.searchJs for script url:', url);
+ this.url = url;
+ this.resultReady = resultReady;
+ this.jsCode = jsCode;
+ this.shortText = jsCode.replace(bugfix, '').substring(0,100);
+ this.notification = require("ui/notification")
+ .createNotification(this.shortText).notification;
+
+ var verbatimCode = this.jsCode.replace(bugfix, '');
+ this.hash = scriptsCached.getHash(verbatimCode);
+ var isCached = scriptsCached.isCached(verbatimCode, this.hash);
+ if (isCached) {
+ console.debug("We have it cached indeed!");
+ // there is an existing entry for this exact copy
+ // of script text.
+ console.debug('this script result is cached', this.hash,
+ isCached.result.type);
+ console.debug("Return right away");
+ // we are not generating a parse tree.
+ this.parseTree = {};
+ // fake the result is from parse tree.
+ this.parseTree.freeTrivialCheck = isCached.result;
+
+ this.relationChecker = isCached.relationChecker;
+ // leave without doing parsing/analysis part.
+ this.resultReady();
+ this.removeNotification();
+ return;
+ }
+
+ console.debug('url is not cached:', url);
+
+ try {
+ // no cache, continue.
+ this.relationChecker = relationChecker.relationChecker();
+ this.freeToken = types.emptyTypeObj();
+ this.nontrivialness = types.emptyTypeObj();
+
+ // use this.hash to keep track of comments made by the nontrivial
+ // checker code about why/how the code is found to be nontrivial.
+ this.nonTrivialChecker =
+ nonTrivialModule.nonTrivialChecker(this.hash);
+
+ // register callback and hash. So that result
+ // can be passed.
+ setHashCallback(
+ this.hash, this.handleTree.bind(this), this.notification);
+
+ // parse using ChromeWorker.
+ console.debug(
+ 'JsChecker.searchJs(): starting narcissusWorker.parse()');
+ narcissusWorker.parse(this.jsCode, this.hash);
+ } catch (x) {
+ console.debug('error', x);
+ this.handleTree(false, x);
+ this.removeNotification();
+ }
+};
+
+JsChecker.prototype.handleTree = function(tree, errorMessage) {
+ var that = this;
+
+ if (tree == false || tree == undefined) {
+ // error parsing tree. Just return nonfree nontrivial.
+ this.parseTree = {};
+ this.parseTree.freeTrivialCheck = types.nontrivialWithComment(
+ 'error parsing: ' + errorMessage);
+
+ // cache result with hash of script for future checks.
+ scriptsCached.addEntry(this.jsCode, this.parseTree.freeTrivialCheck,
+ this.relationChecker, true, this.url);
+ this.resultReady();
+ } else {
+ try {
+ // no need to keep parseTree in property
+ this.parseTree = {}; //tree;
+ //console.debug(tree);
+ this.walkTree(tree);
+ } catch (x) {
+ console.debug(x, x.lineNumber, x.fileName);
+ }
+ }
+};
+
+/**
+ * getCheckerResult
+ *
+ * Callback to Assign result from walkTree to property.
+ * reset parse tree. create cache entry.
+ *
+ */
+JsChecker.prototype.getCheckerResult = function(result) {
+ // done with parse tree. Get rid of it.
+ this.parseTree = {};
+ this.removeNotification();
+
+ this.parseTree.nonTrivialChecker = this.nonTrivialChecker;
+
+ // actual result stored here. hack since we used parseTree before.
+ this.parseTree.freeTrivialCheck = result;
+
+ // cache result with hash of script for future checks.
+ scriptsCached.addEntry(this.jsCode, this.parseTree.freeTrivialCheck,
+ this.relationChecker, true, this.url);
+
+ this.resultReady();
+};
+
+/**
+ * trivialCheck
+ *
+ * Runs nodes through a series of conditional statements
+ * to find out whether it is trivial or not.
+ *
+ * @param {object} n. The current node being studied.
+ * @param {string} t. The type of node being studied
+ * (initializer, functionbody, try block, ...)
+ *
+ */
+JsChecker.prototype.trivialCheck = function(n) {
+ return this.nonTrivialChecker.checkNontrivial(n);
+};
+
+/**
+ * freeCheck
+ *
+ * Check if comments above current node could be a free licence.
+ * If it is, then the script will be flagged as free.
+ *
+ * @param {object} n. The current node being studied.
+ * (initializer, functionbody, try block, ...)
+ *
+ */
+JsChecker.prototype.freeCheck = function(n, ntype) {
+ var check = freeChecker.freeCheck.checkNodeFreeLicense(n, this.queue);
+ return check;
+};
+
+/**
+ * walkTree
+ *
+ * An iterative functionwalking the parse tree generated by
+ * Narcissus.
+ *
+ * @param {object} node. The original node.
+ *
+ */
+JsChecker.prototype.walkTree = function(node) {
+ var queue = [node];
+ var i,
+ len,
+ n, counter = 0,
+ result,
+ processQueue,
+ that = this;
+
+ this.queue = queue; // set as property.
+
+ // set top node as visited.
+ node.visited = true;
+
+ /**
+ * functionwalking the tree for a given
+ * amount of time, before calling itself again.
+ */
+ processQueue = function() {
+ var nodeResult, end;
+
+ // record start time of functionexecution.
+ var start = Date.now();
+
+ if (that.walkTreeCancelled) {
+ // tree walking already completed.
+ return;
+ }
+
+ while (queue.length) {
+ n = queue.shift();
+ n.counter = counter++;
+ console.debug("Under review", n.type);
+ if (n.children != undefined) {
+ // fetch all the children.
+ len = n.children.length;
+ for (i = 0; i < len; i++) {
+ if (n.children[i] != undefined &&
+ n.children[i].visited == undefined
+ ) {
+ // figure out siblings.
+ if (i > 0) {
+ n.children[i].previous = n.children[i-1];
+ }
+
+ if (i < len) {
+ n.children[i].next = n.children[i+1];
+ }
+ // set parent property.
+ n.children[i].parent = n;
+ n.children[i].visited = true;
+ queue.push(n.children[i]);
+ }
+ }
+ }
+
+ if (n.type != undefined) {
+ // fetch all properties that may have nodes.
+ for (var item in n) {
+ if (item != 'tokenizer' &&
+ item != 'children' &&
+ item != 'length' &&
+ n[item] != null &&
+ typeof n[item] === 'object' &&
+ n[item].type != undefined &&
+ n[item].visited == undefined
+ ) {
+ n[item].visited = true;
+ // set parent property
+ n[item].parent = n;
+ queue.push(n[item]);
+ }
+ }
+ }
+
+ that.checkNode(n);
+
+ if (that.freeToken.type === checkTypes.FREE ||
+ that.freeToken.type === checkTypes.FREE_SINGLE_ITEM
+ ) {
+ // nothing more to look for. We are done.
+ that.walkTreeComplete(that.freeToken);
+ return;
+ } else if (that.nontrivialness.type === checkTypes.NONTRIVIAL) {
+ // nontrivial
+ // we are done.
+ that.walkTreeComplete(that.nontrivialness);
+ return;
+ }
+ // call processQueue again if needed.
+ end = Date.now();
+
+ if (queue.length) {
+ // there are more nodes in the queue.
+
+ if ((end - start) > 30) {
+
+ // been running more than 20ms, pause
+ // for 10 ms before calling processQueue
+ // again.
+ timer.setTimeout(processQueue, 8);
+ return;
+ }
+ } else {
+ // we are done.
+ that.removeNotification();
+ that.walkTreeComplete();
+ return;
+ }
+ }
+ };
+
+ if (node.type === token.SCRIPT) {
+ // this is the global scope.
+ node.global = true;
+ node.parent = null;
+
+ this.relationChecker.storeGlobalDeclarations(node);
+
+ queue.push(node);
+ processQueue();
+ }
+};
+
+/**
+ * set walk tree cancelled bool as true.
+ * the walk tree method won't run after the variable
+ * is set to true.
+ */
+JsChecker.prototype.cancelWalkTree = function() {
+ // prevent any further work on node codes.
+ this.walkTreeCancelled = true;
+};
+
+/**
+ * walkTreeComplete
+ *
+ * Trigger when the walkTree has been completed or
+ * when it has been cut short.
+ *
+ */
+JsChecker.prototype.walkTreeComplete = function(result) {
+ var that = this;
+ this.removeNotification();
+
+ if (this.walkTreeCancelled) {
+ // we already triggered complete.
+ return;
+ }
+
+ // we set the token to cancel further processing.
+ this.cancelWalkTree();
+
+ if (result != undefined) {
+ // walkTree was returned faster, use it instead.
+ this.getCheckerResult(result);
+
+ // we are done.
+ return;
+ }
+
+ // if all code was fully analyzed.
+ if (this.nontrivialness.type === checkTypes.NONTRIVIAL) {
+ this.getCheckerResult(this.nontrivialness);
+ } else if (this.freeToken.type === checkTypes.FREE) {
+ // this is free and may or may not define functions, we don't care.
+ this.getCheckerResult(this.freeToken);
+ } else if (this.nontrivialness.type ===
+ checkTypes.TRIVIAL_DEFINES_FUNCTION) {
+ // trivial scripts should become nontrivial if an external script.
+ // it may or may not be trivial if inline.
+ this.getCheckerResult(this.nontrivialness);
+ } else {
+ // found no nontrivial constructs or free license, so it's
+ // trivial.
+
+ this.getCheckerResult(
+ types.trivialFuncWithComment("This script is trivial"));
+ }
+};
+
+
+/**
+ * checkNode
+ *
+ * checks a single node.
+ *
+ */
+JsChecker.prototype.checkNode = function(n) {
+ var sub;
+ var fc = this.freeCheck(n);
+ var tc = this.trivialCheck(n);
+
+ var nodeResult;
+
+ // check if identifier may be window property (assumption).
+ this.relationChecker.checkIdentifierIsWindowProperty(n);
+
+ /*if (fc) {
+ console.debug("FC is", fc, "type is", fc.type);
+ }*/
+ if (fc && fc.type == checkTypes.FREE) {
+ // this is free!
+ // freeToken is persistent across nodes analyzed and valid
+ // for an entire script.
+ this.freeToken = types.freeWithComment(
+ "Script appears to be free under the following license: " +
+ fc.licenseName);
+ return;
+ } else if (fc && fc.type == checkTypes.FREE_SINGLE_ITEM) {
+ console.debug("free single item");
+ this.freeToken = types.singleFreeWithComment(
+ "Script appears to be free under the following license: " +
+ fc.licenseName);
+ return;
+ }
+
+ if (tc) {
+ if (tc.type === checkTypes.NONTRIVIAL) {
+ // nontrivial_global is deprecated
+ this.nontrivialness = tc;
+ return;
+ } else if (tc.type === checkTypes.TRIVIAL_DEFINES_FUNCTION) {
+ this.nontrivialness = tc;
+ return;
+ }
+ }
+};
+
+JsChecker.prototype.removeNotification = function() {
+ console.debug('JsChecker.removeNotification()');
+ if (this.notification &&
+ typeof this.notification.close === 'function'
+ ) {
+ console.debug('removing', this.shortText);
+ // remove notification early on.
+ this.notification.close();
+ this.notification = null;
+ }
+};
+
+exports.jsChecker = function() {
+ return new JsChecker();
+};
+
+exports.removeHashCallback = removeHashCallback;
diff --git a/lib/js_checker/license_definitions.js b/lib/js_checker/license_definitions.js
new file mode 100644
index 0000000..79e5090
--- /dev/null
+++ b/lib/js_checker/license_definitions.js
@@ -0,0 +1,249 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+exports.types = {
+ SHORT: 'short',
+ LAZY: 'lazy',
+ FULL: 'full'
+};
+
+var type = exports.types;
+
+/**
+ * List of all the licenses.
+ * Currently only short substrings are used with regex.
+ */
+exports.licenses = {
+ CC0: {
+ licenseName: 'Creative Commons CC0 1.0 Universal',
+ canonicalUrl: [
+ 'http://creativecommons.org/publicdomain/zero/1.0/legalcode',
+ 'magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt'
+ ],
+ identifier: 'CC0-1.0',
+ licenseFragments: []
+ },
+
+
+ gplv2: {
+ licenseName: 'GNU General Public License (GPL) version 2',
+ canonicalUrl: [
+ 'http://www.gnu.org/licenses/gpl-2.0.html',
+ 'magnet:?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt'
+ ],
+ identifier: 'GNU-GPL-2.0',
+ licenseFragments: [{text: "<THISPROGRAM> 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 2 of the License, or (at your option) any later version.", type: type.SHORT},
+ {text:"Alternatively, the contents of this file may be used under the terms of either the GNU General Public License Version 2 or later (the \"GPL\"), or the GNU Lesser General Public License Version 2.1 or later (the \"LGPL\"), in which case the provisions of the GPL or the LGPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of either the GPL or the LGPL, and not to allow others to use your version of this file under the terms of the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL or the LGPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the MPL, the GPL or the LGPL.", type: type.SHORT}]
+ },
+
+ gplv3: {
+ licenseName: 'GNU General Public License (GPL) version 3',
+ canonicalUrl: [
+ 'http://www.gnu.org/licenses/gpl-3.0.html',
+ 'magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt'
+ ],
+ identifier: 'GNU-GPL-3.0',
+ licenseFragments: [
+ {text: "The JavaScript code in this page is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License (GNU GPL) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. The code is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. As additional permission under GNU GPL version 3 section 7, you may distribute non-source (e.g., minimized or compacted) forms of that code without the copy of the GNU GPL normally required by section 4, provided you include this license notice and a URL through which recipients can access the Corresponding Source.", type: type.SHORT},
+ {text: "<THISPROGRAM> 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.", type: type.SHORT}]
+ },
+
+ gnuAllPermissive: {
+ licenseName: 'GNU All-Permissive License',
+ licenseFragments: [{text: "Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty.", type: type.SHORT}]
+ },
+
+ apache_2License: {
+ licenseName: 'Apache License, Version 2.0',
+ canonicalUrl: [
+ 'http://www.apache.org/licenses/LICENSE-2.0',
+ 'magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt'
+ ],
+ identifier: 'Apache-2.0',
+ licenseFragments: [{text: "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0", type: type.SHORT}]
+ },
+
+ lgpl21: {
+ licenseName: 'GNU Lesser General Public License, version 2.1',
+ canonicalUrl: [
+ 'http://www.gnu.org/licenses/lgpl-2.1.html',
+ 'magnet:?xt=urn:btih:5de60da917303dbfad4f93fb1b985ced5a89eac2&dn=lgpl-2.1.txt'
+ ],
+ identifier: 'GNU-LGPL-2.1',
+ licenseFragments: [{text: "<THISLIBRARY> is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.", type: type.SHORT}]
+ },
+
+ lgplv3: {
+ licenseName: 'GNU Lesser General Public License, version 3',
+ canonicalUrl: [
+ 'http://www.gnu.org/licenses/lgpl-3.0.html',
+ 'magnet:?xt=urn:btih:0ef1b8170b3b615170ff270def6427c317705f85&dn=lgpl-3.0.txt'
+ ],
+ identifier: 'GNU-LGPL-3.0',
+ licenseFragments: [{text: "<THISPROGRAM> is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.", type: type.SHORT}]
+ },
+
+ agplv3: {
+ licenseName: 'GNU AFFERO GENERAL PUBLIC LICENSE version 3',
+ canonicalUrl: [
+ 'http://www.gnu.org/licenses/agpl-3.0.html',
+ 'magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt'
+ ],
+ identifier: 'GNU-AGPL-3.0',
+ licenseFragments: [{text: "<THISPROGRAM> is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.", type: type.SHORT}]
+ },
+
+ boostSoftware: {
+ licenseName: 'Boost Software License',
+ canonicalUrl: [
+ 'http://www.boost.org/LICENSE_1_0.txt',
+ 'magnet:?xt=urn:btih:89a97c535628232f2f3888c2b7b8ffd4c078cec0&dn=Boost-1.0.txt'
+ ],
+ licenseFragments: [{text: "Boost Software License <VERSION> <DATE> Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the \"Software\") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following", type: type.SHORT}]
+ },
+
+ bsd3: {
+ licenseName: "BSD 3-Clause License",
+ canonicalUrl: [
+ 'http://opensource.org/licenses/BSD-3-Clause',
+ 'magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt'
+ ],
+ licenseFragments: [{text: "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of <ORGANIZATION> nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.", type: type.SHORT}]
+ },
+
+ bsd2: {
+ licenseName: "BSD 2-Clause License",
+ licenseFragments: [{text: "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.", type: type.SHORT}]
+ },
+
+ epl_1_0: {
+ licenseName: "Eclipse Public License Version 1.0",
+ identifier: "EPL-1.0",
+ canonicalUrl: [
+ "http://www.eclipse.org/legal/epl-v10.html",
+ "magnet:?xt=urn:btih:4c6a2ad0018cd461e9b0fc44e1b340d2c1828b22&dn=epl-1.0.txt"
+ ],
+ licenseFragments: [
+ {
+ text: "THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.",
+ type: type.SHORT
+ }
+ ]
+ },
+
+ mozillaPublicLicense_2_0: {
+ licenseName: 'Mozilla Public License Version 2.0',
+ identifier: 'MPL-2.0',
+ canonicalUrl: [
+ 'http://www.mozilla.org/MPL/2.0',
+ 'magnet:?xt=urn:btih:3877d6d54b3accd4bc32f8a48bf32ebc0901502a&dn=mpl-2.0.txt'
+ ],
+ licenseFragments: [{text: "This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.", type: type.SHORT }]
+ },
+
+ expat: {
+ licenseName: 'Expat License (sometimes called MIT Licensed)',
+ identifier: 'Expat',
+ canonicalUrl: [
+ 'http://www.jclark.com/xml/copying.txt',
+ 'magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt'
+ ],
+ licenseFragments: [{text: "Copyright <YEAR> <NAME> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", type: type.SHORT}]
+ },
+
+ X11: {
+ licenseName: 'X11 License',
+ canonicalUrl: [
+ 'magnet:?xt=urn:btih:5305d91886084f776adcf57509a648432709a7c7&dn=x11.txt'
+ ],
+ licenseFragments: [{text: "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", type: type.SHORT}]
+ },
+
+ XFree86: {
+ licenseName: "XFree86 License",
+ identifier: 'Modified-BSD',
+ canonicalUrl: [
+ 'http://www.xfree86.org/3.3.6/COPYRIGHT2.html#3',
+ 'http://www.xfree86.org/current/LICENSE4.html',
+ 'magnet:?xt=urn:btih:12f2ec9e8de2a3b0002a33d518d6010cc8ab2ae9&dn=xfree86.txt'
+ ],
+ licenseFragments: [{text: "All rights reserved.\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n1. Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution, and in the same place and form as other copyright, license and disclaimer information.\n3. The end-user documentation included with the redistribution, if any, must include the following acknowledgment: \"This product includes software developed by The XFree86 Project, Inc (http://www.xfree86.org/) and its contributors\", in the same place and form as other third-party acknowledgments. Alternately, this acknowledgment may appear in the software itself, in the same form and location as other such third-party acknowledgments.4. Except as contained in this notice, the name of The XFree86 Project, Inc shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from The XFree86 Project, Inc.", type: type.SHORT}
+ ]
+ },
+
+ FreeBSD: {
+ licenseName: "FreeBSD License",
+ canonicalUrl: [
+ 'http://www.freebsd.org/copyright/freebsd-license.html',
+ 'magnet:?xt=urn:btih:87f119ba0b429ba17a44b4bffcab33165ebdacc0&dn=freebsd.txt'
+ ],
+ licenseFragments: [{text: "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.", type: type.SHORT}]
+ },
+
+ ISC: {
+ licenseName: "The ISC License",
+ canonicalUrl: [
+ 'https://www.isc.org/downloads/software-support-policy/isc-license/',
+ 'magnet:?xt=urn:btih:b8999bbaf509c08d127678643c515b9ab0836bae&dn=ISC.txt'
+ ],
+ licenseFragments: [{text: "Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.", type: type.SHORT},
+ {text: "Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.", type: type.SHORT}]
+ },
+
+ jQueryTools: {
+ licenseName: "jQuery Tools",
+ licenseFragments: [{text: 'NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.', type: type.SHORT}]
+ },
+
+ ArtisticLicense2: {
+ licenseName: "Artistic License 2.0",
+ canonicalUrl: [
+ "http://www.perlfoundation.org/artistic_license_2_0",
+ "magnet:?xt=urn:btih:54fd2283f9dbdf29466d2df1a98bf8f65cafe314&dn=artistic-2.0.txt"
+ ],
+ licenseFragments: []
+ },
+
+ PublicDomain: {
+ licenseName: "Public Domain",
+ canonicalUrl: [
+ 'magnet:?xt=urn:btih:e95b018ef3580986a04669f1b5879592219e2a7a&dn=public-domain.txt'
+ ],
+ licenseFragments: []
+ },
+
+ CPALv1: {
+ licenseName: 'Common Public Attribution License Version 1.0 (CPAL)',
+ canonicalUrl: [
+ 'http://opensource.org/licenses/cpal_1.0',
+ 'magnet:?xt=urn:btih:84143bc45939fc8fa42921d619a95462c2031c5c&dn=cpal-1.0.txt'
+ ],
+ identifier: 'CPAL-1.0',
+ licenseFragments: [
+ {
+ text: 'The contents of this file are subject to the Common Public Attribution License Version 1.0',
+ type: type.SHORT
+ },
+ {
+ text: 'The term "External Deployment" means the use, distribution, or communication of the Original Code or Modifications in any way such that the Original Code or Modifications may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Code or Modifications as a distribution under section 3.1 and make Source Code available under Section 3.2.',
+ type: type.SHORT
+ }
+ ]
+ }
+};
diff --git a/lib/js_checker/nontrivial_checker.js b/lib/js_checker/nontrivial_checker.js
new file mode 100644
index 0000000..4d3bc8b
--- /dev/null
+++ b/lib/js_checker/nontrivial_checker.js
@@ -0,0 +1,374 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+const types = require("js_checker/constant_types");
+
+// constants from Narcissus for function types.
+const DECLARED_FORM = 0, EXPRESSED_FORM = 1, STATEMENT_FORM = 2;
+
+const token = types.token;
+
+var checkTypes = types.checkTypes;
+
+var utils = {
+ /**
+ * nodeContains
+ * Checks that node contains both a type and a value.
+ * Shortcut to check for null/undefined.
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if matching.
+ */
+ nodeContains: function (n, type, value) {
+ if (n != undefined) {
+ return n.type === type &&
+ n.value === value;
+ }
+ },
+
+ /**
+ * isType
+ * Checks that node is of a certain type.
+ * Shortcut to check for null/undefined.
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean}. True if it's the right type.
+ */
+ isType: function (n, type) {
+ return n != undefined && n.type === type;
+ },
+
+ isNotType: function (n, type) {
+ return n != undefined && n.type !== type;
+ },
+
+ /**
+ * hasChildren
+ *
+ * Checks the token on the left
+ * and on the right.
+ *
+ * @param {object} n. The current node being studied.
+ * @param {leftType} token constant. The type on the
+ * left.
+ * @param {rightType} token constant. The type of child
+ * on the right
+ *
+ */
+ hasChildren: function (n, leftType, rightType) {
+ if (types == undefined) {
+ return false;
+ }
+ return this.isType(n.children[0], leftType) &&
+ this.isType(n.children[1], rightType);
+ },
+
+ /**
+ * findScriptTag
+ *
+ * This method should probably be replaced with DOM testing
+ * as regex is rather insufficiant, and this wouldn't cover
+ * tricky constructs as shown in http://ha.ckers.org/xss.html.
+ */
+ findScriptTag: function (n) {
+ return n.value != undefined &&
+ /<script[^>]*?>/i.test(n.value);
+ }
+};
+
+var NonTrivialChecker = function() {
+ this.definesFunction = false;
+ this.hash = null;
+};
+
+/**
+ * definesFunctionFound
+ *
+ * Returns true if it finds a node of type FUNCTION
+ * that isn't a callback or an IIFE.
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if found.
+ */
+NonTrivialChecker.prototype.definesFunctionFound = function (n) {
+ var isFunction = false;
+ if (n.type === token.FUNCTION &&
+ n.body != undefined) {
+
+ if (n.functionForm !== token.DECLARED_FORM &&
+ ((n.parent.type === token.LIST &&
+ n.parent.parent.type === token.CALL) ||
+ n.parent.type === token.CALL) &&
+ n.name == undefined) {
+ // this is a callback or an immediately
+ // invoked function expression "IIFE".
+ isFunction = false;
+ } else {
+ // this is a regular function declaration or
+ // function expression assigned to a variable.
+ //console.log("THIS DEFINES FUNCTION");
+ isFunction = true;
+ }
+ }
+
+ // look for Function constructor.
+ if (n.type === token.IDENTIFIER &&
+ n.value === 'Function' &&
+ (n.parent.type === token.NEW_WITH_ARGS ||
+ n.parent.type === token.CALL)) {
+ // this is a Function constructor.
+ //console.log("THIS DEFINES FUNCTION");
+ isFunction = true;
+ }
+
+ return isFunction;
+};
+
+
+/**
+ * invokesEval
+ *
+ * Returns true (nontrivial) if it finds any use of
+ * the eval function. For simplicity, we assume any
+ * use of an identifier "eval" is the eval function.
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if found.
+ */
+NonTrivialChecker.prototype.invokesEval = function (n) {
+ return (n.type === token.CALL &&
+ utils.nodeContains(n.children[0], token.IDENTIFIER, 'eval') ||
+ n.type === token.IDENTIFIER && n.value === 'eval');
+};
+
+/**
+ * evalIdentifier
+ *
+ * Returns true (nontrivial) if it finds any use of
+ * the eval function. For simplicity, we assume any
+ * use of an identifier "eval" is the eval function.
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if found.
+ */
+NonTrivialChecker.prototype.evalIdentifier = function (n) {
+ return n.type === token.IDENTIFIER && n.value === 'eval';
+};
+
+
+/**
+ * invokesMethodBracketSuffix
+ *
+ * Finds a method being invoked using the bracket suffix notation
+ * rather than the dot notation. It is difficult without keeping track of
+ * variable values to check for what method is actually being called.
+ * So we're just flagging any use of this construct as nontrivial.
+ * e.g., should catch: xhr[a+b]('GET', 'http://www.example.com');
+ * Should not catch other uses such as: myArray[num];
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if found.
+ */
+NonTrivialChecker.prototype.invokesMethodBracketSuffix = function (n) {
+ return n.type === token.CALL && utils.isType(n.children[0], token.INDEX);
+};
+
+/**
+ * createsXhrObject
+ *
+ * Creates an xhr object.
+ * Since all "new XMLHttpRequest", "XMLHttpRequest()",
+ * and "new window.XMLHttpRequest" instantiate the xhr object,
+ * we assume (without further proof) that any use
+ * of the identifier "XMLHttpRequest" and "ActiveXObject"
+ * is an xhr object.
+ * Constructs like window[a+b]() are already caught by the
+ * bracket suffix check.
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if found.
+ */
+NonTrivialChecker.prototype.createsXhrObject = function (n) {
+ return (n.type === token.IDENTIFIER) &&
+ (n.value === 'XMLHttpRequest' ||
+ n.value === 'ActiveXObject');
+};
+
+/**
+ * invokesXhrOpen
+ *
+ * Here we assume the call of an open method must be an xhr request
+ * (and not some other object) by checking the number of arguments.
+ * In most cases this method won't be used since createsXhrObject
+ * will already have caught the xhr.
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if found.
+ *
+ */
+NonTrivialChecker.prototype.invokesXhrOpen = function (n) {
+ return n.type === token.CALL &&
+ utils.hasChildren(n, token.DOT, token.LIST) &&
+ utils.isType(n.children[0].children[0], token.IDENTIFIER) &&
+ utils.nodeContains(n.children[0].children[1], token.IDENTIFIER, 'open') &&
+ n.children[1].children.length > 1;
+};
+
+/**
+ * createsScriptElement
+ *
+ * Checks for document.createElement() that create a script. In the case
+ * it creates an element from a variable, we assume it's a script. In the
+ * future we might want to check for the value of that string variable
+ * (e.g., if a variable is assigned 'script', raise a flag)
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if found.
+ *
+ *
+ */
+NonTrivialChecker.prototype.createsScriptElement = function (n) {
+ return n.type === token.CALL &&
+ utils.hasChildren(n, token.DOT, token.LIST) &&
+ utils.isType(n.children[0].children[0], token.IDENTIFIER) &&
+ utils.nodeContains(n.children[0].children[1], token.IDENTIFIER, 'createElement') &&
+ (utils.nodeContains(n.children[1].children[0], token.STRING, 'script') ||
+ utils.isType(n.children[1].children[0], token.IDENTIFIER));
+};
+
+/**
+ * writesScriptAsHtmlString
+ *
+ * catches myObj.write('<script></script>');
+ * or any myObj.write(myStringVariable);
+ * or concatenation such as:
+ * myObj.write('<scri' + stringVariable);
+ * or 'something' + 'somethingelse'.
+ *
+ * To check for javascript here we might want to look at the list
+ * from ha.ckers.org/xss.html for the future.
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if found.
+ *
+ */
+NonTrivialChecker.prototype.writesScriptAsHtmlString = function (n) {
+ var listArg;
+
+ if (n.type === token.CALL &&
+ utils.hasChildren(n, token.DOT, token.LIST) &&
+ utils.isType(n.children[0].children[0], token.IDENTIFIER) &&
+ utils.nodeContains(n.children[0].children[1], token.IDENTIFIER, 'write')
+ ) {
+ if (utils.isNotType(n.children[1].children[0], token.STRING)) {
+ // return true if any operation or concatenation.
+ // We are cautious (as it could
+ // embed a script) and flag this as nontrivial.
+
+ return true;
+ }
+ return utils.findScriptTag(n.children[1].children[0]);
+ } else {
+ return false;
+ }
+};
+
+/**
+ * nontrivial anytime we see an identifier as innerHTML
+ */
+NonTrivialChecker.prototype.innerHTMLIdentifier = function (n) {
+ if ((n.type === token.IDENTIFIER ||
+ n.type === token.STRING) &&
+ n.value === 'innerHTML'
+ ) {
+ return true;
+ }
+};
+
+/**
+ * checkNontrivial
+ *
+ * Contains all the conditionals that try to identify,
+ * step by step, all code that could be flagged as
+ * nontrivial.
+ *
+ * @param {object} n. The current node being studied.
+ * @return {boolean} . True if found.
+ *
+ */
+NonTrivialChecker.prototype.checkNontrivial = function (n, t) {
+
+ if (n.type === token.IDENTIFIER && this.evalIdentifier(n)) {
+ //console.log("NONTRIVIAL: eval has been found in code");
+ return types.nontrivialWithComment("NONTRIVIAL: eval has been found in code");
+ }
+
+ if (this.innerHTMLIdentifier(n)) {
+ //console.log("NONTRIVIAL: innerHTML identifier");
+ return types.nontrivialWithComment("NONTRIVIAL: innerHTML identifier");
+ }
+
+ // the node is an identifier
+ if (n.type === token.IDENTIFIER && this.createsXhrObject(n)) {
+ //console.log('NONTRIVIAL: Creates an xhr object');
+ return types.nontrivialWithComment('NONTRIVIAL: Creates an xhr object');
+ }
+
+ // this is a method/function call
+ if (n.type === token.CALL) {
+
+ if (this.invokesEval(n)) {
+ //console.log("NONTRIVIAL: eval has been found in code");
+ return types.nontrivialWithComment("NONTRIVIAL: eval has been found in code");
+ }
+
+ if (this.invokesMethodBracketSuffix(n)) {
+ //console.log('NONTRIVIAL: square bracket suffix method call detected');
+ return types.nontrivialWithComment("NONTRIVIAL: eval has been found in code");
+ }
+
+ if (this.invokesXhrOpen(n)) {
+ //console.log('NONTRIVIAL: an open method similar to xhr.open is used');
+ return types.nontrivialWithComment('NONTRIVIAL: square bracket suffix method call detected');
+ }
+
+ if (this.createsScriptElement(n)) {
+ //console.log('NONTRIVIAL: creates script element dynamically.');
+ return types.nontrivialWithComment('NONTRIVIAL: an open method similar to xhr.open is used');
+ }
+
+ if (this.writesScriptAsHtmlString(n)) {
+ //console.log('NONTRIVIAL: writes script as html dynamically.');
+ return types.nontrivialWithComment('NONTRIVIAL: creates script element dynamically.');
+ }
+ }
+
+ // The node is a function definition.
+ // Most common occurence.
+ if (this.definesFunctionFound(n)) {
+ return types.trivialFuncWithComment("Script is trivial but defines one or more functions");
+ }
+
+ // found nothing else, so trivial.
+ return types.trivialWithComment("Script is trivial");
+};
+
+exports.nonTrivialChecker = function () {
+ return new NonTrivialChecker();
+};
diff --git a/lib/js_checker/pattern_utils.js b/lib/js_checker/pattern_utils.js
new file mode 100644
index 0000000..00b0c26
--- /dev/null
+++ b/lib/js_checker/pattern_utils.js
@@ -0,0 +1,40 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+exports.patternUtils = {
+ /**
+ * removeNonalpha
+ *
+ * Remove all nonalphanumeric values, except for
+ * < and >, since they are what we use for tokens.
+ *
+ */
+ removeNonalpha: function (str) {
+ var regex = /[^a-z0-9<>@]+/gi;
+ return str.replace(regex, '');
+ },
+ removeWhitespace: function (str) {
+ return str.replace(/\s+/gmi, '');
+ },
+ replaceTokens: function (str) {
+ var regex = /<.*?>/gi;
+ return str.replace(regex, '.*?');
+ }
+};
diff --git a/lib/js_checker/privacy_checker.js b/lib/js_checker/privacy_checker.js
new file mode 100644
index 0000000..7b8917a
--- /dev/null
+++ b/lib/js_checker/privacy_checker.js
@@ -0,0 +1,44 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+const privacyThreatJs = require('js_checker/privacy_threat_definitions.js');
+const patternUtils = require('js_checker/pattern_utils').patternUtils;
+
+exports.privacyCheck = {
+ checkScriptPrivacyThreat: function (currentScript) {
+ var list = privacyThreatJs.js;
+ var i;
+ var item;
+ var max;
+
+ currentScript = patternUtils.removeWhitespace(currentScript);
+
+ for (item in list) {
+ max = list[item].length;
+
+ for (i = 0; i < max; i++) {
+ if (list[item][i].test(currentScript)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+};
diff --git a/lib/js_checker/privacy_threat_definitions.js b/lib/js_checker/privacy_threat_definitions.js
new file mode 100644
index 0000000..66b073d
--- /dev/null
+++ b/lib/js_checker/privacy_threat_definitions.js
@@ -0,0 +1,52 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+exports.js = {
+ /**
+ * googleAnalytics
+ * Tracking code for Google Analytics.
+ * It corresponds to:
+ * var _gaq = _gaq || [];
+ * _gaq.push(['_setAccount', 'UA-XXXXXXX-X']);
+ * _gaq.push(['_trackPageview']);
+ *
+ * (function() {
+ * var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ * ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ * var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+ * })();
+ *
+ * It also matches GA code that doesn't track a page view, like this:
+ * var _gaq = _gaq || [];
+ * _gaq.push(['_setAccount', 'UA-5936383-6']);
+ *
+ * (function() {
+ * var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ * ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ * var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+ * })();
+ *
+ */
+ googleAnalytics: [
+ /var_gaq=_gaq\|\|\[\];_gaq\.push\(\['_setAccount','UA[0-9\-]*?'\]\);(_gaq.push\(\['_setDomainName','[a-z\.]*?'\]\);)?(_gaq\.push\(\['_trackPageview'\]\);)?\(function\(\){varga=document\.createElement\('script'\);ga\.type='text\/javascript\';ga\.async=true;ga\.src=\(\'https:\'==document\.location\.protocol\?'https:\/\/ssl':'http:\/\/www'\)\+'\.google\-analytics\.com\/ga\.js';vars=document\.getElementsByTagName\('script'\)\[0\];s\.parentNode\.insertBefore\(ga,s\);}\)\(\);/i,
+ /vargaJsHost\=\(\(\"https\:\"\=\=document\.location\.protocol\)\?\"https\:\/\/ssl\.\"\:\"http\:\/\/www\.\"\)\;document\.write\(unescape\(\"\%3Cscriptsrc\=\'\"\+gaJsHost\+\"google\-analytics\.com\/ga\.js\'type\=\'text\/javascript\'\%3E\%3C\/script\%3E\"\)\)\;/i,
+ /try{varpageTracker\=\_gat\.\_getTracker\(\"UA[0-9\-]*?\"\)\;pageTracker\.\_trackPageview\(\)\;}catch\(err\){}/i
+ ]
+};
diff --git a/lib/js_checker/relation_checker.js b/lib/js_checker/relation_checker.js
new file mode 100644
index 0000000..1904812
--- /dev/null
+++ b/lib/js_checker/relation_checker.js
@@ -0,0 +1,289 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+/**
+ * relation_checker.js
+ *
+ * Finds out if two scripts are related to each other.
+ *
+ */
+const types = require("js_checker/constant_types");
+
+const token = types.token;
+
+// all predefined window properties (methods and variables).
+const windowPropertiesHash = {
+ "addEventListener": 1, "alert": 1, "applicationCache": 1,
+ "Array": 1, "ArrayBuffer": 1, "atob": 1, "back": 1, "blur": 1,
+ "Boolean": 1, "btoa": 1, "captureEvents": 1, "CharacterData": 1,
+ "clearInterval": 1, "clearTimeout": 1, "close": 1, "closed": 1,
+ "Components": 1, "confirm": 1, "console": 1, "constructor": 1,
+ "content": 1, "controllers": 1, "crypto": 1,
+ "CSSStyleDeclaration": 1, "Date": 1, "decodeURI": 1,
+ "decodeURIComponent": 1, "defaultStatus": 1,
+ "disableExternalCapture": 1, "dispatchEvent": 1, "Document": 1,
+ "document": 1, "DocumentType": 1, "dump": 1, "Element": 1,
+ "enableExternalCapture": 1, "encodeURI": 1, "encodeURIComponent": 1,
+ "Error": 1, "escape": 1, "eval": 1, "EvalError": 1, "Event": 1,
+ "find": 1, "Float32Array": 1, "Float64Array": 1, "focus": 1,
+ "forward": 1, "frameElement": 1, "frames": 1, "fullScreen": 1,
+ "Function": 1, "Generator": 1, "getComputedStyle": 1,
+ "getInterface": 1, "getSelection": 1, "globalStorage": 1,
+ "history": 1, "home": 1, "HTMLBodyElement": 1, "HTMLCollection": 1,
+ "HTMLDivElement": 1, "HTMLDocument": 1, "HTMLElement": 1,
+ "HTMLHeadElement": 1, "HTMLHeadingElement": 1, "HTMLHtmlElement": 1,
+ "HTMLStyleElement": 1, "HTMLUnknownElement": 1, "Infinity": 1,
+ "innerHeight": 1, "innerWidth": 1, "InstallTrigger": 1,
+ "Int16Array": 1, "Int32Array": 1, "Int8Array": 1, "InternalError": 1,
+ "isFinite": 1, "isNaN": 1, "isXMLName": 1, "Iterator": 1,
+ "JSON": 1, "length": 1, "localStorage": 1, "Location": 1,
+ "location": 1, "locationbar": 1, "matchMedia": 1, "Math": 1,
+ "menubar": 1, "moveBy": 1, "moveTo": 1, "mozAnimationStartTime": 1,
+ "mozIndexedDB": 1, "mozInnerScreenX": 1, "mozInnerScreenY": 1,
+ "mozPaintCount": 1, "mozRequestAnimationFrame": 1, "name": 1,
+ "Namespace": 1, "NaN": 1, "navigator": 1, "netscape": 1,
+ "Node": 1, "NodeList": 1, "Number": 1, "Object": 1, "open": 1,
+ "openDialog": 1, "opener": 1, "outerHeight": 1, "outerWidth": 1,
+ "pageXOffset": 1, "pageYOffset": 1, "parent": 1, "parseFloat": 1,
+ "parseInt": 1, "performance": 1, "personalbar": 1, "pkcs11": 1,
+ "postMessage": 1, "print": 1, "prompt": 1, "QName": 1,
+ "RangeError": 1, "ReferenceError": 1, "RegExp": 1,
+ "releaseEvents": 1, "removeEventListener": 1, "resizeBy": 1,
+ "resizeTo": 1, "routeEvent": 1, "screen": 1, "screenX": 1,
+ "screenY": 1, "scroll": 1, "scrollbars": 1, "scrollBy": 1,
+ "scrollByLines": 1, "scrollByPages": 1, "scrollMaxX": 1,
+ "scrollMaxY": 1, "scrollTo": 1, "scrollX": 1, "scrollY": 1,
+ "self": 1, "sessionStorage": 1, "setInterval": 1, "setResizable": 1,
+ "setTimeout": 1, "showModalDialog": 1, "sizeToContent": 1,
+ "status": 1, "statusbar": 1, "stop": 1, "StopIteration": 1,
+ "StorageList": 1, "String": 1, "SyntaxError": 1, "Text": 1,
+ "toolbar": 1, "top": 1, "TypeError": 1, "Uint16Array": 1,
+ "Uint32Array": 1, "Uint8Array": 1, "Uint8ClampedArray": 1,
+ "undefined": 1, "unescape": 1, "uneval": 1, "updateCommands": 1,
+ "URIError": 1, "URL": 1, "WeakMap": 1, "Window": 1, "window": 1,
+ "XML": 1, "XMLList": 1, "XPCNativeWrapper": 1};
+
+// all predefined document properties.
+const documentPropertiesHash = {'activeElement': 1, 'addBinding': 1,
+ 'addEventListener': 1, 'adoptNode': 1, 'alinkColor': 1, 'anchors': 1,
+ 'appendChild': 1, 'applets': 1, 'ATTRIBUTE_NODE': 1, 'attributes': 1,
+ 'baseURI': 1, 'bgColor': 1, 'body': 1, 'captureEvents': 1,
+ 'CDATA_SECTION_NODE': 1, 'characterSet': 1, 'childNodes': 1, 'clear':
+ 1, 'cloneNode': 1, 'close': 1, 'COMMENT_NODE': 1,
+ 'compareDocumentPosition': 1, 'compatMode': 1, 'contentType': 1,
+ 'cookie': 1, 'createAttribute': 1, 'createAttributeNS': 1,
+ 'createCDATASection': 1, 'createComment': 1, 'createDocumentFragment':
+ 1, 'createElement': 1, 'createElementNS': 1, 'createEvent': 1,
+ 'createExpression': 1, 'createNodeIterator': 1, 'createNSResolver': 1,
+ 'createProcessingInstruction': 1, 'createRange': 1, 'createTextNode':
+ 1, 'createTreeWalker': 1, 'currentScript': 1, 'defaultView': 1,
+ 'designMode': 1, 'dir': 1, 'dispatchEvent': 1, 'doctype': 1,
+ 'DOCUMENT_FRAGMENT_NODE': 1, 'DOCUMENT_NODE': 1,
+ 'DOCUMENT_POSITION_CONTAINED_BY': 1, 'DOCUMENT_POSITION_CONTAINS': 1,
+ 'DOCUMENT_POSITION_DISCONNECTED': 1, 'DOCUMENT_POSITION_FOLLOWING': 1,
+ 'DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC': 1,
+ 'DOCUMENT_POSITION_PRECEDING': 1, 'DOCUMENT_TYPE_NODE': 1,
+ 'documentElement': 1, 'documentURI': 1, 'domain': 1, 'ELEMENT_NODE':
+ 1, 'elementFromPoint': 1, 'embeds': 1, 'enableStyleSheetsForSet': 1,
+ 'ENTITY_NODE': 1, 'ENTITY_REFERENCE_NODE': 1, 'evaluate': 1,
+ 'execCommand': 1, 'execCommandShowHelp': 1, 'fgColor': 1,
+ 'firstChild': 1, 'forms': 1, 'getAnonymousElementByAttribute': 1,
+ 'getAnonymousNodes': 1, 'getBindingParent': 1, 'getElementById': 1,
+ 'getElementsByClassName': 1, 'getElementsByName': 1,
+ 'getElementsByTagName': 1, 'getElementsByTagNameNS': 1,
+ 'getSelection': 1, 'getUserData': 1, 'hasAttributes': 1,
+ 'hasChildNodes': 1, 'hasFocus': 1, 'head': 1, 'images': 1,
+ 'implementation': 1, 'importNode': 1, 'inputEncoding': 1,
+ 'insertBefore': 1, 'isDefaultNamespace': 1, 'isEqualNode': 1,
+ 'isSameNode': 1, 'isSupported': 1, 'lastChild': 1, 'lastModified': 1,
+ 'lastStyleSheetSet': 1, 'linkColor': 1, 'links': 1,
+ 'loadBindingDocument': 1, 'localName': 1, 'location': 1,
+ 'lookupNamespaceURI': 1, 'lookupPrefix': 1, 'mozSetImageElement': 1,
+ 'mozSyntheticDocument': 1, 'namespaceURI': 1, 'nextSibling': 1,
+ 'nodeName': 1, 'nodeType': 1, 'nodeValue': 1, 'normalize': 1,
+ 'NOTATION_NODE': 1, 'open': 1, 'ownerDocument': 1, 'parentNode': 1,
+ 'plugins': 1, 'preferredStyleSheetSet': 1, 'prefix': 1,
+ 'previousSibling': 1, 'PROCESSING_INSTRUCTION_NODE': 1,
+ 'queryCommandEnabled': 1, 'queryCommandIndeterm': 1,
+ 'queryCommandState': 1, 'queryCommandSupported': 1,
+ 'queryCommandText': 1, 'queryCommandValue': 1, 'querySelector': 1,
+ 'querySelectorAll': 1, 'readyState': 1, 'referrer': 1,
+ 'releaseCapture': 1, 'releaseEvents': 1, 'removeBinding': 1,
+ 'removeChild': 1, 'removeEventListener': 1, 'replaceChild': 1,
+ 'routeEvent': 1, 'selectedStyleSheetSet': 1, 'setUserData': 1,
+ 'styleSheets': 1, 'styleSheetSets': 1, 'TEXT_NODE': 1, 'textContent':
+ 1, 'title': 1, 'URL': 1, 'vlinkColor': 1, 'write': 1, 'writeln': 1,
+ 'xmlEncoding': 1, 'xmlStandalone': 1, 'xmlVersion': 1};
+
+var relationChecker = {
+
+ // identifies scripts across modules.
+ scriptId: null,
+
+ // stores all left-side identifier in 'assign' types.
+ assignments: null,
+
+ // stores var declarations in global scope.
+ variableDeclarations: null,
+
+ // stores top declarations in global scope.
+ functionDeclarations: null,
+
+ nonWindowProperties: null,
+
+ init: function (scriptId) {
+ this.scriptId = scriptId;
+ this.assignments = [];
+ this.variableDeclarations = {};
+ this.functionDeclarations = {};
+ this.nonWindowProperties = {};
+ },
+
+ isWindowProperty: function (identifier) {
+ return (identifier in windowPropertiesHash) ? true : false;
+ },
+
+ isDocumentProperty: function (identifier) {
+ return (identifier in documentPropertiesHash) ? true : false;
+ },
+
+ storeNodeVars: function (n) {
+ if (n.varDecls != undefined) {
+ var i = 0, le = n.varDecls.length;
+ for (; i < le; i++) {
+ this.variableDeclarations[n.varDecls[i].value] = 1;
+ }
+ }
+ },
+
+ storeNodeFunctions: function (n) {
+ if (n.funDecls != undefined) {
+ var i = 0, le = n.funDecls.length;
+ for (; i < le; i++) {
+ this.functionDeclarations[n.funDecls[i].name] = 1;
+ }
+ }
+ },
+ storeGlobalDeclarations: function (topNode) {
+ this.storeNodeVars(topNode);
+ this.storeNodeFunctions(topNode);
+ },
+ storeNodeGlobalDeclarations: function (n) {
+ if (n.global === true) {
+ this.storeNodeVars(n);
+ this.storeNodeFunctions(n);
+ }
+ },
+
+ storeNodeAssignments: function (n) {
+ if (n.type === token.ASSIGN &&
+ n.children != undefined &&
+ n.children[0].type === token.IDENTIFIER) {
+ this.assignments.push(n.children[0].value);
+ }
+ },
+
+ // checks the parent script is in global scope.
+ isInGlobalScope: function (n) {
+ var currentNode = n;
+
+ while (currentNode != undefined) {
+ if (currentNode.type === token.SCRIPT &&
+ currentNode.global === true) {
+ return true;
+ } else if (currentNode.type === token.SCRIPT) {
+ return false;
+ }
+ currentNode = currentNode.parent;
+ }
+ },
+
+ // looks for an identifier being declared as either a
+ // variable or a function within the scope. Currently,
+ // we don't care about assignments.
+ lookForIdentifierInAllScopes: function (n, val) {
+
+ var currentNode = n, i, le, vars, funcs;
+ while (currentNode != undefined) {
+ if (currentNode.varDecls != undefined) {
+ vars = currentNode.varDecls;
+ le = vars.length;
+ for (i = 0; i < le; i++) {
+ if (vars[i].value === val) {
+ console.debug('FOUND declaration for', val);
+ return true;
+ }
+ }
+ }
+ if (currentNode.funDecls != undefined) {
+ funcs = currentNode.funDecls;
+ le = funcs.length;
+ for (i = 0; i < le; i++) {
+ if (funcs[i].name === val) {
+ console.debug('FOUND function declaration for', val);
+ return true;
+ }
+ }
+ }
+ currentNode = currentNode.parent;
+ }
+ console.debug('did not find declaration or assignment for', val);
+ },
+
+ // Heuristic method for window properties.
+ // this doesn't prove they are window properties, but
+ // it allows to make a good guess. These variables could have
+ // been assigned to something else...
+ checkIdentifierIsWindowProperty: function (n) {
+
+ if (n.type === token.IDENTIFIER &&
+ (n.parent.type === token.CALL ||
+ (n.parent.type === token.DOT &&
+ n.previous != undefined &&
+ (n.previous.type === token.THIS ||
+ (n.previous.type === token.IDENTIFIER &&
+ n.previous.value === 'window')))) &&
+ n.value in windowPropertiesHash) {
+
+ this.lookForIdentifierInAllScopes(n, n.value);
+
+ }
+
+ else if (n.type === token.IDENTIFIER &&
+ n.parent != undefined &&
+ n.parent.type === token.DOT &&
+ n.previous != undefined &&
+ n.previous.type === token.THIS &&
+ this.isInGlobalScope(n)) {
+ console.debug(n.type, 'use of this in the global scope, seems ok.', n.value);
+ }
+ else if (n.type === token.IDENTIFIER) {
+ // not found.
+ console.debug(n.type, 'probably not a window prop', n.value);
+ this.nonWindowProperties[n.value] = 1;
+ }
+ }
+
+};
+
+exports.relationChecker = function (scriptId) {
+ var obj = Object.create(relationChecker);
+ obj.init(scriptId);
+ return obj;
+};
diff --git a/lib/js_load_observer/js_load_observer.js b/lib/js_load_observer/js_load_observer.js
new file mode 100644
index 0000000..a8792e2
--- /dev/null
+++ b/lib/js_load_observer/js_load_observer.js
@@ -0,0 +1,143 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+var observerService = Cc["@mozilla.org/observer-service;1"]
+.getService(Ci.nsIObserverService);
+
+var acceptedScripts = require("script_entries/accepted_scripts").acceptedScripts;
+var allowedRef = require('http_observer/allowed_referrers').allowedReferrers;
+
+var urlHandler = require("url_handler/url_handler");
+
+var ScriptAnalyzer = function() {
+ // the URL of the current page.
+ this.pageURL = null;
+};
+
+/*
+ * analyzeScriptBeforeExec
+ *
+ * Ensure that the script is found in the acceptedScript before
+ * allowing to execute it.
+ *
+ */
+ScriptAnalyzer.prototype.analyzeScriptBeforeExec = function (e) {
+ if (typeof e.target.textContent === 'undefined') {
+ throw new TypeError('event.target must be a script element');
+ }
+ console.debug('analyzeScriptBeforeExec executed');
+
+ var script = e.target, isAccepted;
+ var text = script.src ?
+ script.src : script.textContent.substring(0,100);
+ var notif = require("ui/notification")
+ .createNotification(text).notification;
+ this.pageURL = urlHandler.removeFragment(script.ownerDocument.URL);
+ if (!allowedRef.urlInAllowedReferrers(this.pageURL)) {
+
+ if (script.src !== undefined && script.src !== '') {
+ isAccepted = this.checkExternalScript(script);
+ } else {
+ isAccepted = this.checkInlineScript(script);
+ }
+
+ if (isAccepted === false &&
+ !(/^(file:\/\/|chrome:\/\/|about\:)/.test(this.pageURL))
+ ) {
+ console.debug(this.pageURL);
+ // file:// types of pages are allowed.
+
+ // set invalid type so that the script is detected
+ // by LibreJS as blocked (although it's blocked using
+ // preventDefault().
+ script.setAttribute('type', 'librejs/blocked');
+ //script.setAttribute('data-librejs-blocked', 'dynamically');
+ script.setAttribute('data-librejs-blocked-src', script.src);
+ script.removeAttribute('src');
+
+ e.preventDefault();
+ return false;
+ } else {
+ console.debug("script is accepted", script.src);
+ script.setAttribute('data-librejs-accepted', 'dynamically');
+ return true;
+ }
+
+ } else {
+ return true;
+ }
+
+ return false;
+};
+
+ScriptAnalyzer.prototype.checkExternalScript = function (script) {
+ var url = urlHandler.resolve(this.pageURL, script.src);
+
+ if (this.isScriptAccepted(url, false)) {
+ // url in src attribute is found as accepted.
+ return true;
+ }
+
+ else {
+ // the script hasn't been accepted before.
+ // block it.
+ console.debug("script is not accepted", script.src);
+ return false;
+ }
+
+};
+
+ScriptAnalyzer.prototype.checkInlineScript = function (script) {
+ return this.isScriptAccepted(script.text, true);
+};
+
+ScriptAnalyzer.prototype.isScriptAccepted = function (contents, inline) {
+
+ if (!acceptedScripts.isFound(this.pageURL, {'inline': inline, 'contents': contents})) {
+ return false;
+ }
+
+ else {
+ return true;
+ }
+};
+
+var scriptAnalyzer = new ScriptAnalyzer();
+
+var jsLoadObserver = {
+ observe: function (domWindow) {
+ domWindow.document.addEventListener(
+ "beforescriptexecute",
+ scriptAnalyzer.analyzeScriptBeforeExec.bind(scriptAnalyzer),
+ false);
+ }
+};
+
+observerService.addObserver(jsLoadObserver,
+ 'content-document-global-created',
+ false);
+
+exports.removeJsLoadObserver = function () {
+ observerService.removeObserver(jsLoadObserver,
+ 'content-document-global-created');
+ console.debug('removing jsLoadObserver');
+};
diff --git a/lib/main.js b/lib/main.js
new file mode 100644
index 0000000..d042eb5
--- /dev/null
+++ b/lib/main.js
@@ -0,0 +1,70 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Uncomment the following to start debugging, or do it from about:config.
+// var name = "extensions.jid1-KtlZuoiikVfFew@jetpack.sdk.console.logLevel";
+// require("sdk/preferences/service").set(name, "all");
+
+const { Cc, Ci } = require("chrome");
+
+const librejsStorage = require("settings/storage").librejsStorage;
+let addonManage = require("addon_management/install_uninstall");
+let httpObserver = require("http_observer/http_request_observer");
+let prefObserver = require("pref_observer/pref_observer");
+let prefChange = require("addon_management/prefchange");
+let uiInfo = require("ui/ui_info");
+let scriptPanel = require("ui/script_panel.js");
+const removeHashCallback = require("js_checker/js_checker").removeHashCallback;
+
+require('ui');
+
+// set whitelist at startup.
+prefChange.init();
+var widgetIsOn = false;
+
+// read storage file.
+var cachedResult = librejsStorage.init();
+librejsStorage.generateCacheFromDB();
+
+exports.main = function(options, callbacks) {
+ if (options.loadReason === 'enable' ||
+ options.loadReason === 'install'
+ ) {
+ addonManage.onLoad();
+ }
+};
+
+exports.onUnload = addonManage.onUnload;
+exports.onLoad = addonManage.onLoad;
+
+var prefs = require('sdk/preferences/service');
+var isJavaScriptEnabled = prefs.get('javascript.enabled');
+if (!isJavaScriptEnabled) {
+ console.debug('JS disabled in add-on init');
+ // remove all http notifications
+ httpObserver.removeHttpObserver();
+ // TODO: the narcissus worker could also be stopped at this
+ // point, but I'm not doing that right now because I don't
+ // know how to re-enable it.
+ //narcissusWorker.stopWorker();
+} else {
+ console.debug('JS enabled in add-on init');
+}
+prefObserver.register();
diff --git a/lib/parser/narcissus_worker.js b/lib/parser/narcissus_worker.js
new file mode 100644
index 0000000..57f9920
--- /dev/null
+++ b/lib/parser/narcissus_worker.js
@@ -0,0 +1,79 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var data = require("sdk/self").data;
+var {Cu} = require("chrome");
+var {ChromeWorker} = Cu.import("resource://gre/modules/Services.jsm", null);
+var worker = new ChromeWorker(
+ data.url("chrome_worker/parser/parse.js"));
+
+var NarcissusWorker = function() {
+ this.worker = worker;
+ var that = this;
+
+ this.worker.onmessage = function(e) {
+ var jsChecker = require("js_checker/js_checker");
+
+ console.debug('onmessage', e.data.hash);
+ try {
+ console.debug('calling jsChecker.callbackHashResult() for hash:',
+ e.data.hash);
+ jsChecker.callbackHashResult(e.data.hash, e.data.tree);
+ } catch (x) {
+ console.debug('error on message', x);
+ jsChecker.callbackHashResult(e.data.hash, false);
+ }
+ jsChecker = null;
+ };
+
+ // Enabling the catch clause in data/chrome_worker/parser
+ // instead of here because we can get the hash from there.
+ /*this.worker.onerror = function (e) {
+ console.debug(
+ 'error', e.lineno,
+ 'in', e.filename,
+ 'e', e.message,
+ 'full message', e
+ );
+ // can't get hash from this context
+ that.worker.postMessage(JSON.stringify({'hash': null}));
+ };*/
+};
+
+NarcissusWorker.prototype.stopWorker = function() {
+ console.debug('stopping worker');
+ this.worker.postMessage('stop');
+};
+
+NarcissusWorker.prototype.parse = function(scriptText, hash) {
+ console.debug('parsing', hash);
+ try {
+ this.worker.postMessage(JSON.stringify({
+ 'code': scriptText,
+ 'hash': hash
+ }));
+ } catch (x) {
+ console.debug('error in lib/narcissus_worker.js', x, x.lineNumber);
+ }
+
+};
+
+var narcissusWorker = new NarcissusWorker();
+exports.narcissusWorker = narcissusWorker;
diff --git a/lib/pref_observer/pref_observer.js b/lib/pref_observer/pref_observer.js
new file mode 100644
index 0000000..60afb82
--- /dev/null
+++ b/lib/pref_observer/pref_observer.js
@@ -0,0 +1,71 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var {Cc, Ci} = require("chrome");
+const httpObserver = require("http_observer/http_request_observer");
+
+// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Preferences#Using_preference_observers
+var prefObserver = {
+ register: function() {
+ // First we'll need the preference services to look for preferences.
+ var prefService = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService);
+
+ // For this.branch we ask for the preferences for
+ // extensions.myextension. and children
+ this.branch = prefService.getBranch("javascript.");
+
+ // Finally add the observer.
+ this.branch.addObserver("", this, false);
+ },
+
+ unregister: function() {
+ this.branch.removeObserver("", this);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ // aSubject is the nsIPrefBranch we're observing (after appropriate QI)
+ // aData is the name of the pref that's been changed (relative to
+ // aSubject)
+ switch (aData) {
+ case "enabled":
+ var prefs = require('sdk/preferences/service');
+ var isJavaScriptEnabled = prefs.get('javascript.enabled');
+ if (!isJavaScriptEnabled) {
+ console.debug('JS disabled in observer');
+ // remove all http notifications
+ httpObserver.removeHttpObserver();
+
+ // TODO: the narcissus worker could also be stopped at this
+ // point, but I'm not doing that right now because I don't
+ // know how to re-enable it.
+ // narcissusWorker.stopWorker();
+ } else {
+ console.debug('JS enabled in observer');
+ httpObserver.startHttpObserver();
+ }
+ break;
+ }
+ }
+};
+
+exports.register = function() {
+ prefObserver.register();
+};
diff --git a/lib/preferences/preferences.js b/lib/preferences/preferences.js
new file mode 100644
index 0000000..2685484
--- /dev/null
+++ b/lib/preferences/preferences.js
@@ -0,0 +1,72 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+var data = require("sdk/self").data;
+var panel = require("sdk/panel");
+var simpleStorage = require("sdk/simple-storage");
+
+/*
+ *Create pref panel
+ */
+exports.prefpanel = panel.Panel({
+ contentURL: data.url("preferences_panel/preferences_panel.html"),
+ contentScriptFile: data.url("preferences_panel/contentscript.js"),
+ contentScriptWhen: "ready",
+ widht:400,
+ height:400,
+ onMessage: function(message) {
+ handlePrefPanelMessage(message);
+ },
+ onShow: function() {
+ getAllPrefPanelPrefs();
+ }
+});
+
+var prefpanel = exports.prefpanel;
+
+function handlePrefPanelMessage(message) {
+ var prefSplit = message.split(":");
+ if(prefSplit[0] == "SETPREF") {
+ simpleStorage.storage[prefSplit[1]] = prefSplit[2];
+ } else {
+ console.debug(message);
+ }
+}
+
+function getAllPrefPanelPrefs() {
+ var allprefnames = ['INCOMING PREFS', 'nolazy'];
+
+ var allprefvalues = [];
+
+ allprefvalues[0] = "INCOMINGPREFS";
+
+ for(var i = 1; i < allprefnames.length+1; i++) {
+ var pref = simpleStorage.storage[allprefnames[i]];
+
+ if(typeof(pref) == 'undefined') {
+ // If it doesn't exist yet, default to false and set it for the future
+ simpleStorage.storage[allprefnames[i]] = "false";
+ pref = "false";
+ }
+
+ allprefvalues[i] = allprefnames[i] + ":" + pref;
+ }
+
+ prefpanel.postMessage(allprefvalues);
+}
diff --git a/lib/script_entries/accepted_scripts.js b/lib/script_entries/accepted_scripts.js
new file mode 100644
index 0000000..1a8ac9a
--- /dev/null
+++ b/lib/script_entries/accepted_scripts.js
@@ -0,0 +1,67 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var allScripts = require('script_entries/all_scripts').allScripts;
+
+var AcceptedScripts = function() {
+ this.scripts = {};
+ this.truncateJsData = allScripts.truncateJsData;
+ this.getScripts = allScripts.getScripts;
+ this.isFound = allScripts.isFound;
+ this.returnWhenFound = allScripts.returnWhenFound;
+ this.getOrInitScripts = allScripts.getOrInitScripts;
+ this.setHash = allScripts.setHash;
+};
+
+AcceptedScripts.prototype.clearScripts = function (url) {
+ this.scripts[url] = [];
+};
+
+/**
+ * addAScript
+ * adds a single script to the scripts array.
+ * @param {string} url - the url of the page where it is loaded.
+ * @param {object} scriptObj - Additional data regarding this script,
+ * including: inline: boolean,
+ * contents: string,
+ * removalReason: string.
+ */
+AcceptedScripts.prototype.addAScript = function (url, scriptObj) {
+ var exists;
+
+ if (this.scripts[url] === undefined) {
+ this.clearScripts(url);
+ }
+
+ // check if content is actually js code.
+ if (scriptObj.inline === true) {
+ this.setHash(scriptObj);
+ this.truncateJsData(scriptObj);
+ }
+ exists = this.isFound(url, scriptObj);
+ if (!exists) {
+ this.scripts[url].push(scriptObj);
+ return true;
+ } else {
+ return false;
+ }
+};
+
+exports.acceptedScripts = new AcceptedScripts();
diff --git a/lib/script_entries/all_scripts.js b/lib/script_entries/all_scripts.js
new file mode 100644
index 0000000..c08b4ac
--- /dev/null
+++ b/lib/script_entries/all_scripts.js
@@ -0,0 +1,108 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var crypto = require('script_entries/crypto');
+
+var AllScripts = function() {
+ this.scripts = {};
+};
+
+AllScripts.prototype.truncateJsData = function (scriptObj) {
+ if (scriptObj.contents === undefined) {
+ console.debug('this is an inline script', scriptObj.value);
+ console.debug('this is an src', scriptObj.url);
+ }
+ if (scriptObj.contents.length > 1000) {
+ scriptObj.contents = scriptObj.contents.substring(0, 1000);
+ scriptObj.contents += '…';
+ }
+};
+
+AllScripts.prototype.setHash = function (scriptObj) {
+ scriptObj.hash = crypto.sha1Encrypt(scriptObj.contents);
+ return scriptObj.hash;
+};
+
+AllScripts.prototype.getScripts = function (url) {
+ if (!this.scripts[url]) {
+ return false;
+ } else {
+ return this.scripts[url];
+ }
+};
+
+AllScripts.prototype.reverseArray = function (url) {
+ this.scripts[url].reverse();
+};
+
+AllScripts.prototype.getOrInitScripts = function (url) {
+ if (this.scripts[url] === undefined) {
+ this.scripts[url] = [];
+ }
+ return this.scripts[url];
+};
+
+AllScripts.prototype.returnWhenFound = function(url, data) {
+ var pageScripts = this.getOrInitScripts(url),
+ i = 0,
+ le = pageScripts.length;
+
+ // check that entry doesn't exist.
+ if (data.inline === false) {
+ for (; i < le; i++) {
+ if (pageScripts[i].contents === data.url) {
+ return pageScripts[i];
+ }
+ }
+ } else if (data.inline === true) {
+ for (; i < le; i++) {
+ if (pageScripts[i].hash === crypto.sha1Encrypt(data.contents)) {
+ return pageScripts[i];
+ }
+ }
+ }
+
+ return false;
+};
+
+AllScripts.prototype.isFound = function(url, data) {
+ var pageScripts = this.getOrInitScripts(url),
+ i = 0,
+ le = pageScripts.length;
+
+ // check that entry doesn't exist.
+ if (data.inline === false) {
+ for (; i < le; i++) {
+ if (pageScripts[i].url === data.url) {
+ return true;
+ }
+ }
+ } else if (data.inline === true) {
+ for (; i < le; i++) {
+ if (pageScripts[i].hash === crypto.sha1Encrypt(data.contents)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+};
+
+exports.allScripts = new AllScripts();
diff --git a/lib/script_entries/crypto.js b/lib/script_entries/crypto.js
new file mode 100644
index 0000000..e8844b1
--- /dev/null
+++ b/lib/script_entries/crypto.js
@@ -0,0 +1,60 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var {Cc, Ci, Cu, Cm, Cr} = require("chrome");
+
+var CryptoString = function() {
+ this.cryptoHash = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ this.converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ this.hashAlgorithm = null;
+};
+
+CryptoString.prototype.init = function(hashAlgorithm, charset) {
+ this.converter.charset = charset;
+ this.hashAlgorithm = hashAlgorithm;
+ this.cryptoHash.init(this.cryptoHash[this.hashAlgorithm]);
+
+};
+
+CryptoString.prototype.encryptString = function(str) {
+ var result = {};
+ var data = this.converter.convertToByteArray(str, result);
+ this.cryptoHash.update(data, data.length);
+ var hash = this.cryptoHash.finish(false);
+ return [this.toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+};
+
+CryptoString.prototype.toHexString = function(charCode) {
+ return ("0" + charCode.toString(16)).slice(-2);
+};
+
+var cryptoString = new CryptoString();
+
+exports.sha1Encrypt = function(str) {
+ cryptoString.init('SHA1', 'UTF-8');
+ return cryptoString.encryptString(str);
+};
+
+exports.sha256Encrypt = function(str) {
+ cryptoString.init('SHA256', 'UTF-8');
+ return cryptoString.encryptString(str);
+};
diff --git a/lib/script_entries/dryrun_scripts.js b/lib/script_entries/dryrun_scripts.js
new file mode 100644
index 0000000..d32263b
--- /dev/null
+++ b/lib/script_entries/dryrun_scripts.js
@@ -0,0 +1,76 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var allScripts = require('script_entries/all_scripts').allScripts;
+const urlHandler = require('url_handler/url_handler');
+
+exports.dryRunScripts = {
+ scripts: {},
+
+ truncateJsData: allScripts.truncateJsData,
+
+ getScripts: allScripts.getScripts,
+
+ isFound: allScripts.isFound,
+
+ returnWhenFound: allScripts.returnWhenFound,
+
+ getOrInitScripts: allScripts.getOrInitScripts,
+
+ reverseArray: allScripts.reverseArray,
+
+ setHash: allScripts.setHash,
+
+ clearScripts: function (url) {
+ this.scripts[url] = [];
+ },
+
+ /**
+ * addAScript
+ * adds a single script to the scripts array.
+ * @param {string} url - the url of the page where it is loaded.
+ * @param {object} scriptObj - Additional data regarding this script,
+ * including: inline: boolean,
+ * contents: string,
+ * removalReason: string.
+ */
+ addAScript: function (url, scriptObj, absoluteUrl) {
+ var exists;
+
+ if (this.scripts[url] === undefined) {
+ this.clearScripts(url);
+ }
+ if (scriptObj.inline === true) {
+ this.setHash(scriptObj);
+ this.truncateJsData(scriptObj);
+ } else if (absoluteUrl !== undefined &&
+ scriptObj.inline === false) {
+ scriptObj.contents = urlHandler.resolve(absoluteUrl, scriptObj.contents);
+ }
+ exists = this.isFound(url, scriptObj);
+
+ if (!exists) {
+ this.scripts[url].push(scriptObj);
+ return true;
+ } else {
+ return false;
+ }
+ }
+};
diff --git a/lib/script_entries/free_libraries.js b/lib/script_entries/free_libraries.js
new file mode 100644
index 0000000..ee97eea
--- /dev/null
+++ b/lib/script_entries/free_libraries.js
@@ -0,0 +1,65 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// THIS MODULE IS DEPRECATED IN FAVOR OF THE NEW WHITELISTING MODULE (LibreJS 6.0)
+
+var relationChecker = require("js_checker/relation_checker").relationChecker;
+var checkTypes = require("js_checker/constant_types").checkTypes;
+var scriptsCached = require("./scripts_cache").scriptsCached;
+
+
+// find the json database path.
+var dbContents = require("sdk/self").data.load("script_libraries/script-libraries.json");
+
+const AUTHOR_REASON = "This script has been tagged as free software by LibreJS authors.";
+
+var freeLibraries = JSON.parse(dbContents); /* a database of the free libraries recognized by default */
+
+/*
+ * List of free libraries and their SHA256 hash.
+ * This is used to recognize the most common free libraries.
+ */
+
+var init = function () {
+
+ // relationChecker, which roughly checks if variables are window
+ // variables or not, is useless in this case. Use the same
+ // object for all entries.
+ var rc = relationChecker();
+ var library, hash;
+ var freeObj = { "type": 4, "reason": AUTHOR_REASON};
+ console.debug("Building init");
+ for (hash in freeLibraries) {
+ library = freeLibraries[hash];
+
+ // assign empty relationChecker object.
+ library.relationChecker = rc;
+
+ // make them free and nontrivial.
+ library.result = freeObj;
+
+ scriptsCached.addObjectEntry(hash, library);
+ }
+};
+
+//init();
+
+exports.init = init;
+exports.AUTHOR_REASON = AUTHOR_REASON;
diff --git a/lib/script_entries/removed_scripts.js b/lib/script_entries/removed_scripts.js
new file mode 100644
index 0000000..8f65c4d
--- /dev/null
+++ b/lib/script_entries/removed_scripts.js
@@ -0,0 +1,71 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var allScripts = require('script_entries/all_scripts').allScripts;
+const urlHandler = require('url_handler/url_handler');
+
+var RemovedScripts = function() {
+ this.scripts = {};
+ this.truncateJsData = allScripts.truncateJsData;
+ this.getScripts = allScripts.getScripts;
+ this.isFound = allScripts.isFound;
+ this.returnWhenFound = allScripts.returnWhenFound;
+ this.getOrInitScripts = allScripts.getOrInitScripts;
+ this.reverseArray = allScripts.reverseArray;
+ this.setHash = allScripts.setHash;
+};
+
+RemovedScripts.prototype.clearScripts = function (url) {
+ this.scripts[url] = [];
+};
+
+/**
+ * addAScript
+ * adds a single script to the scripts array.
+ * @param {string} url - the url of the page where it is loaded.
+ * @param {object} scriptObj - Additional data regarding this script,
+ * including: inline: boolean,
+ * contents: string,
+ * removalReason: string.
+ */
+RemovedScripts.prototype.addAScript = function (url, scriptObj, absoluteUrl) {
+ var exists;
+
+ if (this.scripts[url] === undefined) {
+ this.clearScripts(url);
+ }
+ if (scriptObj.inline === true) {
+ this.setHash(scriptObj);
+ this.truncateJsData(scriptObj);
+ } else if (absoluteUrl !== undefined &&
+ scriptObj.inline === false) {
+ scriptObj.contents = urlHandler.resolve(absoluteUrl, scriptObj.contents);
+ }
+ exists = this.isFound(url, scriptObj);
+
+ if (!exists) {
+ this.scripts[url].push(scriptObj);
+ return true;
+ } else {
+ return false;
+ }
+};
+
+exports.removedScripts = new RemovedScripts();
diff --git a/lib/script_entries/scripts_cache.js b/lib/script_entries/scripts_cache.js
new file mode 100644
index 0000000..ca477cc
--- /dev/null
+++ b/lib/script_entries/scripts_cache.js
@@ -0,0 +1,189 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+var relationCheckerObj = require("js_checker/relation_checker").relationChecker;
+
+// import free_libraries to populate the cache hash map.
+var free_libraries = require("script_entries/free_libraries");
+
+var crypto = require('script_entries/crypto');
+const checkTypes = require("js_checker/constant_types").checkTypes;
+
+// cachedResults contains objects with result/relationChecker for
+// scripts entries indexed by SHA1sum
+var cachedResults = {};
+
+/**
+ * ScriptsCached keeps a cache of whitelisted scripts in the browser
+ * session.
+ */
+var ScriptsCached = function() {
+};
+
+ScriptsCached.prototype.getHash = function(scriptText) {
+ require('ui/notification').createNotification(scriptText.substring(0,100));
+
+ return crypto.sha1Encrypt(scriptText);
+};
+
+/**
+ * resetCache
+ * Resets the full cache and re-initialize
+ * the free libraries list.
+ */
+ScriptsCached.prototype.resetCache = function () {
+ cachedResults = {};
+ free_libraries.init();
+};
+
+/**
+ *
+ * addEntry
+ *
+ * Adds a script entry to the cache by providing the results
+ * and the actual script text.
+ *
+ */
+ScriptsCached.prototype.addEntry = function(
+ scriptText, result, relationChecker, allowTrivial, url) {
+ console.debug("result addEntry is", JSON.stringify(result));
+ cachedResults[this.getHash(scriptText)] = {
+ 'result': result,
+ 'relationChecker': relationCheckerObj(),
+ 'allowTrivial': allowTrivial,
+ 'url': url
+ };
+};
+
+/**
+ *
+ * addEntry
+ *
+ * Adds a script entry to the cache by providing the results
+ * using the script's hash.
+ *
+ */
+ScriptsCached.prototype.addEntryByHash = function(
+ hash, result, relationChecker, allowTrivial, url) {
+ cachedResults[hash] = {
+ 'result': result,
+ 'relationChecker': relationCheckerObj(),
+ 'allowTrivial': allowTrivial,
+ 'url': url || ''
+ };
+};
+
+/**
+ * removeEntryByHash
+ *
+ * Remove an entry from the cache using hash key.
+ */
+ScriptsCached.prototype.removeEntryByHash = function(hash) {
+ delete cachedResults[hash];
+};
+
+/**
+ * addEntryIfNotCached
+ *
+ * Checks first if entry is cached before attempting to cache result.
+ */
+ScriptsCached.prototype.addEntryIfNotCached = function(
+ scriptText, result, relationChecker, allowTrivial, url) {
+ // save a bit of computing by getting hash once.
+ var hash = this.getHash(scriptText);
+ console.debug('hash is then', hash);
+ if (!this.isCached(scriptText, hash)) {
+ cachedResults[hash] = {
+ 'result': result,
+ 'relationChecker': relationCheckerObj(),
+ 'allowTrivial': allowTrivial,
+ 'url': url || ''
+ };
+ }
+ return hash;
+};
+
+/**
+ *
+ * addObjectEntry
+ *
+ * Adds a script entry by providing an object.
+ * Used to provide free library hashes from free_libraries.js
+ *
+ */
+ScriptsCached.prototype.addObjectEntry = function(hash, script) {
+ cachedResults[hash] = script;
+};
+
+ScriptsCached.prototype.isCached = function(scriptText, hash) {
+ var scriptHash;
+ console.debug("Is CACHED start?");
+ try {
+ if (typeof hash === 'string') {
+ scriptHash = hash;
+ } else {
+ scriptHash = this.getHash(scriptText);
+ }
+ if (typeof scriptHash === 'string') {
+ let cachedResult = cachedResults[scriptHash];
+ if (cachedResult) {
+ // exact copy of file has already been cached.
+ console.debug('scriptHash is', cachedResult);
+ if (cachedResult.relationChecker == "[rl]") {
+ cachedResult.relationChecker = {}; //relationCheckerObj();
+ }
+ console.debug("Is Cached ENd TRUE");
+ return cachedResult;
+ }
+ }
+ return false;
+ } catch (e) {
+ console.debug("an error", scriptHash, e, e.linenumber, e.filename);
+ }
+};
+
+/**
+ * Writes allowed scripts to the cache.
+ * nonfree/nontrivial scripts are not added to the cache.
+ */
+ScriptsCached.prototype.getCacheForWriting = function() {
+ var formattedResults = {};
+ for (let item in cachedResults) {
+ let type = cachedResults[item].result.type;
+ if (type != checkTypes.NONTRIVIAL &&
+ type != checkTypes.TRIVIAL_DEFINES_FUNCTION
+ ) {
+ formattedResults[item] = cachedResults[item];
+ }
+ }
+ return formattedResults;
+};
+
+/**
+ * Import data from database into cachedResults.
+ * Calling this function replaces the current cache if it exists.
+ */
+ScriptsCached.prototype.bulkImportCache = function(data) {
+ cachedResults = data;
+ console.debug("Imported data. Number of keys ISSS ",
+ Object.keys(cachedResults).length);
+ console.debug("It looks like ", JSON.stringify(cachedResults));
+};
+
+exports.scriptsCached = new ScriptsCached();
diff --git a/lib/settings/settings.js b/lib/settings/settings.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/settings/settings.js
diff --git a/lib/settings/settings_tab.js b/lib/settings/settings_tab.js
new file mode 100644
index 0000000..bb9a3f6
--- /dev/null
+++ b/lib/settings/settings_tab.js
@@ -0,0 +1,78 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+const tabs = require("sdk/tabs");
+const data = require("sdk/self").data;
+const storage = require("settings/storage").librejsStorage;
+const scriptsCached = require("script_entries/scripts_cache").scriptsCached;
+
+exports.settingsManager = settingsManager;
+
+let settingsManager = {
+ settingsTab: {
+ url: data.url("settings/index.html"),
+ onReady: function (tab) {
+ console.debug("populating form");
+ var that = this;
+ let cache_data = scriptsCached.getCacheForWriting();
+ let worker = tab.attach({
+ contentScriptFile: [
+ data.url('settings/js/pagescript-listener.js'),
+ data.url('settings/js/pagescript-emitter.js')
+ ]
+ });
+ worker.port.emit("populate-form", cache_data);
+ worker.port.on("rules-form-delete", function (hash) {
+ try {
+ scriptsCached.removeEntryByHash(hash);
+ } catch (e) {
+ console.log('caught!', e, e.lineNumber, e.filename);
+ }
+ //worker.port.emit("populate-form", scriptsCached.getCacheForWriting());
+ });
+ worker.port.on("rules-form-delete-all", function () {
+ scriptsCached.resetCache();
+ });
+ },
+ onActivate: function (tab) {
+ // just reload the form.
+ console.debug("Tab is activated again");
+ var that = this;
+ let cache_data = scriptsCached.getCacheForWriting();
+ let worker = tab.attach({
+ contentScriptFile: [
+ data.url('settings/js/pagescript-listener.js'),
+ data.url('settings/js/pagescript-emitter.js')
+ ]
+ });
+ worker.port.emit("populate-form", cache_data);
+ }
+ },
+
+ init: function () {
+ settings.onLoad(function (data) {});
+ },
+
+ open: function () {
+ console.debug("settings tab data url is", this.settingsTab.url);
+ tabs.open(this.settingsTab);
+ }
+};
+
+exports.settingsManager = settingsManager;
diff --git a/lib/settings/storage.js b/lib/settings/storage.js
new file mode 100644
index 0000000..a576781
--- /dev/null
+++ b/lib/settings/storage.js
@@ -0,0 +1,175 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+/*jshint esnext: true */
+
+const { Cc, Ci, Cu, components } = require("chrome");
+
+var { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm");
+var { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm");
+var relationChecker = require("js_checker/relation_checker").relationChecker;
+var rc = relationChecker(); // a dummy object for legacy module.
+const AUTHOR_REASON = require("script_entries/free_libraries").AUTHOR_REASON;
+var relationChecker = require("js_checker/relation_checker").relationChecker;
+
+const scriptsCached = require("script_entries/scripts_cache").scriptsCached;
+
+let librejsStorage = {
+
+ file: null,
+ filename: 'librejs-whitelist.json',
+ data: [],
+
+ onLoad: function (callback) {
+ // will read the json file.
+ this.init();
+ this.read(callback);
+ },
+
+ init: function () {
+ // get the "librejs-whitelist.json" file in the profile directory
+ this.file = FileUtils.getFile("ProfD", [this.filename]);
+ },
+
+ read: function (callback) {
+ // Content type hint is useful on mobile platforms where the filesystem
+ // would otherwise try to determine the content type.
+ var channel = NetUtil.newChannel(this.file);
+ var that = this;
+ channel.contentType = "application/json";
+ try {
+ NetUtil.asyncFetch(channel, function(inputStream, status) {
+
+ if (!components.isSuccessCode(status)) {
+ require("script_entries/free_libraries").init();
+ that.initialWrite();
+ }
+
+ var raw_data = NetUtil.readInputStreamToString(
+ inputStream, inputStream.available());
+ // expand json file back to original contents.
+ var re = new RegExp(
+ "[freelib]".replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),
+ 'g'
+ );
+ raw_data = raw_data.replace(re, AUTHOR_REASON);
+ //console.debug("raw_data is ", raw_data);
+ // The file data is contained within inputStream.
+ // You can read it into a string with
+ // get string into json also
+ that.data = JSON.parse(raw_data);
+
+ callback(that.data);
+ });
+ } catch (e) {
+ that.initialWrite();
+ }
+ },
+
+ initialWrite: function (callback) {
+ console.debug("About to write free libraries");
+ // our file is not populated with default contents.
+ // use free_libraries.js to populate.
+ require("script_entries/free_libraries").init();
+ this.writeCacheToDB(callback);
+ },
+
+ /**
+ * writes the contents of scriptsCached to the persistent
+ * JSON file.
+ */
+ writeCacheToDB: function (callback) {
+ console.debug("writing to db");
+ data = scriptsCached.getCacheForWriting();
+ json = JSON.stringify(data);
+
+ // make json file smaller.
+ var re = new RegExp(
+ AUTHOR_REASON.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g');
+ json = json.replace(re, "[freelib]");
+
+ var rc = JSON.stringify(relationChecker());
+ re = new RegExp(rc.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g');
+ json = json.replace(re, "\"[rl]\"");
+ //console.debug("this.data is now", this.data);
+ this.write(callback, json);
+ },
+ generateCacheFromDB: function (callback) {
+ if (typeof callback === 'undefined') {
+ callback = function () {
+ // nothing to do.
+ };
+ }
+ this.read(function (data) {
+ scriptsCached.bulkImportCache(data);
+ });
+ },
+ write: function (onDataWritten, json) {
+
+ this.init();
+ var str;
+ if (typeof json === 'undefined') {
+ str = JSON.stringify(this.data);
+ } else {
+ // we are passing json already formatted.
+ str = json;
+ }
+ var ostream = FileUtils.openSafeFileOutputStream(this.file);
+ var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ var istream = converter.convertToInputStream(str);
+ // The last argument (the callback) is optional.
+ NetUtil.asyncCopy(istream, ostream, function(status) {
+ if (!components.isSuccessCode(status)) {
+ // Handle error!
+ return;
+ }
+ if (!onDataWritten) {
+ console.debug("onDataWritten is not defined");
+ onDataWritten = function () {
+ console.debug("onDataWritten dummy callback triggered");
+ };
+ }
+ // Data has been written to the file.
+ onDataWritten();
+ });
+ },
+
+ /**
+ * getEntry -- Returns a storage entry if it is present.
+ */
+ getEntry: function (hash) {
+ var entry = this.data[hash];
+ if (entry) {
+ if (entry.result === '[freelib]') {
+ entry.result = {
+ 'type': 4,
+ 'reason': 'This script has been tagged as free ' +
+ 'software by LibreJS authors.'
+ };
+ }
+ entry.relationChecker = rc;
+ return entry;
+ }
+ return false;
+ }
+};
+
+exports.librejsStorage = librejsStorage;
diff --git a/lib/ui.js b/lib/ui.js
new file mode 100644
index 0000000..8280a3b
--- /dev/null
+++ b/lib/ui.js
@@ -0,0 +1,186 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+let data = require("sdk/self").data;
+let panel = require('sdk/panel');
+let tabs = require("sdk/tabs");
+let {getMostRecentBrowserWindow} = require('sdk/window/utils');
+let {ToggleButton} = require('sdk/ui/button/toggle');
+
+let settings_tab = require('settings/settings_tab');
+let allowedRef = require("http_observer/allowed_referrers").allowedReferrers;
+let urlHandler = require("url_handler/url_handler");
+let removedScripts = require("script_entries/removed_scripts").removedScripts;
+let acceptedScripts = require("script_entries/accepted_scripts")
+ .acceptedScripts;
+let jsLabelsPagesVisited = require('html_script_finder/web_labels/js_web_labels')
+ .jsLabelsPagesVisited;
+let dryRunScripts = require("script_entries/dryrun_scripts").dryRunScripts;
+let types = require("js_checker/constant_types");
+
+// move to sub-module later
+const scriptsCached = require("script_entries/scripts_cache").scriptsCached;
+
+function generateDataURI (encodedText) {
+ return "data:text/html;charset=UTF-8;base64," + encodedText;
+}
+
+/**
+ * UI
+ *
+ * A singleton that starts the user interface content scripts.
+ */
+let UI = exports.UI = {
+ init: function() {
+ var mainPanel = panel.Panel({
+ contentURL: data.url('display_panel/content/display-panel.html'),
+ width: 800,
+ height: 500,
+ contentScriptFile: [
+ data.url('settings/third-party/jquery/jquery.min.js'),
+ data.url('display_panel/main_panel.js')
+ ],
+ onShow: this.showPanelContent,
+ onHide: function() {
+ toggleButton.state('window', { checked: false });
+ }
+ });
+
+ mainPanel.port.on('hideMainPanel', function () {
+ mainPanel.hide();
+ });
+
+ mainPanel.port.on('allowAllClicked', function (url) {
+ url = urlHandler.removeFragment(url);
+ allowedRef.addPage(url);
+ tabs.activeTab.reload();
+ });
+
+ mainPanel.port.on('disallowAllClicked', function (url) {
+ console.debug('url is', url);
+ url = urlHandler.removeFragment(url);
+ console.debug('before clear, url is in allowedRef',
+ allowedRef.urlInAllowedReferrers(url));
+ allowedRef.clearSinglePageEntry(url);
+ console.debug('after clear, url is in allowedRef',
+ allowedRef.urlInAllowedReferrers(url));
+ mainPanel.hide();
+ tabs.activeTab.reload();
+ });
+
+ mainPanel.port.on('openInTab', function (text) {
+ var str = generateDataURI(text);
+ tabs.open(str);
+ mainPanel.hide();
+ });
+
+ mainPanel.port.on('whitelistByHash', function(
+ hash, url, name, reason
+ ) {
+ console.debug("hash is", hash);
+ url = urlHandler.removeFragment(url);
+ /* var cached_result = scriptsCached.isCached(hash);
+ if (cached_results) {
+ reason = cached_result['reason'];
+ }*/
+ scriptsCached.addEntryByHash(
+ hash, types.whitelisted(reason), {}, true, url);
+ });
+
+ mainPanel.port.on('removeFromWhitelistByHash', function (hash) {
+ scriptsCached.removeEntryByHash(hash);
+ removeHashCallback(hash);
+ });
+
+ mainPanel.port.on('openSesame', function () {
+ // open the settings tab.
+ settings_tab.settingsManager.open();
+ mainPanel.hide();
+ });
+
+ var toggleButton = ToggleButton({
+ id: 'librejs-toggle-switch',
+ label: 'LibreJS',
+ icon: {
+ '16': './widget/images/librejs.png',
+ '32': './widget/images/librejs-32.png',
+ '64': './widget/images/librejs-64.png'
+ },
+ panel: mainPanel,
+ onChange: function(state) {
+ if (state.checked) {
+ mainPanel.show({
+ position: toggleButton
+ });
+ }
+ }
+ });
+
+ var menuitem = require("menuitems").Menuitem({
+ id: 'librejs_settings',
+ menuid: 'menu_ToolsPopup',
+ label: 'LibreJS Whitelist',
+ onCommand: function() {
+ settings_tab.settingsManager.open();
+ },
+ insertBefore: "menu_pageInfo"
+ });
+ },
+
+ showPanelContent: function() {
+ let that = this;
+ var message, externalEntries,
+ externalScripts, urlTabIndex, tabData;
+
+ var worker = tabs.activeTab.attach({
+ contentScriptFile: [
+ data.url('complain/contact_regex.js'),
+ data.url('complain/link_types.js'),
+ data.url('settings/third-party/jquery/jquery.min.js'),
+ data.url('complain/contact_finder.js'),
+ data.url('complain/pagemod_finder.js'),
+ data.url('script_detector/script_detector.js')
+ ],
+ contentScriptWhen: 'ready',
+ onMessage: function(respData) {
+ var url = urlHandler.removeFragment(tabs.activeTab.url);
+
+ if (respData.event === 'scriptsFetched') {
+ var scriptsData = {
+ 'jsLabelsPagesVisited': jsLabelsPagesVisited,
+ 'removed': removedScripts.getScripts(url),
+ 'accepted': acceptedScripts.getScripts(url),
+ 'dryRun': dryRunScripts.getScripts(url)
+ };
+ that.postMessage({
+ 'pageURL': url,
+ 'urlData': scriptsData,
+ 'isAllowed': allowedRef.urlInAllowedReferrers(url)
+ });
+ worker.port.emit('pageUrl', url);
+ } else {
+ that.postMessage(respData);
+ }
+ }
+ });
+ }
+};
+
+UI.init();
diff --git a/lib/ui/notification.js b/lib/ui/notification.js
new file mode 100644
index 0000000..35aceb9
--- /dev/null
+++ b/lib/ui/notification.js
@@ -0,0 +1,76 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// this module is used to display a notification when LibreJS
+// is running to inform the user it is indeed busy working.
+const timer = require("sdk/timers");
+const self = require("sdk/self");
+const isDisplayNotifications = require("addon_management/prefchange")
+ .isDisplayNotifications;
+
+exports.createCriticalNotification = function (text) {
+ if (text === undefined) {
+ text = "";
+ }
+ var self = require('sdk/self');
+ var notif = require("notification-box").NotificationBox({
+ 'value': 'librejs-critical-notification-js-web-labels',
+ 'label': text,
+ 'priority': 'CRITICAL_LOW',
+ 'image': self.data.url("assets/images/torchy2.png"),
+ });
+ return notif;
+};
+
+var fakeNotification = {
+ 'close': function () {
+ return;
+ }
+};
+
+exports.createNotification = function (jsValue) {
+ if (!isDisplayNotifications()) {
+ return fakeNotification;
+ }
+ if (jsValue === undefined) {
+ jsValue = "";
+ }
+ var self = require('sdk/self');
+ var notif = require("notification-box").NotificationBox({
+ 'value': 'librejs-message',
+ 'label': 'LibreJS is analyzing: ' + jsValue + " ...",
+ 'priority': 'INFO_LOW',
+ 'image': self.data.url("assets/images/torchy2.png"),
+ /*'buttons': [{'label': "Fine",
+ 'onClick': function () { }}]*/
+ });
+ timer.setTimeout(function () {
+ // ensure notifications are ALWAYS removed at some point.
+ console.debug("removing after 2 seconds");
+ try {
+ var n = notif.notificationbox
+ .getNotificationWithValue('librejs-message');
+ n.close();
+ } catch(x) {
+ // do nothing
+ }
+ }, 2000);
+ return notif;
+};
diff --git a/lib/ui/script_panel.js b/lib/ui/script_panel.js
new file mode 100644
index 0000000..85c22a4
--- /dev/null
+++ b/lib/ui/script_panel.js
@@ -0,0 +1,73 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+var urlHandler = require("url_handler/url_handler");
+
+/**
+ * addScriptsToPanelList
+ *
+ * Looks for scripts that are either valid or flagged with libreJS
+ *
+ */
+exports.addScriptsToPanelList = function (url, respData) {
+
+ var panelRemovedScripts = [];
+ var panelAcceptedScripts = [];
+ var panelDryRunScripts = [];
+
+ // use url. remove fragment.
+ url = urlHandler.removeFragment(url);
+ var contents, i, reason;
+
+ for (i = 0; i < respData.value.blocked.length; i++) {
+ // if external script only.
+ pathToUrl(respData.value.blocked[i], url);
+ panelRemovedScripts.push(respData.value.blocked[i]);
+ }
+
+ for (i = 0; i < respData.value.accepted.length; i++) {
+
+ // if external script only.
+ pathToUrl(respData.value.accepted[i], url);
+ panelAcceptedScripts.push(respData.value.accepted[i]);
+ }
+ for (i = 0; i < respData.value.dryRun.length; i++) {
+ // if external script only.
+ pathToUrl(respData.value.dryRun[i], url);
+ panelDryRunScripts.push(respData.value.dryRun[i]);
+ }
+
+ return {'removed': panelRemovedScripts,
+ 'accepted': panelAcceptedScripts,
+ 'dryRun': panelDryRunScripts};
+};
+
+
+/**
+ * pathToUrl
+ *
+ * convert a relative path to a url.
+ *
+ */
+var pathToUrl = function (scriptEntry, url) {
+ if (scriptEntry.inline === false) {
+ scriptEntry.url = urlHandler.resolve(url, scriptEntry.url);
+ }
+};
diff --git a/lib/ui/ui_info.js b/lib/ui/ui_info.js
new file mode 100644
index 0000000..55707d9
--- /dev/null
+++ b/lib/ui/ui_info.js
@@ -0,0 +1,202 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// page mod executing content script at every page load.
+
+var data = require("sdk/self").data;
+var pageMod = require("sdk/page-mod");
+var urlHandler = require("url_handler/url_handler");
+var pageWorker = require("sdk/page-worker");
+var tabs = require("sdk/tabs");
+var prefs = require("addon_management/prefchange");
+
+// contain list of recently found contact links or email addresses.
+var contactList = {};
+
+// constants. Also available in lib/ui_info.js
+const CERTAIN_EMAIL_ADDRESS_FOUND = 'certainEmailAddressFound';
+const UNCERTAIN_EMAIL_ADDRESS_FOUND = 'uncertainEmailAddresFound';
+
+// Looking for contact links
+const CERTAIN_LINK_FOUND = 'certainLinkFound';
+const PROBABLE_LINK_FOUND = 'probableLinkFound';
+const UNCERTAIN_LINK_FOUND = 'uncertainLinkFound';
+const LINK_NOT_FOUND = 'contactLinkNotFound';
+
+// Looking for identi.ca and twitter accounts.
+const TWITTER_LINK_FOUND = 'twitterLinkFound';
+const IDENTICA_LINK_FOUND = 'identicaLinkFound';
+
+// phone number and address
+const PHONE_NUMBER_FOUND = 'phoneNumberFound';
+const SNAIL_ADDRESS_FOUND = 'snailAddressFound';
+
+/**
+ * main pageMod.
+ * Find blocked script in all pages being opened.
+ * Launch the scripts that search for a complaint contact.
+ *
+ */
+
+pageMod.PageMod({
+ include: ['file://*', '*', 'data:*', 'about:*'],
+ contentScriptWhen: 'end',
+
+ contentScriptFile: [
+ data.url('complain/contact_regex.js'),
+ data.url('complain/link_types.js'),
+ data.url('settings/third-party/jquery/jquery.min.js'),
+ data.url('complain/contact_finder.js'),
+ data.url('complain/pagemod_finder.js'),
+ data.url('script_detector/script_detector.js')
+ ],
+
+ onAttach: function onAttach(worker) {
+ if (worker.tab !== undefined && prefs.isComplaintTab()) {
+ // this is a tab.
+ if (!foundInContactList(worker.url)) {
+ // the hostname doesn't appear in the object literal.
+ // run script fetching/complaint feature if applicable.
+ tabProcess(worker);
+ } else {
+ worker.postMessage(foundInContactList(worker.url));
+ }
+ }
+ }
+});
+
+/**
+ * foundInContactList
+ *
+ * Provides link if contact link is found for given url, or else
+ * false.
+ */
+var foundInContactList = function (url) {
+ var hostname = urlHandler.getHostname(url);
+ if (contactList[hostname] !== undefined) {
+ return contactList[hostname];
+ } else {
+ return false;
+ }
+};
+
+/**
+ * tabProcess
+ * Find blocked/accepted scripts, prepare
+ * display panel and complaint panel.
+ */
+var tabProcess = function (worker) {
+ var visitedUrl = {};
+
+ // webmaster email is better than a webpage.
+ worker.emailFound = false;
+ var modUrl = '',
+ searchUrl = '';
+
+ modUrl = worker.url;
+ console.debug('pagemod triggered');
+
+ worker.port.emit('prefs', {
+ complaintEmailSubject: prefs.complaintEmailSubject(),
+ complaintEmailBody: prefs.complaintEmailBody()
+ });
+
+ // send local path to complain button graphic.
+ worker.port.emit('assetsUri',
+ {'event': 'assets-uri',
+ 'value': data.url('assets/')});
+ worker.port.emit('pageUrl',
+ {'event': 'page-url',
+ 'value': modUrl});
+
+ worker.on('message', function (respData) {
+ console.debug('worker is receiving a message', respData.event);
+ var pw;
+
+ worker.on('detach', function () {
+ console.debug('detaching worker');
+ if (pw) {
+ pw.destroy();
+ }
+ });
+ if (respData.contact !== undefined) {
+ // pass the message to the complaint display panel.
+ worker.port.emit('complaintLinkFound', respData);
+ } else if (respData.event === 'complaintSearch') {
+ if (worker.tab) {
+ console.debug('worker tab url', worker.tab.url);
+ }
+ if (!(respData.urlSearch.linkValue in visitedUrl)) {
+ visitedUrl[respData.urlSearch.linkValue] = 1;
+ respData.urlSearch.linkValue = urlHandler.addFragment(
+ respData.urlSearch.linkValue, 'librejs=true');
+ pw = searchSecondLevelPage(
+ this, respData.urlSearch.linkValue, this.url);
+ }
+ // currently not needed.
+ /*else {
+ console.debug(respData.urlSearch.linkValue, 'already visited');
+ }*/
+ }
+ });
+};
+
+var searchSecondLevelPage = function(
+ worker, urlToSearch, originalUrl) {
+ return;
+ var originalWorker = worker;
+
+ console.debug('searchSecondLevelPage');
+ console.debug(urlToSearch, 'and', originalUrl);
+
+ if (urlHandler.haveSameHostname(urlToSearch, originalUrl)) {
+ return pageWorker.Page({
+ contentURL: urlToSearch,
+ contentScriptFile: [
+ data.url('complain/contact_regex.js'),
+ data.url('complain/link_types.js'),
+ data.url('settings/third-party/jquery/jquery.min.js'),
+ data.url('complain/contact_finder.js'),
+ data.url('complain/worker_finder.js')
+ ],
+ contentScriptWhen: "end",
+ onMessage: function (respData) {
+ console.debug(JSON.stringify(respData));
+ console.debug(originalWorker.url);
+ originalWorker.postMessage(respData);
+
+ if (respData.event === 'destroy') {
+ try {
+ console.debug('destroying worker', this.contentURL);
+ this.destroy();
+ } catch (e) {
+ console.debug('in worker', e);
+ }
+ }
+ }
+ });
+ }
+};
+
+exports.testModule = {
+ 'contactList': contactList,
+ 'foundInContactList': foundInContactList,
+ 'tabProcess': tabProcess
+};
diff --git a/lib/url_handler/node_punycode.js b/lib/url_handler/node_punycode.js
new file mode 100644
index 0000000..163923c
--- /dev/null
+++ b/lib/url_handler/node_punycode.js
@@ -0,0 +1,510 @@
+/*! http://mths.be/punycode v1.2.0 by @mathias */
+;(function(root) {
+
+ /**
+ * The `punycode` object.
+ * @name punycode
+ * @type Object
+ */
+ var punycode,
+
+ /** Detect free variables `define`, `exports`, `module` and `require` */
+ freeDefine = typeof define == 'function' && typeof define.amd == 'object' &&
+ define.amd && define,
+ freeExports = typeof exports == 'object' && exports,
+ freeModule = typeof module == 'object' && module,
+ freeRequire = typeof require == 'function' && require,
+
+ /** Highest positive signed 32-bit float value */
+ maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1
+
+ /** Bootstring parameters */
+ base = 36,
+ tMin = 1,
+ tMax = 26,
+ skew = 38,
+ damp = 700,
+ initialBias = 72,
+ initialN = 128, // 0x80
+ delimiter = '-', // '\x2D'
+
+ /** Regular expressions */
+ regexPunycode = /^xn--/,
+ regexNonASCII = /[^ -~]/, // unprintable ASCII chars + non-ASCII chars
+ regexSeparators = /\x2E|\u3002|\uFF0E|\uFF61/g, // RFC 3490 separators
+
+ /** Error messages */
+ errors = {
+ 'overflow': 'Overflow: input needs wider integers to process',
+ 'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
+ 'invalid-input': 'Invalid input'
+ },
+
+ /** Convenience shortcuts */
+ baseMinusTMin = base - tMin,
+ floor = Math.floor,
+ stringFromCharCode = String.fromCharCode,
+
+ /** Temporary variable */
+ key;
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * A generic error utility function.
+ * @private
+ * @param {String} type The error type.
+ * @returns {Error} Throws a `RangeError` with the applicable error message.
+ */
+ function error(type) {
+ throw RangeError(errors[type]);
+ }
+
+ /**
+ * A generic `Array#map` utility function.
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} callback The function that gets called for every array
+ * item.
+ * @returns {Array} A new array of values returned by the callback function.
+ */
+ function map(array, fn) {
+ var length = array.length;
+ while (length--) {
+ array[length] = fn(array[length]);
+ }
+ return array;
+ }
+
+ /**
+ * A simple `Array#map`-like wrapper to work with domain name strings.
+ * @private
+ * @param {String} domain The domain name.
+ * @param {Function} callback The function that gets called for every
+ * character.
+ * @returns {Array} A new string of characters returned by the callback
+ * function.
+ */
+ function mapDomain(string, fn) {
+ return map(string.split(regexSeparators), fn).join('.');
+ }
+
+ /**
+ * Creates an array containing the decimal code points of each Unicode
+ * character in the string. While JavaScript uses UCS-2 internally,
+ * this function will convert a pair of surrogate halves (each of which
+ * UCS-2 exposes as separate characters) into a single code point,
+ * matching UTF-16.
+ * @see `punycode.ucs2.encode`
+ * @see <http://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode.ucs2
+ * @name decode
+ * @param {String} string The Unicode input string (UCS-2).
+ * @returns {Array} The new array of code points.
+ */
+ function ucs2decode(string) {
+ var output = [],
+ counter = 0,
+ length = string.length,
+ value,
+ extra;
+ while (counter < length) {
+ value = string.charCodeAt(counter++);
+ if ((value & 0xF800) == 0xD800 && counter < length) {
+ // high surrogate, and there is a next character
+ extra = string.charCodeAt(counter++);
+ if ((extra & 0xFC00) == 0xDC00) { // low surrogate
+ output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
+ } else {
+ output.push(value, extra);
+ }
+ } else {
+ output.push(value);
+ }
+ }
+ return output;
+ }
+
+ /**
+ * Creates a string based on an array of decimal code points.
+ * @see `punycode.ucs2.decode`
+ * @memberOf punycode.ucs2
+ * @name encode
+ * @param {Array} codePoints The array of decimal code points.
+ * @returns {String} The new Unicode string (UCS-2).
+ */
+ function ucs2encode(array) {
+ return map(array, function(value) {
+ var output = '';
+ if (value > 0xFFFF) {
+ value -= 0x10000;
+ output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
+ value = 0xDC00 | value & 0x3FF;
+ }
+ output += stringFromCharCode(value);
+ return output;
+ }).join('');
+ }
+
+ /**
+ * Converts a basic code point into a digit/integer.
+ * @see `digitToBasic()`
+ * @private
+ * @param {Number} codePoint The basic (decimal) code point.
+ * @returns {Number} The numeric value of a basic code point (for use in
+ * representing integers) in the range `0` to `base - 1`, or `base` if
+ * the code point does not represent a value.
+ */
+ function basicToDigit(codePoint) {
+ return codePoint - 48 < 10
+ ? codePoint - 22
+ : codePoint - 65 < 26
+ ? codePoint - 65
+ : codePoint - 97 < 26
+ ? codePoint - 97
+ : base;
+ }
+
+ /**
+ * Converts a digit/integer into a basic code point.
+ * @see `basicToDigit()`
+ * @private
+ * @param {Number} digit The numeric value of a basic code point.
+ * @returns {Number} The basic code point whose value (when used for
+ * representing integers) is `digit`, which needs to be in the range
+ * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
+ * used; else, the lowercase form is used. The behavior is undefined
+ * if flag is non-zero and `digit` has no uppercase form.
+ */
+ function digitToBasic(digit, flag) {
+ // 0..25 map to ASCII a..z or A..Z
+ // 26..35 map to ASCII 0..9
+ return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
+ }
+
+ /**
+ * Bias adaptation function as per section 3.4 of RFC 3492.
+ * http://tools.ietf.org/html/rfc3492#section-3.4
+ * @private
+ */
+ function adapt(delta, numPoints, firstTime) {
+ var k = 0;
+ delta = firstTime ? floor(delta / damp) : delta >> 1;
+ delta += floor(delta / numPoints);
+ for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) {
+ delta = floor(delta / baseMinusTMin);
+ }
+ return floor(k + (baseMinusTMin + 1) * delta / (delta + skew));
+ }
+
+ /**
+ * Converts a basic code point to lowercase if `flag` is falsy, or to
+ * uppercase if `flag` is truthy. The code point is unchanged if it's
+ * caseless. The behavior is undefined if `codePoint` is not a basic code
+ * point.
+ * @private
+ * @param {Number} codePoint The numeric value of a basic code point.
+ * @returns {Number} The resulting basic code point.
+ */
+ function encodeBasic(codePoint, flag) {
+ codePoint -= (codePoint - 97 < 26) << 5;
+ return codePoint + (!flag && codePoint - 65 < 26) << 5;
+ }
+
+ /**
+ * Converts a Punycode string of ASCII code points to a string of Unicode
+ * code points.
+ * @memberOf punycode
+ * @param {String} input The Punycode string of ASCII code points.
+ * @returns {String} The resulting string of Unicode code points.
+ */
+ function decode(input) {
+ // Don't use UCS-2
+ var output = [],
+ inputLength = input.length,
+ out,
+ i = 0,
+ n = initialN,
+ bias = initialBias,
+ basic,
+ j,
+ index,
+ oldi,
+ w,
+ k,
+ digit,
+ t,
+ length,
+ /** Cached calculation results */
+ baseMinusT;
+
+ // Handle the basic code points: let `basic` be the number of input code
+ // points before the last delimiter, or `0` if there is none, then copy
+ // the first basic code points to the output.
+
+ basic = input.lastIndexOf(delimiter);
+ if (basic < 0) {
+ basic = 0;
+ }
+
+ for (j = 0; j < basic; ++j) {
+ // if it's not a basic code point
+ if (input.charCodeAt(j) >= 0x80) {
+ error('not-basic');
+ }
+ output.push(input.charCodeAt(j));
+ }
+
+ // Main decoding loop: start just after the last delimiter if any basic code
+ // points were copied; start at the beginning otherwise.
+
+ for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) {
+
+ // `index` is the index of the next character to be consumed.
+ // Decode a generalized variable-length integer into `delta`,
+ // which gets added to `i`. The overflow checking is easier
+ // if we increase `i` as we go, then subtract off its starting
+ // value at the end to obtain `delta`.
+ for (oldi = i, w = 1, k = base; /* no condition */; k += base) {
+
+ if (index >= inputLength) {
+ error('invalid-input');
+ }
+
+ digit = basicToDigit(input.charCodeAt(index++));
+
+ if (digit >= base || digit > floor((maxInt - i) / w)) {
+ error('overflow');
+ }
+
+ i += digit * w;
+ t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
+
+ if (digit < t) {
+ break;
+ }
+
+ baseMinusT = base - t;
+ if (w > floor(maxInt / baseMinusT)) {
+ error('overflow');
+ }
+
+ w *= baseMinusT;
+
+ }
+
+ out = output.length + 1;
+ bias = adapt(i - oldi, out, oldi == 0);
+
+ // `i` was supposed to wrap around from `out` to `0`,
+ // incrementing `n` each time, so we'll fix that now:
+ if (floor(i / out) > maxInt - n) {
+ error('overflow');
+ }
+
+ n += floor(i / out);
+ i %= out;
+
+ // Insert `n` at position `i` of the output
+ output.splice(i++, 0, n);
+
+ }
+
+ return ucs2encode(output);
+ }
+
+ /**
+ * Converts a string of Unicode code points to a Punycode string of ASCII
+ * code points.
+ * @memberOf punycode
+ * @param {String} input The string of Unicode code points.
+ * @returns {String} The resulting Punycode string of ASCII code points.
+ */
+ function encode(input) {
+ var n,
+ delta,
+ handledCPCount,
+ basicLength,
+ bias,
+ j,
+ m,
+ q,
+ k,
+ t,
+ currentValue,
+ output = [],
+ /** `inputLength` will hold the number of code points in `input`. */
+ inputLength,
+ /** Cached calculation results */
+ handledCPCountPlusOne,
+ baseMinusT,
+ qMinusT;
+
+ // Convert the input in UCS-2 to Unicode
+ input = ucs2decode(input);
+
+ // Cache the length
+ inputLength = input.length;
+
+ // Initialize the state
+ n = initialN;
+ delta = 0;
+ bias = initialBias;
+
+ // Handle the basic code points
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+ if (currentValue < 0x80) {
+ output.push(stringFromCharCode(currentValue));
+ }
+ }
+
+ handledCPCount = basicLength = output.length;
+
+ // `handledCPCount` is the number of code points that have been handled;
+ // `basicLength` is the number of basic code points.
+
+ // Finish the basic string - if it is not empty - with a delimiter
+ if (basicLength) {
+ output.push(delimiter);
+ }
+
+ // Main encoding loop:
+ while (handledCPCount < inputLength) {
+
+ // All non-basic code points < n have been handled already. Find the next
+ // larger one:
+ for (m = maxInt, j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+ if (currentValue >= n && currentValue < m) {
+ m = currentValue;
+ }
+ }
+
+ // Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
+ // but guard against overflow
+ handledCPCountPlusOne = handledCPCount + 1;
+ if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
+ error('overflow');
+ }
+
+ delta += (m - n) * handledCPCountPlusOne;
+ n = m;
+
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+
+ if (currentValue < n && ++delta > maxInt) {
+ error('overflow');
+ }
+
+ if (currentValue == n) {
+ // Represent delta as a generalized variable-length integer
+ for (q = delta, k = base; /* no condition */; k += base) {
+ t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
+ if (q < t) {
+ break;
+ }
+ qMinusT = q - t;
+ baseMinusT = base - t;
+ output.push(
+ stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))
+ );
+ q = floor(qMinusT / baseMinusT);
+ }
+
+ output.push(stringFromCharCode(digitToBasic(q, 0)));
+ bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength);
+ delta = 0;
+ ++handledCPCount;
+ }
+ }
+
+ ++delta;
+ ++n;
+
+ }
+ return output.join('');
+ }
+
+ /**
+ * Converts a Punycode string representing a domain name to Unicode. Only the
+ * Punycoded parts of the domain name will be converted, i.e. it doesn't
+ * matter if you call it on a string that has already been converted to
+ * Unicode.
+ * @memberOf punycode
+ * @param {String} domain The Punycode domain name to convert to Unicode.
+ * @returns {String} The Unicode representation of the given Punycode
+ * string.
+ */
+ function toUnicode(domain) {
+ return mapDomain(domain, function(string) {
+ return regexPunycode.test(string)
+ ? decode(string.slice(4).toLowerCase())
+ : string;
+ });
+ }
+
+ /**
+ * Converts a Unicode string representing a domain name to Punycode. Only the
+ * non-ASCII parts of the domain name will be converted, i.e. it doesn't
+ * matter if you call it with a domain that's already in ASCII.
+ * @memberOf punycode
+ * @param {String} domain The domain name to convert, as a Unicode string.
+ * @returns {String} The Punycode representation of the given domain name.
+ */
+ function toASCII(domain) {
+ return mapDomain(domain, function(string) {
+ return regexNonASCII.test(string)
+ ? 'xn--' + encode(string)
+ : string;
+ });
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ /** Define the public API */
+ punycode = {
+ /**
+ * A string representing the current Punycode.js version number.
+ * @memberOf punycode
+ * @type String
+ */
+ 'version': '1.2.0',
+ /**
+ * An object of methods to convert from JavaScript's internal character
+ * representation (UCS-2) to decimal Unicode code points, and back.
+ * @see <http://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode
+ * @type Object
+ */
+ 'ucs2': {
+ 'decode': ucs2decode,
+ 'encode': ucs2encode
+ },
+ 'decode': decode,
+ 'encode': encode,
+ 'toASCII': toASCII,
+ 'toUnicode': toUnicode
+ };
+
+ /** Expose `punycode` */
+ if (freeExports) {
+ if (freeModule && freeModule.exports == freeExports) {
+ // in Node.js or Ringo 0.8+
+ freeModule.exports = punycode;
+ } else {
+ // in Narwhal or Ringo 0.7-
+ for (key in punycode) {
+ punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]);
+ }
+ }
+ } else if (freeDefine) {
+ // via curl.js or RequireJS
+ define('punycode', punycode);
+ } else {
+ // in a browser or Rhino
+ root.punycode = punycode;
+ }
+
+}(this));
diff --git a/lib/url_handler/node_querystring.js b/lib/url_handler/node_querystring.js
new file mode 100644
index 0000000..f8c7921
--- /dev/null
+++ b/lib/url_handler/node_querystring.js
@@ -0,0 +1,213 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// Query String Utilities
+
+var QueryString = exports;
+
+
+// If obj.hasOwnProperty has been overridden, then calling
+// obj.hasOwnProperty(prop) will break.
+// See: https://github.com/joyent/node/issues/1707
+function hasOwnProperty(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+}
+
+
+function charCode(c) {
+ return c.charCodeAt(0);
+}
+
+
+// a safe fast alternative to decodeURIComponent
+QueryString.unescapeBuffer = function(s, decodeSpaces) {
+ var out = new Buffer(s.length);
+ var state = 'CHAR'; // states: CHAR, HEX0, HEX1
+ var n, m, hexchar;
+
+ for (var inIndex = 0, outIndex = 0; inIndex <= s.length; inIndex++) {
+ var c = s.charCodeAt(inIndex);
+ switch (state) {
+ case 'CHAR':
+ switch (c) {
+ case charCode('%'):
+ n = 0;
+ m = 0;
+ state = 'HEX0';
+ break;
+ case charCode('+'):
+ if (decodeSpaces) c = charCode(' ');
+ // pass thru
+ default:
+ out[outIndex++] = c;
+ break;
+ }
+ break;
+
+ case 'HEX0':
+ state = 'HEX1';
+ hexchar = c;
+ if (charCode('0') <= c && c <= charCode('9')) {
+ n = c - charCode('0');
+ } else if (charCode('a') <= c && c <= charCode('f')) {
+ n = c - charCode('a') + 10;
+ } else if (charCode('A') <= c && c <= charCode('F')) {
+ n = c - charCode('A') + 10;
+ } else {
+ out[outIndex++] = charCode('%');
+ out[outIndex++] = c;
+ state = 'CHAR';
+ break;
+ }
+ break;
+
+ case 'HEX1':
+ state = 'CHAR';
+ if (charCode('0') <= c && c <= charCode('9')) {
+ m = c - charCode('0');
+ } else if (charCode('a') <= c && c <= charCode('f')) {
+ m = c - charCode('a') + 10;
+ } else if (charCode('A') <= c && c <= charCode('F')) {
+ m = c - charCode('A') + 10;
+ } else {
+ out[outIndex++] = charCode('%');
+ out[outIndex++] = hexchar;
+ out[outIndex++] = c;
+ break;
+ }
+ out[outIndex++] = 16 * n + m;
+ break;
+ }
+ }
+
+ // TODO support returning arbitrary buffers.
+
+ return out.slice(0, outIndex - 1);
+};
+
+
+QueryString.unescape = function(s, decodeSpaces) {
+ try {
+ return decodeURIComponent(s);
+ } catch (e) {
+ return QueryString.unescapeBuffer(s, decodeSpaces).toString();
+ }
+};
+
+
+QueryString.escape = function(str) {
+ return encodeURIComponent(str);
+};
+
+var stringifyPrimitive = function(v) {
+ switch (typeof v) {
+ case 'string':
+ return v;
+
+ case 'boolean':
+ return v ? 'true' : 'false';
+
+ case 'number':
+ return isFinite(v) ? v : '';
+
+ default:
+ return '';
+ }
+};
+
+
+QueryString.stringify = QueryString.encode = function(obj, sep, eq, name) {
+ sep = sep || '&';
+ eq = eq || '=';
+ if (obj === null) {
+ obj = undefined;
+ }
+
+ if (typeof obj === 'object') {
+ return Object.keys(obj).map(function(k) {
+ var ks = QueryString.escape(stringifyPrimitive(k)) + eq;
+ if (Array.isArray(obj[k])) {
+ return obj[k].map(function(v) {
+ return ks + QueryString.escape(stringifyPrimitive(v));
+ }).join(sep);
+ } else {
+ return ks + QueryString.escape(stringifyPrimitive(obj[k]));
+ }
+ }).join(sep);
+
+ }
+
+ if (!name) return '';
+ return QueryString.escape(stringifyPrimitive(name)) + eq +
+ QueryString.escape(stringifyPrimitive(obj));
+};
+
+// Parse a key=val string.
+QueryString.parse = QueryString.decode = function(qs, sep, eq, options) {
+ sep = sep || '&';
+ eq = eq || '=';
+ var obj = {};
+
+ if (typeof qs !== 'string' || qs.length === 0) {
+ return obj;
+ }
+
+ var regexp = /\+/g;
+ qs = qs.split(sep);
+
+ var maxKeys = 1000;
+ if (options && typeof options.maxKeys === 'number') {
+ maxKeys = options.maxKeys;
+ }
+
+ var len = qs.length;
+ // maxKeys <= 0 means that we should not limit keys count
+ if (maxKeys > 0 && len > maxKeys) {
+ len = maxKeys;
+ }
+
+ for (var i = 0; i < len; ++i) {
+ var x = qs[i].replace(regexp, '%20'),
+ idx = x.indexOf(eq),
+ kstr, vstr, k, v;
+
+ if (idx >= 0) {
+ kstr = x.substr(0, idx);
+ vstr = x.substr(idx + 1);
+ } else {
+ kstr = x;
+ vstr = '';
+ }
+
+ k = QueryString.unescape(kstr, true);
+ v = QueryString.unescape(vstr, true);
+
+ if (!hasOwnProperty(obj, k)) {
+ obj[k] = v;
+ } else if (Array.isArray(obj[k])) {
+ obj[k].push(v);
+ } else {
+ obj[k] = [obj[k], v];
+ }
+ }
+
+ return obj;
+};
diff --git a/lib/url_handler/node_url.js b/lib/url_handler/node_url.js
new file mode 100644
index 0000000..71d31a6
--- /dev/null
+++ b/lib/url_handler/node_url.js
@@ -0,0 +1,691 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var punycode = require('url_handler/node_punycode');
+
+exports.parse = urlParse;
+exports.resolve = urlResolve;
+exports.resolveObject = urlResolveObject;
+exports.format = urlFormat;
+
+exports.Url = Url;
+
+function Url() {
+ this.protocol = null;
+ this.slashes = null;
+ this.auth = null;
+ this.host = null;
+ this.port = null;
+ this.hostname = null;
+ this.hash = null;
+ this.search = null;
+ this.query = null;
+ this.pathname = null;
+ this.path = null;
+ this.href = null;
+}
+
+// Reference: RFC 3986, RFC 1808, RFC 2396
+
+// define these here so at least they only have to be
+// compiled once on the first module load.
+var protocolPattern = /^([a-z0-9.+-]+:)/i,
+ portPattern = /:[0-9]*$/,
+
+ // RFC 2396: characters reserved for delimiting URLs.
+ // We actually just auto-escape these.
+ delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'],
+
+ // RFC 2396: characters not allowed for various reasons.
+ unwise = ['{', '}', '|', '\\', '^', '~', '`'].concat(delims),
+
+ // Allowed by RFCs, but cause of XSS attacks. Always escape these.
+ autoEscape = ['\''].concat(delims),
+ // Characters that are never ever allowed in a hostname.
+ // Note that any invalid chars are also handled, but these
+ // are the ones that are *expected* to be seen, so we fast-path
+ // them.
+ nonHostChars = ['%', '/', '?', ';', '#']
+ .concat(unwise).concat(autoEscape),
+ hostEndingChars = ['/', '?', '#'],
+ hostnameMaxLen = 255,
+ hostnamePartPattern = /^[a-z0-9A-Z_-]{0,63}$/,
+ hostnamePartStart = /^([a-z0-9A-Z_-]{0,63})(.*)$/,
+ // protocols that can allow "unsafe" and "unwise" chars.
+ unsafeProtocol = {
+ 'javascript': true,
+ 'javascript:': true
+ },
+ // protocols that never have a hostname.
+ hostlessProtocol = {
+ 'javascript': true,
+ 'javascript:': true
+ },
+ // protocols that always contain a // bit.
+ slashedProtocol = {
+ 'http': true,
+ 'https': true,
+ 'ftp': true,
+ 'gopher': true,
+ 'file': true,
+ 'http:': true,
+ 'https:': true,
+ 'ftp:': true,
+ 'gopher:': true,
+ 'file:': true
+ },
+ querystring = require('url_handler/node_querystring');
+
+function urlParse(url, parseQueryString, slashesDenoteHost) {
+ if (url && typeof(url) === 'object' && url instanceof Url) return url;
+
+ var u = new Url;
+ u.parse(url, parseQueryString, slashesDenoteHost);
+ return u;
+}
+
+Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
+ if (typeof url !== 'string') {
+ throw new TypeError("Parameter 'url' must be a string, not " + typeof url);
+ }
+
+ // Copy chrome, IE, opera backslash-handling behavior.
+ // See: https://code.google.com/p/chromium/issues/detail?id=25916
+ var hashSplit = url.split('#');
+ hashSplit[0] = hashSplit[0].replace(/\\/g, '/');
+ url = hashSplit.join('#');
+
+ var rest = url;
+
+ // trim before proceeding.
+ // This is to support parse stuff like " http://foo.com \n"
+ rest = rest.trim();
+
+ var proto = protocolPattern.exec(rest);
+ if (proto) {
+ proto = proto[0];
+ var lowerProto = proto.toLowerCase();
+ this.protocol = lowerProto;
+ rest = rest.substr(proto.length);
+ }
+
+ // figure out if it's got a host
+ // user@server is *always* interpreted as a hostname, and url
+ // resolution will treat //foo/bar as host=foo,path=bar because that's
+ // how the browser resolves relative URLs.
+ if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) {
+ var slashes = rest.substr(0, 2) === '//';
+ if (slashes && !(proto && hostlessProtocol[proto])) {
+ rest = rest.substr(2);
+ this.slashes = true;
+ }
+ }
+
+ if (!hostlessProtocol[proto] &&
+ (slashes || (proto && !slashedProtocol[proto]))) {
+
+ // there's a hostname.
+ // the first instance of /, ?, ;, or # ends the host.
+ //
+ // If there is an @ in the hostname, then non-host chars *are* allowed
+ // to the left of the last @ sign, unless some host-ending character
+ // comes *before* the @-sign.
+ // URLs are obnoxious.
+ //
+ // ex:
+ // http://a@b@c/ => user:a@b host:c
+ // http://a@b?@c => user:a host:c path:/?@c
+
+ // v0.12 TODO(isaacs): This is not quite how Chrome does things.
+ // Review our test case against browsers more comprehensively.
+
+ // find the first instance of any hostEndingChars
+ var hostEnd = -1;
+ for (var i = 0; i < hostEndingChars.length; i++) {
+ var hec = rest.indexOf(hostEndingChars[i]);
+ if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
+ hostEnd = hec;
+ }
+
+ // at this point, either we have an explicit point where the
+ // auth portion cannot go past, or the last @ char is the decider.
+ var auth, atSign;
+ if (hostEnd === -1) {
+ // atSign can be anywhere.
+ atSign = rest.lastIndexOf('@');
+ } else {
+ // atSign must be in auth portion.
+ // http://a@b/c@d => host:b auth:a path:/c@d
+ atSign = rest.lastIndexOf('@', hostEnd);
+ }
+
+ // Now we have a portion which is definitely the auth.
+ // Pull that off.
+ if (atSign !== -1) {
+ auth = rest.slice(0, atSign);
+ rest = rest.slice(atSign + 1);
+ this.auth = decodeURIComponent(auth);
+ }
+
+ // the host is the remaining to the left of the first non-host char
+ hostEnd = -1;
+ for (var i = 0; i < nonHostChars.length; i++) {
+ var hec = rest.indexOf(nonHostChars[i]);
+ if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
+ hostEnd = hec;
+ }
+ // if we still have not hit it, then the entire thing is a host.
+ if (hostEnd === -1)
+ hostEnd = rest.length;
+
+ this.host = rest.slice(0, hostEnd);
+ rest = rest.slice(hostEnd);
+
+ // pull out port.
+ this.parseHost();
+
+ // we've indicated that there is a hostname,
+ // so even if it's empty, it has to be present.
+ this.hostname = this.hostname || '';
+
+ // if hostname begins with [ and ends with ]
+ // assume that it's an IPv6 address.
+ var ipv6Hostname = this.hostname[0] === '[' &&
+ this.hostname[this.hostname.length - 1] === ']';
+
+ // validate a little.
+ if (!ipv6Hostname) {
+ var hostparts = this.hostname.split(/\./);
+ for (var i = 0, l = hostparts.length; i < l; i++) {
+ var part = hostparts[i];
+ if (!part) continue;
+ if (!part.match(hostnamePartPattern)) {
+ var newpart = '';
+ for (var j = 0, k = part.length; j < k; j++) {
+ if (part.charCodeAt(j) > 127) {
+ // we replace non-ASCII char with a temporary placeholder
+ // we need this to make sure size of hostname is not
+ // broken by replacing non-ASCII by nothing
+ newpart += 'x';
+ } else {
+ newpart += part[j];
+ }
+ }
+ // we test again with ASCII char only
+ if (!newpart.match(hostnamePartPattern)) {
+ var validParts = hostparts.slice(0, i);
+ var notHost = hostparts.slice(i + 1);
+ var bit = part.match(hostnamePartStart);
+ if (bit) {
+ validParts.push(bit[1]);
+ notHost.unshift(bit[2]);
+ }
+ if (notHost.length) {
+ rest = '/' + notHost.join('.') + rest;
+ }
+ this.hostname = validParts.join('.');
+ break;
+ }
+ }
+ }
+ }
+
+ if (this.hostname.length > hostnameMaxLen) {
+ this.hostname = '';
+ } else {
+ // hostnames are always lower case.
+ this.hostname = this.hostname.toLowerCase();
+ }
+
+ if (!ipv6Hostname) {
+ // IDNA Support: Returns a punycoded representation of "domain".
+ // It only converts parts of the domain name that
+ // have non-ASCII characters, i.e. it doesn't matter if
+ // you call it with a domain that already is ASCII-only.
+ this.hostname = punycode.toASCII(this.hostname);
+ }
+
+ var p = this.port ? ':' + this.port : '';
+ var h = this.hostname || '';
+ this.host = h + p;
+ this.href += this.host;
+
+ // strip [ and ] from the hostname
+ // the host field still retains them, though
+ if (ipv6Hostname) {
+ this.hostname = this.hostname.substr(1, this.hostname.length - 2);
+ if (rest[0] !== '/') {
+ rest = '/' + rest;
+ }
+ }
+ }
+
+ // now rest is set to the post-host stuff.
+ // chop off any delim chars.
+ if (!unsafeProtocol[lowerProto]) {
+
+ // First, make 100% sure that any "autoEscape" chars get
+ // escaped, even if encodeURIComponent doesn't think they
+ // need to be.
+ for (var i = 0, l = autoEscape.length; i < l; i++) {
+ var ae = autoEscape[i];
+ var esc = encodeURIComponent(ae);
+ if (esc === ae) {
+ esc = escape(ae);
+ }
+ rest = rest.split(ae).join(esc);
+ }
+ }
+
+
+ // chop off from the tail first.
+ var hash = rest.indexOf('#');
+ if (hash !== -1) {
+ // got a fragment string.
+ this.hash = rest.substr(hash);
+ rest = rest.slice(0, hash);
+ }
+ var qm = rest.indexOf('?');
+ if (qm !== -1) {
+ this.search = rest.substr(qm);
+ this.query = rest.substr(qm + 1);
+ if (parseQueryString) {
+ this.query = querystring.parse(this.query);
+ }
+ rest = rest.slice(0, qm);
+ } else if (parseQueryString) {
+ // no query string, but parseQueryString still requested
+ this.search = '';
+ this.query = {};
+ }
+ if (rest) this.pathname = rest;
+ if (slashedProtocol[lowerProto] &&
+ this.hostname && !this.pathname) {
+ this.pathname = '/';
+ }
+
+ //to support http.request
+ if (this.pathname || this.search) {
+ var p = this.pathname || '';
+ var s = this.search || '';
+ this.path = p + s;
+ }
+
+ // finally, reconstruct the href based on what has been validated.
+ this.href = this.format();
+ return this;
+};
+
+// format a parsed object into a url string
+function urlFormat(obj) {
+ // ensure it's an object, and not a string url.
+ // If it's an obj, this is a no-op.
+ // this way, you can call url_format() on strings
+ // to clean up potentially wonky urls.
+ if (typeof(obj) === 'string') obj = urlParse(obj);
+ if (!(obj instanceof Url)) return Url.prototype.format.call(obj);
+ return obj.format();
+}
+
+Url.prototype.format = function() {
+ var auth = this.auth || '';
+ if (auth) {
+ auth = encodeURIComponent(auth);
+ auth = auth.replace(/%3A/i, ':');
+ auth += '@';
+ }
+
+ var protocol = this.protocol || '',
+ pathname = this.pathname || '',
+ hash = this.hash || '',
+ host = false,
+ query = '';
+
+ if (this.host) {
+ host = auth + this.host;
+ } else if (this.hostname) {
+ host = auth + (this.hostname.indexOf(':') === -1 ?
+ this.hostname :
+ '[' + this.hostname + ']');
+ if (this.port) {
+ host += ':' + this.port;
+ }
+ }
+
+ if (this.query && typeof this.query === 'object' &&
+ Object.keys(this.query).length) {
+ query = querystring.stringify(this.query);
+ }
+
+ var search = this.search || (query && ('?' + query)) || '';
+
+ if (protocol && protocol.substr(-1) !== ':') protocol += ':';
+
+ // only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
+ // unless they had them to begin with.
+ if (this.slashes ||
+ (!protocol || slashedProtocol[protocol]) && host !== false) {
+ host = '//' + (host || '');
+ if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname;
+ } else if (!host) {
+ host = '';
+ }
+
+ if (hash && hash.charAt(0) !== '#') hash = '#' + hash;
+ if (search && search.charAt(0) !== '?') search = '?' + search;
+
+ pathname = pathname.replace(/[?#]/g, function(match) {
+ return encodeURIComponent(match);
+ });
+ search = search.replace('#', '%23');
+
+ return protocol + host + pathname + search + hash;
+};
+
+function urlResolve(source, relative) {
+ return urlParse(source, false, true).resolve(relative);
+}
+
+Url.prototype.resolve = function(relative) {
+ return this.resolveObject(urlParse(relative, false, true)).format();
+};
+
+function urlResolveObject(source, relative) {
+ if (!source) return relative;
+ return urlParse(source, false, true).resolveObject(relative);
+}
+
+Url.prototype.resolveObject = function(relative) {
+ if (typeof relative === 'string') {
+ var rel = new Url();
+ rel.parse(relative, false, true);
+ relative = rel;
+ }
+
+ var result = new Url();
+ Object.keys(this).forEach(function(k) {
+ result[k] = this[k];
+ }, this);
+
+ // hash is always overridden, no matter what.
+ // even href="" will remove it.
+ result.hash = relative.hash;
+
+ // if the relative url is empty, then there's nothing left to do here.
+ if (relative.href === '') {
+ result.href = result.format();
+ return result;
+ }
+
+ // hrefs like //foo/bar always cut to the protocol.
+ if (relative.slashes && !relative.protocol) {
+ // take everything except the protocol from relative
+ Object.keys(relative).forEach(function(k) {
+ if (k !== 'protocol')
+ result[k] = relative[k];
+ });
+
+ //urlParse appends trailing / to urls like http://www.example.com
+ if (slashedProtocol[result.protocol] &&
+ result.hostname && !result.pathname) {
+ result.path = result.pathname = '/';
+ }
+
+ result.href = result.format();
+ return result;
+ }
+
+ if (relative.protocol && relative.protocol !== result.protocol) {
+ // if it's a known url protocol, then changing
+ // the protocol does weird things
+ // first, if it's not file:, then we MUST have a host,
+ // and if there was a path
+ // to begin with, then we MUST have a path.
+ // if it is file:, then the host is dropped,
+ // because that's known to be hostless.
+ // anything else is assumed to be absolute.
+ if (!slashedProtocol[relative.protocol]) {
+ Object.keys(relative).forEach(function(k) {
+ result[k] = relative[k];
+ });
+ result.href = result.format();
+ return result;
+ }
+
+ result.protocol = relative.protocol;
+ if (!relative.host && !hostlessProtocol[relative.protocol]) {
+ var relPath = (relative.pathname || '').split('/');
+ while (relPath.length && !(relative.host = relPath.shift()));
+ if (!relative.host) relative.host = '';
+ if (!relative.hostname) relative.hostname = '';
+ if (relPath[0] !== '') relPath.unshift('');
+ if (relPath.length < 2) relPath.unshift('');
+ result.pathname = relPath.join('/');
+ } else {
+ result.pathname = relative.pathname;
+ }
+ result.search = relative.search;
+ result.query = relative.query;
+ result.host = relative.host || '';
+ result.auth = relative.auth;
+ result.hostname = relative.hostname || relative.host;
+ result.port = relative.port;
+ // to support http.request
+ if (result.pathname || result.search) {
+ var p = result.pathname || '';
+ var s = result.search || '';
+ result.path = p + s;
+ }
+ result.slashes = result.slashes || relative.slashes;
+ result.href = result.format();
+ return result;
+ }
+
+ var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'),
+ isRelAbs = (
+ relative.host ||
+ relative.pathname && relative.pathname.charAt(0) === '/'
+ ),
+ mustEndAbs = (isRelAbs || isSourceAbs ||
+ (result.host && relative.pathname)),
+ removeAllDots = mustEndAbs,
+ srcPath = result.pathname && result.pathname.split('/') || [],
+ relPath = relative.pathname && relative.pathname.split('/') || [],
+ psychotic = result.protocol && !slashedProtocol[result.protocol];
+
+ // if the url is a non-slashed url, then relative
+ // links like ../.. should be able
+ // to crawl up to the hostname, as well. This is strange.
+ // result.protocol has already been set by now.
+ // Later on, put the first path part into the host field.
+ if (psychotic) {
+ result.hostname = '';
+ result.port = null;
+ if (result.host) {
+ if (srcPath[0] === '') srcPath[0] = result.host;
+ else srcPath.unshift(result.host);
+ }
+ result.host = '';
+ if (relative.protocol) {
+ relative.hostname = null;
+ relative.port = null;
+ if (relative.host) {
+ if (relPath[0] === '') relPath[0] = relative.host;
+ else relPath.unshift(relative.host);
+ }
+ relative.host = null;
+ }
+ mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
+ }
+
+ if (isRelAbs) {
+ // it's absolute.
+ result.host = (relative.host || relative.host === '') ?
+ relative.host : result.host;
+ result.hostname = (relative.hostname || relative.hostname === '') ?
+ relative.hostname : result.hostname;
+ result.search = relative.search;
+ result.query = relative.query;
+ srcPath = relPath;
+ // fall through to the dot-handling below.
+ } else if (relPath.length) {
+ // it's relative
+ // throw away the existing file, and take the new path instead.
+ if (!srcPath) srcPath = [];
+ srcPath.pop();
+ srcPath = srcPath.concat(relPath);
+ result.search = relative.search;
+ result.query = relative.query;
+ } else if (relative.search !== null && relative.search !== undefined) {
+ // just pull out the search.
+ // like href='?foo'.
+ // Put this after the other two cases because it simplifies the booleans
+ if (psychotic) {
+ result.hostname = result.host = srcPath.shift();
+ //occationaly the auth can get stuck only in host
+ //this especialy happens in cases like
+ //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
+ var authInHost = result.host && result.host.indexOf('@') > 0 ?
+ result.host.split('@') : false;
+ if (authInHost) {
+ result.auth = authInHost.shift();
+ result.host = result.hostname = authInHost.shift();
+ }
+ }
+ result.search = relative.search;
+ result.query = relative.query;
+ //to support http.request
+ if (result.pathname !== null || result.search !== null) {
+ result.path = (result.pathname ? result.pathname : '') +
+ (result.search ? result.search : '');
+ }
+ result.href = result.format();
+ return result;
+ }
+
+ if (!srcPath.length) {
+ // no path at all. easy.
+ // we've already handled the other stuff above.
+ result.pathname = null;
+ //to support http.request
+ if (result.search) {
+ result.path = '/' + result.search;
+ } else {
+ result.path = null;
+ }
+ result.href = result.format();
+ return result;
+ }
+
+ // if a url ENDs in . or .., then it must get a trailing slash.
+ // however, if it ends in anything else non-slashy,
+ // then it must NOT get a trailing slash.
+ var last = srcPath.slice(-1)[0];
+ var hasTrailingSlash = (
+ (result.host || relative.host) && (last === '.' || last === '..') ||
+ last === '');
+
+ // strip single dots, resolve double dots to parent dir
+ // if the path tries to go above the root, `up` ends up > 0
+ var up = 0;
+ for (var i = srcPath.length; i >= 0; i--) {
+ last = srcPath[i];
+ if (last == '.') {
+ srcPath.splice(i, 1);
+ } else if (last === '..') {
+ srcPath.splice(i, 1);
+ up++;
+ } else if (up) {
+ srcPath.splice(i, 1);
+ up--;
+ }
+ }
+
+ // if the path is allowed to go above the root, restore leading ..s
+ if (!mustEndAbs && !removeAllDots) {
+ for (; up--; up) {
+ srcPath.unshift('..');
+ }
+ }
+
+ if (mustEndAbs && srcPath[0] !== '' &&
+ (!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
+ srcPath.unshift('');
+ }
+
+ if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
+ srcPath.push('');
+ }
+
+ var isAbsolute = srcPath[0] === '' ||
+ (srcPath[0] && srcPath[0].charAt(0) === '/');
+
+ // put the host back
+ if (psychotic) {
+ result.hostname = result.host = isAbsolute ? '' :
+ srcPath.length ? srcPath.shift() : '';
+ //occationaly the auth can get stuck only in host
+ //this especialy happens in cases like
+ //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
+ var authInHost = result.host && result.host.indexOf('@') > 0 ?
+ result.host.split('@') : false;
+ if (authInHost) {
+ result.auth = authInHost.shift();
+ result.host = result.hostname = authInHost.shift();
+ }
+ }
+
+ mustEndAbs = mustEndAbs || (result.host && srcPath.length);
+
+ if (mustEndAbs && !isAbsolute) {
+ srcPath.unshift('');
+ }
+
+ if (!srcPath.length) {
+ result.pathname = null;
+ result.path = null;
+ } else {
+ result.pathname = srcPath.join('/');
+ }
+
+ //to support request.http
+ if (result.pathname !== null || result.search !== null) {
+ result.path = (result.pathname ? result.pathname : '') +
+ (result.search ? result.search : '');
+ }
+ result.auth = relative.auth || result.auth;
+ result.slashes = result.slashes || relative.slashes;
+ result.href = result.format();
+ return result;
+};
+
+Url.prototype.parseHost = function() {
+ var host = this.host;
+ var port = portPattern.exec(host);
+ if (port) {
+ port = port[0];
+ if (port !== ':') {
+ this.port = port.substr(1);
+ }
+ host = host.substr(0, host.length - port.length);
+ }
+ if (host) this.hostname = host;
+};
diff --git a/lib/url_handler/url_handler.js b/lib/url_handler/url_handler.js
new file mode 100644
index 0000000..2e5222b
--- /dev/null
+++ b/lib/url_handler/url_handler.js
@@ -0,0 +1,114 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * url_handler
+ * A module using the url tool from Node.js to perform operations on
+ * urls at various spots (tabs, caching, ...) in the add-on.
+ *
+ */
+
+// node.js url module. Makes it easier to resolve
+// urls in that datauri loaded dom
+var nodeJsUrl = require("url_handler/node_url");
+
+var urlUtils = {
+ getFragment: function (url) {
+ var parse = nodeJsUrl.parse(url);
+ if (parse.hash !== undefined) {
+ return(parse.hash);
+ }
+ },
+
+ removeFragment: function (url) {
+ var parse = nodeJsUrl.parse(url);
+ if (parse.hash !== undefined) {
+ // Amazon track package bug fix.
+ // when url has query string and fragment
+ // the add-on wouldn't remove cache entry
+ // properly.
+ delete parse.hash;
+ }
+ return nodeJsUrl.format(parse);
+ },
+
+ addFragment: function (url, query) {
+ var parse = nodeJsUrl.parse(url);
+
+ // replace hash if it exists.
+ parse.hash = '#' + query;
+
+ return nodeJsUrl.format(parse);
+ },
+
+ addQuery: function (url, query) {
+ var parse = nodeJsUrl.parse(url);
+ console.debug('my parse search', parse.search);
+ if (parse.search === undefined) {
+ parse.search = '?' + query;
+ } else {
+ parse.search = parse.search + '&' + query;
+ console.debug('parse search is now' + parse.search);
+ }
+ return nodeJsUrl.format(parse);
+ },
+
+ getHostname: function (url) {
+ return nodeJsUrl.parse(url).hostname;
+ },
+
+ /**
+ * remove www from hostname.
+ */
+ removeWWW: function (str) {
+ if (str !== undefined) {
+ return str.replace("www.", "", 'i');
+ }
+ return "";
+ },
+
+ /**
+ *
+ * haveSameHostname
+ * Compare that two urls have the same hostname.
+ *
+ */
+ haveSameHostname: function (url1, url2) {
+ try {
+ var host1 = this.removeWWW(this.getHostname(url1)).toLowerCase();
+ var host2 = this.removeWWW(this.getHostname(url2)).toLowerCase();
+ return host1 === host2;
+ } catch (x) {
+ console.debug('error with url_handler', x, x.fileName, x.lineNumber);
+ }
+ }
+};
+
+exports.parse = nodeJsUrl.parse;
+exports.resolve = nodeJsUrl.resolve;
+exports.resolveObject = nodeJsUrl.resolveObject;
+exports.format = nodeJsUrl.format;
+exports.removeFragment = urlUtils.removeFragment;
+exports.addQuery = urlUtils.addQuery;
+exports.getFragment = urlUtils.getFragment;
+exports.addFragment = urlUtils.addFragment;
+exports.getHostname = urlUtils.getHostname;
+exports.haveSameHostname = urlUtils.haveSameHostname;
+exports.removeWWW = urlUtils.removeWWW;