/**
 * 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("sdk/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();

        //console.log("checking script for page", this.d.pageURL/*, JSON.stringify(script)*/);
		    if (this.d.removedAllScripts) {
		      // all js has already been removed.
		      // stop check.
          // console.log("removed all");
		      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) {
          // console.log("analyzing script", script);
		      this.analyzeJs(script, 
				                 script.text, 
				                 this.checkSingleInlineScript.bind(this));
		      

		    }
		    else if (script.type === scriptTypes.ATTRIBUTE) {
          // console.log("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.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) {
    // console.log("check.reason is", check.reason, "and type", check.type);
	  if (this.d.inlineJsFree === true) {
      // console.log('tagging', script.text, 'as accepted');
	    script.tagAsAccepted(this.d.pageURL, this.d.freeReason + " -- " + check.reason);

	  }

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

	    // this is free.
      // console.log('tagging', script.text, 'as accepted');
 	    this.d.inlineJsFree = true;
      this.d.freeReason = check.reason;
	    // add entry as accepted.
	    script.tagAsAccepted(this.d.pageURL, check.reason);
 	  }
	  
	  else if (check.type === checkTypes.FREE_SINGLE_ITEM) {
	    // accept this script.
	    script.tagAsAccepted(this.d.pageURL, check.reason);
	  }
	  
	  else if (check.type === checkTypes.NONTRIVIAL) {
	    
	    if (this.d.inlineJsFree) {
		    // inline is free. So accept.
        // console.log('tagging', script.text, 'as accepted');
		    script.tagAsAccepted(this.d.pageURL, this.d.freeReason + ' -- ' + check.reason);
		    
	    }
      else {
        // console.log('tagging', script.text, 'as removed');
        this.d.removeGivenJs(script, check.reason);
      }
	  }
	  
	  
	  else if (!this.d.inlineJsFree &&
		         this.d.loadsHtmlExternalScripts &&
		         check.type === checkTypes.TRIVIAL_DEFINES_FUNCTION) {
	    // nontrivial, because defines function and loads
	    // external scripts
      // console.log('tagging', script.text, 'as removed');
	    this.d.removeGivenJs(script, reasons.FUNCTIONS_INLINE);
	  } 
	  else if (!this.d.loadsHtmlExternalScripts &&
		         check === checkTypes.TRIVIAL_DEFINES_FUNCTION) {
      // console.log("Tag as accepted doesn't load another external script");
	    script.tagAsAccepted(this.d.pageURL, check.reason);
	    
	  }
    else if (check.type === checkTypes.TRIVIAL || 
             check.type === checkTypes.TRIVIAL_DEFINES_FUNCTION) {
	    // add entry as accepted.
      // console.log("Trivial accepted");
	    script.tagAsAccepted(this.d.pageURL, check.reason);
	  } 

	  
	  // 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;
      //console.log("script result is", check.type);
 	    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;
	  // console.log("externalScripts length", this.d.externalScripts.length);
	  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) { 
                 // console.log("doing xhr", script.url);
			           if (scriptText == false) {
			             that.d.removeGivenJs(script);
			             that.d.scriptHasBeenTested();
			             that.externalCheckIsDone();
			             return;
			           }

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



  },

  wrapUpBeforeLeaving: function () {

    //console.log("wrap up before leaving triggered");
	  //console.log('wrapping up');
	  this.d.callback(this.d.dom);

  },

  analyzeJs: function (script, scriptText, callback) {
	  try {
	    // console.log("checking ", script.url);
	    var checker = jsChecker.jsChecker();
      var url = "";
      if (script['url'] != undefined) {
        url = script['url'];
      }
	    checker.searchJs(scriptText, function () {
        // console.log("Analyze JS"/*, JSON.stringify(checker)*/);
			  callback(script, scriptText, checker);
		  }, url);
	  } 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.type === checkTypes.FREE || check.type === checkTypes.FREE_SINGLE_ITEM) {
	      
	      // add entry as accepted.
	      script.tagAsAccepted(this.d.pageURL, check.reason);

	    } 
	    
	    else if (check.type === checkTypes.NONTRIVIAL) {
        // console.log("Removing given js", check.reason);
	      this.d.removeGivenJs(script, check.reason);
	    }
	    
	    else if (check.type === checkTypes.TRIVIAL) {
	      
		    // if it's accepted, allow.
		    script.tagAsAccepted(this.d.pageURL, check.reason);

	    }

	    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.type === checkTypes.TRIVIAL ||
			        this.d.scripts[i].result.type === 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;

};

exports.xhr = domCheckerObject.xhr;
