freedomjs/freedom

View on GitHub
src/link/frame.js

Summary

Maintainability
A
2 hrs
Test Coverage
/*jslint indent:2, white:true, node:true, sloppy:true */
/*globals URL,webkitURL */
var Link = require('../link');
var util = require('../util');

/**
 * A port providing message transport between two freedom contexts via iFrames.
 * @class Frame
 * @extends Link
 * @uses handleEvents
 * @constructor
 */
var Frame = function(id, resource) {
  Link.call(this, id, resource);
};

/**
 * Get the document to use for the frame. This allows overrides in downstream
 * links that want to essentially make an iFrame, but need to do it in another
 * context.
 * @method getDocument
 * @protected
 */
Frame.prototype.getDocument = function () {
  this.document = document;
  if (!this.document.body) {
    this.document.appendChild(this.document.createElement("body"));
  }
  this.root = document.body;
};

/**
 * Start this port by listening or creating a frame.
 * @method start
 * @private
 */
Frame.prototype.start = function() {
  if (this.config.moduleContext) {
    this.config.global.DEBUG = true;
    this.setupListener();
    this.src = 'in';
  } else {
    this.setupFrame();
    this.src = 'out';
  }
};

/**
 * Stop this port by deleting the frame.
 * @method stop
 * @private
 */
Frame.prototype.stop = function() {
  // Function is determined by setupListener or setupFrame as appropriate.
};

/**
 * Get the textual description of this port.
 * @method toString
 * @return {String} the description of this port.
 */
Frame.prototype.toString = function() {
  return "[Frame" + this.id + "]";
};

/**
 * Set up a global listener to handle incoming messages to this
 * freedom.js context.
 * @method setupListener
 */
Frame.prototype.setupListener = function() {
  var onMsg = function(msg) {
    if (msg.data.src && msg.data.src !== 'in') {
      this.emitMessage(msg.data.flow, msg.data.message);
    }
  }.bind(this);
  this.obj = this.config.global;
  this.obj.addEventListener('message', onMsg, true);
  this.stop = function() {
    this.obj.removeEventListener('message', onMsg, true);
    delete this.obj;
  };
  this.emit('started');
  this.obj.postMessage("Ready For Messages", "*");
};

/**
 * Get a URL of a blob object for inclusion in a frame.
 * Polyfills implementations which don't have a current URL object, like
 * phantomjs.
 * @method getURL
 */
Frame.prototype.getURL = function(blob) {
  if (typeof URL !== 'object' && typeof webkitURL !== 'undefined') {
    return webkitURL.createObjectURL(blob);
  } else {
    return URL.createObjectURL(blob);
  }
};

/**
 * Deallocate the URL of a blob object.
 * Polyfills implementations which don't have a current URL object, like
 * phantomjs.
 * @method getURL
 */
Frame.prototype.revokeURL = function(url) {
  if (typeof URL !== 'object' && typeof webkitURL !== 'undefined') {
    webkitURL.revokeObjectURL(url);
  } else {
    URL.revokeObjectURL(url);
  }
};

/**
 * Set up an iFrame with an isolated freedom.js context inside.
 * @method setupFrame
 */
Frame.prototype.setupFrame = function() {
  var frame, onMsg;
  this.getDocument();
  frame = this.makeFrame(this.config.source, this.config.inject);
  
  this.root.appendChild(frame);

  onMsg = function(frame, msg) {
    if (!this.obj) {
      this.obj = frame;
      this.emit('started');
    }
    if (msg.data.src && msg.data.src !== 'out') {
      this.emitMessage(msg.data.flow, msg.data.message);
    }
  }.bind(this, frame.contentWindow);

  frame.contentWindow.addEventListener('message', onMsg, true);
  this.stop = function() {
    frame.contentWindow.removeEventListener('message', onMsg, true);
    if (this.obj) {
      delete this.obj;
    }
    this.revokeURL(frame.src);
    frame.src = "about:blank";
    this.root.removeChild(frame);
  };
};

/**
 * Make frames to replicate freedom isolation without web-workers.
 * iFrame isolation is non-standardized, and access to the DOM within frames
 * means that they are insecure. However, debugging of webworkers is
 * painful enough that this mode of execution can be valuable for debugging.
 * @method makeFrame
 */
Frame.prototype.makeFrame = function(src, inject) {
  // TODO(willscott): add sandboxing protection.
  var frame = this.document.createElement('iframe'),
      extra = '',
      loader,
      blob;

  if (inject) {
    if (!inject.length) {
      inject = [inject];
    }
    inject.forEach(function(script) {
      extra += '<script src="' + script + '" onerror="' +
      'throw new Error(\'Injection of ' + script +' Failed!\');' +
      '"></script>';
    });
  }
  loader = '<html><meta http-equiv="Content-type" content="text/html;' +
    'charset=UTF-8">' + extra + '<script src="' + src + '" onerror="' +
    'throw new Error(\'Loading of ' + src +' Failed!\');' +
    '"></script></html>';
  blob = util.getBlob(loader, 'text/html');
  frame.src = this.getURL(blob);

  return frame;
};

/**
 * Receive messages from the hub to this port.
 * Received messages will be emitted from the other side of the port.
 * @method deliverMessage
 * @param {String} flow the channel/flow of the message.
 * @param {Object} message The Message.
 */
Frame.prototype.deliverMessage = function(flow, message) {
  if (this.obj) {
    //fdom.debug.log('message sent to worker: ', flow, message);
    this.obj.postMessage({
      src: this.src,
      flow: flow,
      message: message
    }, '*');
  } else {
    this.once('started', this.onMessage.bind(this, flow, message));
  }
};

module.exports = Frame;