views:

139

answers:

4

Problem...

Poorly-coded scripts exist which need to be included on a web page.

These scripts pollute the global scope by doing things like:

  • Assigning values to undeclared identifiers
  • Adding properties to built-in constructor functions (like Object and Array) and their prototypes
  • Other nasty stuff.

Solution?

I want to include the scripts without the adverse side effects. I think it can be achieved by loading the script in an iframe and exporting objects as properties of the parent window. Here's what Ive got so far:

<script>

(function(){
  var g=this, frameIndex=frames.length, f=document.createElement('iframe');

  // hide it like this instead of display:none, because some old browser ignores
  // iframes with display:none, or is this an ancient habit I can drop?
  f.style.width='0px'; f.style.height='0px'; 
  f.style.border='none'; f.style.position='absolute';

  // append it to document.body or document.documentElement?
  // documentElement seems to work before body is loaded,
  // but is it cross-browser safe?  
  document.body.appendChild(f);

  // window object for our iframe
  var w=frames[frameIndex];

  // callback function pulls the object into the current window when script loads
  w.cb=function(){ g.SomeObject=w.SomeObject };

  // will this work on IE, or do I need to use document.createElement?
  // wanted to avoid document.createElement in this case because I'm not sure 
  // whether to call it from window.document or frames[frameIndex].document
  w.document.innerHTML='<script onload="cb()" src="myscript.js"><\/script>';

}());

</script>

Questions:

  • Will there be potential havoc if a script modifies built-in prototypes and I move it into another window, or will my parent window's built-ins stay clean and everything will "just work?"

  • Is this idea going to work on 'most' browsers, or is there a show-stopper? Haven't tested on anything besides chrome and moz so far.

  • I'd like to remove the iframe after pulling the object into the current window, but moz will lose the object reference if the iframe is removed. Does anyone know of a way around that?

  • Has this already been done, or is there a better way to accomplish my goal? If so, what's the name of the script or technique I should to be looking for?

(question transplanted from here)

+1  A: 

To copy a function you could cast it to a string and then eval it.... The code below also demonstrates that the iframe can be removed after doing this and your copy remains intact.

The following code sample using FF

Child.html snippet

<script>

//
// modify the prototype
//
Object.prototype.test = function(msg)
{
        alert(msg);
};  

//
// Simply declare a function
//
var whoo_hoo = function(){alert("whoo hoo");}
</script>

Parent with iframe:

 <iframe id="help_frame" src="http://localhost/child.html"
 onLoad="javascript:Help.import_functions(this)"></iframe>

    <script>
    var Help = {

          imported_function :null,
              import_functions : function(iframe)
   {
    this.imported_function = String(iframe.contentWindow.whoo_hoo);
    eval("this.imported_function = " + this.imported_function);
    iframe.parentNode.removeChild(iframe);

   //
   // displays 'whoo hoo' in an alert box
   //
   this.imported_function();

   try
   {
      //
      // If the Object prototype was changed in the parent
      // this would have displayed 'should not work' in an alert
      //
      this.test('should not work');
   }
   catch(e){alert('object prototype is unmodified');}

   }, 
    </script>

http://thecodeabode.blogspot.com/

Ben
Will your example still work if `whoo_hoo` relies on `Object.prototype.test`?
no
no it wont unfortunately - as for all purposes, the imported function is really just an eval'ed string in parent, content for any function can be imported as a string... even content relying on non existing prototype modifications. Errors would result when eval'ing, or executing the imported function.The only way it would work is if you allowed modification of the parent's js prototypes - so if this is what you really wanted - you may as well include the script as per usual.
Ben
+1  A: 

This could be a possible solution:

  • wrap all the foreign code into a class
  • make all the undeclared identifiers members of that class
  • before invoking the messy code, make a copy of the built-in classes and name them differently
    (is this possible??).

I think this should solve all the problems.

With my suggestion, your sample

var badA = "hahaha";
this.badB = "hehehe";
badC = "hohoho";

String.prototype.star = function(){ return '***' + this + '***' }

var somethingUseful = {
  doStuff: function () {
    alert((badA + badB + badC).star());
  }
}

should get like this

// Identifies the added properties to prototypes (ie String and Function)
// for later filtering if you need a for-in loop.
var stringAddons = [];
var functionAddons = []
var _string = new String();
var _function = function() {};
for (var property in _string) { if (!_string.hasOwnProperty(property)) { stringAddons.push(property); }}
for (var property in _function) { if (!_function.hasOwnProperty(property)) { functionAddons.push(property); }}

// Wraps the undeclared identifiers
var global = function()
{
  this.badA = "hahaha";
  this.badB = "hehehe";
  this.badC = "hohoho";

  String.prototype.star = function(){ return '***' + this + '***' }

  this.somethingUseful = {
    doStuff: function () {
      alert((global.badA + global.badB + global.badC).star());
    }
  }
}
var global = new Global();
global.somethingUseful.doStuff();

The tricky part is making ALL the undeclared identifiers global properties. Maybe a good regex script could make it. Im not that good with regex :)

