Merge branch 'v3'

Conflicts:
	CHANGELOG.md
	LICENSE
	builds/appchan-x.user.js
	builds/crx/script.js
	src/Posting/QR.captcha.coffee
This commit is contained in:
Zixaphir 2014-04-02 17:21:16 -07:00
commit 9f7026e419
9 changed files with 142 additions and 375 deletions

View File

@ -1,4 +1,9 @@
### v2.9.9 **MayhemYDG**:
- Fix captcha submission:
Captchas were reloaded the instant a post was submitted to 4chan. Unfortunately, a recent change to reCAPTCHA made it so reloading captchas invalidates the ones that loaded but not yet used. This is now fixed by only unloading the captcha, and only load new ones after the post is submitted.<br>
This also kills captcha caching, so the feature was removed.
### v2.9.9
*2014-03-27* *2014-03-27*
**MayhemYDG**: **MayhemYDG**:

View File

@ -1,5 +1,5 @@
/* /*
* appchan x - Version 2.9.9 - 2014-03-27 * appchan x - Version 2.9.9 - 2014-04-02
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE

View File

@ -25,7 +25,7 @@
// ==/UserScript== // ==/UserScript==
/* /*
* appchan x - Version 2.9.9 - 2014-03-27 * appchan x - Version 2.9.9 - 2014-04-02
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@ -8063,6 +8063,9 @@
if (Conf['Comment Expansion']) { if (Conf['Comment Expansion']) {
ExpandComment.callbacks.push(this.node); ExpandComment.callbacks.push(this.node);
} }
if (Conf['Embedding'] || Conf['Link Title']) {
this.embedProcess = Function('link', "var data = this.services(link); if (data) { " + ((Conf['Embedding'] ? 'this.embed(data);\n' : '') + (Conf['Title Link'] ? 'this.title(data);' : '')) + " }");
}
return Post.callbacks.push({ return Post.callbacks.push({
name: 'Linkify', name: 'Linkify',
cb: this.node cb: this.node
@ -8132,17 +8135,7 @@
Linkify.embedProcess(Linkify.makeLink(link, this)); Linkify.embedProcess(Linkify.makeLink(link, this));
} }
}, },
embedProcess: function(link) { embedProcess: function() {},
var data;
if (data = Linkify.services(link)) {
if (Conf['Embedding']) {
Linkify.embed(data);
}
if (Conf['Link Title']) {
return Linkify.title(data);
}
}
},
regString: /((https?|mailto|git|magnet|ftp|irc):([a-z\d%\/])|[-a-z\d]+[.](aero|asia|biz|cat|com|coop|info|int|jobs|mobi|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2})([:\/]|(?!.))|[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}|[-\w\d.@]+@[a-z\d.-]+\.[a-z\d])/i, regString: /((https?|mailto|git|magnet|ftp|irc):([a-z\d%\/])|[-a-z\d]+[.](aero|asia|biz|cat|com|coop|info|int|jobs|mobi|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2})([:\/]|(?!.))|[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}|[-\w\d.@]+@[a-z\d.-]+\.[a-z\d])/i,
makeRange: function(startNode, endNode, startOffset, endOffset) { makeRange: function(startNode, endNode, startOffset, endOffset) {
var range; var range;
@ -9354,6 +9347,10 @@
onload: QR.response, onload: QR.response,
onerror: function() { onerror: function() {
delete QR.req; delete QR.req;
if (QR.captcha.isEnabled) {
QR.captcha.destroy();
QR.captcha.setup();
}
post.unlock(); post.unlock();
QR.cooldown.auto = false; QR.cooldown.auto = false;
QR.status(); QR.status();
@ -9383,9 +9380,12 @@
return QR.status(); return QR.status();
}, },
response: function() { response: function() {
var URL, ban, board, captchasCount, err, h1, isReply, m, notif, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1; var URL, ban, board, err, h1, isReply, m, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1;
req = QR.req; req = QR.req;
delete QR.req; delete QR.req;
if (QR.captcha.isEnabled) {
QR.captcha.destroy();
}
post = QR.posts[0]; post = QR.posts[0];
post.unlock(); post.unlock();
resDoc = req.response; resDoc = req.response;
@ -9407,15 +9407,13 @@
if (/captcha|verification/i.test(err.textContent) || err === 'Connection error with sys.4chan.org.') { if (/captcha|verification/i.test(err.textContent) || err === 'Connection error with sys.4chan.org.') {
if (/mistyped/i.test(err.textContent)) { if (/mistyped/i.test(err.textContent)) {
err = 'You seem to have mistyped the CAPTCHA.'; err = 'You seem to have mistyped the CAPTCHA.';
} else if (/expired/i.test(err.textContent)) {
err = 'This CAPTCHA is no longer valid because it has expired.';
} }
QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : err === 'Connection error with sys.4chan.org.' ? true : false; QR.cooldown.auto = false;
QR.cooldown.set({ QR.cooldown.set({
delay: 2 delay: 2
}); });
} else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) { } else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) {
QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : true; QR.cooldown.auto = !QR.captcha.isEnabled;
QR.cooldown.set({ QR.cooldown.set({
delay: m[1] delay: m[1]
}); });
@ -9454,22 +9452,6 @@
}); });
postsCount = QR.posts.length - 1; postsCount = QR.posts.length - 1;
QR.cooldown.auto = postsCount && isReply; QR.cooldown.auto = postsCount && isReply;
if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) {
notif = new Notification('Quick reply warning', {
body: "You are running low on cached captchas. Cache count: " + captchasCount + ".",
icon: Favicon.logo
});
notif.onclick = function() {
QR.open();
QR.captcha.nodes.input.focus();
return window.focus();
};
notif.onshow = function() {
return setTimeout(function() {
return notif.close();
}, 7 * $.SECOND);
};
}
if (!(Conf['Persistent QR'] || QR.cooldown.auto)) { if (!(Conf['Persistent QR'] || QR.cooldown.auto)) {
QR.close(); QR.close();
} else { } else {
@ -9525,24 +9507,21 @@
QR.captcha = { QR.captcha = {
init: function() { init: function() {
var container, imgContainer, input; var imgContainer, input;
if (d.cookie.indexOf('pass_enabled=1') >= 0) { if (d.cookie.indexOf('pass_enabled=1') >= 0) {
return; return;
} }
container = $.id('captchaContainer'); if (!(this.isEnabled = !!$.id('captchaContainer'))) {
if (!(this.isEnabled = !!container)) {
return; return;
} }
imgContainer = $.el('div', { imgContainer = $.el('div', {
className: 'captcha-img', className: 'captcha-img',
title: 'Reload reCAPTCHA', title: 'Reload reCAPTCHA',
innerHTML: '<div><img></div>', innerHTML: '<div><img></div>'
hidden: true
}); });
input = $.el('input', { input = $.el('input', {
className: 'captcha-input field', className: 'captcha-input field',
title: 'Verification', title: 'Verification',
placeholder: 'Focus to load reCAPTCHA',
autocomplete: 'off', autocomplete: 'off',
spellcheck: false, spellcheck: false,
tabIndex: 45 tabIndex: 45
@ -9551,47 +9530,41 @@
img: imgContainer.firstChild.firstChild, img: imgContainer.firstChild.firstChild,
input: input input: input
}; };
$.on(input, 'focus', this.setup);
$.on(input, 'blur', QR.focusout); $.on(input, 'blur', QR.focusout);
$.on(input, 'focus', QR.focusin); $.on(input, 'focus', QR.focusin);
$.addClass(QR.nodes.el, 'has-captcha'); $.addClass(QR.nodes.el, 'has-captcha');
$.after(QR.nodes.com.parentNode, [imgContainer, input]); $.after(QR.nodes.com.parentNode, [imgContainer, input]);
this.beforeSetup();
return this.afterSetup();
},
beforeSetup: function() {
var img, input, _ref;
_ref = this.nodes, img = _ref.img, input = _ref.input;
img.parentNode.parentNode.hidden = true;
input.value = '';
input.placeholder = 'Focus to load reCAPTCHA';
$.on(input, 'focus', this.setup);
this.setupObserver = new MutationObserver(this.afterSetup); this.setupObserver = new MutationObserver(this.afterSetup);
this.setupObserver.observe(container, { return this.setupObserver.observe($.id('captchaContainer'), {
childList: true childList: true
}); });
if (Conf['Auto-load captcha']) {
this.setup();
}
return this.afterSetup();
}, },
setup: function() { setup: function() {
return $.globalEval('loadRecaptcha()'); return $.globalEval('loadRecaptcha()');
}, },
afterSetup: function() { afterSetup: function() {
var challenge, img, input, setLifetime, _ref; var challenge, img, input, _ref;
if (!(challenge = $.id('recaptcha_challenge_field_holder'))) { if (!(challenge = $.id('recaptcha_challenge_field_holder'))) {
return; return;
} }
QR.captcha.setupObserver.disconnect(); QR.captcha.setupObserver.disconnect();
delete QR.captcha.setupObserver; delete QR.captcha.setupObserver;
setLifetime = function(e) {
return 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);
_ref = QR.captcha.nodes, img = _ref.img, input = _ref.input; _ref = QR.captcha.nodes, img = _ref.img, input = _ref.input;
img.parentNode.parentNode.hidden = false; img.parentNode.parentNode.hidden = false;
input.placeholder = 'Verification';
$.off(input, 'focus', QR.captcha.setup); $.off(input, 'focus', QR.captcha.setup);
$.on(input, 'keydown', QR.captcha.keydown.bind(QR.captcha)); $.on(input, 'keydown', QR.captcha.keydown.bind(QR.captcha));
$.on(img.parentNode, 'click', QR.captcha.reload.bind(QR.captcha)); $.on(img.parentNode, 'click', QR.captcha.reload.bind(QR.captcha));
$.get('captchas', [], function(_arg) {
var captchas;
captchas = _arg.captchas;
return QR.captcha.sync(captchas);
});
$.sync('captchas', QR.captcha.sync);
QR.captcha.nodes.challenge = challenge; QR.captcha.nodes.challenge = challenge;
new MutationObserver(QR.captcha.load.bind(QR.captcha)).observe(challenge, { new MutationObserver(QR.captcha.load.bind(QR.captcha)).observe(challenge, {
childList: true, childList: true,
@ -9600,94 +9573,31 @@
}); });
return QR.captcha.load(); return QR.captcha.load();
}, },
sync: function(captchas) { destroy: function() {
QR.captcha.captchas = captchas; $.globalEval('Recaptcha.destroy()');
return QR.captcha.count(); return this.beforeSetup();
}, },
getOne: function() { getOne: function() {
var captcha, challenge, response; var challenge, response;
this.clear(); challenge = this.nodes.img.alt;
if (captcha = this.captchas.shift()) { response = this.nodes.input.value.trim();
challenge = captcha.challenge, response = captcha.response; if (response && !/\s/.test(response)) {
this.count(); response = "" + response + " " + response;
$.set('captchas', this.captchas);
} else {
challenge = this.nodes.img.alt;
if (response = this.nodes.input.value) {
this.reload();
}
}
if (response) {
response = response.trim();
if (!/\s/.test(response)) {
response += " " + response;
}
} }
return { return {
challenge: challenge, challenge: challenge,
response: response response: response
}; };
}, },
save: function() {
var response;
if (!(response = this.nodes.input.value.trim())) {
return;
}
this.captchas.push({
challenge: this.nodes.img.alt,
response: response,
timeout: this.timeout
});
this.count();
this.reload();
return $.set('captchas', this.captchas);
},
clear: function() {
var captcha, i, now, _i, _len, _ref;
if (!this.captchas) {
return;
}
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() { load: function() {
var challenge; var challenge;
if (!this.nodes.challenge.firstChild) { if (!this.nodes.challenge.firstChild) {
return; return;
} }
this.timeout = Date.now() + this.lifetime * $.SECOND - $.MINUTE;
challenge = this.nodes.challenge.firstChild.value; challenge = this.nodes.challenge.firstChild.value;
this.nodes.img.alt = challenge; this.nodes.img.alt = challenge;
this.nodes.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge; this.nodes.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge;
this.nodes.input.value = null; return this.nodes.input.value = null;
return this.clear();
},
count: function() {
var count;
count = this.captchas ? this.captchas.length : 0;
this.nodes.input.placeholder = (function() {
switch (count) {
case 0:
return 'Verification (Shift + Enter to cache)';
case 1:
return 'Verification (1 cached captcha)';
default:
return "Verification (" + count + " cached captchas)";
}
})();
return this.nodes.input.alt = count;
}, },
reload: function(focus) { reload: function(focus) {
$.globalEval('Recaptcha.reload("t")'); $.globalEval('Recaptcha.reload("t")');
@ -9698,8 +9608,6 @@
keydown: function(e) { keydown: function(e) {
if (e.keyCode === 8 && !this.nodes.input.value) { if (e.keyCode === 8 && !this.nodes.input.value) {
this.reload(); this.reload();
} else if (e.keyCode === 13 && e.shiftKey) {
this.save();
} else { } else {
return; return;
} }
@ -10031,6 +9939,9 @@
node.disabled = lock; node.disabled = lock;
} }
} }
if (QR.captcha.isEnabled) {
QR.captcha.nodes.input.disabled = lock;
}
this.nodes.rm.style.visibility = lock ? 'hidden' : ''; this.nodes.rm.style.visibility = lock ? 'hidden' : '';
(lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput); (lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput);
this.nodes.spoiler.disabled = lock; this.nodes.spoiler.disabled = lock;
@ -16133,18 +16044,18 @@
if (this.isClone) { if (this.isClone) {
return; return;
} }
return this.nodes.date.textContent = Time.funk(Time, this.info.date); return this.nodes.date.textContent = Time.funk(this.info.date);
}, },
createFunc: function(format) { createFunc: function(format) {
var code; var code;
code = format.replace(/%([A-Za-z])/g, function(s, c) { code = format.replace(/%([A-Za-z])/g, function(s, c) {
if (c in Time.formatters) { if (c in Time.formatters) {
return "' + Time.formatters." + c + ".call(date) + '"; return "' + this.formatters." + c + ".call(date) + '";
} else { } else {
return s; return s;
} }
}); });
return Function('Time', 'date', "return '" + code + "'"); return Function('date', "return '" + code + "'");
}, },
day: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], day: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
month: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], month: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],

View File

@ -1,6 +1,6 @@
// Generated by CoffeeScript // Generated by CoffeeScript
/* /*
* appchan x - Version 2.9.9 - 2014-03-27 * appchan x - Version 2.9.9 - 2014-04-02
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@ -8116,6 +8116,9 @@
if (Conf['Comment Expansion']) { if (Conf['Comment Expansion']) {
ExpandComment.callbacks.push(this.node); ExpandComment.callbacks.push(this.node);
} }
if (Conf['Embedding'] || Conf['Link Title']) {
this.embedProcess = Function('link', "var data = this.services(link); if (data) { " + ((Conf['Embedding'] ? 'this.embed(data);\n' : '') + (Conf['Title Link'] ? 'this.title(data);' : '')) + " }");
}
return Post.callbacks.push({ return Post.callbacks.push({
name: 'Linkify', name: 'Linkify',
cb: this.node cb: this.node
@ -8185,17 +8188,7 @@
Linkify.embedProcess(Linkify.makeLink(link, this)); Linkify.embedProcess(Linkify.makeLink(link, this));
} }
}, },
embedProcess: function(link) { embedProcess: function() {},
var data;
if (data = Linkify.services(link)) {
if (Conf['Embedding']) {
Linkify.embed(data);
}
if (Conf['Link Title']) {
return Linkify.title(data);
}
}
},
regString: /((https?|mailto|git|magnet|ftp|irc):([a-z\d%\/])|[-a-z\d]+[.](aero|asia|biz|cat|com|coop|info|int|jobs|mobi|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2})([:\/]|(?!.))|[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}|[-\w\d.@]+@[a-z\d.-]+\.[a-z\d])/i, regString: /((https?|mailto|git|magnet|ftp|irc):([a-z\d%\/])|[-a-z\d]+[.](aero|asia|biz|cat|com|coop|info|int|jobs|mobi|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2})([:\/]|(?!.))|[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}|[-\w\d.@]+@[a-z\d.-]+\.[a-z\d])/i,
makeRange: function(startNode, endNode, startOffset, endOffset) { makeRange: function(startNode, endNode, startOffset, endOffset) {
var range; var range;
@ -9398,6 +9391,10 @@
onload: QR.response, onload: QR.response,
onerror: function() { onerror: function() {
delete QR.req; delete QR.req;
if (QR.captcha.isEnabled) {
QR.captcha.destroy();
QR.captcha.setup();
}
post.unlock(); post.unlock();
QR.cooldown.auto = false; QR.cooldown.auto = false;
QR.status(); QR.status();
@ -9427,9 +9424,12 @@
return QR.status(); return QR.status();
}, },
response: function() { response: function() {
var URL, ban, board, captchasCount, err, h1, isReply, m, notif, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1; var URL, ban, board, err, h1, isReply, m, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1;
req = QR.req; req = QR.req;
delete QR.req; delete QR.req;
if (QR.captcha.isEnabled) {
QR.captcha.destroy();
}
post = QR.posts[0]; post = QR.posts[0];
post.unlock(); post.unlock();
resDoc = req.response; resDoc = req.response;
@ -9451,15 +9451,13 @@
if (/captcha|verification/i.test(err.textContent) || err === 'Connection error with sys.4chan.org.') { if (/captcha|verification/i.test(err.textContent) || err === 'Connection error with sys.4chan.org.') {
if (/mistyped/i.test(err.textContent)) { if (/mistyped/i.test(err.textContent)) {
err = 'You seem to have mistyped the CAPTCHA.'; err = 'You seem to have mistyped the CAPTCHA.';
} else if (/expired/i.test(err.textContent)) {
err = 'This CAPTCHA is no longer valid because it has expired.';
} }
QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : err === 'Connection error with sys.4chan.org.' ? true : false; QR.cooldown.auto = false;
QR.cooldown.set({ QR.cooldown.set({
delay: 2 delay: 2
}); });
} else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) { } else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) {
QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : true; QR.cooldown.auto = !QR.captcha.isEnabled;
QR.cooldown.set({ QR.cooldown.set({
delay: m[1] delay: m[1]
}); });
@ -9498,22 +9496,6 @@
}); });
postsCount = QR.posts.length - 1; postsCount = QR.posts.length - 1;
QR.cooldown.auto = postsCount && isReply; QR.cooldown.auto = postsCount && isReply;
if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) {
notif = new Notification('Quick reply warning', {
body: "You are running low on cached captchas. Cache count: " + captchasCount + ".",
icon: Favicon.logo
});
notif.onclick = function() {
QR.open();
QR.captcha.nodes.input.focus();
return window.focus();
};
notif.onshow = function() {
return setTimeout(function() {
return notif.close();
}, 7 * $.SECOND);
};
}
if (!(Conf['Persistent QR'] || QR.cooldown.auto)) { if (!(Conf['Persistent QR'] || QR.cooldown.auto)) {
QR.close(); QR.close();
} else { } else {
@ -9569,24 +9551,21 @@
QR.captcha = { QR.captcha = {
init: function() { init: function() {
var container, imgContainer, input; var imgContainer, input;
if (d.cookie.indexOf('pass_enabled=1') >= 0) { if (d.cookie.indexOf('pass_enabled=1') >= 0) {
return; return;
} }
container = $.id('captchaContainer'); if (!(this.isEnabled = !!$.id('captchaContainer'))) {
if (!(this.isEnabled = !!container)) {
return; return;
} }
imgContainer = $.el('div', { imgContainer = $.el('div', {
className: 'captcha-img', className: 'captcha-img',
title: 'Reload reCAPTCHA', title: 'Reload reCAPTCHA',
innerHTML: '<div><img></div>', innerHTML: '<div><img></div>'
hidden: true
}); });
input = $.el('input', { input = $.el('input', {
className: 'captcha-input field', className: 'captcha-input field',
title: 'Verification', title: 'Verification',
placeholder: 'Focus to load reCAPTCHA',
autocomplete: 'off', autocomplete: 'off',
spellcheck: false, spellcheck: false,
tabIndex: 45 tabIndex: 45
@ -9595,47 +9574,41 @@
img: imgContainer.firstChild.firstChild, img: imgContainer.firstChild.firstChild,
input: input input: input
}; };
$.on(input, 'focus', this.setup);
$.on(input, 'blur', QR.focusout); $.on(input, 'blur', QR.focusout);
$.on(input, 'focus', QR.focusin); $.on(input, 'focus', QR.focusin);
$.addClass(QR.nodes.el, 'has-captcha'); $.addClass(QR.nodes.el, 'has-captcha');
$.after(QR.nodes.com.parentNode, [imgContainer, input]); $.after(QR.nodes.com.parentNode, [imgContainer, input]);
this.beforeSetup();
return this.afterSetup();
},
beforeSetup: function() {
var img, input, _ref;
_ref = this.nodes, img = _ref.img, input = _ref.input;
img.parentNode.parentNode.hidden = true;
input.value = '';
input.placeholder = 'Focus to load reCAPTCHA';
$.on(input, 'focus', this.setup);
this.setupObserver = new MutationObserver(this.afterSetup); this.setupObserver = new MutationObserver(this.afterSetup);
this.setupObserver.observe(container, { return this.setupObserver.observe($.id('captchaContainer'), {
childList: true childList: true
}); });
if (Conf['Auto-load captcha']) {
this.setup();
}
return this.afterSetup();
}, },
setup: function() { setup: function() {
return $.globalEval('loadRecaptcha()'); return $.globalEval('loadRecaptcha()');
}, },
afterSetup: function() { afterSetup: function() {
var challenge, img, input, setLifetime, _ref; var challenge, img, input, _ref;
if (!(challenge = $.id('recaptcha_challenge_field_holder'))) { if (!(challenge = $.id('recaptcha_challenge_field_holder'))) {
return; return;
} }
QR.captcha.setupObserver.disconnect(); QR.captcha.setupObserver.disconnect();
delete QR.captcha.setupObserver; delete QR.captcha.setupObserver;
setLifetime = function(e) {
return 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);
_ref = QR.captcha.nodes, img = _ref.img, input = _ref.input; _ref = QR.captcha.nodes, img = _ref.img, input = _ref.input;
img.parentNode.parentNode.hidden = false; img.parentNode.parentNode.hidden = false;
input.placeholder = 'Verification';
$.off(input, 'focus', QR.captcha.setup); $.off(input, 'focus', QR.captcha.setup);
$.on(input, 'keydown', QR.captcha.keydown.bind(QR.captcha)); $.on(input, 'keydown', QR.captcha.keydown.bind(QR.captcha));
$.on(img.parentNode, 'click', QR.captcha.reload.bind(QR.captcha)); $.on(img.parentNode, 'click', QR.captcha.reload.bind(QR.captcha));
$.get('captchas', [], function(_arg) {
var captchas;
captchas = _arg.captchas;
return QR.captcha.sync(captchas);
});
$.sync('captchas', QR.captcha.sync);
QR.captcha.nodes.challenge = challenge; QR.captcha.nodes.challenge = challenge;
new MutationObserver(QR.captcha.load.bind(QR.captcha)).observe(challenge, { new MutationObserver(QR.captcha.load.bind(QR.captcha)).observe(challenge, {
childList: true, childList: true,
@ -9644,94 +9617,31 @@
}); });
return QR.captcha.load(); return QR.captcha.load();
}, },
sync: function(captchas) { destroy: function() {
QR.captcha.captchas = captchas; $.globalEval('Recaptcha.destroy()');
return QR.captcha.count(); return this.beforeSetup();
}, },
getOne: function() { getOne: function() {
var captcha, challenge, response; var challenge, response;
this.clear(); challenge = this.nodes.img.alt;
if (captcha = this.captchas.shift()) { response = this.nodes.input.value.trim();
challenge = captcha.challenge, response = captcha.response; if (response && !/\s/.test(response)) {
this.count(); response = "" + response + " " + response;
$.set('captchas', this.captchas);
} else {
challenge = this.nodes.img.alt;
if (response = this.nodes.input.value) {
this.reload();
}
}
if (response) {
response = response.trim();
if (!/\s/.test(response)) {
response += " " + response;
}
} }
return { return {
challenge: challenge, challenge: challenge,
response: response response: response
}; };
}, },
save: function() {
var response;
if (!(response = this.nodes.input.value.trim())) {
return;
}
this.captchas.push({
challenge: this.nodes.img.alt,
response: response,
timeout: this.timeout
});
this.count();
this.reload();
return $.set('captchas', this.captchas);
},
clear: function() {
var captcha, i, now, _i, _len, _ref;
if (!this.captchas) {
return;
}
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() { load: function() {
var challenge; var challenge;
if (!this.nodes.challenge.firstChild) { if (!this.nodes.challenge.firstChild) {
return; return;
} }
this.timeout = Date.now() + this.lifetime * $.SECOND - $.MINUTE;
challenge = this.nodes.challenge.firstChild.value; challenge = this.nodes.challenge.firstChild.value;
this.nodes.img.alt = challenge; this.nodes.img.alt = challenge;
this.nodes.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge; this.nodes.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge;
this.nodes.input.value = null; return this.nodes.input.value = null;
return this.clear();
},
count: function() {
var count;
count = this.captchas ? this.captchas.length : 0;
this.nodes.input.placeholder = (function() {
switch (count) {
case 0:
return 'Verification (Shift + Enter to cache)';
case 1:
return 'Verification (1 cached captcha)';
default:
return "Verification (" + count + " cached captchas)";
}
})();
return this.nodes.input.alt = count;
}, },
reload: function(focus) { reload: function(focus) {
$.globalEval('Recaptcha.reload("t")'); $.globalEval('Recaptcha.reload("t")');
@ -9742,8 +9652,6 @@
keydown: function(e) { keydown: function(e) {
if (e.keyCode === 8 && !this.nodes.input.value) { if (e.keyCode === 8 && !this.nodes.input.value) {
this.reload(); this.reload();
} else if (e.keyCode === 13 && e.shiftKey) {
this.save();
} else { } else {
return; return;
} }
@ -10069,6 +9977,9 @@
node.disabled = lock; node.disabled = lock;
} }
} }
if (QR.captcha.isEnabled) {
QR.captcha.nodes.input.disabled = lock;
}
this.nodes.rm.style.visibility = lock ? 'hidden' : ''; this.nodes.rm.style.visibility = lock ? 'hidden' : '';
(lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput); (lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput);
this.nodes.spoiler.disabled = lock; this.nodes.spoiler.disabled = lock;
@ -16154,18 +16065,18 @@
if (this.isClone) { if (this.isClone) {
return; return;
} }
return this.nodes.date.textContent = Time.funk(Time, this.info.date); return this.nodes.date.textContent = Time.funk(this.info.date);
}, },
createFunc: function(format) { createFunc: function(format) {
var code; var code;
code = format.replace(/%([A-Za-z])/g, function(s, c) { code = format.replace(/%([A-Za-z])/g, function(s, c) {
if (c in Time.formatters) { if (c in Time.formatters) {
return "' + Time.formatters." + c + ".call(date) + '"; return "' + this.formatters." + c + ".call(date) + '";
} else { } else {
return s; return s;
} }
}); });
return Function('Time', 'date', "return '" + code + "'"); return Function('date', "return '" + code + "'");
}, },
day: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], day: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
month: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], month: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],

View File

@ -5,6 +5,17 @@ Linkify =
if Conf['Comment Expansion'] if Conf['Comment Expansion']
ExpandComment.callbacks.push @node ExpandComment.callbacks.push @node
if Conf['Embedding'] or Conf['Link Title']
@embedProcess = Function 'link',
"var data = this.services(link);
if (data) {
#{
(if Conf['Embedding'] then 'this.embed(data);\n' else '') +
if Conf['Title Link'] then 'this.title(data);' else ''
}
}
"
Post.callbacks.push Post.callbacks.push
name: 'Linkify' name: 'Linkify'
cb: @node cb: @node
@ -63,10 +74,7 @@ Linkify =
Linkify.embedProcess Linkify.makeLink link, @ Linkify.embedProcess Linkify.makeLink link, @
return return
embedProcess: (link) -> embedProcess: -> return
if data = Linkify.services link
Linkify.embed data if Conf['Embedding']
Linkify.title data if Conf['Link Title']
regString: ///( regString: ///(
# http, magnet, ftp, etc # http, magnet, ftp, etc

View File

@ -8,14 +8,14 @@ Time =
cb: @node cb: @node
node: -> node: ->
return if @isClone return if @isClone
@nodes.date.textContent = Time.funk Time, @info.date @nodes.date.textContent = Time.funk @info.date
createFunc: (format) -> createFunc: (format) ->
code = format.replace /%([A-Za-z])/g, (s, c) -> code = format.replace /%([A-Za-z])/g, (s, c) ->
if c of Time.formatters if c of Time.formatters
"' + Time.formatters.#{c}.call(date) + '" "' + this.formatters.#{c}.call(date) + '"
else else
s s
Function 'Time', 'date', "return '#{code}'" Function 'date', "return '#{code}'"
day: [ day: [
'Sunday' 'Sunday'
'Monday' 'Monday'

View File

@ -1,19 +1,16 @@
QR.captcha = QR.captcha =
init: -> init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0 return if d.cookie.indexOf('pass_enabled=1') >= 0
container = $.id 'captchaContainer' return unless @isEnabled = !!$.id 'captchaContainer'
return unless @isEnabled = !!container
imgContainer = $.el 'div', imgContainer = $.el 'div',
className: 'captcha-img' className: 'captcha-img'
title: 'Reload reCAPTCHA' title: 'Reload reCAPTCHA'
innerHTML: '<div><img></div>' innerHTML: '<div><img></div>'
hidden: true
input = $.el 'input', input = $.el 'input',
className: 'captcha-input field' className: 'captcha-input field'
title: 'Verification' title: 'Verification'
placeholder: 'Focus to load reCAPTCHA'
autocomplete: 'off' autocomplete: 'off'
spellcheck: false spellcheck: false
tabIndex: 45 tabIndex: 45
@ -22,20 +19,25 @@ QR.captcha =
img: imgContainer.firstChild.firstChild img: imgContainer.firstChild.firstChild
input: input input: input
$.on input, 'focus', @setup
$.on input, 'blur', QR.focusout $.on input, 'blur', QR.focusout
$.on input, 'focus', QR.focusin $.on input, 'focus', QR.focusin
$.addClass QR.nodes.el, 'has-captcha' $.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.com.parentNode, [imgContainer, input] $.after QR.nodes.com.parentNode, [imgContainer, input]
@setupObserver = new MutationObserver @afterSetup @beforeSetup()
@setupObserver.observe container, childList: true
@setup() if Conf['Auto-load captcha']
@afterSetup() # reCAPTCHA might have loaded before the QR. @afterSetup() # reCAPTCHA might have loaded before the QR.
beforeSetup: ->
{img, input} = @nodes
img.parentNode.parentNode.hidden = true
input.value = ''
input.placeholder = 'Focus to load reCAPTCHA'
$.on input, 'focus', @setup
@setupObserver = new MutationObserver @afterSetup
@setupObserver.observe $.id('captchaContainer'), childList: true
setup: -> setup: ->
$.globalEval 'loadRecaptcha()' $.globalEval 'loadRecaptcha()'
@ -44,21 +46,13 @@ QR.captcha =
QR.captcha.setupObserver.disconnect() QR.captcha.setupObserver.disconnect()
delete QR.captcha.setupObserver delete QR.captcha.setupObserver
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
{img, input} = QR.captcha.nodes {img, input} = QR.captcha.nodes
img.parentNode.parentNode.hidden = false img.parentNode.parentNode.hidden = false
$.off input, 'focus', QR.captcha.setup input.placeholder = 'Verification'
$.off input, 'focus', QR.captcha.setup
$.on input, 'keydown', QR.captcha.keydown.bind QR.captcha $.on input, 'keydown', QR.captcha.keydown.bind QR.captcha
$.on img.parentNode, 'click', QR.captcha.reload.bind QR.captcha $.on img.parentNode, 'click', QR.captcha.reload.bind QR.captcha
$.get 'captchas', [], ({captchas}) ->
QR.captcha.sync captchas
$.sync 'captchas', QR.captcha.sync
QR.captcha.nodes.challenge = challenge QR.captcha.nodes.challenge = challenge
new MutationObserver(QR.captcha.load.bind QR.captcha).observe challenge, new MutationObserver(QR.captcha.load.bind QR.captcha).observe challenge,
childList: true childList: true
@ -66,68 +60,27 @@ QR.captcha =
attributes: true attributes: true
QR.captcha.load() QR.captcha.load()
sync: (captchas) -> destroy: ->
QR.captcha.captchas = captchas $.globalEval 'Recaptcha.destroy()'
QR.captcha.count() @beforeSetup()
getOne: -> getOne: ->
@clear() challenge = @nodes.img.alt
if captcha = @captchas.shift() response = @nodes.input.value.trim()
{challenge, response} = captcha if response and !/\s/.test response
@count()
$.set 'captchas', @captchas
else
challenge = @nodes.img.alt
if response = @nodes.input.value then @reload()
if response
response = response.trim()
# one-word-captcha: # one-word-captcha:
# If there's only one word, duplicate it. # If there's only one word, duplicate it.
response += " #{response}" unless /\s/.test response response = "#{response} #{response}"
{challenge, response} {challenge, response}
save: ->
return unless response = @nodes.input.value.trim()
@captchas.push
challenge: @nodes.img.alt
response: response
timeout: @timeout
@count()
@reload()
$.set 'captchas', @captchas
clear: ->
return unless @captchas # not loaded yet.
now = Date.now()
for captcha, i in @captchas
break if captcha.timeout > now
return unless i
@captchas = @captchas[i..]
@count()
$.set 'captchas', @captchas
load: -> load: ->
return unless @nodes.challenge.firstChild return unless @nodes.challenge.firstChild
# -1 minute to give upload some time. # -1 minute to give upload some time.
@timeout = Date.now() + @lifetime * $.SECOND - $.MINUTE
challenge = @nodes.challenge.firstChild.value challenge = @nodes.challenge.firstChild.value
@nodes.img.alt = challenge @nodes.img.alt = challenge
@nodes.img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}" @nodes.img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}"
@nodes.input.value = null @nodes.input.value = null
@clear()
count: ->
count = if @captchas then @captchas.length else 0
@nodes.input.placeholder = switch count
when 0
'Verification (Shift + Enter to cache)'
when 1
'Verification (1 cached captcha)'
else
"Verification (#{count} cached captchas)"
@nodes.input.alt = count
reload: (focus) -> reload: (focus) ->
# the 't' argument prevents the input from being focused # the 't' argument prevents the input from being focused
@ -138,8 +91,6 @@ QR.captcha =
keydown: (e) -> keydown: (e) ->
if e.keyCode is 8 and not @nodes.input.value if e.keyCode is 8 and not @nodes.input.value
@reload() @reload()
else if e.keyCode is 13 and e.shiftKey
@save()
else else
return return
e.preventDefault() e.preventDefault()

View File

@ -701,6 +701,9 @@ QR =
onerror: -> onerror: ->
# Connection error, or www.4chan.org/banned # Connection error, or www.4chan.org/banned
delete QR.req delete QR.req
if QR.captcha.isEnabled
QR.captcha.destroy()
QR.captcha.setup()
post.unlock() post.unlock()
QR.cooldown.auto = false QR.cooldown.auto = false
QR.status() QR.status()
@ -734,6 +737,7 @@ QR =
{req} = QR {req} = QR
delete QR.req delete QR.req
QR.captcha.destroy() if QR.captcha.isEnabled
post = QR.posts[0] post = QR.posts[0]
post.unlock() post.unlock()
@ -763,25 +767,12 @@ QR =
# Remove the obnoxious 4chan Pass ad. # Remove the obnoxious 4chan Pass ad.
if /mistyped/i.test err.textContent if /mistyped/i.test err.textContent
err = 'You seem to have mistyped the CAPTCHA.' err = 'You seem to have mistyped the CAPTCHA.'
else if /expired/i.test err.textContent QR.cooldown.auto = false
err = 'This CAPTCHA is no longer valid because it has expired.'
# Enable auto-post if we have some cached captchas.
QR.cooldown.auto = if QR.captcha.isEnabled
!!QR.captcha.captchas.length
else if err is 'Connection error with sys.4chan.org.'
true
else
# Something must've gone terribly wrong if you get captcha errors without captchas.
# Don't auto-post indefinitely in that case.
false
# Too many frequent mistyped captchas will auto-ban you! # Too many frequent mistyped captchas will auto-ban you!
# On connection error, the post most likely didn't go through. # On connection error, the post most likely didn't go through.
QR.cooldown.set delay: 2 QR.cooldown.set delay: 2
else if err.textContent and m = err.textContent.match /wait\s+(\d+)\s+second/i else if err.textContent and m = err.textContent.match /wait\s+(\d+)\s+second/i
QR.cooldown.auto = if QR.captcha.isEnabled QR.cooldown.auto = !QR.captcha.isEnabled
!!QR.captcha.captchas.length
else
true
QR.cooldown.set delay: m[1] QR.cooldown.set delay: m[1]
else # stop auto-posting else # stop auto-posting
QR.cooldown.auto = false QR.cooldown.auto = false
@ -821,18 +812,6 @@ QR =
# Enable auto-posting if we have stuff left to post, disable it otherwise. # Enable auto-posting if we have stuff left to post, disable it otherwise.
postsCount = QR.posts.length - 1 postsCount = QR.posts.length - 1
QR.cooldown.auto = postsCount and isReply QR.cooldown.auto = postsCount and isReply
if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount
notif = new Notification 'Quick reply warning',
body: "You are running low on cached captchas. Cache count: #{captchasCount}."
icon: Favicon.logo
notif.onclick = ->
QR.open()
QR.captcha.nodes.input.focus()
window.focus()
notif.onshow = ->
setTimeout ->
notif.close()
, 7 * $.SECOND
unless Conf['Persistent QR'] or QR.cooldown.auto unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close() QR.close()

View File

@ -91,6 +91,8 @@ QR.post = class
return unless @ is QR.selected return unless @ is QR.selected
for name in ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'] when node = QR.nodes[name] for name in ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'] when node = QR.nodes[name]
node.disabled = lock node.disabled = lock
if QR.captcha.isEnabled
QR.captcha.nodes.input.disabled = lock
@nodes.rm.style.visibility = if lock then 'hidden' else '' @nodes.rm.style.visibility = if lock then 'hidden' else ''
(if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput (if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
@nodes.spoiler.disabled = lock @nodes.spoiler.disabled = lock