diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js new file mode 100644 index 0000000000..ad1f1acbbd --- /dev/null +++ b/src/FromWidgetPostMessageApi.js @@ -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); + } +} diff --git a/src/MatrixPostMessageApi.js b/src/MatrixPostMessageApi.js deleted file mode 100644 index 303c7f596d..0000000000 --- a/src/MatrixPostMessageApi.js +++ /dev/null @@ -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); - } - }); - } -} diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js new file mode 100644 index 0000000000..ccaa0207c1 --- /dev/null +++ b/src/ToWidgetPostMessageApi.js @@ -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); + } + }); + } +} diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index bae71bdc8f..effd96dacf 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -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); } } diff --git a/src/WidgetMessagingEndpoint.js b/src/WidgetMessagingEndpoint.js new file mode 100644 index 0000000000..9114e12137 --- /dev/null +++ b/src/WidgetMessagingEndpoint.js @@ -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; + } +} diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index e7f44a8840..a9613530ec 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -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 || [];