mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-24 21:12:38 -04:00
1f4c8d4ed9
[web] double down on error handling in captcha process GitOrigin-RevId: 91692978a24b02e9fa1ca55193a462ca29f68a7e
161 lines
4.3 KiB
JavaScript
161 lines
4.3 KiB
JavaScript
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)
|
|
}
|
|
})
|
|
}
|