diff options
Diffstat (limited to 'lib/html_script_finder/dom_handler/dom_checker.js')
-rw-r--r-- | lib/html_script_finder/dom_handler/dom_checker.js | 478 |
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; |