2017-11-29 19:11:03 +01:00
|
|
|
/*
|
2017-11-30 11:20:29 +01:00
|
|
|
Copyright 2017 New Vector Ltd
|
2017-11-29 19:11:03 +01:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2017-12-01 15:44:14 +01:00
|
|
|
/*
|
2018-02-23 16:11:28 +01:00
|
|
|
* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
|
|
|
|
* spec. details / documentation.
|
2017-12-01 15:44:14 +01:00
|
|
|
*/
|
|
|
|
|
2017-12-05 01:08:17 +01:00
|
|
|
import URL from 'url';
|
2017-12-15 16:24:22 +01:00
|
|
|
import dis from './dispatcher';
|
|
|
|
import MatrixPostMessageApi from './MatrixPostMessageApi';
|
2018-02-26 14:29:16 +01:00
|
|
|
import IntegrationManager from './IntegrationManager';
|
2017-12-05 01:08:17 +01:00
|
|
|
|
2017-12-01 16:56:30 +01:00
|
|
|
const WIDGET_API_VERSION = '0.0.1'; // Current API version
|
|
|
|
const SUPPORTED_WIDGET_API_VERSIONS = [
|
|
|
|
'0.0.1',
|
|
|
|
];
|
2018-02-23 16:11:28 +01:00
|
|
|
const INBOUND_API_NAME = 'fromWidget';
|
|
|
|
const OUTBOUND_API_NAME = 'toWidget';
|
2017-12-01 16:56:30 +01:00
|
|
|
|
2017-12-04 18:54:00 +01:00
|
|
|
if (!global.mxWidgetMessagingListenerCount) {
|
|
|
|
global.mxWidgetMessagingListenerCount = 0;
|
|
|
|
}
|
|
|
|
if (!global.mxWidgetMessagingMessageEndpoints) {
|
|
|
|
global.mxWidgetMessagingMessageEndpoints = [];
|
|
|
|
}
|
2017-11-30 12:30:30 +01:00
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
export default class WidgetMessaging extends MatrixPostMessageApi {
|
2017-12-29 15:34:52 +01:00
|
|
|
constructor(widgetId, targetWindow) {
|
2017-12-15 16:24:22 +01:00
|
|
|
super(targetWindow);
|
2017-12-29 15:34:52 +01:00
|
|
|
this.widgetId = widgetId;
|
2018-01-18 14:16:06 +01:00
|
|
|
|
|
|
|
this.startListening = this.startListening.bind(this);
|
|
|
|
this.stopListening = this.stopListening.bind(this);
|
|
|
|
this.onMessage = this.onMessage.bind(this);
|
2017-12-01 17:17:18 +01:00
|
|
|
}
|
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
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;
|
|
|
|
});
|
2017-12-01 17:17:18 +01:00
|
|
|
}
|
2017-12-15 16:24:22 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Register widget message event listeners
|
|
|
|
*/
|
|
|
|
startListening() {
|
|
|
|
if (global.mxWidgetMessagingListenerCount === 0) {
|
2018-01-18 14:16:06 +01:00
|
|
|
// Start postMessage API listener
|
|
|
|
this.start();
|
|
|
|
// Start widget specific listener
|
|
|
|
window.addEventListener("message", this.onMessage, false);
|
2017-12-15 16:24:22 +01:00
|
|
|
}
|
|
|
|
global.mxWidgetMessagingListenerCount += 1;
|
2017-12-01 17:17:18 +01:00
|
|
|
}
|
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
/**
|
|
|
|
* De-register widget message event listeners
|
|
|
|
*/
|
|
|
|
stopListening() {
|
|
|
|
global.mxWidgetMessagingListenerCount -= 1;
|
|
|
|
if (global.mxWidgetMessagingListenerCount === 0) {
|
2018-01-18 14:16:06 +01:00
|
|
|
// Stop widget specific listener
|
|
|
|
window.removeEventListener("message", this.onMessage, false);
|
|
|
|
// Stop postMessage API listener
|
|
|
|
this.stop();
|
2017-12-15 16:24:22 +01:00
|
|
|
}
|
|
|
|
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);
|
|
|
|
}
|
2017-12-05 01:08:17 +01:00
|
|
|
}
|
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
/**
|
|
|
|
* 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);
|
2017-12-01 17:17:18 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
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;
|
2018-01-04 19:41:47 +01:00
|
|
|
} else {
|
2018-01-04 23:38:52 +01:00
|
|
|
// console.warn(`Adding widget messaging endpoint for ${widgetId}`);
|
2018-01-04 19:41:47 +01:00
|
|
|
global.mxWidgetMessagingMessageEndpoints.push(endpoint);
|
2017-12-15 16:24:22 +01:00
|
|
|
}
|
|
|
|
}
|
2017-12-05 01:08:17 +01:00
|
|
|
}
|
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
/**
|
|
|
|
* 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;
|
2017-12-01 17:17:18 +01:00
|
|
|
}
|
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
/**
|
|
|
|
* 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) ||
|
2018-02-23 16:11:28 +01:00
|
|
|
event.data.api !== INBOUND_API_NAME ||
|
2017-12-15 16:24:22 +01:00
|
|
|
!event.data.widgetId
|
|
|
|
) {
|
|
|
|
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
|
|
|
}
|
2017-11-29 19:11:03 +01:00
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
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, {
|
2018-02-23 16:11:28 +01:00
|
|
|
api: INBOUND_API_NAME,
|
2017-12-15 16:24:22 +01:00
|
|
|
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
|
|
|
});
|
|
|
|
} else if (action === 'api_version') {
|
|
|
|
this.sendResponse(event, {
|
2018-02-23 16:11:28 +01:00
|
|
|
api: INBOUND_API_NAME,
|
2017-12-15 16:24:22 +01:00
|
|
|
version: WIDGET_API_VERSION,
|
|
|
|
});
|
2018-03-12 14:56:02 +01:00
|
|
|
} else if (action === 'm.sticker') {
|
|
|
|
dis.dispatch({action: 'm.sticker', data: event.data.widgetData, widgetId: event.data.widgetId});
|
2018-02-26 14:29:16 +01:00
|
|
|
} else if (action === 'integration_manager_open') {
|
2018-02-26 14:43:16 +01:00
|
|
|
// Close the stickerpicker
|
|
|
|
dis.dispatch({action: 'stickerpicker_close'});
|
|
|
|
// Open the integration manager
|
2018-02-26 14:29:16 +01:00
|
|
|
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);
|
2017-12-15 16:24:22 +01:00
|
|
|
} else {
|
|
|
|
console.warn("Widget postMessage event unhandled");
|
|
|
|
this.sendError(event, {message: "The postMessage was unhandled"});
|
|
|
|
}
|
2017-11-30 11:20:29 +01:00
|
|
|
}
|
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
/**
|
|
|
|
* 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;
|
2017-12-01 16:56:30 +01:00
|
|
|
});
|
2017-11-30 13:26:40 +01:00
|
|
|
}
|
2017-11-30 11:20:29 +01:00
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
/**
|
|
|
|
* 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);
|
2017-11-30 12:30:30 +01:00
|
|
|
}
|
|
|
|
|
2017-12-15 16:24:22 +01:00
|
|
|
/**
|
|
|
|
* 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);
|
2017-12-01 15:56:27 +01:00
|
|
|
}
|
2017-12-15 17:39:04 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Request a screenshot from a widget
|
2018-02-25 23:21:30 +01:00
|
|
|
* @return {Promise} To be resolved with screenshot data when it has been generated
|
2017-12-15 17:39:04 +01:00
|
|
|
*/
|
|
|
|
getScreenshot() {
|
2018-02-25 23:21:30 +01:00
|
|
|
return this.exec({
|
2018-02-23 16:11:28 +01:00
|
|
|
api: OUTBOUND_API_NAME,
|
2017-12-15 22:36:02 +01:00
|
|
|
action: "screenshot",
|
2018-02-25 23:21:30 +01:00
|
|
|
}).then((response) => response.screenshot)
|
|
|
|
.catch((error) => new Error("Failed to get screenshot: " + error.message));
|
2017-12-15 17:39:04 +01:00
|
|
|
}
|
2017-12-16 10:16:24 +01:00
|
|
|
|
2018-02-25 23:21:30 +01:00
|
|
|
/**
|
|
|
|
* Request capabilities required by the widget
|
|
|
|
* @return {Promise} To be resolved with an array of requested widget capabilities
|
|
|
|
*/
|
2017-12-16 10:16:24 +01:00
|
|
|
getCapabilities() {
|
2018-02-25 23:21:30 +01:00
|
|
|
return this.exec({
|
2018-02-23 16:11:28 +01:00
|
|
|
api: OUTBOUND_API_NAME,
|
2017-12-16 10:16:24 +01:00
|
|
|
action: "capabilities",
|
2018-03-13 11:51:01 +01:00
|
|
|
}).then((response) => response.capabilities);
|
2017-12-16 10:16:24 +01:00
|
|
|
}
|
2017-12-01 15:56:27 +01:00
|
|
|
}
|
|
|
|
|
2017-11-30 12:30:30 +01:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
}
|