diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 026fd9817..5a8bb3b7d 100755 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -59,6 +59,8 @@ module.exports = (grunt) -> 'src/General/CrossOrigin.coffee' 'src/Filtering/**/*.coffee' 'src/Quotelinks/**/*.coffee' + 'src/Posting/**/Captcha.coffee' + 'src/Posting/**/Captcha.*.coffee' 'src/Posting/**/QR.coffee' 'src/Posting/**/QR.*.coffee' 'src/Images/**/*.coffee' diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js index 1b424faa4..046600a24 100644 --- a/builds/appchan-x.user.js +++ b/builds/appchan-x.user.js @@ -115,7 +115,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, Captcha, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -8530,6 +8530,574 @@ } }; + Captcha = {}; + + Captcha.noscript = { + lifetime: 2 * $.MINUTE, + iframeURL: '//www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc', + init: function() { + var container, input; + if (d.cookie.indexOf('pass_enabled=1') >= 0) { + return; + } + if (!(this.isEnabled = !!$.id('g-recaptcha'))) { + return; + } + container = $.el('div', { + className: 'captcha-img', + title: 'Reload reCAPTCHA' + }); + input = $.el('input', { + className: 'captcha-input field', + title: 'Verification', + autocomplete: 'off', + spellcheck: false + }); + this.nodes = { + container: container, + input: input + }; + $.on(input, 'keydown', this.keydown.bind(this)); + $.on(this.nodes.container, 'click', (function(_this) { + return function() { + _this.reload(); + return _this.nodes.input.focus(); + }; + })(this)); + this.conn = new Connection(null, "" + location.protocol + "//www.google.com", { + challenge: this.load.bind(this), + token: this.save.bind(this), + error: this.error.bind(this) + }); + $.addClass(QR.nodes.el, 'has-captcha'); + $.after(QR.nodes.com.parentNode, [container, input]); + this.captchas = []; + $.get('captchas', [], function(_arg) { + var captchas; + captchas = _arg.captchas; + QR.captcha.sync(captchas); + return QR.captcha.clear(); + }); + $.sync('captchas', this.sync); + this.beforeSetup(); + return this.setup(); + }, + initFrame: function() { + var cb, conn, img, _ref, _ref1; + conn = new Connection(window.top, "" + location.protocol + "//boards.4chan.org", { + response: function(response) { + $.id('response').value = response; + return $('.fbc-challenge > form').submit(); + } + }); + conn.send({ + token: (_ref = $('.fbc-verification-token > textarea')) != null ? _ref.value : void 0, + error: (_ref1 = $('.fbc-error')) != null ? _ref1.textContent : void 0 + }); + if (!(img = $('.fbc-payload > img'))) { + return; + } + cb = function() { + var canvas; + canvas = $.el('canvas'); + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext('2d').drawImage(img, 0, 0); + return conn.send({ + challenge: canvas.toDataURL() + }); + }; + if (img.complete) { + return cb(); + } else { + return $.on(img, 'load', cb); + } + }, + timers: {}, + cb: { + focus: function() { + return QR.captcha.setup(false, true); + } + }, + beforeSetup: function() { + var container, input, _ref; + _ref = this.nodes, container = _ref.container, input = _ref.input; + container.hidden = true; + input.value = ''; + input.placeholder = 'Focus to load reCAPTCHA'; + this.count(); + return $.on(input, 'focus click', this.cb.focus); + }, + needed: function() { + var captchaCount, postsCount; + captchaCount = this.captchas.length; + if (QR.req) { + captchaCount++; + } + postsCount = QR.posts.length; + if (postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { + postsCount = 0; + } + return captchaCount < postsCount; + }, + onNewPost: function() {}, + onPostChange: function() {}, + setup: function(focus, force) { + if (!(this.isEnabled && (this.needed() || force))) { + return; + } + if (!this.nodes.iframe) { + this.nodes.iframe = $.el('iframe', { + id: 'qr-captcha-iframe', + src: this.iframeURL + }); + $.add(d.body, this.nodes.iframe); + this.conn.target = this.nodes.iframe.contentWindow; + } else if (!this.occupied) { + this.nodes.iframe.src = this.iframeURL; + } + this.occupied = true; + if (focus) { + return this.nodes.input.focus(); + } + }, + afterSetup: function() { + var container, input, _ref; + _ref = this.nodes, container = _ref.container, input = _ref.input; + container.hidden = false; + input.placeholder = 'Verification'; + this.count(); + $.off(input, 'focus click', this.cb.focus); + if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) { + QR.nodes.el.style.top = null; + return QR.nodes.el.style.bottom = '0px'; + } + }, + destroy: function() { + if (!this.isEnabled) { + return; + } + if (this.nodes.img) { + $.rm(this.nodes.img); + } + delete this.nodes.img; + if (this.nodes.iframe) { + $.rm(this.nodes.iframe); + } + delete this.nodes.iframe; + delete this.occupied; + this.unflag(); + return this.beforeSetup(); + }, + sync: function(captchas) { + if (captchas == null) { + captchas = []; + } + QR.captcha.captchas = captchas; + return QR.captcha.count(); + }, + getOne: function() { + var captcha; + this.clear(); + if (captcha = this.captchas.shift()) { + this.count(); + $.set('captchas', this.captchas); + return captcha.response; + } else if (/\S/.test(this.nodes.input.value)) { + return (function(_this) { + return function(cb) { + _this.submitCB = cb; + return _this.sendResponse(); + }; + })(this); + } else { + return null; + } + }, + sendResponse: function() { + var response; + response = this.nodes.input.value; + if (/\S/.test(response)) { + return this.conn.send({ + response: response + }); + } + }, + save: function(token) { + delete this.occupied; + this.nodes.input.value = ''; + if (this.submitCB) { + this.submitCB(token); + delete this.submitCB; + if (this.needed()) { + return this.reload(); + } else { + return this.destroy(); + } + } else { + $.forceSync('captchas'); + this.captchas.push({ + response: token, + timeout: this.timeout + }); + this.count(); + $.set('captchas', this.captchas); + return this.reload(); + } + }, + error: function(message) { + this.occupied = true; + this.nodes.input.value = ''; + if (this.submitCB) { + this.submitCB(); + delete this.submitCB; + } + return QR.error("Captcha Error: " + message); + }, + notify: function(el) { + if (Conf['Captcha Warning Notifications'] && !d.hidden) { + return QR.notify(el); + } else { + $.addClass(this.nodes.input, 'error'); + return $.one(this.nodes.input, 'keydown', this.unflag.bind(this)); + } + }, + unflag: function() { + return $.rmClass(this.nodes.input, 'error'); + }, + clear: function() { + var captcha, i, now, _i, _len, _ref; + if (!this.captchas.length) { + return; + } + $.forceSync('captchas'); + now = Date.now(); + _ref = this.captchas; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + captcha = _ref[i]; + if (captcha.timeout > now) { + break; + } + } + if (!i) { + return; + } + this.captchas = this.captchas.slice(i); + this.count(); + return $.set('captchas', this.captchas); + }, + load: function(src) { + var container, img, input, _ref; + _ref = this.nodes, container = _ref.container, input = _ref.input, img = _ref.img; + this.occupied = true; + this.timeout = Date.now() + this.lifetime; + if (!img) { + img = this.nodes.img = new Image; + $.one(img, 'load', this.afterSetup.bind(this)); + $.on(img, 'load', function() { + return this.hidden = false; + }); + $.add(container, img); + } + img.src = src; + input.value = ''; + this.clear(); + clearTimeout(this.timers.expire); + return this.timers.expire = setTimeout(this.expire.bind(this), this.lifetime); + }, + count: function() { + var count, placeholder; + count = this.captchas ? this.captchas.length : 0; + placeholder = this.nodes.input.placeholder.replace(/\ \(.*\)$/, ''); + placeholder += (function() { + switch (count) { + case 0: + if (placeholder === 'Verification') { + return ' (Shift + Enter to cache)'; + } else { + return ''; + } + break; + case 1: + return ' (1 cached captcha)'; + default: + return " (" + count + " cached captchas)"; + } + })(); + this.nodes.input.placeholder = placeholder; + this.nodes.input.alt = count; + clearTimeout(this.timers.clear); + if (this.captchas.length) { + return this.timers.clear = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now()); + } + }, + expire: function() { + if (!this.nodes.iframe) { + return; + } + if (this.needed() || d.activeElement === this.nodes.input) { + return this.reload(); + } else { + return this.destroy(); + } + }, + reload: function() { + var _ref; + this.nodes.iframe.src = this.iframeURL; + this.occupied = true; + return (_ref = this.nodes.img) != null ? _ref.hidden = true : void 0; + }, + keydown: function(e) { + if (e.keyCode === 8 && !this.nodes.input.value) { + if (this.nodes.iframe) { + this.reload(); + } else { + this.setup(); + } + } else if (e.keyCode === 13 && e.shiftKey) { + this.sendResponse(); + } else { + return; + } + return e.preventDefault(); + } + }; + + Captcha.v2 = { + lifetime: 2 * $.MINUTE, + init: function() { + var counter, root; + if (d.cookie.indexOf('pass_enabled=1') >= 0) { + return; + } + if (!(this.isEnabled = !!$.id('g-recaptcha'))) { + return; + } + this.captchas = []; + $.get('captchas', [], function(_arg) { + var captchas; + captchas = _arg.captchas; + return QR.captcha.sync(captchas); + }); + $.sync('captchas', this.sync.bind(this)); + root = $.el('div', { + className: 'captcha-root' + }); + $.extend(root, { + innerHTML: "
" + }); + counter = $('.captcha-counter > a', root); + this.nodes = { + root: root, + counter: counter + }; + this.count(); + $.addClass(QR.nodes.el, 'has-captcha'); + $.after(QR.nodes.com.parentNode, root); + $.on(counter, 'click', this.toggle.bind(this)); + return $.on(window, 'captcha:success', (function(_this) { + return function() { + return $.queueTask(function() { + return _this.save(false); + }); + }; + })(this)); + }, + shouldFocus: false, + timeouts: {}, + postsCount: 0, + needed: function() { + var captchaCount; + captchaCount = this.captchas.length; + if (QR.req) { + captchaCount++; + } + this.postsCount = QR.posts.length; + if (this.postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { + this.postsCount = 0; + } + return captchaCount < this.postsCount; + }, + onNewPost: function() { + return this.setup(); + }, + onPostChange: function() { + if (this.postsCount === 0) { + this.setup(); + } + if (QR.posts.length === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { + return this.postsCount = 0; + } + }, + toggle: function() { + if (this.nodes.container && !this.timeouts.destroy) { + return this.destroy(); + } else { + return this.setup(true, true); + } + }, + setup: function(focus, force) { + var iframe; + if (!(this.isEnabled && (this.needed() || force))) { + return; + } + $.addClass(QR.nodes.el, 'captcha-open'); + if (focus) { + this.shouldFocus = true; + } + if (this.timeouts.destroy) { + clearTimeout(this.timeouts.destroy); + delete this.timeouts.destroy; + return this.reload(); + } + if (this.nodes.container) { + if (this.shouldFocus && (iframe = $('iframe', this.nodes.container))) { + iframe.focus(); + delete this.shouldFocus; + } + 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 + }); + return $.globalEval('(function() {\n function render() {\n var container = document.querySelector("#qr .captcha-container");\n container.dataset.widgetID = window.grecaptcha.render(container, {\n sitekey: \'6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\',\n theme: document.documentElement.classList.contains(\'tomorrow\') ? \'dark\' : \'light\',\n callback: function(response) {\n window.dispatchEvent(new CustomEvent("captcha:success", {detail: response}));\n }\n });\n }\n if (window.grecaptcha) {\n render();\n } else {\n var cbNative = window.onRecaptchaLoaded;\n window.onRecaptchaLoaded = function() {\n render();\n cbNative();\n }\n }\n})();'); + }, + afterSetup: function(mutations) { + var iframe, mutation, node, textarea, _i, _j, _len, _len1, _ref; + for (_i = 0, _len = mutations.length; _i < _len; _i++) { + mutation = mutations[_i]; + _ref = mutation.addedNodes; + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + node = _ref[_j]; + if (iframe = $.x('./descendant-or-self::iframe', node)) { + this.setupIFrame(iframe); + } + if (textarea = $.x('./descendant-or-self::textarea', node)) { + this.setupTextArea(textarea); + } + } + } + }, + setupIFrame: function(iframe) { + this.setupTime = Date.now(); + if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) { + QR.nodes.el.style.top = null; + QR.nodes.el.style.bottom = '0px'; + } + if (this.shouldFocus) { + iframe.focus(); + } + return this.shouldFocus = false; + }, + setupTextArea: function(textarea) { + return $.one(textarea, 'input', (function(_this) { + return function() { + return _this.save(true); + }; + })(this)); + }, + destroy: function() { + if (!this.isEnabled) { + return; + } + delete this.timeouts.destroy; + $.rmClass(QR.nodes.el, 'captcha-open'); + if (this.nodes.container) { + $.rm(this.nodes.container); + } + return delete this.nodes.container; + }, + sync: function(captchas) { + if (captchas == null) { + captchas = []; + } + this.captchas = captchas; + this.clear(); + return this.count(); + }, + getOne: function() { + var captcha; + this.clear(); + if (captcha = this.captchas.shift()) { + $.set('captchas', this.captchas); + this.count(); + return captcha.response; + } else { + return null; + } + }, + save: function(pasted) { + var _base; + $.forceSync('captchas'); + this.captchas.push({ + response: $('textarea', this.nodes.container).value, + timeout: (pasted ? this.setupTime : Date.now()) + this.lifetime + }); + $.set('captchas', this.captchas); + this.count(); + if (this.needed()) { + if (QR.cooldown.auto || Conf['Post on Captcha Completion']) { + this.shouldFocus = true; + } else { + QR.nodes.status.focus(); + } + this.reload(); + } else { + if (pasted) { + this.destroy(); + } else { + if ((_base = this.timeouts).destroy == null) { + _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND); + } + } + QR.nodes.status.focus(); + } + if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) { + return QR.submit(); + } + }, + notify: function(el) { + return QR.notify(el); + }, + clear: function() { + var captcha, i, now, _i, _len, _ref; + if (!this.captchas.length) { + return; + } + $.forceSync('captchas'); + now = Date.now(); + _ref = this.captchas; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + captcha = _ref[i]; + if (captcha.timeout > now) { + break; + } + } + if (!i) { + return; + } + this.captchas = this.captchas.slice(i); + this.count(); + $.set('captchas', this.captchas); + return this.setup(true); + }, + count: function() { + this.nodes.counter.textContent = "Captchas: " + this.captchas.length; + clearTimeout(this.timeouts.clear); + if (this.captchas.length) { + return this.timeouts.clear = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now()); + } + }, + reload: function() { + return $.globalEval('(function() {\n var container = document.querySelector("#qr .captcha-container");\n window.grecaptcha.reset(container.dataset.widgetID);\n})();'); + } + }; + QR = { mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'], init: function() { diff --git a/builds/crx/script.js b/builds/crx/script.js index 60fb5a525..481dcb430 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -88,7 +88,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, Captcha, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -8575,6 +8575,574 @@ } }; + Captcha = {}; + + Captcha.noscript = { + lifetime: 2 * $.MINUTE, + iframeURL: '//www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc', + init: function() { + var container, input; + if (d.cookie.indexOf('pass_enabled=1') >= 0) { + return; + } + if (!(this.isEnabled = !!$.id('g-recaptcha'))) { + return; + } + container = $.el('div', { + className: 'captcha-img', + title: 'Reload reCAPTCHA' + }); + input = $.el('input', { + className: 'captcha-input field', + title: 'Verification', + autocomplete: 'off', + spellcheck: false + }); + this.nodes = { + container: container, + input: input + }; + $.on(input, 'keydown', this.keydown.bind(this)); + $.on(this.nodes.container, 'click', (function(_this) { + return function() { + _this.reload(); + return _this.nodes.input.focus(); + }; + })(this)); + this.conn = new Connection(null, "" + location.protocol + "//www.google.com", { + challenge: this.load.bind(this), + token: this.save.bind(this), + error: this.error.bind(this) + }); + $.addClass(QR.nodes.el, 'has-captcha'); + $.after(QR.nodes.com.parentNode, [container, input]); + this.captchas = []; + $.get('captchas', [], function(_arg) { + var captchas; + captchas = _arg.captchas; + QR.captcha.sync(captchas); + return QR.captcha.clear(); + }); + $.sync('captchas', this.sync); + this.beforeSetup(); + return this.setup(); + }, + initFrame: function() { + var cb, conn, img, _ref, _ref1; + conn = new Connection(window.top, "" + location.protocol + "//boards.4chan.org", { + response: function(response) { + $.id('response').value = response; + return $('.fbc-challenge > form').submit(); + } + }); + conn.send({ + token: (_ref = $('.fbc-verification-token > textarea')) != null ? _ref.value : void 0, + error: (_ref1 = $('.fbc-error')) != null ? _ref1.textContent : void 0 + }); + if (!(img = $('.fbc-payload > img'))) { + return; + } + cb = function() { + var canvas; + canvas = $.el('canvas'); + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext('2d').drawImage(img, 0, 0); + return conn.send({ + challenge: canvas.toDataURL() + }); + }; + if (img.complete) { + return cb(); + } else { + return $.on(img, 'load', cb); + } + }, + timers: {}, + cb: { + focus: function() { + return QR.captcha.setup(false, true); + } + }, + beforeSetup: function() { + var container, input, _ref; + _ref = this.nodes, container = _ref.container, input = _ref.input; + container.hidden = true; + input.value = ''; + input.placeholder = 'Focus to load reCAPTCHA'; + this.count(); + return $.on(input, 'focus click', this.cb.focus); + }, + needed: function() { + var captchaCount, postsCount; + captchaCount = this.captchas.length; + if (QR.req) { + captchaCount++; + } + postsCount = QR.posts.length; + if (postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { + postsCount = 0; + } + return captchaCount < postsCount; + }, + onNewPost: function() {}, + onPostChange: function() {}, + setup: function(focus, force) { + if (!(this.isEnabled && (this.needed() || force))) { + return; + } + if (!this.nodes.iframe) { + this.nodes.iframe = $.el('iframe', { + id: 'qr-captcha-iframe', + src: this.iframeURL + }); + $.add(d.body, this.nodes.iframe); + this.conn.target = this.nodes.iframe.contentWindow; + } else if (!this.occupied) { + this.nodes.iframe.src = this.iframeURL; + } + this.occupied = true; + if (focus) { + return this.nodes.input.focus(); + } + }, + afterSetup: function() { + var container, input, _ref; + _ref = this.nodes, container = _ref.container, input = _ref.input; + container.hidden = false; + input.placeholder = 'Verification'; + this.count(); + $.off(input, 'focus click', this.cb.focus); + if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) { + QR.nodes.el.style.top = null; + return QR.nodes.el.style.bottom = '0px'; + } + }, + destroy: function() { + if (!this.isEnabled) { + return; + } + if (this.nodes.img) { + $.rm(this.nodes.img); + } + delete this.nodes.img; + if (this.nodes.iframe) { + $.rm(this.nodes.iframe); + } + delete this.nodes.iframe; + delete this.occupied; + this.unflag(); + return this.beforeSetup(); + }, + sync: function(captchas) { + if (captchas == null) { + captchas = []; + } + QR.captcha.captchas = captchas; + return QR.captcha.count(); + }, + getOne: function() { + var captcha; + this.clear(); + if (captcha = this.captchas.shift()) { + this.count(); + $.set('captchas', this.captchas); + return captcha.response; + } else if (/\S/.test(this.nodes.input.value)) { + return (function(_this) { + return function(cb) { + _this.submitCB = cb; + return _this.sendResponse(); + }; + })(this); + } else { + return null; + } + }, + sendResponse: function() { + var response; + response = this.nodes.input.value; + if (/\S/.test(response)) { + return this.conn.send({ + response: response + }); + } + }, + save: function(token) { + delete this.occupied; + this.nodes.input.value = ''; + if (this.submitCB) { + this.submitCB(token); + delete this.submitCB; + if (this.needed()) { + return this.reload(); + } else { + return this.destroy(); + } + } else { + $.forceSync('captchas'); + this.captchas.push({ + response: token, + timeout: this.timeout + }); + this.count(); + $.set('captchas', this.captchas); + return this.reload(); + } + }, + error: function(message) { + this.occupied = true; + this.nodes.input.value = ''; + if (this.submitCB) { + this.submitCB(); + delete this.submitCB; + } + return QR.error("Captcha Error: " + message); + }, + notify: function(el) { + if (Conf['Captcha Warning Notifications'] && !d.hidden) { + return QR.notify(el); + } else { + $.addClass(this.nodes.input, 'error'); + return $.one(this.nodes.input, 'keydown', this.unflag.bind(this)); + } + }, + unflag: function() { + return $.rmClass(this.nodes.input, 'error'); + }, + clear: function() { + var captcha, i, now, _i, _len, _ref; + if (!this.captchas.length) { + return; + } + $.forceSync('captchas'); + now = Date.now(); + _ref = this.captchas; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + captcha = _ref[i]; + if (captcha.timeout > now) { + break; + } + } + if (!i) { + return; + } + this.captchas = this.captchas.slice(i); + this.count(); + return $.set('captchas', this.captchas); + }, + load: function(src) { + var container, img, input, _ref; + _ref = this.nodes, container = _ref.container, input = _ref.input, img = _ref.img; + this.occupied = true; + this.timeout = Date.now() + this.lifetime; + if (!img) { + img = this.nodes.img = new Image; + $.one(img, 'load', this.afterSetup.bind(this)); + $.on(img, 'load', function() { + return this.hidden = false; + }); + $.add(container, img); + } + img.src = src; + input.value = ''; + this.clear(); + clearTimeout(this.timers.expire); + return this.timers.expire = setTimeout(this.expire.bind(this), this.lifetime); + }, + count: function() { + var count, placeholder; + count = this.captchas ? this.captchas.length : 0; + placeholder = this.nodes.input.placeholder.replace(/\ \(.*\)$/, ''); + placeholder += (function() { + switch (count) { + case 0: + if (placeholder === 'Verification') { + return ' (Shift + Enter to cache)'; + } else { + return ''; + } + break; + case 1: + return ' (1 cached captcha)'; + default: + return " (" + count + " cached captchas)"; + } + })(); + this.nodes.input.placeholder = placeholder; + this.nodes.input.alt = count; + clearTimeout(this.timers.clear); + if (this.captchas.length) { + return this.timers.clear = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now()); + } + }, + expire: function() { + if (!this.nodes.iframe) { + return; + } + if (this.needed() || d.activeElement === this.nodes.input) { + return this.reload(); + } else { + return this.destroy(); + } + }, + reload: function() { + var _ref; + this.nodes.iframe.src = this.iframeURL; + this.occupied = true; + return (_ref = this.nodes.img) != null ? _ref.hidden = true : void 0; + }, + keydown: function(e) { + if (e.keyCode === 8 && !this.nodes.input.value) { + if (this.nodes.iframe) { + this.reload(); + } else { + this.setup(); + } + } else if (e.keyCode === 13 && e.shiftKey) { + this.sendResponse(); + } else { + return; + } + return e.preventDefault(); + } + }; + + Captcha.v2 = { + lifetime: 2 * $.MINUTE, + init: function() { + var counter, root; + if (d.cookie.indexOf('pass_enabled=1') >= 0) { + return; + } + if (!(this.isEnabled = !!$.id('g-recaptcha'))) { + return; + } + this.captchas = []; + $.get('captchas', [], function(_arg) { + var captchas; + captchas = _arg.captchas; + return QR.captcha.sync(captchas); + }); + $.sync('captchas', this.sync.bind(this)); + root = $.el('div', { + className: 'captcha-root' + }); + $.extend(root, { + innerHTML: "
" + }); + counter = $('.captcha-counter > a', root); + this.nodes = { + root: root, + counter: counter + }; + this.count(); + $.addClass(QR.nodes.el, 'has-captcha'); + $.after(QR.nodes.com.parentNode, root); + $.on(counter, 'click', this.toggle.bind(this)); + return $.on(window, 'captcha:success', (function(_this) { + return function() { + return $.queueTask(function() { + return _this.save(false); + }); + }; + })(this)); + }, + shouldFocus: false, + timeouts: {}, + postsCount: 0, + needed: function() { + var captchaCount; + captchaCount = this.captchas.length; + if (QR.req) { + captchaCount++; + } + this.postsCount = QR.posts.length; + if (this.postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { + this.postsCount = 0; + } + return captchaCount < this.postsCount; + }, + onNewPost: function() { + return this.setup(); + }, + onPostChange: function() { + if (this.postsCount === 0) { + this.setup(); + } + if (QR.posts.length === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { + return this.postsCount = 0; + } + }, + toggle: function() { + if (this.nodes.container && !this.timeouts.destroy) { + return this.destroy(); + } else { + return this.setup(true, true); + } + }, + setup: function(focus, force) { + var iframe; + if (!(this.isEnabled && (this.needed() || force))) { + return; + } + $.addClass(QR.nodes.el, 'captcha-open'); + if (focus) { + this.shouldFocus = true; + } + if (this.timeouts.destroy) { + clearTimeout(this.timeouts.destroy); + delete this.timeouts.destroy; + return this.reload(); + } + if (this.nodes.container) { + if (this.shouldFocus && (iframe = $('iframe', this.nodes.container))) { + iframe.focus(); + delete this.shouldFocus; + } + 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 + }); + return $.globalEval('(function() {\n function render() {\n var container = document.querySelector("#qr .captcha-container");\n container.dataset.widgetID = window.grecaptcha.render(container, {\n sitekey: \'6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\',\n theme: document.documentElement.classList.contains(\'tomorrow\') ? \'dark\' : \'light\',\n callback: function(response) {\n window.dispatchEvent(new CustomEvent("captcha:success", {detail: response}));\n }\n });\n }\n if (window.grecaptcha) {\n render();\n } else {\n var cbNative = window.onRecaptchaLoaded;\n window.onRecaptchaLoaded = function() {\n render();\n cbNative();\n }\n }\n})();'); + }, + afterSetup: function(mutations) { + var iframe, mutation, node, textarea, _i, _j, _len, _len1, _ref; + for (_i = 0, _len = mutations.length; _i < _len; _i++) { + mutation = mutations[_i]; + _ref = mutation.addedNodes; + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + node = _ref[_j]; + if (iframe = $.x('./descendant-or-self::iframe', node)) { + this.setupIFrame(iframe); + } + if (textarea = $.x('./descendant-or-self::textarea', node)) { + this.setupTextArea(textarea); + } + } + } + }, + setupIFrame: function(iframe) { + this.setupTime = Date.now(); + if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) { + QR.nodes.el.style.top = null; + QR.nodes.el.style.bottom = '0px'; + } + if (this.shouldFocus) { + iframe.focus(); + } + return this.shouldFocus = false; + }, + setupTextArea: function(textarea) { + return $.one(textarea, 'input', (function(_this) { + return function() { + return _this.save(true); + }; + })(this)); + }, + destroy: function() { + if (!this.isEnabled) { + return; + } + delete this.timeouts.destroy; + $.rmClass(QR.nodes.el, 'captcha-open'); + if (this.nodes.container) { + $.rm(this.nodes.container); + } + return delete this.nodes.container; + }, + sync: function(captchas) { + if (captchas == null) { + captchas = []; + } + this.captchas = captchas; + this.clear(); + return this.count(); + }, + getOne: function() { + var captcha; + this.clear(); + if (captcha = this.captchas.shift()) { + $.set('captchas', this.captchas); + this.count(); + return captcha.response; + } else { + return null; + } + }, + save: function(pasted) { + var _base; + $.forceSync('captchas'); + this.captchas.push({ + response: $('textarea', this.nodes.container).value, + timeout: (pasted ? this.setupTime : Date.now()) + this.lifetime + }); + $.set('captchas', this.captchas); + this.count(); + if (this.needed()) { + if (QR.cooldown.auto || Conf['Post on Captcha Completion']) { + this.shouldFocus = true; + } else { + QR.nodes.status.focus(); + } + this.reload(); + } else { + if (pasted) { + this.destroy(); + } else { + if ((_base = this.timeouts).destroy == null) { + _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND); + } + } + QR.nodes.status.focus(); + } + if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) { + return QR.submit(); + } + }, + notify: function(el) { + return QR.notify(el); + }, + clear: function() { + var captcha, i, now, _i, _len, _ref; + if (!this.captchas.length) { + return; + } + $.forceSync('captchas'); + now = Date.now(); + _ref = this.captchas; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + captcha = _ref[i]; + if (captcha.timeout > now) { + break; + } + } + if (!i) { + return; + } + this.captchas = this.captchas.slice(i); + this.count(); + $.set('captchas', this.captchas); + return this.setup(true); + }, + count: function() { + this.nodes.counter.textContent = "Captchas: " + this.captchas.length; + clearTimeout(this.timeouts.clear); + if (this.captchas.length) { + return this.timeouts.clear = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now()); + } + }, + reload: function() { + return $.globalEval('(function() {\n var container = document.querySelector("#qr .captcha-container");\n window.grecaptcha.reset(container.dataset.widgetID);\n})();'); + } + }; + QR = { mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'], init: function() { diff --git a/src/Posting/Captcha.coffee b/src/Posting/Captcha.coffee new file mode 100644 index 000000000..5bcab40bb --- /dev/null +++ b/src/Posting/Captcha.coffee @@ -0,0 +1 @@ +Captcha = {} diff --git a/src/Posting/Captcha.noscript.coffee b/src/Posting/Captcha.noscript.coffee new file mode 100644 index 000000000..c0dd47516 --- /dev/null +++ b/src/Posting/Captcha.noscript.coffee @@ -0,0 +1,236 @@ +Captcha.noscript = + lifetime: 2 * $.MINUTE + iframeURL: '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>' + + init: -> + return if d.cookie.indexOf('pass_enabled=1') >= 0 + return unless @isEnabled = !!$.id 'g-recaptcha' + + container = $.el 'div', + className: 'captcha-img' + title: 'Reload reCAPTCHA' + input = $.el 'input', + className: 'captcha-input field' + title: 'Verification' + autocomplete: 'off' + spellcheck: false + @nodes = {container, input} + + $.on input, 'keydown', @keydown.bind @ + $.on @nodes.container, 'click', => + @reload() + @nodes.input.focus() + + @conn = new Connection null, "#{location.protocol}//www.google.com", + challenge: @load.bind @ + token: @save.bind @ + error: @error.bind @ + + $.addClass QR.nodes.el, 'has-captcha' + $.after QR.nodes.com.parentNode, [container, input] + + @captchas = [] + $.get 'captchas', [], ({captchas}) -> + QR.captcha.sync captchas + QR.captcha.clear() + $.sync 'captchas', @sync + + @beforeSetup() + @setup() + + initFrame: -> + conn = new Connection window.top, "#{location.protocol}//boards.4chan.org", + response: (response) -> + $.id('response').value = response + $('.fbc-challenge > form').submit() + conn.send + token: $('.fbc-verification-token > textarea')?.value + error: $('.fbc-error')?.textContent + return unless img = $ '.fbc-payload > img' + cb = -> + canvas = $.el 'canvas' + canvas.width = img.width + canvas.height = img.height + canvas.getContext('2d').drawImage(img, 0, 0) + conn.send {challenge: canvas.toDataURL()} + if img.complete + cb() + else + $.on img, 'load', cb + + timers: {} + + cb: + focus: -> QR.captcha.setup false, true + + beforeSetup: -> + {container, input} = @nodes + container.hidden = true + input.value = '' + input.placeholder = 'Focus to load reCAPTCHA' + @count() + $.on input, 'focus click', @cb.focus + + 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: -> + + onPostChange: -> + + setup: (focus, force) -> + return unless @isEnabled and (@needed() or force) + if !@nodes.iframe + @nodes.iframe = $.el 'iframe', + id: 'qr-captcha-iframe' + src: @iframeURL + $.add d.body, @nodes.iframe + @conn.target = @nodes.iframe.contentWindow + else if !@occupied + @nodes.iframe.src = @iframeURL + @occupied = true + @nodes.input.focus() if focus + + afterSetup: -> + {container, input} = @nodes + container.hidden = false + input.placeholder = 'Verification' + @count() + $.off input, 'focus click', @cb.focus + + if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight + QR.nodes.el.style.top = null + QR.nodes.el.style.bottom = '0px' + + destroy: -> + return unless @isEnabled + $.rm @nodes.img if @nodes.img + delete @nodes.img + $.rm @nodes.iframe if @nodes.iframe + delete @nodes.iframe + delete @occupied + @unflag() + @beforeSetup() + + sync: (captchas=[]) -> + QR.captcha.captchas = captchas + QR.captcha.count() + + getOne: -> + @clear() + if captcha = @captchas.shift() + @count() + $.set 'captchas', @captchas + captcha.response + else if /\S/.test @nodes.input.value + (cb) => + @submitCB = cb + @sendResponse() + else + null + + sendResponse: -> + response = @nodes.input.value + if /\S/.test response + @conn.send {response} + + save: (token) -> + delete @occupied + @nodes.input.value = '' + if @submitCB + @submitCB token + delete @submitCB + if @needed() then @reload() else @destroy() + else + $.forceSync 'captchas' + @captchas.push + response: token + timeout: @timeout + @count() + $.set 'captchas', @captchas + @reload() + + error: (message) -> + @occupied = true + @nodes.input.value = '' + if @submitCB + @submitCB() + delete @submitCB + QR.error "Captcha Error: #{message}" + + notify: (el) -> + if Conf['Captcha Warning Notifications'] and !d.hidden + QR.notify el + else + $.addClass @nodes.input, 'error' + $.one @nodes.input, 'keydown', @unflag.bind @ + + unflag: -> + $.rmClass @nodes.input, 'error' + + 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 + + load: (src) -> + {container, input, img} = @nodes + @occupied = true + @timeout = Date.now() + @lifetime + unless img + img = @nodes.img = new Image + $.one img, 'load', @afterSetup.bind @ + $.on img, 'load', -> @hidden = false + $.add container, img + img.src = src + input.value = '' + @clear() + clearTimeout @timers.expire + @timers.expire = setTimeout @expire.bind(@), @lifetime + + 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. + clearTimeout @timers.clear + if @captchas.length + @timers.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now() + + expire: -> + return unless @nodes.iframe + if @needed() or d.activeElement is @nodes.input + @reload() + else + @destroy() + + reload: -> + @nodes.iframe.src = @iframeURL + @occupied = true + @nodes.img?.hidden = true + + keydown: (e) -> + if e.keyCode is 8 and not @nodes.input.value + if @nodes.iframe then @reload() else @setup() + else if e.keyCode is 13 and e.shiftKey + @sendResponse() + else + return + e.preventDefault() diff --git a/src/Posting/Captcha.v2.coffee b/src/Posting/Captcha.v2.coffee new file mode 100644 index 000000000..f2cb19c63 --- /dev/null +++ b/src/Posting/Captcha.v2.coffee @@ -0,0 +1,186 @@ +Captcha.v2 = + lifetime: 2 * $.MINUTE + + init: -> + return if d.cookie.indexOf('pass_enabled=1') >= 0 + return unless @isEnabled = !!$.id 'g-recaptcha' + + @captchas = [] + $.get 'captchas', [], ({captchas}) -> + QR.captcha.sync captchas + $.sync 'captchas', @sync.bind @ + + root = $.el 'div', className: 'captcha-root' + $.extend root, <%= html( + '
' + ) %> + counter = $ '.captcha-counter > a', root + @nodes = {root, counter} + @count() + $.addClass QR.nodes.el, 'has-captcha' + $.after QR.nodes.com.parentNode, root + + $.on counter, 'click', @toggle.bind @ + $.on window, 'captcha:success', => + # XXX Greasemonkey 1.x workaround to gain access to GM_* functions. + $.queueTask => @save false + + shouldFocus: false + timeouts: {} + postsCount: 0 + + 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) + $.addClass QR.nodes.el, 'captcha-open' + @shouldFocus = true if focus + if @timeouts.destroy + clearTimeout @timeouts.destroy + delete @timeouts.destroy + return @reload() + + if @nodes.container + if @shouldFocus and iframe = $ 'iframe', @nodes.container + iframe.focus() + delete @shouldFocus + return + + @nodes.container = $.el 'div', className: 'captcha-container' + $.prepend @nodes.root, @nodes.container + new MutationObserver(@afterSetup.bind @).observe @nodes.container, + childList: true + subtree: true + + $.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) -> + @setupTime = Date.now() + if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight + QR.nodes.el.style.top = null + QR.nodes.el.style.bottom = '0px' + iframe.focus() if @shouldFocus + @shouldFocus = false + + 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 + + sync: (captchas=[]) -> + @captchas = captchas + @clear() + @count() + + getOne: -> + @clear() + if captcha = @captchas.shift() + $.set 'captchas', @captchas + @count() + captcha.response + else + null + + save: (pasted) -> + $.forceSync 'captchas' + @captchas.push + response: $('textarea', @nodes.container).value + timeout: (if pasted then @setupTime else Date.now()) + @lifetime + $.set 'captchas', @captchas + @count() + + if @needed() + if QR.cooldown.auto or Conf['Post on Captcha Completion'] + @shouldFocus = true + else + QR.nodes.status.focus() + @reload() + else + if pasted + @destroy() + else + @timeouts.destroy ?= setTimeout @destroy.bind(@), 3 * $.SECOND + QR.nodes.status.focus() + + QR.submit() if Conf['Post on Captcha Completion'] and !QR.cooldown.auto + + notify: (el) -> + QR.notify el + + 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: -> + $.globalEval ''' + (function() { + var container = document.querySelector("#qr .captcha-container"); + window.grecaptcha.reset(container.dataset.widgetID); + })(); + '''