@elliemae/em-ssf-guest
Version:
ICE Secure Scripting Framework Guest Library
703 lines (583 loc) • 25.8 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["script"] = factory();
else
root["elli"] = root["elli"] || {}, root["elli"]["script"] = factory();
})(typeof self !== 'undefined' ? self : this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
;
//
// Provides very basic logging capabilities to the SSF system
//
module.exports = {
// Constants for the different log levels
levels: {
Verbose: 0,
Trace: 0,
Info: 1,
Warning: 2,
Error: 3,
None: 10
},
// Colors used for logging
colors: ['grey', 'black', 'darkorange', 'firebrick'],
// The current log verbosity level
logLevel: 3,
// Writes an entry to the log
log: function log(text, level, src) {
level = level ? level : this.levels.Verbose;
if (level >= this.logLevel) {
var val = (src ? '(' + src + '): ' : '') + text;
if (this.colors && this.colors[level]) console.log('%c' + val, 'color: ' + this.colors[level]);else console.log(val);
}
},
// Writes a warning to the log
info: function info(text, src) {
this.log(text, this.levels.Info, src);
},
// Writes a warning to the log
warn: function warn(text, src) {
this.log(text, this.levels.Warning, src);
},
// Writes a warning to the log
error: function error(text, src) {
this.log(text, this.levels.Error, src);
},
// Writes a verbose/trace to the log
trace: function trace(text, src) {
this.log(text, this.levels.Trace, src);
}
};
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
;
//
// Provides functions for proxying objects and functions/events between the parent window
// and the sandboxed script.
//
// Get the required libraries
var remoting = __webpack_require__(2);
var logger = __webpack_require__(0);
var logSource = "ssf-guest";
// Create the elli namespace, if not already present
var script = function () {
// Private map that stores the event listeners for different object events
var eventListeners = new Map();
// Provides a callback token
var nextToken = 1;
var isConnected = false;
// Provides a list of capabilities of this version of teh scripting framework
var capabilities = {
eventFeedback: true
// Converts a JSON object into a Control which can be provided to the script
};function fromJSON(controlJSON) {
// Ensure we have JSON
if (!controlJSON || !controlJSON.objectId) {
logger.error('Deserialization of scripting object failed. Object does not have an Object ID.', logSource);
throw "Cannot deserialize object JSON into proxy.";
}
// Create a new object
var ctrl = new script.Proxy(controlJSON.objectId);
// Dynamically build the function set on the control
if (controlJSON.functions) {
controlJSON.functions.forEach(function (functionName) {
ctrl[functionName] = function () {
return invoke(ctrl.id, functionName, [].slice.call(arguments));
};
});
}
// Dynamically create the event proxies
if (controlJSON.events) {
controlJSON.events.forEach(function (eventName) {
ctrl[eventName] = new script.ProxyEvent(ctrl.id, eventName);
});
}
logger.trace('Created guest proxy for scripting object (id = "' + ctrl.id + '")', logSource);
return ctrl;
}
// Invokes an remote object via the remoting framework
function invoke(objectId, functionName, functionParams) {
logger.info('Invoking scripting object function ' + objectId + '.' + functionName + '()...', logSource);
// Find any function params that are promises
var promises = [];
if (functionParams) {
var _loop = function _loop(i) {
var p = functionParams[i];
if (p instanceof Promise) {
promises.push(p.then(function (val) {
functionParams[i] = val;
}));
}
};
for (var i = 0; i < functionParams.length; i++) {
_loop(i);
}
}
// Now wait to resolve all of the promises and then call our invoke
return Promise.all(promises).then(function () {
return remoting.invoke(window.parent, "object:invoke", {
objectId: objectId,
functionName: functionName,
functionParams: functionParams
}).then(function (response) {
logger.info('Received response for invocation of function ' + objectId + '.' + functionName + '()', logSource);
return handleResponse(response);
});
});
}
// Handles events meant to control the guest's behavior
function handleConfigChangeEvent(sourceWin, msgId, msgType, msgBody) {
// Handle log level changes
if (msgBody && typeof msgBody.logLevel != "undefined") {
logger.logLevel = msgBody.logLevel;
logger.info('Log level changed by host to ' + logger.logLevel, logSource);
}
}
// Handle an object event
function handleObjectEvent(sourceWin, msgId, msgType, msgBody) {
// Deserialize the object
var object = fromJSON(msgBody.object);
if (object == null) return null;
// Get the listeners for the event
var eventId = getEventId(object.id, msgBody.eventName);
logger.info('Received event "' + eventId + '" from host', logSource);
// If an event handler is specified in the request body, then we will target
// the event to that function only.
var listeners;
if (msgBody.eventHandler) {
listeners = [{ callback: window[msgBody.eventHandler] }];
} else {
listeners = eventListeners.get(eventId) || [];
}
// Notify each listener, recording a promise for each response
var promises = [];
listeners.forEach(function (callbackInfo) {
if (callbackInfo && callbackInfo.callback) {
logger.info('Invoking event subscriber ' + callbackInfo.callback.name + ' for event ' + eventId, logSource);
var retVal = callbackInfo.callback(object, msgBody.eventParams, msgBody.eventOptions);
if (retVal && retVal.then && typeof retVal.then == "function") {
promises.push(retVal);
} else if (typeof retVal !== "undefined") {
promises.push(Promise.resolve(retVal));
}
}
});
logger.info('All (# = ' + promises.length + ') subscriber callbacks completed for event ' + eventId, logSource);
// Resolve all promises and return a response to the calling window
if (msgId) {
Promise.all(promises).then(function (values) {
logger.info('All subscriber promises fulfilled for event ' + eventId, logSource);
remoting.respond(sourceWin, msgId, values);
});
}
}
// Returns a normalized event ID for an object and event
function getEventId(objectId, eventName) {
return objectId.toLowerCase() + '.' + eventName.toLowerCase();
}
// Deserialize the response into an object
function handleResponse(response) {
if (response && response.type == 'object') {
return fromJSON(response.object);
} else if (response && response.type == 'value') {
return response.value;
} else {
return response;
}
}
// Return an object with the public facing functions of the script class
return {
// Returns a control to the caller to act on
getObject: function getObject(objectId) {
logger.info('Retrieving scripting object "' + objectId + '" from host...', logSource);
return remoting.invoke(window.parent, "object:get", {
objectId: objectId
}).then(function (response) {
logger.info('Scripting object "' + objectId + '" returned successfully.', logSource);
return handleResponse(response);
});
},
// Add an event handler for a specific object's events
subscribe: function subscribe(objectId, eventName, callback) {
logger.info('Registering subscription for event ' + objectId + '.' + eventName + ' to ' + callback.name, logSource);
var eventId = getEventId(objectId, eventName);
var listeners = eventListeners.get(eventId);
if (listeners == null) listeners = [];
var token = nextToken++;
listeners.push({ callback: callback, token: token.toString() });
eventListeners.set(eventId, listeners);
return token;
},
// Removes an event handler for a specific object's events
unsubscribe: function unsubscribe(objectId, eventName, token) {
var eventId = getEventId(objectId, eventName);
var listeners = eventListeners.get(eventId);
if (listeners) {
listeners = listeners.filter(function (callbackInfo) {
return callbackInfo.token != token;
});
eventListeners.set(eventId, listeners);
}
},
// Connect to the remoting system
connect: function connect(guestWindowOrOptions) {
var guestWindow = window;
var guestOptions = Object.assign({}, capabilities);
// Allow first parameter to be Window object or options object for
// backwards compatibility
if (guestWindowOrOptions && guestWindowOrOptions.addEventListener) {
guestWindow = guestWindowOrOptions;
} else if (guestWindowOrOptions) {
// Pull the guestWindow from the options, if present
guestWindow = guestWindowOrOptions.window || guestWindow;
// Clone the guest options and delete the window reference
var clonedOptions = Object.assign({}, guestWindowOrOptions);
delete clonedOptions.window;
// Merge into the guest options object
guestOptions = Object.assign(guestOptions, clonedOptions);
}
if (!isConnected) {
// Initialize messenger
remoting.initialize(guestWindow);
remoting.setLogSource(logSource);
// Listen for the events the guest knows how to process
remoting.listen("object:event", handleObjectEvent.bind(this));
remoting.listen("host:config", handleConfigChangeEvent.bind(this));
// Let the host know the guest is ready for events, and send along
// the guests's capabilities to allow for compatibility with future
// host versions
remoting.send(window.parent, "guest:ready", guestOptions);
isConnected = true;
logger.info('elli.script framework connected', logSource);
}
},
// The ready function is preserved for historical purposes
ready: function ready() {
this.connect();
remoting.send(window.parent, "guest:readyComplete", {});
},
// Sets the log level for the guest
setLogLevel: function setLogLevel(level) {
logger.logLevel = level;
logger.info('Log level changed by guest to ' + logger.logLevel, logSource);
},
// Expose the log levels from the logger object
logLevels: logger.levels
};
}();
// Defines the class for a Control
script.Proxy = function Proxy(objectId) {
this.__TYPE__ = "Proxy";
// Store the control's ID
this.id = objectId;
};
// Adds an event registration
script.Proxy.prototype.addEventListener = function (eventName, eventListener) {
return script.subscribe(this.id, eventName, eventListener);
};
// Revokes an event registration
script.Proxy.prototype.removeEventListener = function (eventName, token) {
script.unsubscribe(this.id, eventName, token);
};
// Placeholder object for a proxy event
script.ProxyEvent = function ProxyEvent(objectId, eventName) {
// Allow for subscription to the event
this.subscribe = function (callback) {
return script.subscribe(objectId, eventName, callback);
};
// Allow for un-subscription to the event
this.unsubscribe = function (token) {
script.unsubscribe(objectId, eventName, token);
};
};
// Create the guests
script.guest = function () {
// Indicates if the guest is created
var created = false;
return {
// The init function is invoked upon loading the plugin frame. It notifies the parent
// window that the plugin is ready to receive messages
create: function create(scriptUri, containerElement) {
if (created) return;
if (scriptUri) {
// Load the script and await its load event before signaling that the window is
// ready to receive events
var scriptElement = document.createElement("script");
scriptElement.setAttribute("src", scriptUri);
scriptElement.addEventListener("load", function () {
script.connect(window);
});
containerElement.appendChild(scriptElement);
} else {
// Notify the parent that the guest is ready to receive events
script.connect();
}
// Set the created flag
created = true;
}
};
}();
// Export the script object
module.exports = script;
/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {
;
//
// Provides core messaging capabilities for cross-frame interactions in a sandboxed environment.
// The elli.remoting object is used by both the parent and child windows to pass messages in a
// consistent manner and allow for async invocations with responses.
//
var logger = __webpack_require__(0);
var logSource = "ssf-remoting";
var remoting = function () {
// The next invocation ID
var nextInvocationId = 1;
// Represents the set of active invocations, keyed by the message ID
var invocations = new Map();
// The set of listeners for the messages
var listeners = new Map();
// String constants
var messageSource = "elli:remoting";
var responseMessageType = "elli:remoting:response";
var exceptionMessageType = "elli:remoting:exception";
// Creates the shell of a message
function createMessage(messageType, messageBody) {
return {
source: messageSource,
type: messageType,
body: messageBody
};
}
// The handle of the timeout interval
var timeoutMonitorHandle = null;
// Evaluates the timeouts on any waiting invocations
function evaluateTimeouts() {
var ts = Date.now();
// Make a list of items to remove
var canceledItems = [];
// Check each invocation to see if it's been handled yet
invocations.forEach(function (eventData, key) {
logger.trace("Checking response timeout for request (id = " + eventData.requestId + ") @ " + eventData.cancelTime, logSource);
if (eventData.cancelTime && eventData.cancelTime < ts) {
logger.info("Detected response timeout for request (id = " + eventData.requestId + ")...", logSource);
canceledItems.push(key);
eventData.resolve();
logger.trace("Aborted waiting for response to request (id = " + eventData.requestId + ")", logSource);
}
});
// Remove all elements that were cancelled from the invocations map
canceledItems.forEach(function (key) {
invocations.delete(key);
});
// Stop the response monitor if there are no pending invocations
if (invocations.size == 0) {
stopResponseMonitor();
}
}
// Set a timer interval to catch any invocations that didn't respond in a timely manner
function startResponseMonitor() {
if (timeoutMonitorHandle == null) {
logger.trace("Staring response timeout evaluator", logSource);
timeoutMonitorHandle = window.setInterval(evaluateTimeouts, 200);
}
}
// Stops the timeout monitor interval
function stopResponseMonitor() {
if (timeoutMonitorHandle != null) {
window.clearInterval(timeoutMonitorHandle);
timeoutMonitorHandle = null;
logger.trace("Stopped response timeout evaluator", logSource);
}
}
// Pops an invocation from the incovation list
function popInvocation(requestId) {
var e = invocations.get(requestId);
invocations.delete(requestId);
return e;
}
// Return the public implementation of the object
return {
// Subscribes a listener to a message type
listen: function listen(messageType, callback) {
var items = listeners.get(messageType);
if (!items) items = [];
items.push(callback);
listeners.set(messageType, items);
},
// Sends an invocation which generates a Promise to be used to get a response
invoke: function invoke(targetWin, messageType, messageBody, responseTimeoutMs) {
return new Promise(function (resolve, reject) {
// Construct the message to post
var msg = createMessage(messageType, messageBody);
msg.requestId = nextInvocationId++,
// Register this invocation to connect the callback to the Promise
invocations.set(msg.requestId, {
requestId: msg.requestId,
resolve: resolve,
reject: reject,
cancelTime: responseTimeoutMs ? Date.now() + responseTimeoutMs : null
});
// Post the message to the parent window
targetWin.postMessage(msg, "*");
logger.trace('Posted invocation message of type ' + messageType + ' (requestId = ' + msg.requestId + ')', logSource);
// Start monitoring if needed
if (responseTimeoutMs) startResponseMonitor();
});
},
// Sends a message without any form of response
send: function send(targetWin, messageType, messageBody) {
// Construct the message to post
var msg = createMessage(messageType, messageBody);
// Post the message to the parent window
targetWin.postMessage(msg, "*");
logger.trace('Posted one-way message of type "' + messageType + '"', logSource);
},
// Receives a message from another window and invokes any event handlers
receive: function receive(sourceWin, message) {
logger.trace('Received message of type "' + message.type + '"', logSource);
// Find listeners for this message type
var callbacks = listeners.get(message.type);
if (!callbacks) return false;
callbacks.forEach(function (callback) {
logger.trace('Invoking message handler ' + callback.name + '...', logSource);
callback(sourceWin, message.requestId, message.type, message.body);
});
return true;
},
// Handles a response to a prior cross-frame invocation
processResponse: function processResponse(message) {
logger.trace('Response received for invocation (requestId = ' + message.requestId + ')', logSource);
// Retrieve and remove the promise callback values for this invocation
var eventData = popInvocation(message.requestId);
if (!eventData) {
logger.warn('Received response to stale/invalid request (requestId = ' + message.requestId + ')', logSource);
return false;
}
eventData.resolve(message.body);
},
// Handles a response to a prior cross-frame invocation
processException: function processException(message) {
logger.trace('Exception received for invocation (requestId = ' + message.requestId + ')', logSource);
// Retrieve and remove the promise callback values for this invocation
var eventData = popInvocation(message.requestId);
if (!eventData) {
logger.warn('Received exception for stale/invalid request (requestId = ' + message.requestId + ')', logSource);
return false;
}
eventData.reject(new Error(message.body));
},
// Sends a response message to a child frame
respond: function respond(targetWin, requestId, response) {
// Formulate the response message
var msg = createMessage(responseMessageType, response);
msg.requestId = requestId,
// Send to the target window
targetWin.postMessage(msg, "*");
logger.trace('Response sent to caller for invocation (requestId = ' + requestId + ')', logSource);
},
// Sends a response message to a child frame
raiseException: function raiseException(targetWin, requestId, ex) {
// Formulate the response message
var msg = createMessage(exceptionMessageType, ex);
msg.requestId = requestId,
// Send to the target window
targetWin.postMessage(msg, "*");
logger.trace('Exception sent to caller for invocation (requestId = ' + requestId + ')', logSource);
},
// Processes a message received thru the window's message event
processMessage: function processMessage(message) {
if (message.data && message.data.source && message.data.source == messageSource) {
if (message.data.type == responseMessageType) remoting.processResponse(message.data);else if (message.data.type == exceptionMessageType) remoting.processException(message.data);else remoting.receive(message.source, message.data);
return true;
}
return false;
},
// Updates the log source
setLogSource: function setLogSource(source) {
logSource = source;
},
// Initialize the messenger to listen to message from the given window
initialize: function initialize(win) {
win.removeEventListener("message", remoting.processMessage);
win.addEventListener("message", remoting.processMessage);
}
};
}();
// Return the remoting object
module.exports = remoting;
/***/ })
/******/ ]);
});