Add postmessage api and move functions in to class

This commit is contained in:
Richard Lewis 2017-12-15 15:24:22 +00:00
parent f410112983
commit c234e209fb
2 changed files with 286 additions and 163 deletions

101
src/MatrixPostMessageApi.js Normal file
View file

@ -0,0 +1,101 @@
import Promise from "bluebird";
function defer() {
let resolve, reject;
let isPending = true;
let promise = new Promise(function(...args) {
resolve = args[0];
reject = args[1];
});
return {
resolve: function(...args) {
if (!isPending) {
return;
}
isPending = false;
resolve(args[0]);
},
reject: function(...args) {
if (!isPending) {
return;
}
isPending = false;
reject(args[0]);
},
isPending: function() {
return isPending;
},
promise: promise,
};
}
// NOTE: PostMessageApi only handles message events with a data payload with a
// response field
export default class PostMessageApi {
constructor(targetWindow, timeoutMs) {
this._window = targetWindow || window.parent; // default to parent window
this._timeoutMs = timeoutMs || 5000; // default to 5s timer
this._counter = 0;
this._pending = {
// $ID: Deferred
};
}
start() {
addEventListener('message', this.getOnMessageCallback());
}
stop() {
removeEventListener('message', this.getOnMessageCallback());
}
// Somewhat convoluted so we can successfully capture the PostMessageApi 'this' instance.
getOnMessageCallback() {
if (this._onMsgCallback) {
return this._onMsgCallback;
}
let self = this;
this._onMsgCallback = function(ev) {
// THIS IS ALL UNSAFE EXECUTION.
// We do not verify who the sender of `ev` is!
let payload = ev.data;
// NOTE: Workaround for running in a mobile WebView where a
// postMessage immediately triggers this callback even though it is
// not the response.
if (payload.response === undefined) {
return;
}
let deferred = self._pending[payload._id];
if (!deferred) {
return;
}
if (!deferred.isPending()) {
return;
}
delete self._pending[payload._id];
deferred.resolve(payload);
};
return this._onMsgCallback;
}
exec(action, target) {
this._counter += 1;
target = target || "*";
action._id = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
let d = defer();
this._pending[action._id] = d;
this._window.postMessage(action, target);
if (this._timeoutMs > 0) {
setTimeout(function() {
if (!d.isPending()) {
return;
}
console.error("postMessage request timed out. Sent object: " + JSON.stringify(action));
d.reject(new Error("Timed out"));
}, this._timeoutMs);
}
return d.promise;
}
}

View file