Gabriel
Tried the same.. putting foreign code in a function gets rid of messy globals in foreign code, but prototype methods is still a problem. Cant make copies of Array, String as far as i know.. will put in code in separate answer...
Ravindra Sane
This also doesn't help if values are assigned to identifiers without using the `var` keyword... see `badC` in my example... you can wrap it in a function but you'll still have `window.badC`.
no
window.open?... load your helper js in the new window and have it collate all the objects/methods and stuff them into an object. then from that JS, cose the window and return the object. (it would be annoying from a user's perspective and with all those popup blockers, may or may not work) too late here to give it a try. Your thoughts? Feasible?
Ravindra Sane
I read something interesting on document domains and how it restricts JavaScipt code from accessing each other's details. Anyone who's used it, is that the answer? The question's got me very much interested in learning the solution
Ravindra Sane
The cross-domain thing doesn't come into play here; the iframe doesn't belong to a domain so the parent window has full access to it.
no
+1  A: 

code for comment under Gabriel's answer..

var r = {
    init : null,
    _init: function(){
        var obj = new XMLHttpRequest();
        obj.onreadystatechange = function(){
            if ((this.status == 200) && this.readyState==4){
                try{
                    eval("r.init = function(){" + this.responseText + "}");
                    r.init();
                } catch(e){/*something bad in the script...*/}
            }
        }
        obj.open("GET","/jspolute_bad.js", true);
        obj.send();
    }   
}
r._init();

With methods being added to prototype, you might be in trouble if one or two of the exported functions expect the method as it is modified in foreign code. tedious solution that comes to mind is to Regex the responseText before eval'ing it for array.prototype,string.prototype and fix it some how. Will try that and let you know.. but it would mostly cater to straightforward scripts only.

Ravindra Sane
A: 

None of the answers so far seem to work as well as the iframe thing. I'm fairly convinced the iframe jail is going to be the solution to the problem. I'm putting my current solution here as an answer because it seems to work better than the other answers provided so far. I'd really like to refine this iframe technique into something foolproof, and I'd love to understand how the modified prototypes work. This example works in chrome and moz.

test.js

var badA = "hahaha";
this.badB = "hehehe";
badC = "hohoho";

String.prototype.star = function(){ return '***' + this + '***' }

var somethingUseful = {
  doStuff: function () {
    alert((badA + badB + badC).star());
  }

}

test.html

<html>
  <head>
    <script>
    /** 
      safeLoad - load a script in an iframe jail

      @param {String} scriptPath    path to a javascript file
      @param {String} target        name of global object to import
      @param {Function} callback    function to execute after script loads
    */ 
    function safeLoad (scriptPath, target, callback) {
      var g=this, f=document.createElement('iframe'), frameIndex=frames.length;

      f.style.width='0px'; 
      f.style.height='0px'; 
      f.style.border='none'; 
      f.style.position='absolute';

      f.onload=function(){
        var w=frames[frameIndex];
        var s=w.document.createElement('script');
        s.src=scriptPath;
        s.onload=function(){
          g[target]=w[target];
          if (callback && callback.apply) callback(w);
        };
        w.document.body.appendChild(s);
      }
      document.documentElement.appendChild(f);
    }
    </script>  

    <script>

    safeLoad('test.js', 'somethingUseful', function init () {
      // next line should give ***hahahahehehehohoho***
      somethingUseful.doStuff();
      // next line should give undefinedundefinedundefinedundefined
      alert(typeof badA + typeof badB + typeof badC + String.prototype.star);
    });

    </script>   
  </head>
</html>
no