Add postmessage api and move functions in to class
This commit is contained in:
parent
f410112983
commit
c234e209fb
2 changed files with 286 additions and 163 deletions
101
src/MatrixPostMessageApi.js
Normal file
101
src/MatrixPostMessageApi.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,176 +127,205 @@ if (!global.mxWidgetMessagingMessageEndpoints) {
|
||||||
global.mxWidgetMessagingMessageEndpoints = [];
|
global.mxWidgetMessagingMessageEndpoints = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default class WidgetMessaging extends MatrixPostMessageApi {
|
||||||
/**
|
constructor(targetWindow) {
|
||||||
* Register widget message event listeners
|
super(targetWindow);
|
||||||
*/
|
|
||||||
function startListening() {
|
|
||||||
if (global.mxWidgetMessagingListenerCount === 0) {
|
|
||||||
window.addEventListener("message", onMessage, false);
|
|
||||||
}
|
|
||||||
global.mxWidgetMessagingListenerCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-register widget message event listeners
|
|
||||||
*/
|
|
||||||
function stopListening() {
|
|
||||||
global.mxWidgetMessagingListenerCount -= 1;
|
|
||||||
if (global.mxWidgetMessagingListenerCount === 0) {
|
|
||||||
window.removeEventListener("message", onMessage);
|
|
||||||
}
|
|
||||||
if (global.mxWidgetMessagingListenerCount < 0) {
|
|
||||||
// Make an error so we get a stack trace
|
|
||||||
const e = new Error(
|
|
||||||
"WidgetMessaging: mismatched startListening / stopListening detected." +
|
|
||||||
" Negative count",
|
|
||||||
);
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a widget endpoint for trusted postMessage communication
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
*/
|
|
||||||
function addEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn("Invalid origin:", endpointUrl);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
exec(action) {
|
||||||
const endpoint = new WidgetMessageEndpoint(widgetId, origin);
|
return super.exec(action).then((data) => {
|
||||||
if (global.mxWidgetMessagingMessageEndpoints) {
|
// check for errors and reject if found
|
||||||
if (global.mxWidgetMessagingMessageEndpoints.some(function(ep) {
|
if (data.response === undefined) { // null is valid
|
||||||
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
throw new Error("Missing 'response' field");
|
||||||
})) {
|
}
|
||||||
// Message endpoint already registered
|
if (data.response && data.response.error) {
|
||||||
console.warn("Endpoint already registered");
|
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
|
||||||
|
*/
|
||||||
|
startListening() {
|
||||||
|
if (global.mxWidgetMessagingListenerCount === 0) {
|
||||||
|
window.addEventListener("message", this.onMessage, false);
|
||||||
|
}
|
||||||
|
global.mxWidgetMessagingListenerCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* De-register widget message event listeners
|
||||||
|
*/
|
||||||
|
stopListening() {
|
||||||
|
global.mxWidgetMessagingListenerCount -= 1;
|
||||||
|
if (global.mxWidgetMessagingListenerCount === 0) {
|
||||||
|
window.removeEventListener("message", this.onMessage);
|
||||||
|
}
|
||||||
|
if (global.mxWidgetMessagingListenerCount < 0) {
|
||||||
|
// Make an error so we get a stack trace
|
||||||
|
const e = new Error(
|
||||||
|
"WidgetMessaging: mismatched startListening / stopListening detected." +
|
||||||
|
" Negative count",
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a widget endpoint for trusted postMessage communication
|
||||||
|
* @param {string} widgetId Unique widget identifier
|
||||||
|
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||||
|
*/
|
||||||
|
addEndpoint(widgetId, endpointUrl) {
|
||||||
|
const u = URL.parse(endpointUrl);
|
||||||
|
if (!u || !u.protocol || !u.host) {
|
||||||
|
console.warn("Invalid origin:", endpointUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
global.mxWidgetMessagingMessageEndpoints.push(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const origin = u.protocol + '//' + u.host;
|
||||||
* De-register a widget endpoint from trusted communication sources
|
const endpoint = new WidgetMessageEndpoint(widgetId, origin);
|
||||||
* @param {string} widgetId Unique widget identifier
|
if (global.mxWidgetMessagingMessageEndpoints) {
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
if (global.mxWidgetMessagingMessageEndpoints.some(function(ep) {
|
||||||
* @return {boolean} True if endpoint was successfully removed
|
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
||||||
*/
|
})) {
|
||||||
function removeEndpoint(widgetId, endpointUrl) {
|
// Message endpoint already registered
|
||||||
const u = URL.parse(endpointUrl);
|
console.warn("Endpoint already registered");
|
||||||
if (!u || !u.protocol || !u.host) {
|
return;
|
||||||
console.warn("Invalid origin");
|
}
|
||||||
return;
|
global.mxWidgetMessagingMessageEndpoints.push(endpoint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
/**
|
||||||
if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) {
|
* De-register a widget endpoint from trusted communication sources
|
||||||
const length = global.mxWidgetMessagingMessageEndpoints.length;
|
* @param {string} widgetId Unique widget identifier
|
||||||
global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.filter(function(endpoint) {
|
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||||
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
|
* @return {boolean} True if endpoint was successfully removed
|
||||||
});
|
*/
|
||||||
return (length > global.mxWidgetMessagingMessageEndpoints.length);
|
removeEndpoint(widgetId, endpointUrl) {
|
||||||
}
|
const u = URL.parse(endpointUrl);
|
||||||
return false;
|
if (!u || !u.protocol || !u.host) {
|
||||||
}
|
console.warn("Invalid origin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = u.protocol + '//' + u.host;
|
||||||
/**
|
if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) {
|
||||||
* Handle widget postMessage events
|
const length = global.mxWidgetMessagingMessageEndpoints.length;
|
||||||
* @param {Event} event Event to handle
|
global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.
|
||||||
* @return {undefined}
|
filter(function(endpoint) {
|
||||||
*/
|
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
|
||||||
function onMessage(event) {
|
});
|
||||||
if (!event.origin) { // Handle chrome
|
return (length > global.mxWidgetMessagingMessageEndpoints.length);
|
||||||
event.origin = event.originalEvent.origin;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Event origin is empty string if undefined
|
|
||||||
if (
|
|
||||||
event.origin.length === 0 ||
|
|
||||||
!trustedEndpoint(event.origin) ||
|
|
||||||
event.data.api !== "widget" ||
|
|
||||||
!event.data.widgetId
|
|
||||||
) {
|
|
||||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = event.data.action;
|
|
||||||
const widgetId = event.data.widgetId;
|
|
||||||
if (action === 'content_loaded') {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'widget_content_loaded',
|
|
||||||
widgetId: widgetId,
|
|
||||||
});
|
|
||||||
sendResponse(event, {success: true});
|
|
||||||
} else if (action === 'supported_api_versions') {
|
|
||||||
sendResponse(event, {
|
|
||||||
api: "widget",
|
|
||||||
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
|
||||||
});
|
|
||||||
} else if (action === 'api_version') {
|
|
||||||
sendResponse(event, {
|
|
||||||
api: "widget",
|
|
||||||
version: WIDGET_API_VERSION,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn("Widget postMessage event unhandled");
|
|
||||||
sendError(event, {message: "The postMessage was unhandled"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if message origin is registered as trusted
|
|
||||||
* @param {string} origin PostMessage origin to check
|
|
||||||
* @return {boolean} True if trusted
|
|
||||||
*/
|
|
||||||
function trustedEndpoint(origin) {
|
|
||||||
if (!origin) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return global.mxWidgetMessagingMessageEndpoints.some((endpoint) => {
|
|
||||||
return endpoint.endpointUrl === origin;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a postmessage response to a postMessage request
|
* Handle widget postMessage events
|
||||||
* @param {Event} event The original postMessage request event
|
* @param {Event} event Event to handle
|
||||||
* @param {Object} res Response data
|
* @return {undefined}
|
||||||
*/
|
*/
|
||||||
function sendResponse(event, res) {
|
onMessage(event) {
|
||||||
const data = JSON.parse(JSON.stringify(event.data));
|
if (!event.origin) { // Handle chrome
|
||||||
data.response = res;
|
event.origin = event.originalEvent.origin;
|
||||||
event.source.postMessage(data, event.origin);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Event origin is empty string if undefined
|
||||||
* Send an error response to a postMessage request
|
if (
|
||||||
* @param {Event} event The original postMessage request event
|
event.origin.length === 0 ||
|
||||||
* @param {string} msg Error message
|
!this.trustedEndpoint(event.origin) ||
|
||||||
* @param {Error} nestedError Nested error event (optional)
|
event.data.api !== "widget" ||
|
||||||
*/
|
!event.data.widgetId
|
||||||
function sendError(event, msg, nestedError) {
|
) {
|
||||||
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||||
const data = JSON.parse(JSON.stringify(event.data));
|
}
|
||||||
data.response = {
|
|
||||||
error: {
|
const action = event.data.action;
|
||||||
message: msg,
|
const widgetId = event.data.widgetId;
|
||||||
},
|
if (action === 'content_loaded') {
|
||||||
};
|
dis.dispatch({
|
||||||
if (nestedError) {
|
action: 'widget_content_loaded',
|
||||||
data.response.error._error = nestedError;
|
widgetId: widgetId,
|
||||||
|
});
|
||||||
|
this.sendResponse(event, {success: true});
|
||||||
|
} else if (action === 'supported_api_versions') {
|
||||||
|
this.sendResponse(event, {
|
||||||
|
api: "widget",
|
||||||
|
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
||||||
|
});
|
||||||
|
} else if (action === 'api_version') {
|
||||||
|
this.sendResponse(event, {
|
||||||
|
api: "widget",
|
||||||
|
version: WIDGET_API_VERSION,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn("Widget postMessage event unhandled");
|
||||||
|
this.sendError(event, {message: "The postMessage was unhandled"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if message origin is registered as trusted
|
||||||
|
* @param {string} origin PostMessage origin to check
|
||||||
|
* @return {boolean} True if trusted
|
||||||
|
*/
|
||||||
|
trustedEndpoint(origin) {
|
||||||
|
if (!origin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return global.mxWidgetMessagingMessageEndpoints.some((endpoint) => {
|
||||||
|
return endpoint.endpointUrl === origin;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a postmessage response to a postMessage request
|
||||||
|
* @param {Event} event The original postMessage request event
|
||||||
|
* @param {Object} res Response data
|
||||||
|
*/
|
||||||
|
sendResponse(event, res) {
|
||||||
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
data.response = res;
|
||||||
|
event.source.postMessage(data, event.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an error response to a postMessage request
|
||||||
|
* @param {Event} event The original postMessage request event
|
||||||
|
* @param {string} msg Error message
|
||||||
|
* @param {Error} nestedError Nested error event (optional)
|
||||||
|
*/
|
||||||
|
sendError(event, msg, nestedError) {
|
||||||
|
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
||||||
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
data.response = {
|
||||||
|
error: {
|
||||||
|
message: msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (nestedError) {
|
||||||
|
data.response.error._error = 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,
|
|
||||||
};
|
|
||||||
|
|
Loading…
Reference in a new issue