Merge pull request #6401 from Palid/fix/15142/fix-grecaptcha-race-condition

pull/21833/head
Michael Telatynski 2021-07-20 16:38:19 +01:00 committed by GitHub
commit 91cf27e252
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 56 additions and 32 deletions

View File

@ -92,6 +92,7 @@ declare global {
mxUIStore: UIStore; mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore; mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore; mxRoomScrollStateStore?: RoomScrollStateStore;
mxOnRecaptchaLoaded?: () => void;
} }
interface Document { interface Document {
@ -116,7 +117,7 @@ declare global {
} }
interface StorageEstimate { interface StorageEstimate {
usageDetails?: {[key: string]: number}; usageDetails?: { [key: string]: number };
} }
interface HTMLAudioElement { interface HTMLAudioElement {
@ -187,6 +188,21 @@ declare global {
parameterDescriptors?: AudioParamDescriptor[]; parameterDescriptors?: AudioParamDescriptor[];
} }
); );
// eslint-disable-next-line no-var
var grecaptcha:
| undefined
| {
reset: (id: string) => void;
render: (
divId: string,
options: {
sitekey: string;
callback: (response: string) => void;
},
) => string;
isReady: () => boolean;
};
} }
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */

View File

@ -15,66 +15,74 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
const DIV_ID = 'mx_recaptcha'; const DIV_ID = 'mx_recaptcha';
interface ICaptchaFormProps {
sitePublicKey: string;
onCaptchaResponse: (response: string) => void;
}
interface ICaptchaFormState {
errorText?: string;
}
/** /**
* A pure UI component which displays a captcha form. * A pure UI component which displays a captcha form.
*/ */
@replaceableComponent("views.auth.CaptchaForm") @replaceableComponent("views.auth.CaptchaForm")
export default class CaptchaForm extends React.Component { export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICaptchaFormState> {
static propTypes = {
sitePublicKey: PropTypes.string,
// called with the captcha response
onCaptchaResponse: PropTypes.func,
};
static defaultProps = { static defaultProps = {
onCaptchaResponse: () => {}, onCaptchaResponse: () => {},
}; };
constructor(props) { private captchaWidgetId?: string;
private recaptchaContainer = createRef<HTMLDivElement>();
constructor(props: ICaptchaFormProps) {
super(props); super(props);
this.state = { this.state = {
errorText: null, errorText: undefined,
}; };
this._captchaWidgetId = null;
this._recaptchaContainer = createRef();
CountlyAnalytics.instance.track("onboarding_grecaptcha_begin"); CountlyAnalytics.instance.track("onboarding_grecaptcha_begin");
} }
componentDidMount() { componentDidMount() {
// Just putting a script tag into the returned jsx doesn't work, annoyingly, // Just putting a script tag into the returned jsx doesn't work, annoyingly,
// so we do this instead. // so we do this instead.
if (global.grecaptcha) { if (this.isRecaptchaReady()) {
// already loaded // already loaded
this._onCaptchaLoaded(); this.onCaptchaLoaded();
} else { } else {
console.log("Loading recaptcha script..."); console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();}; window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); };
const scriptTag = document.createElement('script'); const scriptTag = document.createElement('script');
scriptTag.setAttribute( scriptTag.setAttribute(
'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`, 'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit`,
); );
this._recaptchaContainer.current.appendChild(scriptTag); this.recaptchaContainer.current.appendChild(scriptTag);
} }
} }
componentWillUnmount() { componentWillUnmount() {
this._resetRecaptcha(); this.resetRecaptcha();
} }
_renderRecaptcha(divId) { // Borrowed directly from: https://github.com/codeep/react-recaptcha-google/commit/e118fa5670fa268426969323b2e7fe77698376ba
if (!global.grecaptcha) { private isRecaptchaReady(): boolean {
return typeof window !== "undefined" &&
typeof global.grecaptcha !== "undefined" &&
typeof global.grecaptcha.render === 'function';
}
private renderRecaptcha(divId: string) {
if (!this.isRecaptchaReady()) {
console.error("grecaptcha not loaded!"); console.error("grecaptcha not loaded!");
throw new Error("Recaptcha did not load successfully"); throw new Error("Recaptcha did not load successfully");
} }
@ -84,26 +92,26 @@ export default class CaptchaForm extends React.Component {
console.error("No public key for recaptcha!"); console.error("No public key for recaptcha!");
throw new Error( throw new Error(
"This server has not supplied enough information for Recaptcha " "This server has not supplied enough information for Recaptcha "
+ "authentication"); + "authentication");
} }
console.info("Rendering to %s", divId); console.info("Rendering to %s", divId);
this._captchaWidgetId = global.grecaptcha.render(divId, { this.captchaWidgetId = global.grecaptcha.render(divId, {
sitekey: publicKey, sitekey: publicKey,
callback: this.props.onCaptchaResponse, callback: this.props.onCaptchaResponse,
}); });
} }
_resetRecaptcha() { private resetRecaptcha() {
if (this._captchaWidgetId !== null) { if (this.captchaWidgetId !== null) {
global.grecaptcha.reset(this._captchaWidgetId); global.grecaptcha.reset(this.captchaWidgetId);
} }
} }
_onCaptchaLoaded() { private onCaptchaLoaded() {
console.log("Loaded recaptcha script."); console.log("Loaded recaptcha script.");
try { try {
this._renderRecaptcha(DIV_ID); this.renderRecaptcha(DIV_ID);
// clear error if re-rendered // clear error if re-rendered
this.setState({ this.setState({
errorText: null, errorText: null,
@ -128,7 +136,7 @@ export default class CaptchaForm extends React.Component {
} }
return ( return (
<div ref={this._recaptchaContainer}> <div ref={this.recaptchaContainer}>
<p>{ _t( <p>{ _t(
"This homeserver would like to make sure you are not a robot.", "This homeserver would like to make sure you are not a robot.",
) }</p> ) }</p>