aboutsummaryrefslogtreecommitdiffstats
path: root/lib/html_script_finder/dom_handler/dom_checker.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/html_script_finder/dom_handler/dom_checker.js')
-rw-r--r--lib/html_script_finder/dom_handler/dom_checker.js478
1 files changed, 478 insertions, 0 deletions
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;