/**
 * 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/>.
 *
 */


var scriptProperties = require("html_script_finder/dom_handler/script_properties");

const scriptTypes = scriptProperties.scriptTypes;

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 intrinsecEvents = attributeHelpers.intrinsecEvents;

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

    // domHandler object.
    d: null,

    /**
     * init
     * 
     * assign a reference domHandler object
     * to access/updates its properties.
     * 
     */
    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.
     *
     */
    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.
     */
    findScripts: function() {
	this.d.domScripts = this.d.dom.getElementsByTagName('script');
	this.d.numScripts = this.d.domScripts.length;
    },

    /**
     * gatherIntrinsecEvents
     * 
     * Fetches all the event attributes that might contain JavaScript
     * as well as all element attributes that start with
     * "javascript:".
     *
     */
    gatherIntrinsecEvents: 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.
     *
     */
    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 = {};

		    scriptEntry.result = checkTypes.FREE;
 		    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.log('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.
			
 			currentScript = stripHtmlCommentsInScript(this.d.domScripts[i].text) + '\n\nthis.narcissusBugFixLibreJS';

			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.log('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 domGatherer = Object.create(domGathererObject);

    domGatherer.init(domHandler);
    
    return domGatherer;

};