4chan-XZ/src/Posting/Captcha.js
Lalle 27d267b4f0
Revert "Typescript and more"
This reverts commit 12483e97c52eb96965811a8e6c4c28cd3c45b19f.
2023-04-16 21:54:11 +02:00

513 lines
13 KiB
JavaScript

import $ from '../platform/$'
import CaptchaReplace from './Captcha.replace'
import CaptchaT from './Captcha.t'
import meta from '../../package.json'
import Main from '../main/Main'
import Keybinds from '../Miscellaneous/Keybinds'
import $$ from '../platform/$$'
import QR from './QR'
import { Conf, d } from '../globals/globals'
import { MINUTE, SECOND } from '../platform/helpers'
const Captcha = {
Cache: {
init() {
$.on(d, 'SaveCaptcha', (e) => {
return this.saveAPI(e.detail)
})
return $.on(d, 'NoCaptcha', (e) => {
return this.noCaptcha(e.detail)
})
},
captchas: [],
getCount() {
return this.captchas.length
},
neededRaw() {
return (
!(
this.haveCookie() ||
this.captchas.length ||
QR.req ||
this.submitCB
) &&
(QR.posts.length > 1 ||
Conf['Auto-load captcha'] ||
!QR.posts[0].isOnlyQuotes() ||
QR.posts[0].file)
)
},
needed() {
return this.neededRaw() && $.event('LoadCaptcha')
},
prerequest() {
if (!Conf['Prerequest Captcha']) {
return
}
// Post count temporarily off by 1 when called from QR.post.rm, QR.close, or QR.submit
return $.queueTask(() => {
if (
!this.prerequested &&
this.neededRaw() &&
!$.event('LoadCaptcha') &&
!QR.captcha.occupied() &&
QR.cooldown.seconds <= 60 &&
QR.selected === QR.posts[QR.posts.length - 1] &&
!QR.selected.isOnlyQuotes()
) {
const isReply = QR.selected.thread !== 'new'
if (!$.event('RequestCaptcha', { isReply })) {
this.prerequested = true
this.submitCB = (captcha) => {
if (captcha) {
return this.save(captcha)
}
}
return this.updateCount()
}
}
})
},
haveCookie() {
return /\b_ct=/.test(d.cookie) && QR.posts[0].thread !== 'new'
},
getOne() {
let captcha
delete this.prerequested
this.clear()
if ((captcha = this.captchas.shift())) {
this.count()
return captcha
} else {
return null
}
},
request(isReply) {
if (!this.submitCB) {
if ($.event('RequestCaptcha', { isReply })) {
return
}
}
return (cb) => {
this.submitCB = cb
return this.updateCount()
}
},
abort() {
if (this.submitCB) {
delete this.submitCB
$.event('AbortCaptcha')
return this.updateCount()
}
},
saveAPI(captcha) {
let cb
if ((cb = this.submitCB)) {
delete this.submitCB
cb(captcha)
return this.updateCount()
} else {
return this.save(captcha)
}
},
noCaptcha(detail) {
let cb
if ((cb = this.submitCB)) {
if (!this.haveCookie() || detail?.error) {
QR.error(detail?.error || 'Failed to retrieve captcha.')
QR.captcha.setup(d.activeElement === QR.nodes.status)
}
delete this.submitCB
cb()
return this.updateCount()
}
},
save(captcha) {
let cb
if ((cb = this.submitCB)) {
this.abort()
cb(captcha)
return
}
this.captchas.push(captcha)
this.captchas.sort((a, b) => a.timeout - b.timeout)
return this.count()
},
clear() {
if (this.captchas.length) {
let i
const now = Date.now()
for (i = 0; i < this.captchas.length; i++) {
var captcha = this.captchas[i]
if (captcha.timeout > now) {
break
}
}
if (i) {
this.captchas = this.captchas.slice(i)
return this.count()
}
}
},
count() {
clearTimeout(this.timer)
if (this.captchas.length) {
this.timer = setTimeout(
this.clear.bind(this),
this.captchas[0].timeout - Date.now(),
)
}
return this.updateCount()
},
updateCount() {
return $.event('CaptchaCount', this.captchas.length)
},
},
Replace: CaptchaReplace,
t: CaptchaT,
v2: {
lifetime: 2 * MINUTE,
init() {
if (d.cookie.indexOf('pass_enabled=1') >= 0) {
return
}
if (
!(this.isEnabled =
!!$('#g-recaptcha, #captcha-forced-noscript') || !$.id('postForm'))
) {
return
}
if ((this.noscript = Conf['Force Noscript Captcha'] || !Main.jsEnabled)) {
$.addClass(QR.nodes.el, 'noscript-captcha')
}
Captcha.cache.init()
$.on(d, 'CaptchaCount', this.count.bind(this))
const root = $.el('div', { className: 'captcha-root' })
$.extend(root, {
innerHTML:
'<div class="captcha-counter"><a href="javascript:;"></a></div>',
})
const counter = $('.captcha-counter > a', root)
this.nodes = { root, counter }
this.count()
$.addClass(QR.nodes.el, 'has-captcha', 'captcha-v2')
$.after(QR.nodes.com.parentNode, root)
$.on(counter, 'click', this.toggle.bind(this))
$.on(counter, 'keydown', (e) => {
if (Keybinds.keyCode(e) !== 'Space') {
return
}
this.toggle()
e.preventDefault()
return e.stopPropagation()
})
return $.on(window, 'captcha:success', () => {
// XXX Greasemonkey 1.x workaround to gain access to GM_* functions.
return $.queueTask(() => this.save(false))
})
},
timeouts: {},
prevNeeded: 0,
noscriptURL() {
let lang
let url = `https://www.google.com/recaptcha/api/fallback?k=${meta.recaptchaKey}`
if ((lang = Conf['captchaLanguage'].trim())) {
url += `&hl=${encodeURIComponent(lang)}`
}
return url
},
moreNeeded() {
// Post count temporarily off by 1 when called from QR.post.rm, QR.close, or QR.submit
return $.queueTask(() => {
const needed = Captcha.cache.needed()
if (needed && !this.prevNeeded) {
this.setup(QR.cooldown.auto && d.activeElement === QR.nodes.status)
}
return (this.prevNeeded = needed)
})
},
toggle() {
if (this.nodes.container && !this.timeouts.destroy) {
return this.destroy()
} else {
return this.setup(true, true)
}
},
setup(focus, force) {
if (!this.isEnabled || (!Captcha.cache.needed() && !force)) {
return
}
if (focus) {
$.addClass(QR.nodes.el, 'focus')
this.nodes.counter.focus()
}
if (this.timeouts.destroy) {
clearTimeout(this.timeouts.destroy)
delete this.timeouts.destroy
return this.reload()
}
if (this.nodes.container) {
// XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1226835
$.queueTask(() => {
let iframe
if (
this.nodes.container &&
d.activeElement === this.nodes.counter &&
(iframe = $(
'iframe[src^="https://www.google.com/recaptcha/"]',
this.nodes.container,
))
) {
iframe.focus()
return QR.focus()
}
}) // Event handler not fired in Firefox
return
}
this.nodes.container = $.el('div', { className: 'captcha-container' })
$.prepend(this.nodes.root, this.nodes.container)
new MutationObserver(this.afterSetup.bind(this)).observe(
this.nodes.container,
{
childList: true,
subtree: true,
},
)
if (this.noscript) {
return this.setupNoscript()
} else {
return this.setupJS()
}
},
setupNoscript() {
const iframe = $.el('iframe', {
id: 'qr-captcha-iframe',
scrolling: 'no',
src: this.noscriptURL(),
})
const div = $.el('div')
const textarea = $.el('textarea')
$.add(div, textarea)
return $.add(this.nodes.container, [iframe, div])
},
setupJS() {
return $.global(function () {
const render = function () {
const { classList } = document.documentElement
const container = document.querySelector('#qr .captcha-container')
return (container.dataset.widgetID = window.grecaptcha.render(
container,
{
sitekey: meta.recaptchaKey,
theme:
classList.contains('tomorrow') ||
classList.contains('spooky') ||
classList.contains('dark-captcha')
? 'dark'
: 'light',
callback(response) {
return window.dispatchEvent(
new CustomEvent('captcha:success', { detail: response }),
)
},
},
))
}
if (window.grecaptcha) {
return render()
} else {
const cbNative = window.onRecaptchaLoaded
window.onRecaptchaLoaded = function () {
render()
return cbNative()
}
if (
!document.head.querySelector(
'script[src^="https://www.google.com/recaptcha/api.js"]',
)
) {
const script = document.createElement('script')
script.src =
'https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoaded&render=explicit'
return document.head.appendChild(script)
}
}
})
},
afterSetup(mutations) {
for (var mutation of mutations) {
for (var node of mutation.addedNodes) {
var iframe, textarea
if (
(iframe = $.x(
'./descendant-or-self::iframe[starts-with(@src, "https://www.google.com/recaptcha/")]',
node,
))
) {
this.setupIFrame(iframe)
}
if ((textarea = $.x('./descendant-or-self::textarea', node))) {
this.setupTextArea(textarea)
}
}
}
},
setupIFrame(iframe) {
let needle
if (!doc.contains(iframe)) {
return
}
Captcha.replace.iframe(iframe)
$.addClass(QR.nodes.el, 'captcha-open')
this.fixQRPosition()
$.on(iframe, 'load', this.fixQRPosition)
if (d.activeElement === this.nodes.counter) {
iframe.focus()
}
// XXX Make sure scroll on space prevention (see src/css/style.css) doesn't cause scrolling of div
if (
['blink', 'edge'].includes($.engine) &&
((needle = iframe.parentNode),
$$('#qr .captcha-container > div > div:first-of-type').includes(needle))
) {
return $.on(iframe.parentNode, 'scroll', function () {
return (this.scrollTop = 0)
})
}
},
fixQRPosition() {
if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) {
QR.nodes.el.style.top = ''
return (QR.nodes.el.style.bottom = '0px')
}
},
setupTextArea(textarea) {
return $.one(textarea, 'input', () => this.save(true))
},
destroy() {
if (!this.isEnabled) {
return
}
delete this.timeouts.destroy
$.rmClass(QR.nodes.el, 'captcha-open')
if (this.nodes.container) {
$.global(function () {
const container = document.querySelector('#qr .captcha-container')
return window.grecaptcha.reset(container.dataset.widgetID)
})
$.rm(this.nodes.container)
return delete this.nodes.container
}
},
getOne(isReply) {
return Captcha.cache.getOne(isReply)
},
save(pasted, token) {
Captcha.cache.save({
response: token || $('textarea', this.nodes.container).value,
timeout: Date.now() + this.lifetime,
})
const focus =
d.activeElement?.nodeName === 'IFRAME' &&
/https?:\/\/www\.google\.com\/recaptcha\//.test(d.activeElement.src)
if (Captcha.cache.needed()) {
if (focus) {
if (QR.cooldown.auto || Conf['Post on Captcha Completion']) {
this.nodes.counter.focus()
} else {
QR.nodes.status.focus()
}
}
this.reload()
} else {
if (pasted) {
this.destroy()
} else {
if (this.timeouts.destroy == null) {
this.timeouts.destroy = setTimeout(
this.destroy.bind(this),
3 * SECOND,
)
}
}
if (focus) {
QR.nodes.status.focus()
}
}
if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) {
return QR.submit()
}
},
count() {
const count = Captcha.cache.getCount()
const loading = Captcha.cache.submitCB ? '...' : ''
this.nodes.counter.textContent = `Captchas: ${count}${loading}`
return this.moreNeeded()
},
reload() {
if (
$(
'iframe[src^="https://www.google.com/recaptcha/api/fallback?"]',
this.nodes.container,
)
) {
this.destroy()
return this.setup(false, true)
} else {
return $.global(function () {
const container = document.querySelector('#qr .captcha-container')
return window.grecaptcha.reset(container.dataset.widgetID)
})
}
},
occupied() {
return !!this.nodes.container && !this.timeouts.destroy
},
},
}
export default Captcha