concord-consortium/lara

View on GitHub
app/assets/javascripts/iframe-phone.js

Summary

Maintainability
F
2 wks
Test Coverage
//
// version 1.3.1
//
// NOTE: once the files like interactive_iframe.js.coffee that exist outside of lara-typescript use iframe-phone
//       this file needs to stay in the build (referenced in application.js)
//
!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.iframePhone=e():"undefined"!=typeof global?global.iframePhone=e():"undefined"!=typeof self&&(self.iframePhone=e())}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
  var structuredClone = require('./structured-clone');
  var HELLO_INTERVAL_LENGTH = 200;
  var HELLO_TIMEOUT_LENGTH = 60000;

  function IFrameEndpoint() {
    var listeners = {};
    var isInitialized = false;
    var connected = false;
    var postMessageQueue = [];
    var helloInterval;

    function postToParent(message) {
      // See http://dev.opera.com/articles/view/window-postmessage-messagechannel/#crossdoc
      //     https://github.com/Modernizr/Modernizr/issues/388
      //     http://jsfiddle.net/ryanseddon/uZTgD/2/
      if (structuredClone.supported()) {
        window.parent.postMessage(message, '*');
      } else {
        window.parent.postMessage(JSON.stringify(message), '*');
      }
    }

    function post(type, content) {
      var message;
      // Message object can be constructed from 'type' and 'content' arguments or it can be passed
      // as the first argument.
      if (arguments.length === 1 && typeof type === 'object' && typeof type.type === 'string') {
        message = type;
      } else {
        message = {
          type: type,
          content: content
        };
      }
      if (connected) {
        postToParent(message);
      } else {
        postMessageQueue.push(message);
      }
    }

    function postHello() {
      postToParent({
        type: 'hello'
      });
    }

    function addListener(type, fn) {
      listeners[type] = fn;
    }

    function removeListener(type) {
      delete listeners[type];
    }

    function removeAllListeners() {
      listeners = {};
    }

    function getListenerNames() {
      return Object.keys(listeners);
    }

    function messageListener(message) {
      // Anyone can send us a message. Only pay attention to messages from parent.
      if (message.source !== window.parent) return;
      var messageData = message.data;
      if (typeof messageData === 'string') messageData = JSON.parse(messageData);

      if (!connected && messageData.type === 'hello') {
        connected = true;
        stopPostingHello();
        while (postMessageQueue.length > 0) {
          post(postMessageQueue.shift());
        }
      }

      if (connected && listeners[messageData.type]) {
        listeners[messageData.type](messageData.content);
      }
    }

    function disconnect() {
      connected = false;
      stopPostingHello();
      removeAllListeners();
      window.removeEventListener('message', messageListener);
    }

    /**
      Initialize communication with the parent frame. This should not be called until the app's custom
      listeners are registered (via our 'addListener' public method) because, once we open the
      communication, the parent window may send any messages it may have queued. Messages for which
      we don't have handlers will be silently ignored.
    */
    function initialize() {
      if (isInitialized) {
        return;
      }
      isInitialized = true;
      if (window.parent === window) return;

      // We kick off communication with the parent window by sending a "hello" message. Then we wait
      // for a handshake (another "hello" message) from the parent window.
      startPostingHello();
      window.addEventListener('message', messageListener, false);
    }

    function startPostingHello() {
      if (helloInterval) {
        stopPostingHello();
      }
      helloInterval = window.setInterval(postHello, HELLO_INTERVAL_LENGTH);
      window.setTimeout(stopPostingHello, HELLO_TIMEOUT_LENGTH);
      // Post the first msg immediately.
      postHello();
    }

    function stopPostingHello() {
      window.clearInterval(helloInterval);
      helloInterval = null;
    }

    // Public API.
    return {
      initialize: initialize,
      getListenerNames: getListenerNames,
      addListener: addListener,
      removeListener: removeListener,
      removeAllListeners: removeAllListeners,
      disconnect: disconnect,
      post: post
    };
  }

  var instance = null;

  // IFrameEndpoint is a singleton, as iframe can't have multiple parents anyway.
  module.exports = function getIFrameEndpoint() {
    if (!instance) {
      instance = new IFrameEndpoint();
    }
    return instance;
  };

  },{"./structured-clone":4}],2:[function(require,module,exports){
  var ParentEndpoint = require('./parent-endpoint');
  var getIFrameEndpoint = require('./iframe-endpoint');

  // Not a real UUID as there's an RFC for that (needed for proper distributed computing).
  // But in this fairly parochial situation, we just need to be fairly sure to avoid repeats.
  function getPseudoUUID() {
    var chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
    var len = chars.length;
    var ret = [];

    for (var i = 0; i < 10; i++) {
      ret.push(chars[Math.floor(Math.random() * len)]);
    }
    return ret.join('');
  }

  module.exports = function IframePhoneRpcEndpoint(handler, namespace, targetWindow, targetOrigin, phone) {
    var pendingCallbacks = Object.create({});

    // if it's a non-null object, rather than a function, 'handler' is really an options object
    if (handler && typeof handler === 'object') {
      namespace = handler.namespace;
      targetWindow = handler.targetWindow;
      targetOrigin = handler.targetOrigin;
      phone = handler.phone;
      handler = handler.handler;
    }

    if (!phone) {
      if (targetWindow === window.parent) {
        phone = getIFrameEndpoint();
        phone.initialize();
      } else {
        phone = new ParentEndpoint(targetWindow, targetOrigin);
      }
    }

    phone.addListener(namespace, function (message) {
      var callbackObj;

      if (message.messageType === 'call' && typeof this.handler === 'function') {
        this.handler.call(undefined, message.value, function (returnValue) {
          phone.post(namespace, {
            messageType: 'returnValue',
            uuid: message.uuid,
            value: returnValue
          });
        });
      } else if (message.messageType === 'returnValue') {
        callbackObj = pendingCallbacks[message.uuid];

        if (callbackObj) {
          window.clearTimeout(callbackObj.timeout);
          if (callbackObj.callback) {
            callbackObj.callback.call(undefined, message.value);
          }
          pendingCallbacks[message.uuid] = null;
        }
      }
    }.bind(this));

    function call(message, callback) {
      var uuid = getPseudoUUID();

      pendingCallbacks[uuid] = {
        callback: callback,
        timeout: window.setTimeout(function () {
          if (callback) {
            callback(undefined, new Error("IframePhone timed out waiting for reply"));
          }
        }, 2000)
      };

      phone.post(namespace, {
        messageType: 'call',
        uuid: uuid,
        value: message
      });
    }

    function disconnect() {
      phone.disconnect();
    }

    this.handler = handler;
    this.call = call.bind(this);
    this.disconnect = disconnect.bind(this);
  };

  },{"./iframe-endpoint":1,"./parent-endpoint":3}],3:[function(require,module,exports){
  var structuredClone = require('./structured-clone');

  /**
    Call as:
      new ParentEndpoint(targetWindow, targetOrigin, afterConnectedCallback)
        targetWindow is a WindowProxy object. (Messages will be sent to it)

        targetOrigin is the origin of the targetWindow. (Messages will be restricted to this origin)

        afterConnectedCallback is an optional callback function to be called when the connection is
          established.

    OR (less secure):
      new ParentEndpoint(targetIframe, afterConnectedCallback)

        targetIframe is a DOM object (HTMLIframeElement); messages will be sent to its contentWindow.

        afterConnectedCallback is an optional callback function

      In this latter case, targetOrigin will be inferred from the value of the src attribute of the
      provided DOM object at the time of the constructor invocation. This is less secure because the
      iframe might have been navigated to an unexpected domain before constructor invocation.

    Note that it is important to specify the expected origin of the iframe's content to safeguard
    against sending messages to an unexpected domain. This might happen if our iframe is navigated to
    a third-party URL unexpectedly. Furthermore, having a reference to Window object (as in the first
    form of the constructor) does not protect against sending a message to the wrong domain. The
    window object is actualy a WindowProxy which transparently proxies the Window object of the
    underlying iframe, so that when the iframe is navigated, the "same" WindowProxy now references a
    completely differeent Window object, possibly controlled by a hostile domain.

    See http://www.esdiscuss.org/topic/a-dom-use-case-that-can-t-be-emulated-with-direct-proxies for
    more about this weird behavior of WindowProxies (the type returned by <iframe>.contentWindow).
  */

  module.exports = function ParentEndpoint(targetWindowOrIframeEl, targetOrigin, afterConnectedCallback) {
    var postMessageQueue = [];
    var connected = false;
    var handlers = {};
    var targetWindowIsIframeElement;

    function getIframeOrigin(iframe) {
      return iframe.src.match(/(.*?\/\/.*?)\//)[1];
    }

    function post(type, content) {
      var message;
      // Message object can be constructed from 'type' and 'content' arguments or it can be passed
      // as the first argument.
      if (arguments.length === 1 && typeof type === 'object' && typeof type.type === 'string') {
        message = type;
      } else {
        message = {
          type: type,
          content: content
        };
      }
      if (connected) {
        var tWindow = getTargetWindow();
        // if we are laready connected ... send the message
        // See http://dev.opera.com/articles/view/window-postmessage-messagechannel/#crossdoc
        //     https://github.com/Modernizr/Modernizr/issues/388
        //     http://jsfiddle.net/ryanseddon/uZTgD/2/
        if (structuredClone.supported()) {
          tWindow.postMessage(message, targetOrigin);
        } else {
          tWindow.postMessage(JSON.stringify(message), targetOrigin);
        }
      } else {
        // else queue up the messages to send after connection complete.
        postMessageQueue.push(message);
      }
    }

    function addListener(messageName, func) {
      handlers[messageName] = func;
    }

    function removeListener(messageName) {
      delete handlers[messageName];
    }

    function removeAllListeners() {
      handlers = {};
    }

    // Note that this function can't be used when IFrame element hasn't been added to DOM yet
    // (.contentWindow would be null). At the moment risk is purely theoretical, as the parent endpoint
    // only listens for an incoming 'hello' message and the first time we call this function
    // is in #receiveMessage handler (so iframe had to be initialized before, as it could send 'hello').
    // It would become important when we decide to refactor the way how communication is initialized.
    function getTargetWindow() {
      if (targetWindowIsIframeElement) {
        var tWindow = targetWindowOrIframeEl.contentWindow;
        if (!tWindow) {
          throw "IFrame element needs to be added to DOM before communication " +
                "can be started (.contentWindow is not available)";
        }
        return tWindow;
      }
      return targetWindowOrIframeEl;
    }

    function receiveMessage(message) {
      var messageData;
      if (message.source === getTargetWindow() && (targetOrigin === '*' || message.origin === targetOrigin)) {
        messageData = message.data;
        if (typeof messageData === 'string') {
          messageData = JSON.parse(messageData);
        }
        if (handlers[messageData.type]) {
          handlers[messageData.type](messageData.content);
        } else {
          console.log("cant handle type: " + messageData.type);
        }
      }
    }

    function disconnect() {
      connected = false;
      removeAllListeners();
      window.removeEventListener('message', receiveMessage);
    }

    // handle the case that targetWindowOrIframeEl is actually an <iframe> rather than a Window(Proxy) object
    // Note that if it *is* a WindowProxy, this probe will throw a SecurityException, but in that case
    // we also don't need to do anything
    try {
      targetWindowIsIframeElement = targetWindowOrIframeEl.constructor === HTMLIFrameElement;
    } catch (e) {
      targetWindowIsIframeElement = false;
    }

    if (targetWindowIsIframeElement) {
      // Infer the origin ONLY if the user did not supply an explicit origin, i.e., if the second
      // argument is empty or is actually a callback (meaning it is supposed to be the
      // afterConnectionCallback)
      if (!targetOrigin || targetOrigin.constructor === Function) {
        afterConnectedCallback = targetOrigin;
        targetOrigin = getIframeOrigin(targetWindowOrIframeEl);
      }
    }

    // Handle pages served through file:// protocol. Behaviour varies in different browsers. Safari sets origin
    // to 'file://' and everything works fine, but Chrome and Safari set message.origin to null.
    // Also, https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage says:
    //  > Lastly, posting a message to a page at a file: URL currently requires that the targetOrigin argument be "*".
    //  > file:// cannot be used as a security restriction; this restriction may be modified in the future.
    // So, using '*' seems like the only possible solution.
    if (targetOrigin === 'file://') {
      targetOrigin = '*';
    }

    // when we receive 'hello':
    addListener('hello', function () {
      connected = true;

      // send hello response
      post({
        type: 'hello',
        // `origin` property isn't used by IframeEndpoint anymore (>= 1.2.0), but it's being sent to be
        // backward compatible with old IframeEndpoint versions (< v1.2.0).
        origin: window.location.href.match(/(.*?\/\/.*?)\//)[1]
      });

      // give the user a chance to do things now that we are connected
      // note that is will happen before any queued messages
      if (afterConnectedCallback && typeof afterConnectedCallback === "function") {
        afterConnectedCallback();
      }

      // Now send any messages that have been queued up ...
      while (postMessageQueue.length > 0) {
        post(postMessageQueue.shift());
      }
    });

    window.addEventListener('message', receiveMessage, false);

    // Public API.
    return {
      post: post,
      addListener: addListener,
      removeListener: removeListener,
      removeAllListeners: removeAllListeners,
      disconnect: disconnect,
      getTargetWindow: getTargetWindow,
      targetOrigin: targetOrigin
    };
  };

  },{"./structured-clone":4}],4:[function(require,module,exports){
  var featureSupported = {
    'structuredClones': 0
  };

  (function () {
    var result = 0;

    if (!!window.postMessage) {
      try {
        // Spec states you can't transmit DOM nodes and it will throw an error
        // postMessage implementations that support cloned data will throw.
        window.postMessage(document.createElement("a"), "*");
      } catch (e) {
        // BBOS6 throws but doesn't pass through the correct exception
        // so check error message
        result = (e.DATA_CLONE_ERR || e.message === "Cannot post cyclic structures.") ? 1 : 0;
        featureSupported = {
          'structuredClones': result
        };
      }
    }
  }());

  exports.supported = function supported() {
    return featureSupported && featureSupported.structuredClones > 0;
  };

  },{}],5:[function(require,module,exports){
  module.exports = {
    /**
     * Allows to communicate with an iframe.
     */
    ParentEndpoint:  require('./lib/parent-endpoint'),
    /**
     * Allows to communicate with a parent page.
     * IFrameEndpoint is a singleton, as iframe can't have multiple parents anyway.
     */
    getIFrameEndpoint: require('./lib/iframe-endpoint'),
    structuredClone: require('./lib/structured-clone'),

    // TODO: May be misnamed
    IframePhoneRpcEndpoint: require('./lib/iframe-phone-rpc-endpoint')

  };

  },{"./lib/iframe-endpoint":1,"./lib/iframe-phone-rpc-endpoint":2,"./lib/parent-endpoint":3,"./lib/structured-clone":4}]},{},[5])
  (5)
  });
  ;