/**
 * 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_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/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 attributeHelpers = require("html_script_finder/dom_handler/attributes");

// javascript:*
var jsInAttrRe = attributeHelpers.jsInAttrRe;

// the list of all available event attributes
var intrinsecEvents = attributeHelpers.intrinsecEvents;

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 domHandlerObject = {

    // external object with methods used
    // in domHandlerObject.
    domGatherer: null,

    // external object with methods used
    // in domHandlerObject.
    domChecker: null,

    dom: null,

    pageURL: null,

    // fragment found in url.
    fragment: null,

    // array containing all scripts on a page.
    domScripts: null,

    // array containing all scripts on a page,
    // data related to them, such as parse tree, ...
    inlineScripts:null,

    externalScripts: null,

    // all scripts.
    scripts:null,
    
    // keeps track of the number of scripts.
    numScripts: 0,
    
    // store the reference to the callback method
    // presumably from htmlParser.
    callback: null,

    // boolean set to true if external scripts are loaded
    // from the html page.
    loadsHtmlExternalScripts: false,

    jsCheckString: null,

    /* object containing boolean property set to false if trivialness
    is not allowed anymore (if another script defines ajax requests,
    ...)  */
    allowTrivial: null,

    // boolean set to true if inline JavaScript
    // is found to be free.
    inlineJsFree: null,

    // boolean set to true when at least one script
    // has been removed.
    hasRemovedScripts: null,

    // boolean to check if scripts were removed
    // prevents removeAllJs from running multiple times.
    removedAllScripts: null,
    
    // will eventually contain an array of data
    // for the js web labels licenses.
    licenseList: null,

    // the response status for the page (200, 404, ...)
    responseStatus: null,

    // count the number of scripts fully tested.
    scriptsTested: null,
    
    // number of external scripts to be tested.
    numExternalScripts: null,

    // number of inline/inattribute scripts
    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.
     *
     */
    init: function(domObject, pageURL, fragment, responseStatus, callback) {
	// initialize object properties.
	
	//console.log('init', pageURL);
	var that = this;

	this.reset();

	// arguments passed.
	this.dom = domObject;
	this.pageURL = pageURL;
	this.fragment = fragment;
	this.responseStatus = responseStatus;



	//console.log('in dom handler, responseStatus is', this.responseStatus);

	// make callback function available
	// for the entire object.
	this.callback = function (dom) { 
	    callback(dom);
	    that.destroy();
	}; 



    },
    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;


    },

    destroy: function () {
	this.domGatherer = null;
	this.domChecker = null;
        /* destroy callback so that it can't be called multiple times. */
        this.callback = function () {};
	//this.reset();
    },

    scriptHasBeenTested: function () {
	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.
     *
     */
    scriptHasJsWebLabel: function (script) {
	if (this.licenseList) {
	    var url = urlHandler.resolve(this.pageURL, script.src),
	    i = 0, 
	    len = this.licenseList.length;

	    // console.log('looking for web label');

	    for (; i < len; i++) {
		if (this.licenseList[i].fileUrl === url &&
		    this.licenseList[i].free === true) {
		    //console.log('found something true');
		    return true;
		}
	    }
	}

    },

    /**
     * processScripts.
     * Starts by looking for a js web labels page
     * then calls the complete function, which runs
     * the rest of the check.
     */
    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.
            that.checkAllScripts();

        });

    },

    /**
     * jsWebLabelsComplete
     *
     */
    checkAllScripts: function () {
	try {
	//console.log('found in', this.pageURL, JSON.stringify(this.licenseList));
	//console.log('checkAllScripts triggered async');

	// use domGatherer to gather scripts.
	this.domGatherer.findScripts();
	this.domGatherer.gatherScriptsContent();
	this.domGatherer.gatherIntrinsecEvents();

	//console.log('fragment is', fragment);

	if ((this.fragment == undefined || 
	    this.fragment.indexOf('librejs=true') < 0) &&
	    this.responseStatus != 404) {
	    
	    try {
		
	    // use domChecker to check scripts.
	    this.domChecker.checkAllInlineScripts();


	    } catch (x) {
		console.log('error in domChecker:', x, x.lineNumber);
		this.removeAllJs();
	    }



	} else {
	    //console.log('404 or 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.log('fragment found, remove js');
	    this.removeAllJs();
	}

	    
	} catch (x) {
	    console.log('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, ...)
     *
     */
    lookForJsWebLabels: function (completed) {
	var that = this;

	if (this.fragment != '#librejs=true') {

	    webLabelFinder.init(this.dom, 
				this.pageURL, 

				function (licenseList) {
				    // assign array returned to property.
				    that.licenseList = licenseList;
				    completed();
				});

	} else {
	    completed();
	}
    },


    checkScriptForJsWebLabels: function (script) {
	var scriptEntry;

	if (this.hasSrc(script) &&
	    this.scriptHasJsWebLabel(script)) {

	    // 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.
     */
    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.)
     *
     */
    removeScriptIfDependent: function (script) {

	var nonWindowProps = script.tree.relationChecker.nonWindowProperties;
	
	for (var entry in nonWindowProps) {
	    if (nonWindowProps[entry]) {
		//console.log('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.
     *
     */
    removeGivenJs: function (script, reason, singleton) {
	var commentedOut;
	if (script.status != statusTypes.REJECTED &
	    script.status != statusTypes.JSWEBLABEL) {

	    //console.log('removing a', script.type);
	    if (script.type === scriptTypes.ATTRIBUTE) {
		this.removeGivenAttribute(script, reason);
		return;
	    }

	    // set invalid type.
	    script.element.setAttribute('type', 'librejs/blocked');
	    
	    // add entry as removed.
	    script.tagAsRemoved(this.pageURL, reason);

	    if (singleton === true) {
		// flag singletons.
		script.element.setAttribute('data-singleton', 'true');
	    }


	    // remove src.
	    if (script.element.getAttribute('src') != undefined) {
		script.element.setAttribute('data-librejs-blocked-src', script.element.getAttribute('src'));
		script.element.removeAttribute('src');
	    }

	    commentedOut = this.dom.createComment('LibreJS: script blocked.');
	    // add a comment for curious source readers.
	    script.element.parentNode.appendChild(commentedOut);
	    script.element.parentNode.insertBefore(commentedOut, script.element);
	    this.hasRemovedScripts = true;

	    script.status = statusTypes.REJECTED;
	    
	}
    },

    removeGivenAttribute: function (script, reason) {
	var i = 0,
	le = script.jsAttributes.length;

	//console.log('removing given attribute');
	script.element.setAttribute('data-librejs-blocked-event', 
				    JSON.stringify(script.jsAttributes));


	script.tagAsRemoved(this.pageURL, reason);

	// might need to be removed.
	script.element.setAttribute('data-librejs-blocked-value', '');

	
	for (; i < le; i++) {
	    //console.log('removing attribute', JSON.stringify(script.jsAttributes));
	    script.element.removeAttribute(script.jsAttributes[i].attribute);
	    
	}
	

	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.
     *
     */
    removeAllJs: function (reason) {
	
	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.log('in removeAllJs method: ', x, 'number of scripts is', this.numScripts);

	    this.callback(this.dom);

	}

    },

    removeAllArray: function(scriptArray, reason) {
	var script, i = 0, le;
	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
 * Instantiate a brand new clone of the domHandlerObject.
 * @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) {

    var domHandler = Object.create(domHandlerObject);

    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.
    domHandler.processScripts();
    
};