diff --git a/package.json b/package.json
index 6a95375e8..d71f7d073 100755
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"oldVersions": "https://raw.githubusercontent.com/ccd0/4chan-x/",
"faq": "https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions",
"appid": "lacclbnghgdicfifcamcmcnilckjamag",
+ "recaptchaKey": "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc",
"youtubeAPIKey": "AIzaSyB5_zaen_-46Uhz1xGR-lz1YoUMHqCD6CE",
"buildsPath": "builds/",
"mainBranch": "master",
@@ -67,4 +68,4 @@
"engines": {
"node": ">=0.10"
}
-}
\ No newline at end of file
+}
diff --git a/src/General/Config.coffee b/src/General/Config.coffee
index 4b2479d19..790775c22 100755
--- a/src/General/Config.coffee
+++ b/src/General/Config.coffee
@@ -373,7 +373,7 @@ Config =
]
'Auto-load captcha': [
false
- 'Automatically load the captcha when you open a thread, and reload it after you post.'
+ 'Automatically load the captcha in the QR even if your post is empty.'
]
'Bottom QR Link': [
true
diff --git a/src/General/css/style.css b/src/General/css/style.css
index 64af98308..1fd43e5f1 100755
--- a/src/General/css/style.css
+++ b/src/General/css/style.css
@@ -10,7 +10,6 @@
#thread-watcher {
box-shadow: -1px 2px 2px rgba(0, 0, 0, 0.25);
}
-.captcha-img,
.field {
background-color: #FFF;
border: 1px solid #CCC;
@@ -28,7 +27,6 @@
font-size: 13px !important;
opacity: 1.0 !important;
}
-.captch-img:hover,
.field:hover {
border-color: #999;
}
@@ -947,7 +945,7 @@ span.hide-announcement {
/* QR */
:root.hide-original-post-form #togglePostFormLink,
:root:not(.catalog) #togglePostFormLink,
-#qr.autohide:not(.focus):not(:hover):not(:active) > form,
+#qr.autohide:not(.focus):not(:hover):not(:active):not(.captcha-open) > form,
:root.thread-view #qr:not(.show-new-thread-option) select[data-name="thread"],
#file-n-submit:not(.has-file) #qr-filerm {
display: none;
@@ -961,8 +959,7 @@ span.hide-announcement {
#qr select,
#dump-button,
#url-button,
-.remove,
-.captcha-img {
+.remove {
cursor: pointer;
}
#qr {
@@ -1055,20 +1052,12 @@ input.field.tripped:not(:hover):not(:focus) {
#qr textarea {
resize: both;
}
-.captcha-img {
- margin: 0px;
+#qr .captcha-counter {
+ display: block;
text-align: center;
- background-image: #fff;
- font-size: 0px;
- min-height: 59px;
- min-width: 302px;
}
-.captcha-input{
- width: 100%;
- margin: 1px 0 0;
-}
-.captcha-input.error:focus {
- border-color: rgb(255,0,0) !important;
+#qr .captcha-container > div > div {
+ margin: auto;
}
.field {
-moz-box-sizing: border-box;
diff --git a/src/General/html/Features/QuickReply.html b/src/General/html/Features/QuickReply.html
index 4ec950149..7f7ff5d1c 100755
--- a/src/General/html/Features/QuickReply.html
+++ b/src/General/html/Features/QuickReply.html
@@ -10,20 +10,20 @@
diff --git a/src/Posting/QR.captcha.coffee b/src/Posting/QR.captcha.coffee
index 02de79e05..849cf79a0 100644
--- a/src/Posting/QR.captcha.coffee
+++ b/src/Posting/QR.captcha.coffee
@@ -3,104 +3,106 @@ QR.captcha =
return if d.cookie.indexOf('pass_enabled=1') >= 0
return unless @isEnabled = !!$.id 'captchaContainer'
- $.globalEval 'loadRecaptcha()' if Conf['Auto-load captcha']
-
- imgContainer = $.el 'div',
- className: 'captcha-img'
- title: 'Reload reCAPTCHA'
- $.extend imgContainer, <%= html('
') %>
- input = $.el 'input',
- className: 'captcha-input field'
- title: 'Verification'
- autocomplete: 'off'
- spellcheck: false
- tabIndex: 45
- @nodes =
- img: imgContainer.firstChild
- input: input
-
- $.on input, 'blur', QR.focusout
- $.on input, 'focus', QR.focusin
- $.on input, 'keydown', QR.captcha.keydown.bind QR.captcha
- $.on @nodes.img.parentNode, 'click', QR.captcha.reload.bind QR.captcha
-
- $.addClass QR.nodes.el, 'has-captcha'
- $.after QR.nodes.com.parentNode, [imgContainer, input]
-
@captchas = []
$.get 'captchas', [], ({captchas}) ->
QR.captcha.sync captchas
- QR.captcha.clear()
- $.sync 'captchas', @sync
+ $.sync 'captchas', @sync.bind @
- new MutationObserver(@afterSetup).observe $.id('captchaContainer'), childList: true
-
- @beforeSetup()
- @afterSetup() # reCAPTCHA might have loaded before the QR.
- beforeSetup: ->
- {img, input} = @nodes
- img.parentNode.hidden = true
- input.value = ''
- input.placeholder = 'Focus to load reCAPTCHA'
+ counter = $.el 'a',
+ className: 'captcha-counter'
+ href: 'javascript:;'
+ container = $.el 'div',
+ className: 'captcha-container'
+ @nodes = {counter, container}
@count()
- $.on input, 'focus', @setup
- setup: ->
- $.globalEval 'loadRecaptcha()'
- afterSetup: ->
- return unless challenge = $.id 'recaptcha_challenge_field_holder'
- return if challenge is QR.captcha.nodes.challenge
+ $.addClass QR.nodes.el, 'has-captcha'
+ $.after QR.nodes.com.parentNode, [counter, container]
- setLifetime = (e) -> QR.captcha.lifetime = e.detail
- $.on window, 'captcha:timeout', setLifetime
- $.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
- $.off window, 'captcha:timeout', setLifetime
+ $.on counter, 'click', @toggle.bind @
+ $.on window, 'captcha:success', @save.bind @
- {img, input} = QR.captcha.nodes
- img.parentNode.hidden = false
- input.placeholder = 'Verification'
- QR.captcha.count()
- $.off input, 'focus', QR.captcha.setup
+ timeouts: {}
- QR.captcha.nodes.challenge = challenge
- new MutationObserver(QR.captcha.load.bind QR.captcha).observe challenge,
+ needed: ->
+ captchaCount = @captchas.length
+ captchaCount++ if @nodes.container.dataset.widgetID and !@timeouts.destroy
+ 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
+
+ toggle: ->
+ if @nodes.container.dataset.widgetID and !@timeouts.destroy
+ @destroy()
+ else
+ @setup true
+
+ setup: (force) ->
+ return unless @isEnabled and (@needed() or force)
+ $.addClass QR.nodes.el, 'captcha-open' # suppress autohide so that captcha pop-up works
+ if @timeouts.destroy
+ clearTimeout @timeouts.destroy
+ delete @timeouts.destroy
+ return @reload()
+ return if @nodes.container.dataset.widgetID
+ @observer?.disconnect()
+ @observer = new MutationObserver @afterSetup.bind @
+ @observer.observe @nodes.container,
childList: true
subtree: true
- attributes: true
- QR.captcha.load()
+ $.globalEval '''
+ (function() {
+ 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}));
+ }
+ });
+ })();
+ '''
+ afterSetup: ->
+ return unless @nodes.container.firstElementChild?.firstElementChild
+ @observer.disconnect()
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
QR.nodes.el.style.top = null
QR.nodes.el.style.bottom = '0px'
+
destroy: ->
- $.globalEval 'Recaptcha.destroy()'
- @beforeSetup()
+ return unless @isEnabled
+ delete @timeouts.destroy
+ $.rmClass QR.nodes.el, 'captcha-open'
+ $.rmAll @nodes.container
+ # XXX https://github.com/greasemonkey/greasemonkey/issues/1571
+ @nodes.container.removeAttribute 'data-widget-i-d'
sync: (captchas) ->
- QR.captcha.captchas = captchas
- QR.captcha.count()
+ @captchas = captchas
+ @clear()
+ @count()
getOne: ->
@clear()
if captcha = @captchas.shift()
- {challenge, response} = captcha
@count()
$.set 'captchas', @captchas
+ captcha.response
else
- challenge = @nodes.img.alt
- if response = @nodes.input.value
- if Conf['Auto-load captcha'] then @reload() else @destroy()
- {challenge, response}
+ null
- save: ->
- return unless /\S/.test(response = @nodes.input.value)
- @nodes.input.value = ''
+ save: (e) ->
+ if @needed()
+ @reload()
+ else
+ @timeouts.destroy ?= setTimeout @destroy.bind(@), 3 * $.SECOND
+ $.forceSync 'captchas'
@captchas.push
- challenge: @nodes.img.alt
- response: response
- timeout: @timeout
+ response: e.detail
+ timeout: Date.now() + 2 * $.MINUTE
@count()
- @reload()
$.set 'captchas', @captchas
+ @nodes.counter.focus()
clear: ->
return unless @captchas.length
@@ -111,42 +113,18 @@ QR.captcha =
@captchas = @captchas[i..]
@count()
$.set 'captchas', @captchas
-
- load: ->
- return unless @nodes.challenge.firstChild
- return unless challenge_image = $.id 'recaptcha_challenge_image'
- # -1 minute to give upload some time.
- @timeout = Date.now() + @lifetime * $.SECOND - $.MINUTE
- challenge = @nodes.challenge.firstChild.value
- @nodes.img.alt = challenge
- @nodes.img.src = challenge_image.src
- @nodes.input.value = null
- @clear()
+ @setup()
count: ->
- count = if @captchas then @captchas.length else 0
- placeholder = @nodes.input.placeholder.replace /\ \(.*\)$/, ''
- placeholder += switch count
- when 0
- if placeholder is 'Verification' then ' (Shift + Enter to cache)' else ''
- when 1
- ' (1 cached captcha)'
- else
- " (#{count} cached captchas)"
- @nodes.input.placeholder = placeholder
- @nodes.input.alt = count # For XTRM RICE.
+ @nodes.counter.textContent = "Captchas: #{@captchas.length}"
+ clearTimeout @timeouts.clear
+ if @captchas.length
+ @timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now()
reload: (focus) ->
- # Hack to prevent the input from being focused
- $.globalEval 'Recaptcha.reload(); Recaptcha.should_focus = false;'
- # Focus if we meant to.
- @nodes.input.focus() if focus
-
- keydown: (e) ->
- if e.keyCode is 8 and not @nodes.input.value
- @reload()
- else if e.keyCode is 13 and e.shiftKey
- @save()
- else
- return
- e.preventDefault()
+ $.globalEval '''
+ (function() {
+ var container = document.querySelector("#qr .captcha-container");
+ window.grecaptcha.reset(container.dataset.widgetID);
+ })();
+ '''
diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee
index 8b1fe1ba8..780dab32c 100644
--- a/src/Posting/QR.coffee
+++ b/src/Posting/QR.coffee
@@ -95,6 +95,7 @@ QR =
if QR.nodes
QR.nodes.el.hidden = false
QR.unhide()
+ QR.captcha.setup()
return
try
QR.dialog()
@@ -111,8 +112,6 @@ QR =
QR.cleanNotifications()
d.activeElement.blur()
$.rmClass QR.nodes.el, 'dump'
- unless Conf['Captcha Warning Notifications']
- $.rmClass QR.captcha.nodes.input, 'error' if QR.captcha.isEnabled
if Conf['QR Shortcut']
$.toggleClass $('.qr-shortcut'), 'disabled'
new QR.post true
@@ -120,8 +119,7 @@ QR =
post.delete()
QR.cooldown.auto = false
QR.status()
- if QR.captcha.isEnabled and not Conf['Auto-load captcha']
- QR.captcha.destroy()
+ QR.captcha.destroy()
focusin: ->
$.addClass QR.nodes.el, 'focus'
focusout: ->
@@ -147,18 +145,8 @@ QR =
el = err
el.removeAttribute 'style'
if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
- if QR.captcha.captchas.length is 0
- # Focus the captcha input on captcha error.
- QR.captcha.nodes.input.focus()
- QR.captcha.setup()
- if Conf['Captcha Warning Notifications'] and !d.hidden
- QR.notify el
- else
- $.addClass QR.captcha.nodes.input, 'error'
- $.on QR.captcha.nodes.input, 'keydown', ->
- $.rmClass QR.captcha.nodes.input, 'error'
- else
- QR.notify el
+ QR.captcha.setup()
+ QR.notify el
alert el.textContent if d.hidden
notify: (el) ->
@@ -539,6 +527,7 @@ QR =
QR.cooldown.init()
QR.captcha.init()
$.add d.body, dialog
+ QR.captcha.setup()
# Create a custom event when the QR dialog is first initialized.
# Use it to extend the QR's functionalities, or for XTRM RICE.
@@ -629,7 +618,7 @@ QR =
err = 'Max limit of image replies has been reached.'
if QR.captcha.isEnabled and !err
- {challenge, response} = QR.captcha.getOne()
+ response = QR.captcha.getOne()
err = 'No valid captcha.' unless response
QR.cleanNotifications()
@@ -663,8 +652,7 @@ QR =
textonly: textOnly
mode: 'regist'
pwd: QR.persona.pwd
- recaptcha_challenge_field: challenge
- recaptcha_response_field: response
+ 'g-recaptcha-response': response
options =
responseType: 'document'
@@ -793,7 +781,6 @@ QR =
icon: Favicon.logo
notif.onclick = ->
QR.open()
- QR.captcha.nodes.input.focus()
window.focus()
notif.onshow = ->
setTimeout ->
@@ -803,9 +790,8 @@ QR =
unless Conf['Persistent QR'] or postsCount
QR.close()
else
- if QR.posts.length > 1 and QR.captcha.isEnabled and QR.captcha.captchas.length is 0
- QR.captcha.setup()
post.rm()
+ QR.captcha.setup()
QR.cooldown.add req.uploadEndTime, threadID, postID
diff --git a/src/Posting/QR.post.coffee b/src/Posting/QR.post.coffee
index 643171855..b228642a1 100644
--- a/src/Posting/QR.post.coffee
+++ b/src/Posting/QR.post.coffee
@@ -37,6 +37,7 @@ QR.post = class
prev = QR.posts[QR.posts.length - 1]
QR.posts.push @
+ QR.captcha.setup()
@nodes.spoiler.checked = @spoiler = if prev and Conf['Remember Spoiler']
prev.spoiler
else
@@ -131,6 +132,7 @@ QR.post = class
QR.status()
when 'com'
@nodes.span.textContent = @com
+ QR.captcha.setup()
QR.characterCount()
# Disable auto-posting if you're typing in the first post
# during the last 5 seconds of the cooldown.
@@ -159,6 +161,7 @@ QR.post = class
@filename = file.name
@filesize = $.bytesToString file.size
@nodes.label.hidden = false if QR.spoiler
+ QR.captcha.setup()
URL.revokeObjectURL @URL
if @ is QR.selected
@showFileData()