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.
|
||||
*/
|
||||
|
||||
import URL from 'url';
|
||||
import dis from './dispatcher';
|
||||
import MatrixPostMessageApi from './MatrixPostMessageApi';
|
||||
import IntegrationManager from './IntegrationManager';
|
||||
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
|
||||
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
|
||||
|
||||
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';
|
||||
|
||||
if (!global.mxWidgetMessagingListenerCount) {
|
||||
global.mxWidgetMessagingListenerCount = 0;
|
||||
}
|
||||
if (!global.mxWidgetMessagingMessageEndpoints) {
|
||||
global.mxWidgetMessagingMessageEndpoints = [];
|
||||
}
|
||||
|
||||
export default class WidgetMessaging extends MatrixPostMessageApi {
|
||||
constructor(widgetId, targetWindow) {
|
||||
super(targetWindow);
|
||||
export default class WidgetMessaging {
|
||||
constructor(widgetId, widgetUrl, target) {
|
||||
this.widgetId = widgetId;
|
||||
|
||||
this.startListening = this.startListening.bind(this);
|
||||
this.stopListening = this.stopListening.bind(this);
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
this.widgetUrl = widgetUrl;
|
||||
this.target = target;
|
||||
this.fromWidget = global.mxFromWidgetMessaging;
|
||||
this.toWidget = global.mxToWidgetMessaging;
|
||||
this.start();
|
||||
}
|
||||
|
||||
exec(action) {
|
||||
return super.exec(action).then((data) => {
|
||||
// check for errors and reject if found
|
||||
messageToWidget(action) {
|
||||
return this.toWidget.exec(action, this.target).then((data) => {
|
||||
// Check for errors and reject if found
|
||||
if (data.response === undefined) { // null is valid
|
||||
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.
|
||||
throw new Error(msg);
|
||||
}
|
||||
// return the response field for the request
|
||||
// Return the response field for the request
|
||||
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
|
||||
* @return {Promise} To be resolved with screenshot data when it has been generated
|
||||
*/
|
||||
getScreenshot() {
|
||||
return this.exec({
|
||||
console.warn('Requesting screenshot for', this.widgetId);
|
||||
return this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
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
|
||||
*/
|
||||
getCapabilities() {
|
||||
return this.exec({
|
||||
console.warn('Requesting capabilities for', this.widgetId);
|
||||
return this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
|
||||
start() {
|
||||
this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl);
|
||||
}
|
||||
}
|
||||
|
|
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.onClickMenuBar = this.onClickMenuBar.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
|
||||
try {
|
||||
if (this.widgetMessaging) {
|
||||
this.widgetMessaging.stopListening();
|
||||
this.widgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
||||
this.widgetMessaging.stop();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to stop listening for widgetMessaging events', e.message);
|
||||
|
@ -290,14 +290,16 @@ export default class AppTile extends React.Component {
|
|||
|
||||
_onSnapshotClick(e) {
|
||||
console.warn("Requesting widget snapshot");
|
||||
this.widgetMessaging.getScreenshot().then((screenshot) => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: screenshot,
|
||||
}, true);
|
||||
}).catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
});
|
||||
this.widgetMessaging.getScreenshot()
|
||||
.catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
})
|
||||
.then((screenshot) => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: screenshot,
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget,
|
||||
|
@ -343,9 +345,16 @@ export default class AppTile extends React.Component {
|
|||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
this.widgetMessaging = new WidgetMessaging(this.props.id, this.refs.appFrame.contentWindow);
|
||||
this.widgetMessaging.startListening();
|
||||
this.widgetMessaging.addEndpoint(this.props.id, this.props.url);
|
||||
if (!this.widgetMessaging) {
|
||||
this._onInitialLoad();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
|
||||
requestedCapabilities = requestedCapabilities || [];
|
||||
|
|
Loading…
Reference in a new issue