/**
 * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
 * *
 * Copyright (C) 2011, 2012 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.
 * 
 */

"use strict";

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;

// 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;

var timer = require("timers");

var domCheckerObject = {
  
    // reference to domHandler object
    // using this object.
    d: null,

   /**
     * init
     * 
     * assign a reference domHandler object
     * to access/updates its properties.
     * 
     */
    init: function (domHandler) {
	
	this.d = domHandler;

    },

    destroy: function () {

	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.
     *
     */
    checkAllInlineScripts: function() {
	try {
	    
	    var i = 0, len, script;

	    if (this.d.inlineScripts != undefined &&
		this.d.inlineScripts.length > 0) {


		script = this.d.inlineScripts.shift();

		
		if (this.d.removedAllScripts) {
		    // all js has already been removed.
		    // stop check.
		    return;
		}
		
		if (this.d.inlineJsFree === true) {

		    // add entry as accepted.
		    script.tagAsAccepted(this.d.pageURL, reasons.FREE);

		}

		// even if page is free we need to check for allow trivial.
		if (script.type === scriptTypes.INLINE) {

		    this.analyzeJs(script, 
				   script.text, 
				   this.checkSingleInlineScript.bind(this));
		    

		}
		else if (script.type === scriptTypes.ATTRIBUTE) {

		    this.analyzeJs(script,
				   this.concatAttributes(script), 
				   this.checkSingleElementAttributes.bind(this));

		}

	    } else {
		this.readyForExternal();
	    }
	} catch (x) {
	    console.log('error', x, x.lineNumber, x.fileName);
	}

    },
    concatAttributes: function (script) {
	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
     */
    checkSingleElementAttributes: function (script, loadedScript, checker) {
	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.log('problem checking inline scripts', e, e.lineNumber);
 	    this.d.removeGivenJs(script);
	}
	

	this.processInlineCheckResult(script, check);
     },

    processInlineCheckResult: function (script, check) {

	if (this.d.inlineJsFree === true) {

	    // console.log('tagging', script.text, 'as accepted');
	    script.tagAsAccepted(this.d.pageURL);

	}

 	// process the result.
 	if (check === checkTypes.FREE) {

	    // this is free.
 	    this.d.inlineJsFree = true;
	    // add entry as accepted.
	    script.tagAsAccepted(this.d.pageURL, reasons.FREE);

 	}
	
	else if (check === checkTypes.FREE_NONTRIVIAL_GLOBAL) {

	    // the rest inline should be free.
	    this.d.inlineJsFree = true;

	    // trivial nonfree is disallowed.
	    this.d.allowTrivial = false;

	    // accept this script.
	    script.tagAsAccepted(this.d.pageURL, reasons.FREE);
	}
	
	else if (check === checkTypes.NONTRIVIAL_GLOBAL) {

	    // trivial nonfree is disallowed.
	    this.d.allowTrivial = false;

	    if (!this.d.inlineJsFree) {
		// inline is not free. Remove.
		this.d.removeAllJs(reasons.CONSTRUCT);
		//return;
	    } 
	    
	    else if (this.d.inlineJsFree) {
		// inline is free. So accept.
		script.tagAsAccepted(this.d.pageURL, reasons.FREE);
		
	    }

	}
	
	if (this.d.allowTrivial === true &&
	    check === checkTypes.TRIVIAL) {
	    // add entry as accepted.
	    script.tagAsAccepted(this.d.pageURL, reasons.TRIVIAL);
	} 
	
	else if (!this.d.inlineJsFree &&
		 this.d.loadsHtmlExternalScripts &&
		 check === checkTypes.TRIVIAL_DEFINES_FUNCTION) {
	    // nontrivial, because defines function and loads
	    // external scripts.
	    this.d.removeAllJs(reasons.FUNCTIONS_INLINE);
	} 
	else if (!this.d.loadsHtmlExternalScripts &&
		   check === checkTypes.TRIVIAL_DEFINES_FUNCTION) {
	    
	    script.tagAsAccepted(this.d.pageURL, reasons.TRIVIAL);
	
	}

	else if (!this.d.inlineJsFree &&
		 this.d.allowTrivial.allowed === false &&
		 (check === checkTypes.TRIVIAL ||
		  check === checkTypes.TRIVIAL_DEFINES_FUNCTION)) {
	    this.d.removeGivenJs(script, reasons.TRIVIAL_NOT_ALLOWED);
	}


	if (this.d.allowTrivial === false) {
	    this.checkArrayForTrivial(false);
	} 
	
	// next inline script, if applicable.
	this.checkAllInlineScripts();
    },

    readyForExternal: function () {

	    // done with those inline scripts, continue with
	    // the rest.
	    this.checkExternalScripts();
    },

    /**
     * check a single inline script. 
     */
    checkSingleInlineScript: function (script, loadedScript, checker) {
	var check, text;
	
	try {

 	    check = checker.parseTree.freeTrivialCheck;

  	    // update status.
 	    script.tree = checker;
 	    script.result = check;
 	    script.status = statusTypes.CHECKED;

 	} catch (e) {
 	    console.log('problem checking inline scripts', e, e.lineNumber);
 	    this.d.removeGivenJs(script);
 	}
	
	this.processInlineCheckResult(script, check);

    },

    /**
     * checkExternalScripts
     * Loop through series of external scripts,
     * perform xhr to get their data, and check them
     * to see whether they are free/nontrivial
     *
     */
    checkExternalScripts: function() {
	var i = 0, 
	len = this.d.externalScripts.length,
	that = this;
	
	if (this.d.removedAllScripts ||
	    this.d.externalScripts.length === 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) { 

			 if (scriptText == false) {
			     that.d.removeGivenJs(script);
			     that.d.scriptHasBeenTested();
			     that.externalCheckIsDone();
			     return;
			 }

			 that.analyzeJs(script, 
					scriptText, 
					that.checkSingleExternalScript.bind(that));
		     });
	}



    },

    wrapUpBeforeLeaving: function () {
	
	if (this.d.allowTrivial === false ||
	    this.d.hasRemovedScripts) {
	    
	    this.checkArrayForTrivial(true);

	}

	//console.log('wrapping up');
	this.d.callback(this.d.dom);

    },

    analyzeJs: function (script, scriptText, callback) {
	try {
	    
	var checker = jsChecker.jsChecker();

	checker.searchJs(scriptText, function () {
			     callback(script, scriptText, checker);
		 });
	} catch (x) {
	    console.log('error', x, x. lineNumber, x.fileName);
	}

    },

   /**
     * Check a single external script.
     */
    checkSingleExternalScript: function (script, loadedScript, checker) {
	var check;

	try {

	    check = checker.parseTree.freeTrivialCheck;

	    script.tree = checker;
	    script.result = check;

	    if (script.status != statusTypes.JSWEBLABEL) {
		script.status = statusTypes.CHECKED;		
	    } 
	    

	if (check === checkTypes.FREE_NONTRIVIAL_GLOBAL) {
	    
	    // accept script. it's free!
	    script.tagAsAccepted(this.d.pageURL, reasons.FREE);

	    // set token to look at other scripts in retrospect to
	    // forbid trivial scripts.
	    this.d.allowTrivial = false;
	}


	else if (check === checkTypes.FREE) {
	    
	    // add entry as accepted.
	    script.tagAsAccepted(this.d.pageURL, reasons.FREE);

	} 
	
	else if (check === checkTypes.NONTRIVIAL_GLOBAL) {

	    this.d.removeGivenJs(script, reasons.CONSTRUCT);
	    this.d.allowTrivial = false;
	}
	
	else if (check === checkTypes.TRIVIAL) {
	    
	    if (this.d.allowTrivial === false) {

		// if trivial is forbidden, remove.
		this.d.removeGivenJs(script, reasons.TRIVIAL_NOT_ALLOWED);

	    } else {

		// if it's accepted, allow.
		script.tagAsAccepted(this.d.pageURL, reasons.TRIVIAL);

	    }
	}

	else {
	    // anything else is nontrivial. Including TRIVIAL_DEFINES_FUNCTION.
	    this.d.removeGivenJs(script, reasons.FUNCTIONS_EXTERNAL);
	} 

	} catch (e) {

	    console.log('error in checkExternalScript', e, e.lineNumber, 'for script', script.url);

	    this.d.removeAllJs();

	    this.destroy();

	    return;

	}
	//console.log('script url is', script.url, 'result is', script.result);
	this.d.scriptHasBeenTested();
	this.externalCheckIsDone();

    },

    externalCheckIsDone: function () {

	//console.log('scriptsTested is', this.d.scriptsTested);
	//console.log('num external', this.d.numExternalScripts);

	if (this.d.scriptsTested  >= this.d.numExternalScripts) {
	   // console.log('wrapping up external');
	    this.wrapUpBeforeLeaving();
	}

    },

    /**
     * checkArrayForTrivial
     *
     * In some instance all trivial code must be flagged as nontrivial
     * in retrospect of some constructs found in other scripts.
     */
    checkArrayForTrivial: function(checkAll) {
	var i = 0,
	len = this.d.scripts.length,
	nonWindowProps,
	type;

	try {

	    for (; i < len; i++) {
		type = this.d.scripts[i].type;

		if ((type === scriptTypes.INLINE ||
		     type === scriptTypes.ATTRIBUTE) && 
		    !this.d.inlineJsFree) {



		    if (this.d.scripts[i].result === checkTypes.TRIVIAL ||
			    this.d.scripts[i].result === checkTypes.TRIVIAL_DEFINES_FUNCTION) {
			    this.d.removeScriptIfDependent(this.d.scripts[i]);
			}

		    }
		if (checkAll) {

			if (type === scriptTypes.EXTERNAL &&
			    this.d.scripts[i].result === checkTypes.TRIVIAL) {
			    
			    this.d.removeScriptIfDependent(this.d.scripts[i]);	
			}

		}

	    } 
	    


	} catch(e) {
	    console.log('problem checking array', e, e.lineNumber);
	    this.d.removeAllJs();
	    this.destroy();
	}
    },

    /**
     * xhr
     * Perform a XMLHttpRequest on the url given.
     * @param url string A URL.
     * @return The response text.
     */
    xhr: function(script, responseCallback) {

	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.log('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) {

    var domChecker = Object.create(domCheckerObject);

    domChecker.init(domHandler);

    return domChecker;

};