From f27071ee64db80ddbe551a4d6eac7ac1b00ea5a4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 30 Sep 2020 20:20:31 -0600 Subject: [PATCH] Transition all remaining messaging over (delete the old stuff) --- src/CallHandler.tsx | 1 - src/FromWidgetPostMessageApi.js | 278 ------------------ src/ToWidgetPostMessageApi.js | 84 ------ src/WidgetMessaging.js | 223 -------------- src/WidgetMessagingEndpoint.js | 37 --- src/components/views/elements/AppTile.js | 6 +- .../views/right_panel/WidgetCard.tsx | 5 +- src/stores/ActiveWidgetStore.js | 9 +- src/stores/widgets/StopGapWidget.ts | 16 +- src/stores/widgets/WidgetMessagingStore.ts | 9 + src/utils/WidgetUtils.js | 6 +- src/widgets/WidgetApi.ts | 222 -------------- src/widgets/WidgetType.ts | 1 + 13 files changed, 29 insertions(+), 868 deletions(-) delete mode 100644 src/FromWidgetPostMessageApi.js delete mode 100644 src/ToWidgetPostMessageApi.js delete mode 100644 src/WidgetMessaging.js delete mode 100644 src/WidgetMessagingEndpoint.js delete mode 100644 src/widgets/WidgetApi.ts diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 49f82e3209..2259913c6d 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -75,7 +75,6 @@ import {base32} from "rfc4648"; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import WidgetStore from "./stores/WidgetStore"; -import ActiveWidgetStore from "./stores/ActiveWidgetStore"; import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore"; import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js deleted file mode 100644 index bbccc47d28..0000000000 --- a/src/FromWidgetPostMessageApi.js +++ /dev/null @@ -1,278 +0,0 @@ -/* -Copyright 2018 New Vector Ltd -Copyright 2019 Travis Ralston -Copyright 2019 The Matrix.org Foundation C.I.C. - -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/dispatcher'; -import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; -import ActiveWidgetStore from './stores/ActiveWidgetStore'; -import {MatrixClientPeg} from "./MatrixClientPeg"; -import RoomViewStore from "./stores/RoomViewStore"; -import {IntegrationManagers} from "./integrations/IntegrationManagers"; -import SettingsStore from "./settings/SettingsStore"; -import {Capability} from "./widgets/WidgetApi"; -import {objectClone} from "./utils/objects"; - -const WIDGET_API_VERSION = '0.0.2'; // Current API version -const SUPPORTED_WIDGET_API_VERSIONS = [ - '0.0.1', - '0.0.2', -]; -const INBOUND_API_NAME = 'fromWidget'; - -// Listen for and handle incoming requests using the 'fromWidget' postMessage -// API and initiate responses -export default class FromWidgetPostMessageApi { - constructor() { - this.widgetMessagingEndpoints = []; - this.widgetListeners = {}; // {action: func[]} - - 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); - } - - /** - * Adds a listener for a given action - * @param {string} action The action to listen for. - * @param {Function} callbackFn A callback function to be called when the action is - * encountered. Called with two parameters: the interesting request information and - * the raw event received from the postMessage API. The raw event is meant to be used - * for sendResponse and similar functions. - */ - addListener(action, callbackFn) { - if (!this.widgetListeners[action]) this.widgetListeners[action] = []; - this.widgetListeners[action].push(callbackFn); - } - - /** - * Removes a listener for a given action. - * @param {string} action The action that was subscribed to. - * @param {Function} callbackFn The original callback function that was used to subscribe - * to updates. - */ - removeListener(action, callbackFn) { - if (!this.widgetListeners[action]) return; - - const idx = this.widgetListeners[action].indexOf(callbackFn); - if (idx !== -1) this.widgetListeners[action].splice(idx, 1); - } - - /** - * 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.log(`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((endpoint) => 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 - } - - // Call any listeners we have registered - if (this.widgetListeners[event.data.action]) { - for (const fn of this.widgetListeners[event.data.action]) { - fn(event.data, event); - } - } - - // Although the requestId is required, we don't use it. We'll be nice and process the message - // if the property is missing, but with a warning for widget developers. - if (!event.data.requestId) { - console.warn("fromWidget action '" + event.data.action + "' does not have a requestId"); - } - - const action = event.data.action; - const widgetId = event.data.widgetId; - if (action === 'content_loaded') { - console.log('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); - // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually - const data = event.data.data || event.data.widgetData; - dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId}); - } else if (action === 'integration_manager_open') { - // Close the stickerpicker - dis.dispatch({action: 'stickerpicker_close'}); - // Open the integration manager - // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually - const data = event.data.data || event.data.widgetData; - const integType = (data && data.integType) ? data.integType : null; - const integId = (data && data.integId) ? data.integId : null; - - // TODO: Open the right integration manager for the widget - if (SettingsStore.getValue("feature_many_integration_managers")) { - IntegrationManagers.sharedInstance().openAll( - MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), - `type_${integType}`, - integId, - ); - } else { - IntegrationManagers.sharedInstance().getPrimaryManager().open( - MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), - `type_${integType}`, - integId, - ); - } - } else if (action === 'set_always_on_screen') { - // This is a new message: there is no reason to support the deprecated widgetData here - const data = event.data.data; - const val = data.value; - - if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) { - ActiveWidgetStore.setWidgetPersistence(widgetId, val); - } - - // acknowledge - this.sendResponse(event, {}); - } else if (action === 'get_openid') { - // Handled by caller - } 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 = objectClone(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 = objectClone(event.data); - data.response = { - error: { - message: msg, - }, - }; - if (nestedError) { - data.response.error._error = nestedError; - } - event.source.postMessage(data, event.origin); - } -} diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js deleted file mode 100644 index 00309d252c..0000000000 --- a/src/ToWidgetPostMessageApi.js +++ /dev/null @@ -1,84 +0,0 @@ -/* -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. -*/ - -// 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.requestId]; - if (!promise) { - return; - } - delete this._requestMap[payload.requestId]; - 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.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter; - - return new Promise((resolve, reject) => { - this._requestMap[action.requestId] = {resolve, reject}; - targetWindow.postMessage(action, targetOrigin); - - if (this._timeoutMs > 0) { - setTimeout(() => { - if (!this._requestMap[action.requestId]) { - return; - } - console.error("postMessage request timed out. Sent object: " + JSON.stringify(action), - this._requestMap); - this._requestMap[action.requestId].reject(new Error("Timed out")); - delete this._requestMap[action.requestId]; - }, this._timeoutMs); - } - }); - } -} diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js deleted file mode 100644 index 9394abf025..0000000000 --- a/src/WidgetMessaging.js +++ /dev/null @@ -1,223 +0,0 @@ -/* -Copyright 2017 New Vector Ltd -Copyright 2019 Travis Ralston - -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. -*/ - -/* -* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for -* spec. details / documentation. -*/ - -import FromWidgetPostMessageApi from './FromWidgetPostMessageApi'; -import ToWidgetPostMessageApi from './ToWidgetPostMessageApi'; -import Modal from "./Modal"; -import {MatrixClientPeg} from "./MatrixClientPeg"; -import SettingsStore from "./settings/SettingsStore"; -import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; -import WidgetUtils from "./utils/WidgetUtils"; -import {KnownWidgetActions} from "./widgets/WidgetApi"; - -if (!global.mxFromWidgetMessaging) { - global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); - global.mxFromWidgetMessaging.start(); -} -if (!global.mxToWidgetMessaging) { - global.mxToWidgetMessaging = new ToWidgetPostMessageApi(); - global.mxToWidgetMessaging.start(); -} - -const OUTBOUND_API_NAME = 'toWidget'; - -export default class WidgetMessaging { - /** - * @param {string} widgetId The widget's ID - * @param {string} wurl The raw URL of the widget as in the event (the 'wURL') - * @param {string} renderedUrl The url used in the widget's iframe (either similar to the wURL - * or a different URL of the clients choosing if it is using its own impl). - * @param {bool} isUserWidget If true, the widget is a user widget, otherwise it's a room widget - * @param {object} target Where widget messages should be sent (eg. the iframe object) - */ - constructor(widgetId, wurl, renderedUrl, isUserWidget, target) { - this.widgetId = widgetId; - this.wurl = wurl; - this.renderedUrl = renderedUrl; - this.isUserWidget = isUserWidget; - this.target = target; - this.fromWidget = global.mxFromWidgetMessaging; - this.toWidget = global.mxToWidgetMessaging; - this._onOpenIdRequest = this._onOpenIdRequest.bind(this); - this.start(); - } - - messageToWidget(action) { - action.widgetId = this.widgetId; // Required to be sent for all outbound requests - - 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"); - } - 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 Element). - // 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; - }); - } - - /** - * Tells the widget that the client is ready to handle further widget requests. - * @returns {Promise<*>} Resolves after the widget has acknowledged the ready message. - */ - flagReadyToContinue() { - return this.messageToWidget({ - api: OUTBOUND_API_NAME, - action: KnownWidgetActions.ClientReady, - }); - } - - /** - * Tells the widget that it should terminate now. - * @returns {Promise<*>} Resolves when widget has acknowledged the message. - */ - terminate() { - return this.messageToWidget({ - api: OUTBOUND_API_NAME, - action: KnownWidgetActions.Terminate, - }); - } - - /** - * Tells the widget to hang up on its call. - * @returns {Promise<*>} Resolves when the widget has acknowledged the message. - */ - hangup() { - return this.messageToWidget({ - api: OUTBOUND_API_NAME, - action: KnownWidgetActions.Hangup, - }); - } - - /** - * Request a screenshot from a widget - * @return {Promise} To be resolved with screenshot data when it has been generated - */ - getScreenshot() { - console.log('Requesting screenshot for', this.widgetId); - return this.messageToWidget({ - api: OUTBOUND_API_NAME, - action: "screenshot", - }) - .catch((error) => new Error("Failed to get screenshot: " + error.message)) - .then((response) => response.screenshot); - } - - /** - * Request capabilities required by the widget - * @return {Promise} To be resolved with an array of requested widget capabilities - */ - getCapabilities() { - console.log('Requesting capabilities for', this.widgetId); - return this.messageToWidget({ - api: OUTBOUND_API_NAME, - action: "capabilities", - }).then((response) => { - console.log('Got capabilities for', this.widgetId, response.capabilities); - return response.capabilities; - }); - } - - sendVisibility(visible) { - return this.messageToWidget({ - api: OUTBOUND_API_NAME, - action: "visibility", - visible, - }) - .catch((error) => { - console.error("Failed to send visibility: ", error); - }); - } - - start() { - this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl); - this.fromWidget.addListener("get_openid", this._onOpenIdRequest); - } - - stop() { - this.fromWidget.removeEndpoint(this.widgetId, this.renderedUrl); - this.fromWidget.removeListener("get_openid", this._onOpenIdRequest); - } - - async _onOpenIdRequest(ev, rawEv) { - if (ev.widgetId !== this.widgetId) return; // not interesting - - const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.wurl, this.isUserWidget); - - const settings = SettingsStore.getValue("widgetOpenIDPermissions"); - if (settings.deny && settings.deny.includes(widgetSecurityKey)) { - this.fromWidget.sendResponse(rawEv, {state: "blocked"}); - return; - } - if (settings.allow && settings.allow.includes(widgetSecurityKey)) { - const responseBody = {state: "allowed"}; - const credentials = await MatrixClientPeg.get().getOpenIdToken(); - Object.assign(responseBody, credentials); - this.fromWidget.sendResponse(rawEv, responseBody); - return; - } - - // Confirm that we received the request - this.fromWidget.sendResponse(rawEv, {state: "request"}); - - // Actually ask for permission to send the user's data - Modal.createTrackedDialog("OpenID widget permissions", '', - WidgetOpenIDPermissionsDialog, { - widgetUrl: this.wurl, - widgetId: this.widgetId, - isUserWidget: this.isUserWidget, - - onFinished: async (confirm) => { - const responseBody = { - // Legacy (early draft) fields - success: confirm, - - // New style MSC1960 fields - state: confirm ? "allowed" : "blocked", - original_request_id: ev.requestId, // eslint-disable-line camelcase - }; - if (confirm) { - const credentials = await MatrixClientPeg.get().getOpenIdToken(); - Object.assign(responseBody, credentials); - } - this.messageToWidget({ - api: OUTBOUND_API_NAME, - action: "openid_credentials", - data: responseBody, - }).catch((error) => { - console.error("Failed to send OpenID credentials: ", error); - }); - }, - }, - ); - } -} diff --git a/src/WidgetMessagingEndpoint.js b/src/WidgetMessagingEndpoint.js deleted file mode 100644 index 9114e12137..0000000000 --- a/src/WidgetMessagingEndpoint.js +++ /dev/null @@ -1,37 +0,0 @@ -/* -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 e8ef4de257..df1fbe0f3c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -36,12 +36,12 @@ import SettingsStore from "../../../settings/SettingsStore"; import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; import PersistedElement from "./PersistedElement"; import {WidgetType} from "../../../widgets/WidgetType"; -import {Capability} from "../../../widgets/WidgetApi"; import {SettingLevel} from "../../../settings/SettingLevel"; import WidgetStore from "../../../stores/WidgetStore"; import {Action} from "../../../dispatcher/actions"; import {StopGapWidget} from "../../../stores/widgets/StopGapWidget"; import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions"; +import {MatrixCapabilities} from "matrix-widget-api"; export default class AppTile extends React.Component { constructor(props) { @@ -305,7 +305,7 @@ export default class AppTile extends React.Component { if (payload.widgetId === this.props.app.id) { switch (payload.action) { case 'm.sticker': - if (this._sgWidget.widgetApi.hasCapability(Capability.Sticker)) { + if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) { dis.dispatch({action: 'post_sticker_message', data: payload.data}); } else { console.warn('Ignoring sticker message. Invalid capability'); @@ -562,7 +562,7 @@ export default class AppTile extends React.Component { const canUserModify = this._canUserModify(); const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify); const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify; - const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(Capability.Screenshot) + const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots) && this.props.show; const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu'); diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index 6bb45df109..8efbe3dcf3 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -36,11 +36,12 @@ import IconizedContextMenu, { IconizedContextMenuOptionList, } from "../context_menus/IconizedContextMenu"; import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload"; -import {Capability} from "../../../widgets/WidgetApi"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import classNames from "classnames"; import dis from "../../../dispatcher/dispatcher"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; +import { Capability } from "matrix-widget-api/lib/interfaces/Capabilities"; +import { MatrixCapabilities } from "matrix-widget-api"; interface IProps { room: Room; @@ -80,7 +81,7 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { if (menuDisplayed) { let snapshotButton; const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id); - if (widgetMessaging?.hasCapability(Capability.Screenshot)) { + if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) { const onSnapshotClick = () => { widgetMessaging.takeScreenshot().then(data => { dis.dispatch({ diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js index d6aaf83196..4ae8dfeddb 100644 --- a/src/stores/ActiveWidgetStore.js +++ b/src/stores/ActiveWidgetStore.js @@ -66,14 +66,7 @@ class ActiveWidgetStore extends EventEmitter { if (id !== this._persistentWidgetId) return; const toDeleteId = this._persistentWidgetId; - const result = WidgetMessagingStore.instance.findWidgetById(id); - if (result) { - if (result.room) { - WidgetMessagingStore.instance.stopMessagingForRoomWidget(result.room, result.widget); - } else { - WidgetMessagingStore.instance.stopMessagingForAccountWidget(result.widget); - } - } + WidgetMessagingStore.instance.stopMessagingById(id); this.setWidgetPersistence(toDeleteId, false); this.delRoomId(toDeleteId); diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index cd66522488..76c027bb33 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -14,14 +14,18 @@ * limitations under the License. */ -import {Room} from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; import { - ClientWidgetApi, IStickerActionRequest, + ClientWidgetApi, + IStickerActionRequest, IStickyActionRequest, - IWidget, IWidgetApiRequest, + IWidget, + IWidgetApiRequest, IWidgetApiRequestEmptyData, IWidgetData, - Widget, WidgetApiFromWidgetAction + MatrixCapabilities, + Widget, + WidgetApiFromWidgetAction } from "matrix-widget-api"; import { StopGapWidgetDriver } from "./StopGapWidgetDriver"; import { EventEmitter } from "events"; @@ -33,11 +37,9 @@ import WidgetUtils from '../../utils/WidgetUtils'; import { IntegrationManagers } from "../../integrations/IntegrationManagers"; import SettingsStore from "../../settings/SettingsStore"; import { WidgetType } from "../../widgets/WidgetType"; -import { Capability } from "../../widgets/WidgetApi"; import ActiveWidgetStore from "../ActiveWidgetStore"; import { objectShallowClone } from "../../utils/objects"; import defaultDispatcher from "../../dispatcher/dispatcher"; -import dis from "../../dispatcher/dispatcher"; import { ElementWidgetActions } from "./ElementWidgetActions"; // TODO: Destroy all of this code @@ -171,7 +173,7 @@ export class StopGapWidget extends EventEmitter { if (WidgetType.JITSI.matches(this.mockWidget.type)) { this.messaging.addEventListener("action:set_always_on_screen", (ev: CustomEvent) => { - if (this.messaging.hasCapability(Capability.AlwaysOnScreen)) { + if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) { ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value); ev.preventDefault(); this.messaging.transport.reply(ev.detail, {}); // ack diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts index 828465ce84..83d3ac7df8 100644 --- a/src/stores/widgets/WidgetMessagingStore.ts +++ b/src/stores/widgets/WidgetMessagingStore.ts @@ -61,6 +61,15 @@ export class WidgetMessagingStore extends AsyncStoreWithClient { return this.widgetMap.get(widget.id); } + /** + * Stops the widget messaging instance for a given widget ID. + * @param {string} widgetId The widget ID. + * @deprecated Widget IDs are not globally unique. + */ + public stopMessagingById(widgetId: string) { + this.widgetMap.remove(widgetId)?.stop(); + } + /** * Gets the widget messaging class for a given widget ID. * @param {string} widgetId The widget ID. diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 9373738bf8..6cc95efb25 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -28,11 +28,11 @@ const WIDGET_WAIT_TIME = 20000; import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; -import {Capability} from "../widgets/WidgetApi"; import {Room} from "matrix-js-sdk/src/models/room"; import {WidgetType} from "../widgets/WidgetType"; import {objectClone} from "./objects"; import {_t} from "../languageHandler"; +import {MatrixCapabilities} from "matrix-widget-api"; export default class WidgetUtils { /* Returns true if user is able to send state events to modify widgets in this room @@ -416,13 +416,13 @@ export default class WidgetUtils { static getCapWhitelistForAppTypeInRoomId(appType, roomId) { const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId); - const capWhitelist = enableScreenshots ? [Capability.Screenshot] : []; + const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : []; // Obviously anyone that can add a widget can claim it's a jitsi widget, // so this doesn't really offer much over the set of domains we load // widgets from at all, but it probably makes sense for sanity. if (WidgetType.JITSI.matches(appType)) { - capWhitelist.push(Capability.AlwaysOnScreen); + capWhitelist.push(MatrixCapabilities.AlwaysOnScreen); } return capWhitelist; diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts deleted file mode 100644 index ab9604d155..0000000000 --- a/src/widgets/WidgetApi.ts +++ /dev/null @@ -1,222 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -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. -*/ - -// Dev note: This is largely inspired by Dimension. Used with permission. -// https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts - -import { randomString } from "matrix-js-sdk/src/randomstring"; -import { EventEmitter } from "events"; -import { objectClone } from "../utils/objects"; - -export enum Capability { - Screenshot = "m.capability.screenshot", - Sticker = "m.sticker", - AlwaysOnScreen = "m.always_on_screen", -} - -export enum KnownWidgetActions { - GetSupportedApiVersions = "supported_api_versions", - TakeScreenshot = "screenshot", - GetCapabilities = "capabilities", - SendEvent = "send_event", - UpdateVisibility = "visibility", - GetOpenIDCredentials = "get_openid", - ReceiveOpenIDCredentials = "openid_credentials", - SetAlwaysOnScreen = "set_always_on_screen", - ClientReady = "im.vector.ready", - Terminate = "im.vector.terminate", - Hangup = "im.vector.hangup", -} - -export type WidgetAction = KnownWidgetActions | string; - -export enum WidgetApiType { - ToWidget = "toWidget", - FromWidget = "fromWidget", -} - -export interface WidgetRequest { - api: WidgetApiType; - widgetId: string; - requestId: string; - data: any; - action: WidgetAction; -} - -export interface ToWidgetRequest extends WidgetRequest { - api: WidgetApiType.ToWidget; -} - -export interface FromWidgetRequest extends WidgetRequest { - api: WidgetApiType.FromWidget; - response: any; -} - -export interface OpenIDCredentials { - accessToken: string; - tokenType: string; - matrixServerName: string; - expiresIn: number; -} - -/** - * Handles Element <--> Widget interactions for embedded/standalone widgets. - * - * Emitted events: - * - terminate(wait): client requested the widget to terminate. - * Call the argument 'wait(promise)' to postpone the finalization until - * the given promise resolves. - */ -export class WidgetApi extends EventEmitter { - private readonly origin: string; - private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {}; - private readonly readyPromise: Promise; - private readyPromiseResolve: () => void; - private openIDCredentialsCallback: () => void; - public openIDCredentials: OpenIDCredentials; - - /** - * Set this to true if your widget is expecting a ready message from the client. False otherwise (default). - */ - public expectingExplicitReady = false; - - constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) { - super(); - - this.origin = new URL(currentUrl).origin; - - this.readyPromise = new Promise(resolve => this.readyPromiseResolve = resolve); - - window.addEventListener("message", event => { - if (event.origin !== this.origin) return; // ignore: invalid origin - if (!event.data) return; // invalid schema - if (event.data.widgetId !== this.widgetId) return; // not for us - - const payload = event.data; - if (payload.api === WidgetApiType.ToWidget && payload.action) { - console.log(`[WidgetAPI] Got request: ${JSON.stringify(payload)}`); - - if (payload.action === KnownWidgetActions.GetCapabilities) { - this.onCapabilitiesRequest(payload); - if (!this.expectingExplicitReady) { - this.readyPromiseResolve(); - } - } else if (payload.action === KnownWidgetActions.ClientReady) { - this.readyPromiseResolve(); - - // Automatically acknowledge so we can move on - this.replyToRequest(payload, {}); - } else if (payload.action === KnownWidgetActions.Terminate - || payload.action === KnownWidgetActions.Hangup) { - // Finalization needs to be async, so postpone with a promise - let finalizePromise = Promise.resolve(); - const wait = (promise) => { - finalizePromise = finalizePromise.then(() => promise); - }; - const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup'; - this.emit(emitName, wait); - Promise.resolve(finalizePromise).then(() => { - // Acknowledge that we're shut down now - this.replyToRequest(payload, {}); - }); - } else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) { - // Save OpenID credentials - this.setOpenIDCredentials(payload); - this.replyToRequest(payload, {}); - } else { - console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`); - } - } else if (payload.api === WidgetApiType.FromWidget && this.inFlightRequests[payload.requestId]) { - console.log(`[WidgetAPI] Got reply: ${JSON.stringify(payload)}`); - const handler = this.inFlightRequests[payload.requestId]; - delete this.inFlightRequests[payload.requestId]; - handler(payload); - } else { - console.warn(`[WidgetAPI] Unhandled payload: ${JSON.stringify(payload)}`); - } - }); - } - - public setOpenIDCredentials(value: WidgetRequest) { - const data = value.data; - if (data.state === 'allowed') { - this.openIDCredentials = { - accessToken: data.access_token, - tokenType: data.token_type, - matrixServerName: data.matrix_server_name, - expiresIn: data.expires_in, - } - } else if (data.state === 'blocked') { - this.openIDCredentials = null; - } - if (['allowed', 'blocked'].includes(data.state) && this.openIDCredentialsCallback) { - this.openIDCredentialsCallback() - } - } - - public requestOpenIDCredentials(credentialsResponseCallback: () => void) { - this.openIDCredentialsCallback = credentialsResponseCallback; - this.callAction( - KnownWidgetActions.GetOpenIDCredentials, - {}, - this.setOpenIDCredentials, - ); - } - - public waitReady(): Promise { - return this.readyPromise; - } - - private replyToRequest(payload: ToWidgetRequest, reply: any) { - if (!window.parent) return; - - const request: ToWidgetRequest & {response?: any} = objectClone(payload); - request.response = reply; - - window.parent.postMessage(request, this.origin); - } - - private onCapabilitiesRequest(payload: ToWidgetRequest) { - return this.replyToRequest(payload, {capabilities: this.requestedCapabilities}); - } - - public callAction(action: WidgetAction, payload: any, callback: (reply: FromWidgetRequest) => void) { - if (!window.parent) return; - - const request: FromWidgetRequest = { - api: WidgetApiType.FromWidget, - widgetId: this.widgetId, - action: action, - requestId: randomString(160), - data: payload, - response: {}, // Not used at this layer - it's used when the client responds - }; - - if (callback) { - this.inFlightRequests[request.requestId] = callback; - } - - console.log(`[WidgetAPI] Sending request: `, request); - window.parent.postMessage(request, "*"); - } - - public setAlwaysOnScreen(onScreen: boolean): Promise { - return new Promise(resolve => { - this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null); - resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change. - }); - } -} diff --git a/src/widgets/WidgetType.ts b/src/widgets/WidgetType.ts index e4b37e639c..e42f3ffa9b 100644 --- a/src/widgets/WidgetType.ts +++ b/src/widgets/WidgetType.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO: Move to matrix-widget-api export class WidgetType { public static readonly JITSI = new WidgetType("m.jitsi", "jitsi"); public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");