@ -112,14 +112,14 @@ Example:
*/ */
import URL from 'url'; import URL from 'url';
import dis from './dispatcher';
import MatrixPostMessageApi from './MatrixPostMessageApi';
const WIDGET_API_VERSION = '0.0.1'; // Current API version const WIDGET_API_VERSION = '0.0.1'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [ const SUPPORTED_WIDGET_API_VERSIONS = [
'0.0.1', '0.0.1',
]; ];
import dis from './dispatcher';
if (!global.mxWidgetMessagingListenerCount) { if (!global.mxWidgetMessagingListenerCount) {
global.mxWidgetMessagingListenerCount = 0; global.mxWidgetMessagingListenerCount = 0;
} }
@ -127,13 +127,39 @@ if (!global.mxWidgetMessagingMessageEndpoints) {
global.mxWidgetMessagingMessageEndpoints = []; global.mxWidgetMessagingMessageEndpoints = [];
} }
export default class WidgetMessaging extends MatrixPostMessageApi {
constructor(targetWindow) {
super(targetWindow);
}
exec(action) {
return super.exec(action).then((data) => {
// check for errors and reject if found
if (data.response === undefined) { // null is valid
throw new Error("Missing 'response' field");
}
if (data.response && data.response.error) {
const err = data.response.error;
const msg = String(err.message ? err.message : "An error was returned");
if (err._error) {
console.error(err._error);
}
// Potential XSS attack if 'msg' is not appropriately sanitized,
// as it is untrusted input by our parent window (which we assume is Riot).
// We can't aggressively sanitize [A-z0-9] since it might be a translation.
throw new Error(msg);
}
// return the response field for the request
return data.response;
});
}
/** /**
* Register widget message event listeners * Register widget message event listeners
*/ */
function startListening() { startListening() {
if (global.mxWidgetMessagingListenerCount === 0) { if (global.mxWidgetMessagingListenerCount === 0) {
window.addEventListener("message", onMessage, false); window.addEventListener("message", this.onMessage, false);
} }
global.mxWidgetMessagingListenerCount += 1; global.mxWidgetMessagingListenerCount += 1;
} }
@ -141,10 +167,10 @@ function startListening() {
/** /**
* De-register widget message event listeners * De-register widget message event listeners
*/ */
function stopListening() { stopListening() {
global.mxWidgetMessagingListenerCount -= 1; global.mxWidgetMessagingListenerCount -= 1;
if (global.mxWidgetMessagingListenerCount === 0) { if (global.mxWidgetMessagingListenerCount === 0) {
window.removeEventListener("message", onMessage); window.removeEventListener("message", this.onMessage);
} }
if (global.mxWidgetMessagingListenerCount < 0) { if (global.mxWidgetMessagingListenerCount < 0) {
// Make an error so we get a stack trace // Make an error so we get a stack trace
@ -161,7 +187,7 @@ function stopListening() {
* @param {string} widgetId Unique widget identifier * @param {string} widgetId Unique widget identifier
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host) * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
*/ */
function addEndpoint(widgetId, endpointUrl) { addEndpoint(widgetId, endpointUrl) {
const u = URL.parse(endpointUrl); const u = URL.parse(endpointUrl);
if (!u || !u.protocol || !u.host) { if (!u || !u.protocol || !u.host) {
console.warn("Invalid origin:", endpointUrl); console.warn("Invalid origin:", endpointUrl);
@ -188,7 +214,7 @@ function addEndpoint(widgetId, endpointUrl) {
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host) * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
* @return {boolean} True if endpoint was successfully removed * @return {boolean} True if endpoint was successfully removed
*/ */
function removeEndpoint(widgetId, endpointUrl) { removeEndpoint(widgetId, endpointUrl) {
const u = URL.parse(endpointUrl); const u = URL.parse(endpointUrl);
if (!u || !u.protocol || !u.host) { if (!u || !u.protocol || !u.host) {
console.warn("Invalid origin"); console.warn("Invalid origin");
@ -198,7 +224,8 @@ function removeEndpoint(widgetId, endpointUrl) {
const origin = u.protocol + '//' + u.host; const origin = u.protocol + '//' + u.host;
if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) { if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) {
const length = global.mxWidgetMessagingMessageEndpoints.length; const length = global.mxWidgetMessagingMessageEndpoints.length;
global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.filter(function(endpoint) { global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.
filter(function(endpoint) {
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin); return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
}); });
return (length > global.mxWidgetMessagingMessageEndpoints.length); return (length > global.mxWidgetMessagingMessageEndpoints.length);
@ -212,7 +239,7 @@ function removeEndpoint(widgetId, endpointUrl) {
* @param {Event} event Event to handle * @param {Event} event Event to handle
* @return {undefined} * @return {undefined}
*/ */
function onMessage(event) { onMessage(event) {
if (!event.origin) { // Handle chrome if (!event.origin) { // Handle chrome
event.origin = event.originalEvent.origin; event.origin = event.originalEvent.origin;
} }
@ -220,7 +247,7 @@ function onMessage(event) {
// Event origin is empty string if undefined // Event origin is empty string if undefined
if ( if (
event.origin.length === 0 || event.origin.length === 0 ||
!trustedEndpoint(event.origin) || !this.trustedEndpoint(event.origin) ||
event.data.api !== "widget" || event.data.api !== "widget" ||
!event.data.widgetId !event.data.widgetId
) { ) {
@ -234,20 +261,20 @@ function onMessage(event) {
action: 'widget_content_loaded', action: 'widget_content_loaded',
widgetId: widgetId, widgetId: widgetId,
}); });
sendResponse(event, {success: true}); this.sendResponse(event, {success: true});
} else if (action === 'supported_api_versions') { } else if (action === 'supported_api_versions') {
sendResponse(event, { this.sendResponse(event, {
api: "widget", api: "widget",
supported_versions: SUPPORTED_WIDGET_API_VERSIONS, supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
}); });
} else if (action === 'api_version') { } else if (action === 'api_version') {
sendResponse(event, { this.sendResponse(event, {
api: "widget", api: "widget",
version: WIDGET_API_VERSION, version: WIDGET_API_VERSION,
}); });
} else { } else {
console.warn("Widget postMessage event unhandled"); console.warn("Widget postMessage event unhandled");
sendError(event, {message: "The postMessage was unhandled"}); this.sendError(event, {message: "The postMessage was unhandled"});
} }
} }
@ -256,7 +283,7 @@ function onMessage(event) {
* @param {string} origin PostMessage origin to check * @param {string} origin PostMessage origin to check
* @return {boolean} True if trusted * @return {boolean} True if trusted
*/ */
function trustedEndpoint(origin) { trustedEndpoint(origin) {
if (!origin) { if (!origin) {
return false; return false;
} }
@ -271,7 +298,7 @@ function trustedEndpoint(origin) {
* @param {Event} event The original postMessage request event * @param {Event} event The original postMessage request event
* @param {Object} res Response data * @param {Object} res Response data
*/ */
function sendResponse(event, res) { sendResponse(event, res) {
const data = JSON.parse(JSON.stringify(event.data)); const data = JSON.parse(JSON.stringify(event.data));
data.response = res; data.response = res;
event.source.postMessage(data, event.origin); event.source.postMessage(data, event.origin);
@ -283,7 +310,7 @@ function sendResponse(event, res) {
* @param {string} msg Error message * @param {string} msg Error message
* @param {Error} nestedError Nested error event (optional) * @param {Error} nestedError Nested error event (optional)
*/ */
function sendError(event, msg, nestedError) { sendError(event, msg, nestedError) {
console.error("Action:" + event.data.action + " failed with message: " + msg); console.error("Action:" + event.data.action + " failed with message: " + msg);
const data = JSON.parse(JSON.stringify(event.data)); const data = JSON.parse(JSON.stringify(event.data));
data.response = { data.response = {
@ -296,6 +323,8 @@ function sendError(event, msg, nestedError) {
} }
event.source.postMessage(data, event.origin); event.source.postMessage(data, event.origin);
} }
}
/** /**
* Represents mapping of widget instance to URLs for trusted postMessage communication. * Represents mapping of widget instance to URLs for trusted postMessage communication.
@ -317,10 +346,3 @@ class WidgetMessageEndpoint {
this.endpointUrl = endpointUrl; this.endpointUrl = endpointUrl;
} }
} }
export default {
startListening: startListening,
stopListening: stopListening,
addEndpoint: addEndpoint,
removeEndpoint: removeEndpoint,
};