/* 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 { constructor(widgetId, widgetUrl, isUserWidget, target) { this.widgetId = widgetId; this.widgetUrl = widgetUrl; 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 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; }); } /** * 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, }); } /** * Request a screenshot from a widget * @return {Promise} To be resolved with screenshot data when it has been generated */ getScreenshot() { console.warn('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.warn('Requesting capabilities for', this.widgetId); return this.messageToWidget({ api: OUTBOUND_API_NAME, action: "capabilities", }).then((response) => { console.warn('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.widgetUrl); this.fromWidget.addListener("get_openid", this._onOpenIdRequest); } stop() { this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl); 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.widgetUrl, 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.widgetUrl, widgetId: this.widgetId, isUserWidget: this.isUserWidget, onFinished: async (confirm) => { const responseBody = {success: confirm}; 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); }); }, }, ); } }