| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- import 'abort-controller/polyfill'
- import { postJSON } from '../../infrastructure/fetch-json'
- const grecaptcha = window.grecaptcha
- let recaptchaId, canResetCaptcha, isFromReset, resetFailed
- const recaptchaCallbacks = []
- function resetCaptcha() {
- if (!canResetCaptcha) return
- canResetCaptcha = false
- isFromReset = true
- grecaptcha.reset(recaptchaId)
- }
- function handleAbortedCaptcha() {
- if (recaptchaCallbacks.length > 0) {
- // There is a pending captcha process and the user dismissed it by
- // clicking somewhere else on the page. Show it again.
- // But first clear the timeout to give the user more time to solve the
- // next one.
- recaptchaCallbacks.forEach(({ resetTimeout }) => resetTimeout())
- validateCaptchaV2().catch(() => {
- // The other callback is still there to pick up the result
- })
- }
- }
- function emitToken(token) {
- recaptchaCallbacks.splice(0).forEach(({ resolve, resetTimeout }) => {
- resetTimeout()
- resolve(token)
- })
- // Happy path, let the user solve another one -- if needed.
- canResetCaptcha = true
- resetCaptcha()
- }
- function getMessage(err) {
- return (err && err.message) || 'no details returned'
- }
- function emitError(err, src) {
- if (isFromReset) {
- resetFailed = true
- }
- err = new Error(
- `captcha check failed: ${getMessage(err)}, please retry again`
- )
- // Keep a record of this error. 2nd line might request a screenshot of it.
- console.error(err, src)
- recaptchaCallbacks.splice(0).forEach(({ reject, resetTimeout }) => {
- resetTimeout()
- reject(err)
- })
- // Unhappy path: Only reset if not failed before.
- // This could be a loop without human interaction: error -> reset -> error.
- resetCaptcha()
- }
- export async function canSkipCaptcha(email) {
- let timer
- let canSkip
- try {
- const controller = new AbortController()
- const signal = controller.signal
- timer = setTimeout(() => {
- controller.abort()
- }, 1000)
- canSkip = await postJSON('/login/can-skip-captcha', {
- signal,
- body: { email },
- swallowAbortError: false,
- })
- } catch (e) {
- canSkip = false
- } finally {
- clearTimeout(timer)
- }
- return canSkip
- }
- export async function validateCaptchaV2() {
- if (
- // Detect blocked recaptcha
- typeof grecaptcha === 'undefined' ||
- // Detect stubbed recaptcha
- typeof grecaptcha.render !== 'function' ||
- typeof grecaptcha.execute !== 'function' ||
- typeof grecaptcha.reset !== 'function'
- ) {
- return
- }
- if (recaptchaId === undefined) {
- const el = document.getElementById('recaptcha')
- recaptchaId = grecaptcha.render(el, {
- callback: token => {
- emitToken(token)
- },
- 'error-callback': () => {
- emitError(
- new Error('recaptcha: something went wrong'),
- 'error-callback'
- )
- },
- 'expired-callback': () => {
- emitError(new Error('recaptcha: challenge expired'), 'expired-callback')
- },
- })
- // Attach abort handler once when setting up the captcha.
- document
- .querySelector('.content')
- .addEventListener('click', handleAbortedCaptcha)
- }
- if (resetFailed) {
- throw new Error('captcha not available. try reloading the page')
- }
- // This is likely a human making a submit action. Let them retry on error.
- canResetCaptcha = true
- isFromReset = false
- return await new Promise((resolve, reject) => {
- const timeout = setTimeout(() => {
- // We triggered this error. Ensure that we can reset to captcha.
- canResetCaptcha = true
- emitError(new Error('challenge expired'), 'timeout')
- // The iframe title says it will expire after 2 min. Enforce that here.
- }, 120 * 1000)
- recaptchaCallbacks.push({
- resolve,
- reject,
- resetTimeout: () => clearTimeout(timeout),
- })
- try {
- grecaptcha.execute(recaptchaId).catch(err => {
- emitError(new Error(`recaptcha: ${getMessage(err)}`), '.catch()')
- })
- } catch (err) {
- emitError(new Error(`recaptcha: ${getMessage(err)}`), 'try/catch')
- }
- // Try to (re-)attach a handler to the backdrop element of the popup.
- for (const delay of [1, 10, 100, 1000]) {
- setTimeout(() => {
- const el = document.body.lastChild
- if (el.tagName !== 'DIV') return
- el.removeEventListener('click', handleAbortedCaptcha)
- el.addEventListener('click', handleAbortedCaptcha)
- }, delay)
- }
- })
- }
|