From 6a9e6db3bea4a9f3efc0ef0f527dee8ba5cd977f Mon Sep 17 00:00:00 2001 From: zbettenbuk Date: Fri, 26 Jan 2018 11:19:43 +0100 Subject: [PATCH] [RN] Validate the URL in app-settings --- lang/main.json | 3 + .../components/AbstractAppSettings.js | 22 ++-- .../components/AppSettings.native.js | 118 +++++++++++++++++- .../app-settings/components/FormRow.native.js | 2 +- .../components/FormSectionHeader.native.js | 2 +- react/features/app-settings/functions.js | 37 ++++++ .../features/app-settings/functions.native.js | 22 ---- react/features/app-settings/functions.web.js | 0 react/features/base/react/functions.native.js | 18 +++ 9 files changed, 180 insertions(+), 44 deletions(-) create mode 100644 react/features/app-settings/functions.js delete mode 100644 react/features/app-settings/functions.native.js delete mode 100644 react/features/app-settings/functions.web.js diff --git a/lang/main.json b/lang/main.json index f052bced7..2e6a815e1 100644 --- a/lang/main.json +++ b/lang/main.json @@ -539,6 +539,9 @@ "tooltip": "Get access info about the meeting" }, "profileModal": { + "alertOk": "OK", + "alertTitle": "Warning", + "alertURLText": "The entered server URL is invalid", "conferenceSection": "Conference", "displayName": "Display name", "email": "Email", diff --git a/react/features/app-settings/components/AbstractAppSettings.js b/react/features/app-settings/components/AbstractAppSettings.js index ffc66a8ab..6a425377c 100644 --- a/react/features/app-settings/components/AbstractAppSettings.js +++ b/react/features/app-settings/components/AbstractAppSettings.js @@ -4,8 +4,6 @@ import { Component } from 'react'; import { getProfile, updateProfile } from '../../base/profile'; -import { hideAppSettings } from '../actions'; - /** * The type of the React {@code Component} props of {@link AbstractAppSettings} */ @@ -34,7 +32,12 @@ type Props = { /** * Redux store dispatch function. */ - dispatch: Dispatch<*> + dispatch: Dispatch<*>, + + /** + * The i18n translate function. + */ + t: Function }; /** @@ -44,6 +47,7 @@ type Props = { * @abstract */ export class AbstractAppSettings extends Component { + /** * Initializes a new {@code AbstractAppSettings} instance. * @@ -56,7 +60,6 @@ export class AbstractAppSettings extends Component { this._onChangeDisplayName = this._onChangeDisplayName.bind(this); this._onChangeEmail = this._onChangeEmail.bind(this); this._onChangeServerURL = this._onChangeServerURL.bind(this); - this._onRequestClose = this._onRequestClose.bind(this); this._onStartAudioMutedChange = this._onStartAudioMutedChange.bind(this); this._onStartVideoMutedChange @@ -108,17 +111,6 @@ export class AbstractAppSettings extends Component { }); } - _onRequestClose: () => void; - - /** - * Handles the back button. - * - * @returns {void} - */ - _onRequestClose() { - this.props.dispatch(hideAppSettings()); - } - _onStartAudioMutedChange: (boolean) => void; /** diff --git a/react/features/app-settings/components/AppSettings.native.js b/react/features/app-settings/components/AppSettings.native.js index db70b0322..1e07239b5 100644 --- a/react/features/app-settings/components/AppSettings.native.js +++ b/react/features/app-settings/components/AppSettings.native.js @@ -1,5 +1,8 @@ +// @flow + import React from 'react'; import { + Alert, Modal, ScrollView, Switch, @@ -11,13 +14,14 @@ import { connect } from 'react-redux'; import { ASPECT_RATIO_NARROW } from '../../base/aspect-ratio'; import { translate } from '../../base/i18n'; -import { isIPad } from '../../base/react'; +import { getSafetyOffset, isIPad } from '../../base/react'; import { _mapStateToProps, AbstractAppSettings } from './AbstractAppSettings'; -import BackButton from './BackButton'; -import FormRow from './FormRow'; -import FormSectionHeader from './FormSectionHeader'; -import { getSafetyOffset } from '../functions'; +import { hideAppSettings } from '../actions'; +import BackButton from './BackButton.native'; +import FormRow from './FormRow.native'; +import FormSectionHeader from './FormSectionHeader.native'; +import { normalizeUserInputURL } from '../functions'; import styles, { HEADER_PADDING } from './styles'; /** @@ -26,6 +30,8 @@ import styles, { HEADER_PADDING } from './styles'; * @extends AbstractAppSettings */ class AppSettings extends AbstractAppSettings { + _urlField: Object; + /** * Instantiates a new {@code AppSettings} instance. * @@ -35,6 +41,10 @@ class AppSettings extends AbstractAppSettings { super(props); this._getSafetyPadding = this._getSafetyPadding.bind(this); + this._onBlurServerURL = this._onBlurServerURL.bind(this); + this._onRequestClose = this._onRequestClose.bind(this); + this._setURLFieldReference = this._setURLFieldReference.bind(this); + this._showURLAlert = this._showURLAlert.bind(this); } /** @@ -97,8 +107,10 @@ class AppSettings extends AbstractAppSettings { i18nLabel = 'profileModal.serverURL' > Object; + /** * Calculates header safety padding for mobile devices. See comment in * functions.js. @@ -145,6 +159,100 @@ class AppSettings extends AbstractAppSettings { return undefined; } + + _onBlurServerURL: () => void; + + /** + * Handler the server URL lose focus event. Here we validate the server URL + * and update it to the normalized version, or show an error if incorrect. + * + * @private + * @returns {void} + */ + _onBlurServerURL() { + this._processServerURL(false /* hideOnSuccess */); + } + + _onChangeDisplayName: (string) => void; + + _onChangeEmail: (string) => void; + + _onChangeServerURL: (string) => void; + + _onStartAudioMutedChange: (boolean) => void; + + _onStartVideoMutedChange: (boolean) => void; + + _onRequestClose: () => void; + + /** + * Processes the server URL. It normalizes it and an error alert is + * displayed in case it's incorrect. + * + * @param {boolean} hideOnSuccess - True if the dialog should be hidden if + * normalization / validation succeeds, false otherwise. + * @private + * @returns {void} + */ + _processServerURL(hideOnSuccess: boolean) { + const { serverURL } = this.props._profile; + const normalizedURL = normalizeUserInputURL(serverURL); + + if (normalizedURL === null) { + this._showURLAlert(); + } else { + this._onChangeServerURL(normalizedURL); + if (hideOnSuccess) { + this.props.dispatch(hideAppSettings()); + } + } + } + + /** + * Handles the back button. + * Also invokes normalizeUserInputURL to validate the URL entered + * by the user. + * + * @returns {void} + */ + _onRequestClose() { + this._processServerURL(true /* hideOnSuccess */); + } + + _setURLFieldReference: (React$ElementRef<*> | null) => void; + + /** + * Stores a reference to the URL field for later use. + * + * @protected + * @param {Object} component - The field component. + * @returns {void} + */ + _setURLFieldReference(component) { + this._urlField = component; + } + + _showURLAlert: () => void; + + /** + * Shows an alert telling the user that the URL he/she entered was invalid. + * + * @returns {void} + */ + _showURLAlert() { + const { t } = this.props; + + Alert.alert( + t('profileModal.alertTitle'), + t('profileModal.alertURLText'), + [ + { + onPress: () => this._urlField.focus(), + text: t('profileModal.alertOk') + } + ] + ); + } } export default translate(connect(_mapStateToProps)(AppSettings)); diff --git a/react/features/app-settings/components/FormRow.native.js b/react/features/app-settings/components/FormRow.native.js index d72e6688f..e55ab5470 100644 --- a/react/features/app-settings/components/FormRow.native.js +++ b/react/features/app-settings/components/FormRow.native.js @@ -6,8 +6,8 @@ import { connect } from 'react-redux'; import { ASPECT_RATIO_WIDE } from '../../base/aspect-ratio'; import { translate } from '../../base/i18n'; +import { getSafetyOffset } from '../../base/react'; -import { getSafetyOffset } from '../functions'; import styles, { ANDROID_UNDERLINE_COLOR, CONTAINER_PADDING } from './styles'; /** diff --git a/react/features/app-settings/components/FormSectionHeader.native.js b/react/features/app-settings/components/FormSectionHeader.native.js index d76e89e38..077ede5f6 100644 --- a/react/features/app-settings/components/FormSectionHeader.native.js +++ b/react/features/app-settings/components/FormSectionHeader.native.js @@ -6,8 +6,8 @@ import { connect } from 'react-redux'; import { ASPECT_RATIO_WIDE } from '../../base/aspect-ratio'; import { translate } from '../../base/i18n'; +import { getSafetyOffset } from '../../base/react'; -import { getSafetyOffset } from '../functions'; import styles, { CONTAINER_PADDING } from './styles'; /** diff --git a/react/features/app-settings/functions.js b/react/features/app-settings/functions.js new file mode 100644 index 000000000..bd0191dcd --- /dev/null +++ b/react/features/app-settings/functions.js @@ -0,0 +1,37 @@ +// @flow + +import { parseStandardURIString } from '../base/util'; + +/** + * Normalizes a URL entered by the user. + * FIXME: Consider adding this to base/util/uri. + * + * @param {string} url - The URL to validate. + * @returns {string|null} - The normalized URL, or null if the URL is invalid. + */ +export function normalizeUserInputURL(url: string) { + /* eslint-disable no-param-reassign */ + + if (url) { + url = url.replace(/\s/g, '').toLowerCase(); + const urlRegExp = new RegExp('^(\\w+://)?(.+)$'); + const urlComponents = urlRegExp.exec(url); + + if (!urlComponents[1] || !urlComponents[1].startsWith('http')) { + url = `https://${urlComponents[2]}`; + } + + const parsedURI + = parseStandardURIString(url); + + if (!parsedURI.host) { + return null; + } + + return parsedURI.toString(); + } + + return url; + + /* eslint-enable no-param-reassign */ +} diff --git a/react/features/app-settings/functions.native.js b/react/features/app-settings/functions.native.js deleted file mode 100644 index 18486758c..000000000 --- a/react/features/app-settings/functions.native.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow - -import { isIPhoneX, Platform } from '../base/react'; - -const IPHONE_OFFSET = 20; -const IPHONEX_OFFSET = 44; - -/** - * Determines the offset to be used for the device. This uses a custom - * implementation to minimize empty area around screen, especially on iPhone X. - * - * @returns {number} - */ -export function getSafetyOffset() { - if (Platform.OS === 'android') { - // Android doesn't need offset, except the Essential phone. Should be - // addressed later with a generic solution. - return 0; - } - - return isIPhoneX() ? IPHONEX_OFFSET : IPHONE_OFFSET; -} diff --git a/react/features/app-settings/functions.web.js b/react/features/app-settings/functions.web.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/base/react/functions.native.js b/react/features/base/react/functions.native.js index df4f321eb..63b99dd83 100644 --- a/react/features/base/react/functions.native.js +++ b/react/features/base/react/functions.native.js @@ -6,6 +6,24 @@ import Platform from './Platform'; const IPHONEX_HEIGHT = 812; const IPHONEX_WIDTH = 375; +const IPHONE_OFFSET = 20; +const IPHONEX_OFFSET = 44; + +/** + * Determines the offset to be used for the device. This uses a custom + * implementation to minimize empty area around screen, especially on iPhone X. + * + * @returns {number} + */ +export function getSafetyOffset() { + if (Platform.OS === 'android') { + // Android doesn't need offset, except the Essential phone. Should be + // addressed later with a generic solution. + return 0; + } + + return isIPhoneX() ? IPHONEX_OFFSET : IPHONE_OFFSET; +} /** * Determines if the device is an iPad or not.