4chan-x/src/Posting/Captcha.v2.coffee
ccd0 2d390d0a0f Fix reloading for new way of doing noscript captcha.
Was broken from 210b6bd20161286ace683ad1590a8df637f60de7.
2015-11-20 13:04:26 -08:00

252 lines
7.8 KiB
CoffeeScript

Captcha.v2 =
lifetime: 2 * $.MINUTE
init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0
return unless (@isEnabled = !!$ '#g-recaptcha, #captchaContainerAlt, #captcha-forced-noscript')
if (@noscript = Conf['Force Noscript Captcha'] or not Main.jsEnabled)
$.addClass QR.nodes.el, 'noscript-captcha'
@captchas = []
$.get 'captchas', [], ({captchas}) ->
QR.captcha.sync captchas
$.sync 'captchas', @sync.bind @
root = $.el 'div', className: 'captcha-root'
$.extend root, <%= html(
'<div class="captcha-counter"><a href="javascript:;"></a></div>'
) %>
counter = $ '.captcha-counter > a', root
@nodes = {root, counter}
@count()
$.addClass QR.nodes.el, 'has-captcha', 'captcha-v2'
$.after QR.nodes.com.parentNode, root
$.on counter, 'click', @toggle.bind @
$.on counter, 'keydown', (e) =>
return unless Keybinds.keyCode(e) is 'Space'
@toggle()
e.preventDefault()
e.stopPropagation()
$.on window, 'captcha:success', =>
# XXX Greasemonkey 1.x workaround to gain access to GM_* functions.
$.queueTask => @save false
new MutationObserver(@watchBubbles.bind @).observe d.body,
childList: true
timeouts: {}
postsCount: 0
noscriptURL: ->
url = 'https://www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
if (lang = Conf['captchaLanguage'].trim())
url += "&hl=#{encodeURIComponent lang}"
url
needed: ->
captchaCount = @captchas.length
captchaCount++ if QR.req
@postsCount = QR.posts.length
@postsCount = 0 if @postsCount is 1 and !Conf['Auto-load captcha'] and !QR.posts[0].com and !QR.posts[0].file
captchaCount < @postsCount
onNewPost: ->
@setup()
onPostChange: ->
@setup() if @postsCount is 0
@postsCount = 0 if QR.posts.length is 1 and !Conf['Auto-load captcha'] and !QR.posts[0].com and !QR.posts[0].file
toggle: ->
if @nodes.container and !@timeouts.destroy
@destroy()
else
@setup true, true
setup: (focus, force) ->
return unless @isEnabled and (@needed() or force)
@nodes.counter.focus() if focus and d.activeElement in [QR.nodes.status, d.body]
if @timeouts.destroy
clearTimeout @timeouts.destroy
delete @timeouts.destroy
return @reload()
if @nodes.container
if d.activeElement is @nodes.counter and (iframe = $ 'iframe', @nodes.container)
iframe.focus()
QR.focus() # Event handler not fired in Firefox
return
@nodes.container = $.el 'div', className: 'captcha-container'
$.prepend @nodes.root, @nodes.container
new MutationObserver(@afterSetup.bind @).observe @nodes.container,
childList: true
subtree: true
if @noscript
@setupNoscript()
else
@setupJS()
setupNoscript: ->
iframe = $.el 'iframe',
id: 'qr-captcha-iframe'
src: @noscriptURL()
div = $.el 'div'
textarea = $.el 'textarea'
$.add div, textarea
$.add @nodes.container, [iframe, div]
setupJS: ->
$.globalEval '''
(function() {
function render() {
var container = document.querySelector("#qr .captcha-container");
container.dataset.widgetID = window.grecaptcha.render(container, {
sitekey: '<%= meta.recaptchaKey %>',
theme: document.documentElement.classList.contains('tomorrow') ? 'dark' : 'light',
callback: function(response) {
window.dispatchEvent(new CustomEvent("captcha:success", {detail: response}));
}
});
}
if (window.grecaptcha) {
render();
} else {
var cbNative = window.onRecaptchaLoaded;
window.onRecaptchaLoaded = function() {
render();
cbNative();
}
}
})();
'''
afterSetup: (mutations) ->
for mutation in mutations
for node in mutation.addedNodes
@setupIFrame iframe if (iframe = $.x './descendant-or-self::iframe', node)
@setupTextArea textarea if (textarea = $.x './descendant-or-self::textarea', node)
return
setupIFrame: (iframe) ->
Captcha.replace.iframe iframe
$.addClass QR.nodes.el, 'captcha-open'
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
QR.nodes.el.style.top = null
QR.nodes.el.style.bottom = '0px'
iframe.focus() if d.activeElement is @nodes.counter
# XXX Stop Recaptcha from changing focus from iframe -> body -> iframe on submit.
$.global ->
f = document.querySelector('#qr iframe')
f.focus = f.blur = ->
setupTextArea: (textarea) ->
$.one textarea, 'input', => @save true
destroy: ->
return unless @isEnabled
delete @timeouts.destroy
$.rmClass QR.nodes.el, 'captcha-open'
$.rm @nodes.container if @nodes.container
delete @nodes.container
# Clean up abandoned iframes.
garbage = $.X '//iframe[starts-with(@src, "https://www.google.com/recaptcha/api2/frame")]/ancestor-or-self::*[parent::body]'
i = 0
while node = garbage.snapshotItem i++
$.rm ins if (ins = node.nextSibling)?.nodeName is 'INS'
$.rm node
return
sync: (captchas=[]) ->
@captchas = captchas
@clear()
@count()
getOne: ->
@clear()
if (captcha = @captchas.shift())
$.set 'captchas', @captchas
@count()
captcha
else
null
save: (pasted, token) ->
$.forceSync 'captchas'
@captchas.push
response: token or $('textarea', @nodes.container).value
timeout: Date.now() + @lifetime
@captchas.sort (a, b) -> a.timeout - b.timeout
$.set 'captchas', @captchas
@count()
focus = d.activeElement?.nodeName is 'IFRAME' and /https?:\/\/www\.google\.com\/recaptcha\//.test(d.activeElement.src)
if @needed()
if focus
if QR.cooldown.auto or Conf['Post on Captcha Completion']
@nodes.counter.focus()
else
QR.nodes.status.focus()
@reload()
else
if pasted
@destroy()
else
@timeouts.destroy ?= setTimeout @destroy.bind(@), 3 * $.SECOND
QR.nodes.status.focus() if focus
QR.submit() if Conf['Post on Captcha Completion'] and !QR.cooldown.auto
clear: ->
return unless @captchas.length
$.forceSync 'captchas'
now = Date.now()
for captcha, i in @captchas
break if captcha.timeout > now
return unless i
@captchas = @captchas[i..]
@count()
$.set 'captchas', @captchas
@setup true
count: ->
@nodes.counter.textContent = "Captchas: #{@captchas.length}"
clearTimeout @timeouts.clear
if @captchas.length
@timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now()
reload: ->
if $ 'iframe[src^="https://www.google.com/recaptcha/api/fallback?"]', @nodes.container
@destroy()
@setup false, true
else
$.globalEval '''
(function() {
var container = document.querySelector("#qr .captcha-container");
window.grecaptcha.reset(container.dataset.widgetID);
})();
'''
watchBubbles: (mutations) ->
for mutation in mutations
for node in mutation.addedNodes
if $ 'iframe[src^="https://www.google.com/recaptcha/api2/frame"]', node
new MutationObserver(@fixBubble.bind(@, node)).observe node,
attributes: true
# Prevent Recaptcha from keeping focus when popup dismissed.
if (overlay = $ 'div[style*="position: fixed;"]', node)
$.on overlay, 'click', -> $('#qr iframe')?.blur()
fixBubble: (node) ->
{bottom, right, width} = node.getBoundingClientRect()
if (
getComputedStyle(node).visibility isnt 'hidden' and
bottom > 0 and # not deliberately offscreen
right > doc.clientWidth # offscreen to right
)
qrLeft = QR.nodes.el.getBoundingClientRect().left
newLeft = Math.max 0, qrLeft - width
node.style.left = "#{newLeft}px"