Refactor widget postMessage API.
This commit is contained in:
parent
74628120bf
commit
4ac9653ab9
6 changed files with 386 additions and 336 deletions
201
src/FromWidgetPostMessageApi.js
Normal file
201
src/FromWidgetPostMessageApi.js
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import URL from 'url';
|
||||||
|
import dis from './dispatcher';
|
||||||
|
import IntegrationManager from './IntegrationManager';
|
||||||
|
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||||
|
|
||||||
|
const WIDGET_API_VERSION = '0.0.1'; // Current API version
|
||||||
|
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||||
|
'0.0.1',
|
||||||
|
];
|
||||||
|
const INBOUND_API_NAME = 'fromWidget';
|
||||||
|
|
||||||
|
// Listen for and handle incomming requests using the 'fromWidget' postMessage
|
||||||
|
// API and initiate responses
|
||||||
|
export default class FromWidgetPostMessageApi {
|
||||||
|
constructor() {
|
||||||
|
this.widgetMessagingEndpoints = [];
|
||||||
|
|
||||||
|
this.start = this.start.bind(this);
|
||||||
|
this.stop = this.stop.bind(this);
|
||||||
|
this.onPostMessage = this.onPostMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
window.addEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
window.removeEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = u.protocol + '//' + u.host;
|
||||||
|
const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
|
||||||
|
if (this.widgetMessagingEndpoints.some(function(ep) {
|
||||||
|
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
||||||
|
})) {
|
||||||
|
// Message endpoint already registered
|
||||||
|
console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.warn(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
|
||||||
|
this.widgetMessagingEndpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* De-register a widget endpoint from trusted communication sources
|
||||||
|
* @param {string} widgetId Unique widget identifier
|
||||||
|
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||||
|
* @return {boolean} True if endpoint was successfully removed
|
||||||
|
*/
|
||||||
|
removeEndpoint(widgetId, endpointUrl) {
|
||||||
|
const u = URL.parse(endpointUrl);
|
||||||
|
if (!u || !u.protocol || !u.host) {
|
||||||
|
console.warn('Remove widget messaging endpoint - Invalid origin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = u.protocol + '//' + u.host;
|
||||||
|
if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
|
||||||
|
const length = this.widgetMessagingEndpoints.length;
|
||||||
|
this.widgetMessagingEndpoints = this.widgetMessagingEndpoints.
|
||||||
|
filter(function(endpoint) {
|
||||||
|
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
|
||||||
|
});
|
||||||
|
return (length > this.widgetMessagingEndpoints.length);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle widget postMessage events
|
||||||
|
* Messages are only handled where a valid, registered messaging endpoints
|
||||||
|
* @param {Event} event Event to handle
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
onPostMessage(event) {
|
||||||
|
if (!event.origin) { // Handle chrome
|
||||||
|
event.origin = event.originalEvent.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event origin is empty string if undefined
|
||||||
|
if (
|
||||||
|
event.origin.length === 0 ||
|
||||||
|
!this.trustedEndpoint(event.origin) ||
|
||||||
|
event.data.api !== INBOUND_API_NAME ||
|
||||||
|
!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') {
|
||||||
|
console.warn('Widget reported content loaded for', widgetId);
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'widget_content_loaded',
|
||||||
|
widgetId: widgetId,
|
||||||
|
});
|
||||||
|
this.sendResponse(event, {success: true});
|
||||||
|
} else if (action === 'supported_api_versions') {
|
||||||
|
this.sendResponse(event, {
|
||||||
|
api: INBOUND_API_NAME,
|
||||||
|
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
||||||
|
});
|
||||||
|
} else if (action === 'api_version') {
|
||||||
|
this.sendResponse(event, {
|
||||||
|
api: INBOUND_API_NAME,
|
||||||
|
version: WIDGET_API_VERSION,
|
||||||
|
});
|
||||||
|
} else if (action === 'm.sticker') {
|
||||||
|
// console.warn('Got sticker message from widget', widgetId);
|
||||||
|
dis.dispatch({action: 'm.sticker', data: event.data.widgetData, widgetId: event.data.widgetId});
|
||||||
|
} else if (action === 'integration_manager_open') {
|
||||||
|
// Close the stickerpicker
|
||||||
|
dis.dispatch({action: 'stickerpicker_close'});
|
||||||
|
// Open the integration manager
|
||||||
|
const data = event.data.widgetData;
|
||||||
|
const integType = (data && data.integType) ? data.integType : null;
|
||||||
|
const integId = (data && data.integId) ? data.integId : null;
|
||||||
|
IntegrationManager.open(integType, integId);
|
||||||
|
} 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 this.widgetMessagingEndpoints.some((endpoint) => {
|
||||||
|
// TODO / FIXME -- Should this also check the widgetId?
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,85 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Promise from "bluebird";
|
|
||||||
|
|
||||||
// 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._requestMap = {
|
|
||||||
// $ID: {resolve, reject}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
const self = this;
|
|
||||||
this._onMsgCallback = function(ev) {
|
|
||||||
// THIS IS ALL UNSAFE EXECUTION.
|
|
||||||
// We do not verify who the sender of `ev` is!
|
|
||||||
const 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;
|
|
||||||
}
|
|
||||||
const promise = self._requestMap[payload._id];
|
|
||||||
if (!promise) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
delete self._requestMap[payload._id];
|
|
||||||
promise.resolve(payload);
|
|
||||||
};
|
|
||||||
return this._onMsgCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
exec(action, target) {
|
|
||||||
this._counter += 1;
|
|
||||||
target = target || "*";
|
|
||||||
action._id = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this._requestMap[action._id] = {resolve, reject};
|
|
||||||
this._window.postMessage(action, target);
|
|
||||||
|
|
||||||
if (this._timeoutMs > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!this._requestMap[action._id]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error("postMessage request timed out. Sent object: " + JSON.stringify(action));
|
|
||||||
this._requestMap[action._id].reject(new Error("Timed out"));
|
|
||||||
}, this._timeoutMs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
86
src/ToWidgetPostMessageApi.js
Normal file
86
src/ToWidgetPostMessageApi.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Promise from "bluebird";
|
||||||
|
|
||||||
|
// const OUTBOUND_API_NAME = 'toWidget';
|
||||||
|
|
||||||
|
// Initiate requests using the "toWidget" postMessage API and handle responses
|
||||||
|
// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
|
||||||
|
// response field
|
||||||
|
export default class ToWidgetPostMessageApi {
|
||||||
|
constructor(timeoutMs) {
|
||||||
|
this._timeoutMs = timeoutMs || 5000; // default to 5s timer
|
||||||
|
this._counter = 0;
|
||||||
|
this._requestMap = {
|
||||||
|
// $ID: {resolve, reject}
|
||||||
|
};
|
||||||
|
this.start = this.start.bind(this);
|
||||||
|
this.stop = this.stop.bind(this);
|
||||||
|
this.onPostMessage = this.onPostMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
window.addEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
window.removeEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPostMessage(ev) {
|
||||||
|
// THIS IS ALL UNSAFE EXECUTION.
|
||||||
|
// We do not verify who the sender of `ev` is!
|
||||||
|
const 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;
|
||||||
|
}
|
||||||
|
const promise = this._requestMap[payload._id];
|
||||||
|
if (!promise) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete this._requestMap[payload._id];
|
||||||
|
promise.resolve(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate outbound requests (toWidget)
|
||||||
|
exec(action, targetWindow, targetOrigin) {
|
||||||
|
targetWindow = targetWindow || window.parent; // default to parent window
|
||||||
|
targetOrigin = targetOrigin || "*";
|
||||||
|
this._counter += 1;
|
||||||
|
action._id = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._requestMap[action._id] = {resolve, reject};
|
||||||
|
targetWindow.postMessage(action, targetOrigin);
|
||||||
|
|
||||||
|
if (this._timeoutMs > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this._requestMap[action._id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
|
||||||
|
this._requestMap);
|
||||||
|
this._requestMap[action._id].reject(new Error("Timed out"));
|
||||||
|
delete this._requestMap[action._id];
|
||||||
|
}, this._timeoutMs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,38 +19,33 @@ limitations under the License.
|
||||||
* spec. details / documentation.
|
* spec. details / documentation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import URL from 'url';
|
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
|
||||||
import dis from './dispatcher';
|
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
|
||||||
import MatrixPostMessageApi from './MatrixPostMessageApi';
|
|
||||||
import IntegrationManager from './IntegrationManager';
|
if (!global.mxFromWidgetMessaging) {
|
||||||
|
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
|
||||||
|
global.mxFromWidgetMessaging.start();
|
||||||
|
}
|
||||||
|
if (!global.mxToWidgetMessaging) {
|
||||||
|
global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
|
||||||
|
global.mxToWidgetMessaging.start();
|
||||||
|
}
|
||||||
|
|
||||||
const WIDGET_API_VERSION = '0.0.1'; // Current API version
|
|
||||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
|
||||||
'0.0.1',
|
|
||||||
];
|
|
||||||
const INBOUND_API_NAME = 'fromWidget';
|
|
||||||
const OUTBOUND_API_NAME = 'toWidget';
|
const OUTBOUND_API_NAME = 'toWidget';
|
||||||
|
|
||||||
if (!global.mxWidgetMessagingListenerCount) {
|
export default class WidgetMessaging {
|
||||||
global.mxWidgetMessagingListenerCount = 0;
|
constructor(widgetId, widgetUrl, target) {
|
||||||
}
|
|
||||||
if (!global.mxWidgetMessagingMessageEndpoints) {
|
|
||||||
global.mxWidgetMessagingMessageEndpoints = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class WidgetMessaging extends MatrixPostMessageApi {
|
|
||||||
constructor(widgetId, targetWindow) {
|
|
||||||
super(targetWindow);
|
|
||||||
this.widgetId = widgetId;
|
this.widgetId = widgetId;
|
||||||
|
this.widgetUrl = widgetUrl;
|
||||||
this.startListening = this.startListening.bind(this);
|
this.target = target;
|
||||||
this.stopListening = this.stopListening.bind(this);
|
this.fromWidget = global.mxFromWidgetMessaging;
|
||||||
this.onMessage = this.onMessage.bind(this);
|
this.toWidget = global.mxToWidgetMessaging;
|
||||||
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
exec(action) {
|
messageToWidget(action) {
|
||||||
return super.exec(action).then((data) => {
|
return this.toWidget.exec(action, this.target).then((data) => {
|
||||||
// check for errors and reject if found
|
// Check for errors and reject if found
|
||||||
if (data.response === undefined) { // null is valid
|
if (data.response === undefined) { // null is valid
|
||||||
throw new Error("Missing 'response' field");
|
throw new Error("Missing 'response' field");
|
||||||
}
|
}
|
||||||
|
@ -65,208 +60,23 @@ export default class WidgetMessaging extends MatrixPostMessageApi {
|
||||||
// We can't aggressively sanitize [A-z0-9] since it might be a translation.
|
// We can't aggressively sanitize [A-z0-9] since it might be a translation.
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
// return the response field for the request
|
// Return the response field for the request
|
||||||
return data.response;
|
return data.response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register widget message event listeners
|
|
||||||
*/
|
|
||||||
startListening() {
|
|
||||||
if (global.mxWidgetMessagingListenerCount === 0) {
|
|
||||||
// Start postMessage API listener
|
|
||||||
this.start();
|
|
||||||
// Start widget specific listener
|
|
||||||
window.addEventListener("message", this.onMessage, false);
|
|
||||||
}
|
|
||||||
global.mxWidgetMessagingListenerCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-register widget message event listeners
|
|
||||||
*/
|
|
||||||
stopListening() {
|
|
||||||
global.mxWidgetMessagingListenerCount -= 1;
|
|
||||||
if (global.mxWidgetMessagingListenerCount === 0) {
|
|
||||||
// Stop widget specific listener
|
|
||||||
window.removeEventListener("message", this.onMessage, false);
|
|
||||||
// Stop postMessage API listener
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
const endpoint = new WidgetMessageEndpoint(widgetId, origin);
|
|
||||||
if (global.mxWidgetMessagingMessageEndpoints) {
|
|
||||||
if (global.mxWidgetMessagingMessageEndpoints.some(function(ep) {
|
|
||||||
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
|
||||||
})) {
|
|
||||||
// Message endpoint already registered
|
|
||||||
console.warn("Endpoint already registered");
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// console.warn(`Adding widget messaging endpoint for ${widgetId}`);
|
|
||||||
global.mxWidgetMessagingMessageEndpoints.push(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-register a widget endpoint from trusted communication sources
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
* @return {boolean} True if endpoint was successfully removed
|
|
||||||
*/
|
|
||||||
removeEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn("Invalid origin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) {
|
|
||||||
const length = global.mxWidgetMessagingMessageEndpoints.length;
|
|
||||||
global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.
|
|
||||||
filter(function(endpoint) {
|
|
||||||
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
|
|
||||||
});
|
|
||||||
return (length > global.mxWidgetMessagingMessageEndpoints.length);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle widget postMessage events
|
|
||||||
* @param {Event} event Event to handle
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
|
||||||
onMessage(event) {
|
|
||||||
if (!event.origin) { // Handle chrome
|
|
||||||
event.origin = event.originalEvent.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event origin is empty string if undefined
|
|
||||||
if (
|
|
||||||
event.origin.length === 0 ||
|
|
||||||
!this.trustedEndpoint(event.origin) ||
|
|
||||||
event.data.api !== INBOUND_API_NAME ||
|
|
||||||
!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,
|
|
||||||
});
|
|
||||||
this.sendResponse(event, {success: true});
|
|
||||||
} else if (action === 'supported_api_versions') {
|
|
||||||
this.sendResponse(event, {
|
|
||||||
api: INBOUND_API_NAME,
|
|
||||||
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
|
||||||
});
|
|
||||||
} else if (action === 'api_version') {
|
|
||||||
this.sendResponse(event, {
|
|
||||||
api: INBOUND_API_NAME,
|
|
||||||
version: WIDGET_API_VERSION,
|
|
||||||
});
|
|
||||||
} else if (action === 'm.sticker') {
|
|
||||||
dis.dispatch({action: 'm.sticker', data: event.data.widgetData, widgetId: event.data.widgetId});
|
|
||||||
} else if (action === 'integration_manager_open') {
|
|
||||||
// Close the stickerpicker
|
|
||||||
dis.dispatch({action: 'stickerpicker_close'});
|
|
||||||
// Open the integration manager
|
|
||||||
const data = event.data.widgetData;
|
|
||||||
const integType = (data && data.integType) ? data.integType : null;
|
|
||||||
const integId = (data && data.integId) ? data.integId : null;
|
|
||||||
IntegrationManager.open(integType, integId);
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a screenshot from a widget
|
* Request a screenshot from a widget
|
||||||
* @return {Promise} To be resolved with screenshot data when it has been generated
|
* @return {Promise} To be resolved with screenshot data when it has been generated
|
||||||
*/
|
*/
|
||||||
getScreenshot() {
|
getScreenshot() {
|
||||||
return this.exec({
|
console.warn('Requesting screenshot for', this.widgetId);
|
||||||
|
return this.messageToWidget({
|
||||||
api: OUTBOUND_API_NAME,
|
api: OUTBOUND_API_NAME,
|
||||||
action: "screenshot",
|
action: "screenshot",
|
||||||
}).then((response) => response.screenshot)
|
})
|
||||||
.catch((error) => new Error("Failed to get screenshot: " + error.message));
|
.catch((error) => new Error("Failed to get screenshot: " + error.message))
|
||||||
|
.then((response) => response.screenshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -274,30 +84,22 @@ export default class WidgetMessaging extends MatrixPostMessageApi {
|
||||||
* @return {Promise} To be resolved with an array of requested widget capabilities
|
* @return {Promise} To be resolved with an array of requested widget capabilities
|
||||||
*/
|
*/
|
||||||
getCapabilities() {
|
getCapabilities() {
|
||||||
return this.exec({
|
console.warn('Requesting capabilities for', this.widgetId);
|
||||||
|
return this.messageToWidget({
|
||||||
api: OUTBOUND_API_NAME,
|
api: OUTBOUND_API_NAME,
|
||||||
action: "capabilities",
|
action: "capabilities",
|
||||||
}).then((response) => response.capabilities);
|
}).then((response) => {
|
||||||
|
console.warn('Got capabilities for', this.widgetId, response.capabilities);
|
||||||
|
return response.capabilities;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents mapping of widget instance to URLs for trusted postMessage communication.
|
start() {
|
||||||
*/
|
this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl);
|
||||||
class WidgetMessageEndpoint {
|
|
||||||
/**
|
|
||||||
* Mapping of widget instance to URL for trusted postMessage communication.
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin.
|
|
||||||
*/
|
|
||||||
constructor(widgetId, endpointUrl) {
|
|
||||||
if (!widgetId) {
|
|
||||||
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
|
|
||||||
}
|
}
|
||||||
if (!endpointUrl) {
|
|
||||||
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
|
stop() {
|
||||||
}
|
this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl);
|
||||||
this.widgetId = widgetId;
|
|
||||||
this.endpointUrl = endpointUrl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
37
src/WidgetMessagingEndpoint.js
Normal file
37
src/WidgetMessagingEndpoint.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents mapping of widget instance to URLs for trusted postMessage communication.
|
||||||
|
*/
|
||||||
|
export default class WidgetMessageEndpoint {
|
||||||
|
/**
|
||||||
|
* Mapping of widget instance to URL for trusted postMessage communication.
|
||||||
|
* @param {string} widgetId Unique widget identifier
|
||||||
|
* @param {string} endpointUrl Widget wurl origin.
|
||||||
|
*/
|
||||||
|
constructor(widgetId, endpointUrl) {
|
||||||
|
if (!widgetId) {
|
||||||
|
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
|
||||||
|
}
|
||||||
|
if (!endpointUrl) {
|
||||||
|
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
|
||||||
|
}
|
||||||
|
this.widgetId = widgetId;
|
||||||
|
this.endpointUrl = endpointUrl;
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ export default class AppTile extends React.Component {
|
||||||
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
||||||
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
||||||
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
||||||
|
this._onInitialLoad = this._onInitialLoad.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -172,8 +173,7 @@ export default class AppTile extends React.Component {
|
||||||
// Widget postMessage listeners
|
// Widget postMessage listeners
|
||||||
try {
|
try {
|
||||||
if (this.widgetMessaging) {
|
if (this.widgetMessaging) {
|
||||||
this.widgetMessaging.stopListening();
|
this.widgetMessaging.stop();
|
||||||
this.widgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to stop listening for widgetMessaging events', e.message);
|
console.error('Failed to stop listening for widgetMessaging events', e.message);
|
||||||
|
@ -290,13 +290,15 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
_onSnapshotClick(e) {
|
_onSnapshotClick(e) {
|
||||||
console.warn("Requesting widget snapshot");
|
console.warn("Requesting widget snapshot");
|
||||||
this.widgetMessaging.getScreenshot().then((screenshot) => {
|
this.widgetMessaging.getScreenshot()
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to get screenshot", err);
|
||||||
|
})
|
||||||
|
.then((screenshot) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'picture_snapshot',
|
action: 'picture_snapshot',
|
||||||
file: screenshot,
|
file: screenshot,
|
||||||
}, true);
|
}, true);
|
||||||
}).catch((err) => {
|
|
||||||
console.error("Failed to get screenshot", err);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,9 +345,16 @@ export default class AppTile extends React.Component {
|
||||||
* Called when widget iframe has finished loading
|
* Called when widget iframe has finished loading
|
||||||
*/
|
*/
|
||||||
_onLoaded() {
|
_onLoaded() {
|
||||||
this.widgetMessaging = new WidgetMessaging(this.props.id, this.refs.appFrame.contentWindow);
|
if (!this.widgetMessaging) {
|
||||||
this.widgetMessaging.startListening();
|
this._onInitialLoad();
|
||||||
this.widgetMessaging.addEndpoint(this.props.id, this.props.url);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on initial load of the widget iframe
|
||||||
|
*/
|
||||||
|
_onInitialLoad() {
|
||||||
|
this.widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
|
||||||
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||||
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
|
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
|
||||||
requestedCapabilities = requestedCapabilities || [];
|
requestedCapabilities = requestedCapabilities || [];
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue