From d63c5e7134070a4cd44f36dfb30f94f161370c4f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 13 Mar 2019 00:34:34 -0600 Subject: [PATCH 01/31] Basic widget OpenID reauth implementation Covers the minimum of https://github.com/vector-im/riot-web/issues/7153 This does not handling automatically accepting/blocking widgets yet, however. This could lead to dialog irritation. --- src/FromWidgetPostMessageApi.js | 42 ++++++++++++++++++++++++++++-- src/WidgetMessaging.js | 45 +++++++++++++++++++++++++++++++++ src/i18n/strings/en_EN.json | 4 ++- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index ea7eeba756..577eabf5ec 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -1,5 +1,6 @@ /* Copyright 2018 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. @@ -20,17 +21,19 @@ import IntegrationManager from './IntegrationManager'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; -const WIDGET_API_VERSION = '0.0.1'; // Current API version +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 incomming requests using the 'fromWidget' postMessage +// 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); @@ -45,6 +48,32 @@ export default class FromWidgetPostMessageApi { 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.indexOf(callbackFn); + if (idx !== -1) this.widgetListeners.splice(idx, 1); + } + /** * Register a widget endpoint for trusted postMessage communication * @param {string} widgetId Unique widget identifier @@ -117,6 +146,13 @@ export default class FromWidgetPostMessageApi { 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) { @@ -164,6 +200,8 @@ export default class FromWidgetPostMessageApi { if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } + } else if (action === 'get_openid') { + // Handled by caller } else { console.warn('Widget postMessage event unhandled'); this.sendError(event, {message: 'The postMessage was unhandled'}); diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 5b722df65f..17ce9360b7 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -1,5 +1,6 @@ /* 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. @@ -21,6 +22,10 @@ limitations under the License. import FromWidgetPostMessageApi from './FromWidgetPostMessageApi'; import ToWidgetPostMessageApi from './ToWidgetPostMessageApi'; +import Modal from "./Modal"; +import QuestionDialog from "./components/views/dialogs/QuestionDialog"; +import {_t} from "./languageHandler"; +import MatrixClientPeg from "./MatrixClientPeg"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -40,6 +45,7 @@ export default class WidgetMessaging { this.target = target; this.fromWidget = global.mxFromWidgetMessaging; this.toWidget = global.mxToWidgetMessaging; + this._openIdHandlerRef = this._onOpenIdRequest.bind(this); this.start(); } @@ -109,9 +115,48 @@ export default class WidgetMessaging { start() { this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl); + this.fromWidget.addListener("get_openid", this._openIdHandlerRef); } stop() { this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl); + this.fromWidget.removeListener("get_openid", this._openIdHandlerRef); + } + + _onOpenIdRequest(ev, rawEv) { + if (ev.widgetId !== this.widgetId) return; // not interesting + + // Confirm that we received the request + this.fromWidget.sendResponse(rawEv, {state: "request"}); + + // TODO: Support blacklisting widgets + // TODO: Support whitelisting widgets + + // Actually ask for permission to send the user's data + Modal.createTrackedDialog("OpenID widget permissions", '', QuestionDialog, { + title: _t("A widget would like to verify your identity"), + description: _t( + "A widget located at %(widgetUrl)s would like to verify your identity. " + + "By allowing this, the widget will be able to verify your user ID, but not " + + "perform actions as you.", { + widgetUrl: this.widgetUrl, + }, + ), + button: _t("Allow"), + 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); + }); + }, + }); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8cc85b6036..e13390e226 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -230,6 +230,9 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", + "A widget would like to verify your identity": "A widget would like to verify your identity", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", + "Allow": "Allow", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -924,7 +927,6 @@ "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", "Do you want to load widget from URL:": "Do you want to load widget from URL:", - "Allow": "Allow", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Delete widget": "Delete widget", From f045beafc37ea62c176ccbd7a69c98481b6a352a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Mar 2019 21:33:31 -0600 Subject: [PATCH 02/31] Support whitelisting/blacklisting widgets for OpenID --- res/css/_components.scss | 1 + .../_WidgetOpenIDPermissionsDialog.scss | 28 +++++ src/WidgetMessaging.js | 62 +++++----- .../dialogs/WidgetOpenIDPermissionsDialog.js | 106 ++++++++++++++++++ .../views/elements/LabelledToggleSwitch.js | 20 +++- src/i18n/strings/en_EN.json | 8 +- src/settings/Settings.js | 7 ++ 7 files changed, 199 insertions(+), 33 deletions(-) create mode 100644 res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss create mode 100644 src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 4fb0eed4af..f29e30dcb4 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -70,6 +70,7 @@ @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; +@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss"; @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; @import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss"; @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; diff --git a/res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss b/res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss new file mode 100644 index 0000000000..a419c105a9 --- /dev/null +++ b/res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss @@ -0,0 +1,28 @@ +/* +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. +*/ + +.mx_WidgetOpenIDPermissionsDialog .mx_SettingsFlag { + .mx_ToggleSwitch { + display: inline-block; + vertical-align: middle; + margin-right: 8px; + } + + .mx_SettingsFlag_label { + display: inline-block; + vertical-align: middle; + } +} diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 17ce9360b7..dba703ffb8 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -26,6 +26,8 @@ import Modal from "./Modal"; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import {_t} from "./languageHandler"; import MatrixClientPeg from "./MatrixClientPeg"; +import SettingsStore from "./settings/SettingsStore"; +import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -123,40 +125,46 @@ export default class WidgetMessaging { this.fromWidget.removeListener("get_openid", this._openIdHandlerRef); } - _onOpenIdRequest(ev, rawEv) { + async _onOpenIdRequest(ev, rawEv) { if (ev.widgetId !== this.widgetId) return; // not interesting + const settings = SettingsStore.getValue("widgetOpenIDPermissions"); + if (settings.blacklist && settings.blacklist.includes(this.widgetId)) { + this.fromWidget.sendResponse(rawEv, {state: "blocked"}); + return; + } + if (settings.whitelist && settings.whitelist.includes(this.widgetId)) { + 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"}); - // TODO: Support blacklisting widgets - // TODO: Support whitelisting widgets - // Actually ask for permission to send the user's data - Modal.createTrackedDialog("OpenID widget permissions", '', QuestionDialog, { - title: _t("A widget would like to verify your identity"), - description: _t( - "A widget located at %(widgetUrl)s would like to verify your identity. " + - "By allowing this, the widget will be able to verify your user ID, but not " + - "perform actions as you.", { - widgetUrl: this.widgetUrl, + Modal.createTrackedDialog("OpenID widget permissions", '', + WidgetOpenIDPermissionsDialog, { + widgetUrl: this.widgetUrl, + widgetId: this.widgetId, + + 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); + }); }, - ), - button: _t("Allow"), - 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); - }); }, - }); + ); } } diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js new file mode 100644 index 0000000000..bec71e49a3 --- /dev/null +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -0,0 +1,106 @@ +/* +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {Tab, TabbedView} from "../../structures/TabbedView"; +import {_t, _td} from "../../../languageHandler"; +import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; +import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; +import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; +import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab"; +import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab"; +import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab"; +import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; +import sdk from "../../../index"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; + +export default class WidgetOpenIDPermissionsDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + widgetUrl: PropTypes.string.isRequired, + widgetId: PropTypes.string.isRequired, + }; + + constructor() { + super(); + + this.state = { + rememberSelection: false, + }; + } + + _onAllow = () => { + this._onPermissionSelection(true); + }; + + _onDeny = () => { + this._onPermissionSelection(false); + }; + + _onPermissionSelection(allowed) { + if (this.state.rememberSelection) { + console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`); + + const currentValues = SettingsStore.getValue("widgetOpenIDPermissions"); + if (!currentValues.whitelist) currentValues.whitelist = []; + if (!currentValues.blacklist) currentValues.blacklist = []; + + (allowed ? currentValues.whitelist : currentValues.blacklist).push(this.props.widgetId); + SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues); + } + + this.props.onFinished(allowed); + } + + _onRememberSelectionChange = (newVal) => { + this.setState({rememberSelection: newVal}); + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + +
+

+ {_t( + "A widget located at %(widgetUrl)s would like to verify your identity. " + + "By allowing this, the widget will be able to verify your user ID, but not " + + "perform actions as you.", { + widgetUrl: this.props.widgetUrl, + }, + )} +

+ +
+ +
+ ); + } +} diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.js index 292c978e88..0cb9b224cf 100644 --- a/src/components/views/elements/LabelledToggleSwitch.js +++ b/src/components/views/elements/LabelledToggleSwitch.js @@ -31,15 +31,29 @@ export default class LabelledToggleSwitch extends React.Component { // Whether or not to disable the toggle switch disabled: PropTypes.bool, + + // True to put the toggle in front of the label + // Default false. + toggleInFront: PropTypes.bool, }; render() { // This is a minimal version of a SettingsFlag + + let firstPart = {this.props.label}; + let secondPart = ; + + if (this.props.toggleInFront) { + const temp = firstPart; + firstPart = secondPart; + secondPart = temp; + } + return (
- {this.props.label} - + {firstPart} + {secondPart}
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 21ad35a44c..4322d6cf7b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -230,9 +230,6 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", - "A widget would like to verify your identity": "A widget would like to verify your identity", - "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", - "Allow": "Allow", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -927,6 +924,7 @@ "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", "Do you want to load widget from URL:": "Do you want to load widget from URL:", + "Allow": "Allow", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Delete widget": "Delete widget", @@ -1176,6 +1174,10 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", + "A widget would like to verify your identity": "A widget would like to verify your identity", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", + "Remember my selection for this widget": "Remember my selection for this widget", + "Deny": "Deny", "Unable to load backup status": "Unable to load backup status", "Recovery Key Mismatch": "Recovery Key Mismatch", "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 4fe53633ff..fcf70b4df7 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -340,6 +340,13 @@ export const SETTINGS = { displayName: _td('Show developer tools'), default: false, }, + "widgetOpenIDPermissions": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + whitelisted: [], + blacklisted: [], + }, + }, "RoomList.orderByImportance": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Order rooms in the room list by most important first instead of most recent'), From b48842e070e089872795c107955eea3b536cc6b8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Mar 2019 21:51:19 -0600 Subject: [PATCH 03/31] Fix imports for linter --- src/WidgetMessaging.js | 2 -- .../views/dialogs/WidgetOpenIDPermissionsDialog.js | 11 +---------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index dba703ffb8..501fc34c7a 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -23,8 +23,6 @@ limitations under the License. import FromWidgetPostMessageApi from './FromWidgetPostMessageApi'; import ToWidgetPostMessageApi from './ToWidgetPostMessageApi'; import Modal from "./Modal"; -import QuestionDialog from "./components/views/dialogs/QuestionDialog"; -import {_t} from "./languageHandler"; import MatrixClientPeg from "./MatrixClientPeg"; import SettingsStore from "./settings/SettingsStore"; import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index bec71e49a3..e6e97a3305 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -16,17 +16,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {Tab, TabbedView} from "../../structures/TabbedView"; -import {_t, _td} from "../../../languageHandler"; -import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; +import {_t} from "../../../languageHandler"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; -import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; -import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; -import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; -import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab"; -import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab"; -import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab"; -import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; import sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; From 1ed2e6dcc1def5c3dd8d9684d34d4eaeb733e297 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Mar 2019 15:03:02 -0600 Subject: [PATCH 04/31] Remove the correct widget listener --- src/FromWidgetPostMessageApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 577eabf5ec..4dd3ea6e6d 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -70,8 +70,8 @@ export default class FromWidgetPostMessageApi { removeListener(action, callbackFn) { if (!this.widgetListeners[action]) return; - const idx = this.widgetListeners.indexOf(callbackFn); - if (idx !== -1) this.widgetListeners.splice(idx, 1); + const idx = this.widgetListeners[action].indexOf(callbackFn); + if (idx !== -1) this.widgetListeners[action].splice(idx, 1); } /** From 1c677d35810a8226375f1855b3658a838b5e01a6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 19 Mar 2019 15:33:28 +0000 Subject: [PATCH 05/31] Change to new consistent name for `MemoryStore` --- src/MatrixClientPeg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 1cf29c3e82..ad20988d9e 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -103,7 +103,7 @@ class MatrixClientPeg { } catch (err) { if (dbType === 'indexeddb') { console.error('Error starting matrixclient store - falling back to memory store', err); - this.matrixClient.store = new Matrix.MatrixInMemoryStore({ + this.matrixClient.store = new Matrix.MemoryStore({ localStorage: global.localStorage, }); } else { From 8a0d6562dc5bcac9afe1dc2723500c442233ebc7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 Mar 2019 10:32:22 -0600 Subject: [PATCH 06/31] Use medium agents for the more resource intensive builds --- .buildkite/pipeline.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index 4fc8254514..6a347ec002 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -20,6 +20,10 @@ steps: # image: "node:10" - label: ":karma: Tests" + agents: + # We use a medium sized instance instead of the normal small ones because + # webpack loves to gorge itself on resources. + queue: "medium" command: # Install chrome - "echo '--- Installing Chrome'" @@ -43,6 +47,10 @@ steps: propagate-environment: true - label: "🔧 Riot Tests" + agents: + # We use a medium sized instance instead of the normal small ones because + # webpack loves to gorge itself on resources. + queue: "medium" command: # Install chrome - "echo '--- Installing Chrome'" From 2ac7dd4ca3da3593cbdedc48981ecb728411418e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 21 Mar 2019 15:02:26 +0000 Subject: [PATCH 07/31] Explicitly create `cryptoStore` in React SDK The React SDK has a client creation path that starts 2 out of 3 stores, but then leaves the other one for the JS SDK's default value handling. We'll soon be adding additional code to check the health of stores, so it would be simpler to follow and think about if we create them all in one place. --- src/utils/createMatrixClient.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.js index 2acd1fae28..040f1e96cb 100644 --- a/src/utils/createMatrixClient.js +++ b/src/utils/createMatrixClient.js @@ -48,10 +48,6 @@ export default function createMatrixClient(opts, useIndexedDb) { useAuthorizationHeader: true, }; - if (localStorage) { - storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); - } - if (indexedDB && localStorage && useIndexedDb) { storeOpts.store = new Matrix.IndexedDBStore({ indexedDB: indexedDB, @@ -61,6 +57,16 @@ export default function createMatrixClient(opts, useIndexedDb) { }); } + if (localStorage) { + storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); + } + + if (indexedDB && useIndexedDb) { + storeOpts.cryptoStore = new Matrix.IndexedDBCryptoStore( + indexedDB, "matrix-js-sdk:crypto", + ); + } + opts = Object.assign(storeOpts, opts); return Matrix.createClient(opts); From c17f7a29492ab8a435ef3945ca363f8b4c6e8e18 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 22 Mar 2019 18:50:08 +0000 Subject: [PATCH 08/31] Trim the logging for URL previews It should be sufficient to have the error stack and general log message for URL preview failure. --- src/components/views/rooms/LinkPreviewWidget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 5f32a6a613..8b6f295080 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -52,7 +52,7 @@ module.exports = React.createClass({ this.props.onHeightChanged, ); }, (error)=>{ - console.error("Failed to get preview for " + this.props.link + " " + error); + console.error("Failed to get URL preview: " + error); }).done(); }, From 50ffdc2f1290b471c91e14442407a2a96532ae4d Mon Sep 17 00:00:00 2001 From: Tee Mak Date: Fri, 22 Mar 2019 14:50:13 -0400 Subject: [PATCH 09/31] Fixed drop shadow for tooltip. The box-shadow color value is from $menu-box-shadow-color. The shadow now looks consistent on light or dark theme. --- res/css/views/elements/_Tooltip.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 78604b1564..2f35bd338e 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -54,7 +54,7 @@ limitations under the License. position: fixed; border: 1px solid $menu-border-color; border-radius: 4px; - box-shadow: 4px 4px 12px 0 rgba(118, 131, 156, 0.6); + box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; background-color: $menu-bg-color; z-index: 2000; padding: 10px; From 50614fa7fb95364247de39ddfc7e6f5bfc14c35f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Mar 2019 14:22:20 -0600 Subject: [PATCH 10/31] Attach an onChange listener to the room's blacklist devices option Fixes https://github.com/vector-im/riot-web/issues/9235 The global option in user settings is unaffected by this bug. Users who have previously set the per-room flag without success can simply refresh the page and the change will be picked up. The bug here is that the current session would not update accordingly, however. Introduced in https://github.com/matrix-org/matrix-react-sdk/pull/2523 --- .../views/settings/tabs/room/SecurityRoomSettingsTab.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js index f4293a60dc..b44d7b019d 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js @@ -197,6 +197,10 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; + _updateBlacklistDevicesFlag = (checked) => { + MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked); + }; + _renderRoomAccess() { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); @@ -318,6 +322,7 @@ export default class SecurityRoomSettingsTab extends React.Component { let encryptionSettings = null; if (isEncrypted) { encryptionSettings = ; } From a34a8bb425452a7854df29bf340b8b51b8a47d6e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Mar 2019 17:36:54 -0600 Subject: [PATCH 11/31] Use leaveRoomChain when leaving a room Requires https://github.com/matrix-org/matrix-js-sdk/pull/868 Fixes https://github.com/vector-im/riot-web/issues/8539 We don't need to use leaveRoomChain when rejecting invites because we won't have the references needed. This leaves the couple spots where we do actually leave a room, and use the new function for that. --- src/SlashCommands.js | 2 +- src/components/structures/MatrixChat.js | 41 ++++++++++++++++--------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 039ccb928f..b5f2ed8ba2 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -431,7 +431,7 @@ export const CommandMap = { if (!targetRoomId) targetRoomId = roomId; return success( - cli.leave(targetRoomId).then(function() { + cli.leaveRoomChain(targetRoomId).then(function() { dis.dispatch({action: 'view_next_room'}); }), ); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2622a6bf93..17ce6e0e73 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1058,34 +1058,47 @@ export default React.createClass({ button: _t("Leave"), onFinished: (shouldLeave) => { if (shouldLeave) { - const d = MatrixClientPeg.get().leave(roomId); + const d = MatrixClientPeg.get().leaveRoomChain(roomId); // FIXME: controller shouldn't be loading a view :( const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - d.then(() => { + d.then((errors) => { modal.close(); + + if (errors[roomId]) { + // Something went wrong + const err = errors[roomId]; + console.error("Failed to leave room " + roomId + " " + err); + let title = _t("Failed to leave room"); + let message = _t("Server may be unavailable, overloaded, or you hit a bug."); + if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') { + title = _t("Can't leave Server Notices room"); + message = _t( + "This room is used for important messages from the Homeserver, " + + "so you cannot leave it.", + ); + } else if (err && err.message) { + message = err.message; + } + Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, { + title: title, + description: message, + }); + return; + } + if (this.state.currentRoomId === roomId) { dis.dispatch({action: 'view_next_room'}); } }, (err) => { + // This should only happen if something went seriously wrong with leaving the chain. modal.close(); console.error("Failed to leave room " + roomId + " " + err); - let title = _t("Failed to leave room"); - let message = _t("Server may be unavailable, overloaded, or you hit a bug."); - if (err.errcode == 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') { - title = _t("Can't leave Server Notices room"); - message = _t( - "This room is used for important messages from the Homeserver, " + - "so you cannot leave it.", - ); - } else if (err && err.message) { - message = err.message; - } Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, { title: title, - description: message, + description: _t("Unknown error"), }); }); } From 4fd48988e96683df1dd3c543c700bc1afd0166a2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Mar 2019 17:39:42 -0600 Subject: [PATCH 12/31] Check for any errors leaving the room, not just the top level --- src/components/structures/MatrixChat.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 17ce6e0e73..26956858be 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1067,10 +1067,11 @@ export default React.createClass({ d.then((errors) => { modal.close(); - if (errors[roomId]) { - // Something went wrong - const err = errors[roomId]; - console.error("Failed to leave room " + roomId + " " + err); + for (const leftRoomId of Object.keys(errors)) { + const err = errors[leftRoomId]; + if (!err) continue; + + console.error("Failed to leave room " + leftRoomId + " " + err); let title = _t("Failed to leave room"); let message = _t("Server may be unavailable, overloaded, or you hit a bug."); if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') { From 389abed5745590e1d78b65c5ffc2c48a42bb162e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Mar 2019 19:03:43 -0600 Subject: [PATCH 13/31] Define a title for generic error --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 26956858be..9d8a4adb6f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1098,7 +1098,7 @@ export default React.createClass({ modal.close(); console.error("Failed to leave room " + roomId + " " + err); Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, { - title: title, + title: _t("Failed to leave room"), description: _t("Unknown error"), }); }); From e5f7563decc2124209b51bd8980c3c96a1af33a8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Mar 2019 20:22:13 -0600 Subject: [PATCH 14/31] Ask the user for debug logs when the timeline explodes Fixes https://github.com/vector-im/riot-web/issues/9260 Workaround for https://github.com/vector-im/riot-web/issues/8593 Requires https://github.com/matrix-org/matrix-js-sdk/pull/869 We check if any dialogs are open before moving forward because we don't want to risk showing so many dialogs that the user is unable to click a button. We're also not overly concerned if the dialog being shown is irrelevant because whatever the user is doing will likely be unaffected, and we can scream in pain when they're finished. --- src/Modal.js | 4 + src/components/structures/MatrixChat.js | 12 ++ .../views/dialogs/TimelineExplosionDialog.js | 130 ++++++++++++++++++ src/i18n/strings/en_EN.json | 4 + 4 files changed, 150 insertions(+) create mode 100644 src/components/views/dialogs/TimelineExplosionDialog.js diff --git a/src/Modal.js b/src/Modal.js index 4d90e313ce..96dbd49324 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -124,6 +124,10 @@ class ModalManager { this.closeAll = this.closeAll.bind(this); } + hasDialogs() { + return this._priorityModal || this._staticModal || this._modals.length > 0; + } + getOrCreateContainer() { let container = document.getElementById(DIALOG_CONTAINER_ID); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2622a6bf93..b617e4ca02 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -48,6 +48,7 @@ import { _t, getCurrentLanguage } from '../../languageHandler'; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; +import TimelineExplosionDialog from "../views/dialogs/TimelineExplosionDialog"; const AutoDiscovery = Matrix.AutoDiscovery; @@ -1278,6 +1279,17 @@ export default React.createClass({ return self._loggedInView.child.canResetTimelineInRoom(roomId); }); + cli.on('sync.unexpectedError', function(err) { + if (err.message && err.message.includes("live timeline ") && err.message.includes(" is no longer live ")) { + console.error("Caught timeline explosion - trying to ask user for more information"); + if (Modal.hasDialogs()) { + console.warn("User has another dialog open - skipping prompt"); + return; + } + Modal.createTrackedDialog('Timeline exploded', '', TimelineExplosionDialog, {}); + } + }); + cli.on('sync', function(state, prevState, data) { // LifecycleStore and others cannot directly subscribe to matrix client for // events because flux only allows store state changes during flux dispatches. diff --git a/src/components/views/dialogs/TimelineExplosionDialog.js b/src/components/views/dialogs/TimelineExplosionDialog.js new file mode 100644 index 0000000000..5c8fba8f9c --- /dev/null +++ b/src/components/views/dialogs/TimelineExplosionDialog.js @@ -0,0 +1,130 @@ +/* +Copyright 2019 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 React from 'react'; +import sdk from '../../../index'; +import SdkConfig from '../../../SdkConfig'; +import { _t } from '../../../languageHandler'; + +// Dev note: this should be a temporary dialog while we work out what is +// actually going on. See https://github.com/vector-im/riot-web/issues/8593 +// for more details. This dialog is almost entirely a copy/paste job of +// BugReportDialog. +export default class TimelineExplosionDialog extends React.Component { + static propTypes = { + onFinished: React.PropTypes.func.isRequired, + }; + + constructor(props, context) { + super(props, context); + this.state = { + busy: false, + progress: null, + }; + } + + _onCancel() { + console.log("Reloading without sending logs for timeline explosion"); + window.location.reload(); + } + + _onSubmit = () => { + const userText = "Caught timeline explosion\n\nhttps://github.com/vector-im/riot-web/issues/8593"; + + this.setState({busy: true, progress: null}); + this._sendProgressCallback(_t("Preparing to send logs")); + + require(['../../../rageshake/submit-rageshake'], (s) => { + s(SdkConfig.get().bug_report_endpoint_url, { + userText, + sendLogs: true, + progressCallback: this._sendProgressCallback, + }).then(() => { + console.log("logs sent for timeline explosion - reloading riot"); + window.location.reload(); + }, (err) => { + console.error("Error sending logs for timeline explosion - reloading anyways. ", err); + window.location.reload(); + }); + }); + }; + + _sendProgressCallback = (progress) => { + this.setState({progress: progress}); + }; + + render() { + const Loader = sdk.getComponent("elements.Spinner"); + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + let progress = null; + if (this.state.busy) { + progress = ( +
+ {this.state.progress} ... + +
+ ); + } + + return ( + +
+

+ {_t( + "Riot has run into a problem which makes it difficult to show you " + + "your messages right now. Nothing has been lost, and reloading the app " + + "should fix this for you. In order to assist us in troubleshooting the " + + "problem, we'd like to take a look at your debug logs. You do not need " + + "to send your logs unless you want to, but we would really appreciate " + + "it if you did. We'd also like to apologize for having to show this " + + "message to you - we hope your debug logs are the key to solving the " + + "issue once and for all. If you'd like more information on the bug you've " + + "accidentally run into, please visit the issue.", + {}, + { + 'a': (sub) => { + return {sub}; + }, + }, + )} +

+

+ {_t( + "Debug logs contain application usage data including your " + + "username, the IDs or aliases of the rooms or groups you " + + "have visited and the usernames of other users. They do " + + "not contain messages.", + )} +

+ {progress} +
+ +
+ ); + } +} + diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef4bc75d27..0d0dad74ba 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1173,6 +1173,10 @@ "Share Room Message": "Share Room Message", "Link to selected message": "Link to selected message", "COPY": "COPY", + "Error showing you your room": "Error showing you your room", + "Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost, and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit the issue.": "Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost, and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit the issue.", + "Send debug logs and reload Riot": "Send debug logs and reload Riot", + "Reload Riot without sending logs": "Reload Riot without sending logs", "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.", "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.", "Room contains unknown devices": "Room contains unknown devices", From 00335e246274d1bfd4bcd6f3a340598d5ba3592a Mon Sep 17 00:00:00 2001 From: YaoiFangirl420 <48789208+YaoiFangirl420@users.noreply.github.com> Date: Sat, 23 Mar 2019 20:53:09 -0700 Subject: [PATCH 15/31] Fix bug with NetworkList dropdown The NetworkDropdown component was incorrectly guarding against a null check when retrieving the list of networks Signed-off-by: YaoiFangirl420 <48789208+YaoiFangirl420@users.noreply.github.com> --- src/components/views/directory/NetworkDropdown.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/directory/NetworkDropdown.js b/src/components/views/directory/NetworkDropdown.js index ebfff5ae8c..722fe21c1b 100644 --- a/src/components/views/directory/NetworkDropdown.js +++ b/src/components/views/directory/NetworkDropdown.js @@ -131,10 +131,11 @@ export default class NetworkDropdown extends React.Component { _getMenuOptions() { const options = []; + const roomDirectory = this.props.config.roomDirectory || {}; let servers = []; - if (this.props.config.roomDirectory.servers) { - servers = servers.concat(this.props.config.roomDirectory.servers); + if (roomDirectory.servers) { + servers = servers.concat(roomDirectory.servers); } if (!servers.includes(MatrixClientPeg.getHomeServerName())) { From 21d52a83113eab3654c36cff431660124961535a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 23 Mar 2019 22:50:26 -0600 Subject: [PATCH 16/31] Use the same function name to bind the OpenID request handler --- src/WidgetMessaging.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 501fc34c7a..bbecdfa086 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -45,7 +45,7 @@ export default class WidgetMessaging { this.target = target; this.fromWidget = global.mxFromWidgetMessaging; this.toWidget = global.mxToWidgetMessaging; - this._openIdHandlerRef = this._onOpenIdRequest.bind(this); + this._onOpenIdRequest = this._onOpenIdRequest.bind(this); this.start(); } @@ -115,12 +115,12 @@ export default class WidgetMessaging { start() { this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl); - this.fromWidget.addListener("get_openid", this._openIdHandlerRef); + this.fromWidget.addListener("get_openid", this._onOpenIdRequest); } stop() { this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl); - this.fromWidget.removeListener("get_openid", this._openIdHandlerRef); + this.fromWidget.removeListener("get_openid", this._onOpenIdRequest); } async _onOpenIdRequest(ev, rawEv) { From 2dcb40f1be6a90b628a84581f838a012821547f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 23 Mar 2019 23:25:31 -0600 Subject: [PATCH 17/31] Track OpenID automatic permissions by (widgetLocation, widgetUrl) --- src/WidgetMessaging.js | 11 ++++++--- .../dialogs/WidgetOpenIDPermissionsDialog.js | 8 ++++++- src/components/views/elements/AppTile.js | 2 +- src/settings/Settings.js | 4 ++-- src/utils/WidgetUtils.js | 23 +++++++++++++++++++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index bbecdfa086..8d419ba6eb 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -26,6 +26,7 @@ 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"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -39,9 +40,10 @@ if (!global.mxToWidgetMessaging) { const OUTBOUND_API_NAME = 'toWidget'; export default class WidgetMessaging { - constructor(widgetId, widgetUrl, target) { + 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; @@ -126,12 +128,14 @@ export default class WidgetMessaging { 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.blacklist && settings.blacklist.includes(this.widgetId)) { + if (settings.blacklist && settings.blacklist.includes(widgetSecurityKey)) { this.fromWidget.sendResponse(rawEv, {state: "blocked"}); return; } - if (settings.whitelist && settings.whitelist.includes(this.widgetId)) { + if (settings.whitelist && settings.whitelist.includes(widgetSecurityKey)) { const responseBody = {state: "allowed"}; const credentials = await MatrixClientPeg.get().getOpenIdToken(); Object.assign(responseBody, credentials); @@ -147,6 +151,7 @@ export default class WidgetMessaging { WidgetOpenIDPermissionsDialog, { widgetUrl: this.widgetUrl, widgetId: this.widgetId, + isUserWidget: this.isUserWidget, onFinished: async (confirm) => { const responseBody = {success: confirm}; diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index e6e97a3305..5f341261f8 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -20,12 +20,14 @@ import {_t} from "../../../languageHandler"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import WidgetUtils from "../../../utils/WidgetUtils"; export default class WidgetOpenIDPermissionsDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, widgetUrl: PropTypes.string.isRequired, widgetId: PropTypes.string.isRequired, + isUserWidget: PropTypes.bool.isRequired, }; constructor() { @@ -52,7 +54,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component { if (!currentValues.whitelist) currentValues.whitelist = []; if (!currentValues.blacklist) currentValues.blacklist = []; - (allowed ? currentValues.whitelist : currentValues.blacklist).push(this.props.widgetId); + const securityKey = WidgetUtils.getWidgetSecurityKey( + this.props.widgetId, + this.props.widgetUrl, + this.props.isUserWidget); + (allowed ? currentValues.whitelist : currentValues.blacklist).push(securityKey); SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues); } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 8ed408ffbe..955c2d5480 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -351,7 +351,7 @@ export default class AppTile extends React.Component { _setupWidgetMessaging() { // FIXME: There's probably no reason to do this here: it should probably be done entirely // in ActiveWidgetStore. - const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow); + const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow); ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging); widgetMessaging.getCapabilities().then((requestedCapabilities) => { console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index fcf70b4df7..765b2f85c6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -343,8 +343,8 @@ export const SETTINGS = { "widgetOpenIDPermissions": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: { - whitelisted: [], - blacklisted: [], + whitelist: [], + blacklist: [], }, }, "RoomList.orderByImportance": { diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index b5a2ae31fb..41a241c905 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations Ltd Copyright 2018 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. @@ -25,6 +26,7 @@ import WidgetEchoStore from '../stores/WidgetEchoStore'; // before waitFor[Room/User]Widget rejects its promise const WIDGET_WAIT_TIME = 20000; import SettingsStore from "../settings/SettingsStore"; +import ActiveWidgetStore from "../stores/ActiveWidgetStore"; /** * Encodes a URI according to a set of template variables. Variables will be @@ -396,4 +398,25 @@ export default class WidgetUtils { return capWhitelist; } + + static getWidgetSecurityKey(widgetId, widgetUrl, isUserWidget) { + let widgetLocation = ActiveWidgetStore.getRoomId(widgetId); + + if (isUserWidget) { + const userWidget = WidgetUtils.getUserWidgetsArray() + .find((w) => w.id === widgetId && w.content && w.content.url === widgetUrl); + + if (!userWidget) { + throw new Error("No matching user widget to form security key"); + } + + widgetLocation = userWidget.sender; + } + + if (!widgetLocation) { + throw new Error("Failed to locate where the widget resides"); + } + + return encodeURIComponent(`${widgetLocation}::${widgetUrl}`); + } } From 3654c895ebf75dca452bc7f955e09d470a670662 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 23 Mar 2019 23:31:19 -0600 Subject: [PATCH 18/31] Appease the linter --- src/components/views/elements/AppTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 955c2d5480..c410b31799 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -351,7 +351,8 @@ export default class AppTile extends React.Component { _setupWidgetMessaging() { // FIXME: There's probably no reason to do this here: it should probably be done entirely // in ActiveWidgetStore. - const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow); + const widgetMessaging = new WidgetMessaging( + this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow); ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging); widgetMessaging.getCapabilities().then((requestedCapabilities) => { console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities); From a8ae63bb06759b9ae019e0f6d16ec35348535dcb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 23 Mar 2019 23:50:06 -0600 Subject: [PATCH 19/31] Minimize stickerpicker when the title is clicked Fixes https://github.com/vector-im/riot-web/issues/6437 This also fixes a bug where if the room had apps open and you clicked the sticker picker's title, you'd end up closing the apps and not the picker. --- src/components/views/elements/AppTile.js | 14 +++++++++----- src/i18n/strings/en_EN.json | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 8ed408ffbe..466715dc3c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -447,10 +447,14 @@ export default class AppTile extends React.Component { } // Toggle the view state of the apps drawer - dis.dispatch({ - action: 'appsDrawer', - show: !this.props.show, - }); + if (this.props.userWidget) { + this._onMinimiseClick(); + } else { + dis.dispatch({ + action: 'appsDrawer', + show: !this.props.show, + }); + } } _getSafeUrl() { @@ -626,7 +630,7 @@ export default class AppTile extends React.Component { { /* Maximise widget */ } { showMaximiseButton && } { /* Title */ } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef4bc75d27..1bb1c8e98c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -935,6 +935,7 @@ "Failed to remove widget": "Failed to remove widget", "An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room", "Minimize apps": "Minimize apps", + "Maximize apps": "Maximize apps", "Reload widget": "Reload widget", "Popout widget": "Popout widget", "Picture": "Picture", From 1ba23d6833e41fa9b3d0d49df75be4ba078687f8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 23 Mar 2019 23:53:21 -0600 Subject: [PATCH 20/31] Fix typo preventing users from adding more widgets easily Fixes https://github.com/vector-im/riot-web/issues/9266 --- src/components/views/rooms/AppsDrawer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 449ca5f83d..e0e7a48b8c 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -144,7 +144,7 @@ module.exports = React.createClass({ _launchManageIntegrations: function() { const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager'); - this._scalarClient.connect().done(() => { + this.scalarClient.connect().done(() => { const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') : null; From 8cca1bef23a4b6d3c036fb9ac74133dee2fac96a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 24 Mar 2019 00:07:00 -0600 Subject: [PATCH 21/31] Add a command for creating custom widgets without an integration manager Fixes https://github.com/vector-im/riot-web/issues/4882 --- src/SlashCommands.js | 21 +++++++++++++++++++++ src/i18n/strings/en_EN.json | 3 +++ 2 files changed, 24 insertions(+) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 039ccb928f..fbacaef7c2 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -29,6 +29,7 @@ import * as querystring from "querystring"; import MultiInviter from './utils/MultiInviter'; import { linkifyAndSanitizeHtml } from './HtmlUtils'; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; +import WidgetUtils from "./utils/WidgetUtils"; class Command { constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) { @@ -606,6 +607,26 @@ export const CommandMap = { }, }), + addwidget: new Command({ + name: 'addwidget', + args: '', + description: _td('Adds a custom widget by URL to the room'), + runFn: function(roomId, args) { + if (!args || (!args.startsWith("https://") && !args.startsWith("http://"))) { + return reject(_t("Please supply a https:// or http:// widget URL")); + } + if (WidgetUtils.canUserModifyWidgets(roomId)) { + const userId = MatrixClientPeg.get().getUserId(); + const nowMs = (new Date()).getTime(); + const widgetId = encodeURIComponent(`${roomId}_${userId}_${nowMs}`); + return success(WidgetUtils.setRoomWidget( + roomId, widgetId, "customwidget", args, "Custom Widget", {})); + } else { + return reject(_t("You cannot modify widgets in this room.")); + } + }, + }), + // Verify a user, device, and pubkey tuple verify: new Command({ name: 'verify', diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef4bc75d27..8adbf1a8d8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -161,6 +161,9 @@ "Define the power level of a user": "Define the power level of a user", "Deops user with given id": "Deops user with given id", "Opens the Developer Tools dialog": "Opens the Developer Tools dialog", + "Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room", + "Please supply a https:// or http:// widget URL": "Please supply a https:// or http:// widget URL", + "You cannot modify widgets in this room.": "You cannot modify widgets in this room.", "Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple", "Unknown (user, device) pair:": "Unknown (user, device) pair:", "Device already verified!": "Device already verified!", From f2b64a8e7fe3c3b9614bb99789c1efa616d99a31 Mon Sep 17 00:00:00 2001 From: YaoiFangirl420 <48789208+YaoiFangirl420@users.noreply.github.com> Date: Sun, 24 Mar 2019 02:05:04 -0700 Subject: [PATCH 22/31] Add blocks around homeserver and identity server urls To make the urls visually distinct from the surrounding non-url text Signed-off-by: YaoiFangirl420 <48789208+YaoiFangirl420@users.noreply.github.com> --- .../views/settings/tabs/user/HelpUserSettingsTab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index d001a3f2e6..e45b0d0389 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -235,8 +235,8 @@ export default class HelpUserSettingsTab extends React.Component {
{_t("Advanced")}
- {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
- {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
+ {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
+ {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
{_t("Access Token:") + ' '} From 3e676454b67a563ad5677e3f1d2f6ba67ec8b561 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Mar 2019 11:33:31 -0600 Subject: [PATCH 23/31] Update src/components/views/dialogs/TimelineExplosionDialog.js Co-Authored-By: turt2live --- src/components/views/dialogs/TimelineExplosionDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/TimelineExplosionDialog.js b/src/components/views/dialogs/TimelineExplosionDialog.js index 5c8fba8f9c..a6c6dc5fb2 100644 --- a/src/components/views/dialogs/TimelineExplosionDialog.js +++ b/src/components/views/dialogs/TimelineExplosionDialog.js @@ -53,7 +53,7 @@ export default class TimelineExplosionDialog extends React.Component { sendLogs: true, progressCallback: this._sendProgressCallback, }).then(() => { - console.log("logs sent for timeline explosion - reloading riot"); + console.log("Logs sent for timeline explosion - reloading Riot"); window.location.reload(); }, (err) => { console.error("Error sending logs for timeline explosion - reloading anyways. ", err); From 1e5c0a871314d3874fa5c820bd3922da4aa7bf48 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Mar 2019 11:56:49 -0600 Subject: [PATCH 24/31] Apply suggestions from code review Co-Authored-By: turt2live --- src/components/views/dialogs/TimelineExplosionDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/TimelineExplosionDialog.js b/src/components/views/dialogs/TimelineExplosionDialog.js index a6c6dc5fb2..6e810d0421 100644 --- a/src/components/views/dialogs/TimelineExplosionDialog.js +++ b/src/components/views/dialogs/TimelineExplosionDialog.js @@ -56,7 +56,7 @@ export default class TimelineExplosionDialog extends React.Component { console.log("Logs sent for timeline explosion - reloading Riot"); window.location.reload(); }, (err) => { - console.error("Error sending logs for timeline explosion - reloading anyways. ", err); + console.error("Error sending logs for timeline explosion - reloading anyways.", err); window.location.reload(); }); }); @@ -89,7 +89,7 @@ export default class TimelineExplosionDialog extends React.Component {

{_t( "Riot has run into a problem which makes it difficult to show you " + - "your messages right now. Nothing has been lost, and reloading the app " + + "your messages right now. Nothing has been lost and reloading the app " + "should fix this for you. In order to assist us in troubleshooting the " + "problem, we'd like to take a look at your debug logs. You do not need " + "to send your logs unless you want to, but we would really appreciate " + From 43291c708d5677ab8890977c5ecff621c383e32c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 25 Mar 2019 11:58:11 -0600 Subject: [PATCH 25/31] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0d0dad74ba..f67eda9614 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1174,7 +1174,7 @@ "Link to selected message": "Link to selected message", "COPY": "COPY", "Error showing you your room": "Error showing you your room", - "Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost, and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit the issue.": "Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost, and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit the issue.", + "Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit the issue.": "Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit the issue.", "Send debug logs and reload Riot": "Send debug logs and reload Riot", "Reload Riot without sending logs": "Reload Riot without sending logs", "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.", From 69fcebf045863a9196eea2d9ed18948b3a876212 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 25 Mar 2019 21:14:21 -0600 Subject: [PATCH 26/31] Use allow/deny instead of whitelist/blacklist for terminology --- src/WidgetMessaging.js | 4 ++-- .../views/dialogs/WidgetOpenIDPermissionsDialog.js | 6 +++--- src/settings/Settings.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 8d419ba6eb..1d8e1b9cd3 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -131,11 +131,11 @@ export default class WidgetMessaging { const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.widgetUrl, this.isUserWidget); const settings = SettingsStore.getValue("widgetOpenIDPermissions"); - if (settings.blacklist && settings.blacklist.includes(widgetSecurityKey)) { + if (settings.deny && settings.deny.includes(widgetSecurityKey)) { this.fromWidget.sendResponse(rawEv, {state: "blocked"}); return; } - if (settings.whitelist && settings.whitelist.includes(widgetSecurityKey)) { + if (settings.allow && settings.allow.includes(widgetSecurityKey)) { const responseBody = {state: "allowed"}; const credentials = await MatrixClientPeg.get().getOpenIdToken(); Object.assign(responseBody, credentials); diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index 5f341261f8..62bd1d2521 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -51,14 +51,14 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component { console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`); const currentValues = SettingsStore.getValue("widgetOpenIDPermissions"); - if (!currentValues.whitelist) currentValues.whitelist = []; - if (!currentValues.blacklist) currentValues.blacklist = []; + if (!currentValues.allow) currentValues.allow = []; + if (!currentValues.deny) currentValues.deny = []; const securityKey = WidgetUtils.getWidgetSecurityKey( this.props.widgetId, this.props.widgetUrl, this.props.isUserWidget); - (allowed ? currentValues.whitelist : currentValues.blacklist).push(securityKey); + (allowed ? currentValues.allow : currentValues.deny).push(securityKey); SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues); } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 765b2f85c6..6e17ffbbd7 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -343,8 +343,8 @@ export const SETTINGS = { "widgetOpenIDPermissions": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: { - whitelist: [], - blacklist: [], + allow: [], + deny: [], }, }, "RoomList.orderByImportance": { From 90dbeefcfbc108499b31323244e805d6846b5b2b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Mar 2019 16:22:53 +0000 Subject: [PATCH 27/31] Remove unused option for disabling IndexedDB `createMatrixClient` and surrounding paths support an argument to disable IndexedDB, but it is never actually used. This removes the option to simplify the code. --- src/MatrixClientPeg.js | 4 ++-- src/utils/createMatrixClient.js | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index ad20988d9e..f5994921de 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -171,7 +171,7 @@ class MatrixClientPeg { return matches[1]; } - _createClient(creds: MatrixClientCreds, useIndexedDb) { + _createClient(creds: MatrixClientCreds) { const opts = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, @@ -183,7 +183,7 @@ class MatrixClientPeg { verificationMethods: [verificationMethods.SAS] }; - this.matrixClient = createMatrixClient(opts, useIndexedDb); + this.matrixClient = createMatrixClient(opts); // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.js index 040f1e96cb..dee9324460 100644 --- a/src/utils/createMatrixClient.js +++ b/src/utils/createMatrixClient.js @@ -32,23 +32,18 @@ try { * @param {Object} opts options to pass to Matrix.createClient. This will be * extended with `sessionStore` and `store` members. * - * @param {bool} useIndexedDb True to attempt to use indexeddb, or false to force - * use of the memory store. Default: true. - * * @property {string} indexedDbWorkerScript Optional URL for a web worker script * for IndexedDB store operations. By default, indexeddb ops are done on * the main thread. * * @returns {MatrixClient} the newly-created MatrixClient */ -export default function createMatrixClient(opts, useIndexedDb) { - if (useIndexedDb === undefined) useIndexedDb = true; - +export default function createMatrixClient(opts) { const storeOpts = { useAuthorizationHeader: true, }; - if (indexedDB && localStorage && useIndexedDb) { + if (indexedDB && localStorage) { storeOpts.store = new Matrix.IndexedDBStore({ indexedDB: indexedDB, dbName: "riot-web-sync", @@ -61,7 +56,7 @@ export default function createMatrixClient(opts, useIndexedDb) { storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); } - if (indexedDB && useIndexedDb) { + if (indexedDB) { storeOpts.cryptoStore = new Matrix.IndexedDBCryptoStore( indexedDB, "matrix-js-sdk:crypto", ); From f61a6104b557b268995ea55f227b2c55cb2789ad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 26 Mar 2019 09:23:17 -0600 Subject: [PATCH 28/31] Use m.custom for widget type As per https://github.com/matrix-org/matrix-doc/issues/1236 --- src/SlashCommands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index fbacaef7c2..1c64e15cc8 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -620,7 +620,7 @@ export const CommandMap = { const nowMs = (new Date()).getTime(); const widgetId = encodeURIComponent(`${roomId}_${userId}_${nowMs}`); return success(WidgetUtils.setRoomWidget( - roomId, widgetId, "customwidget", args, "Custom Widget", {})); + roomId, widgetId, "m.custom", args, "Custom Widget", {})); } else { return reject(_t("You cannot modify widgets in this room.")); } From 4c0f459995787e8e7310ef7437c3020b477b7eb9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Mar 2019 17:43:53 +0000 Subject: [PATCH 29/31] Add basic storage consistency check This adds a storage consistency check just before creating a client on login. Each data store we use is checked for data and any problems are logged to the console. Fixes https://github.com/vector-im/riot-web/issues/9271 --- src/Lifecycle.js | 5 ++- src/utils/StorageManager.js | 90 +++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/utils/StorageManager.js diff --git a/src/Lifecycle.js b/src/Lifecycle.js index f7579cf3c0..fbb68481ad 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -31,7 +31,8 @@ import Modal from './Modal'; import sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import PlatformPeg from "./PlatformPeg"; -import {sendLoginRequest} from "./Login"; +import { sendLoginRequest } from "./Login"; +import * as StorageManager from './utils/StorageManager'; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -353,6 +354,8 @@ async function _doSetLoggedIn(credentials, clearStorage) { await _clearStorage(); } + await StorageManager.checkConsistency(); + Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl); if (localStorage) { diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.js new file mode 100644 index 0000000000..4774d3b7e1 --- /dev/null +++ b/src/utils/StorageManager.js @@ -0,0 +1,90 @@ +/* +Copyright 2019 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 Matrix from 'matrix-js-sdk'; + +const localStorage = window.localStorage; + +// just *accessing* indexedDB throws an exception in firefox with +// indexeddb disabled. +let indexedDB; +try { + indexedDB = window.indexedDB; +} catch (e) {} + +// The JS SDK will add a prefix of "matrix-js-sdk:" to the sync store name. +const SYNC_STORE_NAME = "riot-web-sync"; +const CRYPTO_STORE_NAME = "matrix-js-sdk:crypto"; + +function log(msg) { + console.log(`StorageManager: ${msg}`); +} + +function error(msg) { + console.error(`StorageManager: ${msg}`); +} + +export async function checkConsistency() { + log("Checking storage consistency"); + log(`Local storage supported? ${!!localStorage}`); + log(`IndexedDB supported? ${!!indexedDB}`); + + let dataInLocalStorage = false; + let dataInCryptoStore = false; + let healthy = true; + + if (localStorage) { + dataInLocalStorage = localStorage.length > 0; + log(`Local storage contains data? ${dataInLocalStorage}`); + } else { + healthy = false; + error("Local storage cannot be used on this browser"); + } + + if (indexedDB && localStorage) { + const dataInSyncStore = await Matrix.IndexedDBStore.exists( + indexedDB, SYNC_STORE_NAME, + ); + log(`Sync store contains data? ${dataInSyncStore}`); + } else { + healthy = false; + error("Sync store cannot be used on this browser"); + } + + if (indexedDB) { + dataInCryptoStore = await Matrix.IndexedDBCryptoStore.exists( + indexedDB, CRYPTO_STORE_NAME, + ); + log(`Crypto store contains data? ${dataInCryptoStore}`); + } else { + healthy = false; + error("Crypto store cannot be used on this browser"); + } + + if (dataInLocalStorage && !dataInCryptoStore) { + healthy = false; + error( + "Data exists in local storage but not in crypto store. " + + "IndexedDB storage has likely been evicted by the browser!", + ); + } + + if (healthy) { + log("Storage consistency checks passed"); + } else { + error("Storage consistency checks failed"); + } +} From f8dce875f233321f3208a541544f420c079fce21 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 26 Mar 2019 10:09:04 +0000 Subject: [PATCH 30/31] Clarify devices affected by notification settings This clarifies that the notification settings only apply to the current device. This also tries to apply the spirit of https://github.com/matrix-org/matrix-react-sdk/pull/1995 (authored by @aidalgol) which wanted to remove "web" from the label, since there's also a desktop client. --- src/components/views/settings/Notifications.js | 4 ++-- src/i18n/strings/en_EN.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index b8f8279bb0..f344f2c897 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -837,7 +837,7 @@ module.exports = React.createClass({ + label={_t('Enable desktop notifications for this device')} /> + label={_t('Enable audible notifications for this device')} /> { emailNotificationsRows } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef4bc75d27..1f4d97e408 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -497,9 +497,9 @@ "Advanced notification settings": "Advanced notification settings", "There are advanced notifications which are not shown here": "There are advanced notifications which are not shown here", "You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply", - "Enable desktop notifications": "Enable desktop notifications", + "Enable desktop notifications for this device": "Enable desktop notifications for this device", "Show message in desktop notification": "Show message in desktop notification", - "Enable audible notifications in web client": "Enable audible notifications in web client", + "Enable audible notifications for this device": "Enable audible notifications for this device", "Off": "Off", "On": "On", "Noisy": "Noisy", From d06fb0d076783369316b1ab8a8fc4215bbccff89 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 26 Mar 2019 16:36:12 +0000 Subject: [PATCH 31/31] Send telemetry about storage consistency This adds telemetry events about basic storage consistency, so we can start to get an idea of how often IndexedDB eviction occurs in the field. Fixes https://github.com/vector-im/riot-web/issues/9272 --- src/utils/StorageManager.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.js index 4774d3b7e1..5edb43fb0b 100644 --- a/src/utils/StorageManager.js +++ b/src/utils/StorageManager.js @@ -15,6 +15,7 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; +import Analytics from '../Analytics'; const localStorage = window.localStorage; @@ -37,6 +38,10 @@ function error(msg) { console.error(`StorageManager: ${msg}`); } +function track(action) { + Analytics.trackEvent("StorageManager", action); +} + export async function checkConsistency() { log("Checking storage consistency"); log(`Local storage supported? ${!!localStorage}`); @@ -52,6 +57,7 @@ export async function checkConsistency() { } else { healthy = false; error("Local storage cannot be used on this browser"); + track("Local storage disabled"); } if (indexedDB && localStorage) { @@ -62,6 +68,7 @@ export async function checkConsistency() { } else { healthy = false; error("Sync store cannot be used on this browser"); + track("Sync store disabled"); } if (indexedDB) { @@ -72,6 +79,7 @@ export async function checkConsistency() { } else { healthy = false; error("Crypto store cannot be used on this browser"); + track("Crypto store disabled"); } if (dataInLocalStorage && !dataInCryptoStore) { @@ -80,11 +88,14 @@ export async function checkConsistency() { "Data exists in local storage but not in crypto store. " + "IndexedDB storage has likely been evicted by the browser!", ); + track("Crypto store evicted"); } if (healthy) { log("Storage consistency checks passed"); + track("Consistency checks passed"); } else { error("Storage consistency checks failed"); + track("Consistency checks failed"); } }