From 168aa98096a92a88258949471cede00482174f13 Mon Sep 17 00:00:00 2001 From: Zixaphir Date: Fri, 12 Dec 2014 03:03:27 -0700 Subject: [PATCH] Merge @ccd0's capcha changes up to ccd0/4chan-x@bb806b453 --- LICENSE | 2 +- builds/appchan-x.user.js | 343 ++++++++++++++++++--------------- builds/crx/script.js | 341 +++++++++++++++++--------------- src/General/CrossOrigin.coffee | 99 ++++++++++ src/Posting/QR.captcha.coffee | 121 +++++++----- src/Posting/QR.coffee | 110 ++++------- src/Posting/QR.post.coffee | 26 ++- 7 files changed, 596 insertions(+), 446 deletions(-) create mode 100644 src/General/CrossOrigin.coffee diff --git a/LICENSE b/LICENSE index 5e6d6e09e..c2aae7430 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ /* -* appchan x - Version 2.9.40 - 2014-12-09 +* appchan x - Version 2.9.40 - 2014-12-12 * * Licensed under the MIT license. * https://github.com/zixaphir/appchan-x/blob/master/LICENSE diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js index 90a474e3c..b5d236d54 100644 --- a/builds/appchan-x.user.js +++ b/builds/appchan-x.user.js @@ -28,7 +28,7 @@ // ==/UserScript== /* -* appchan x - Version 2.9.40 - 2014-12-09 +* appchan x - Version 2.9.40 - 2014-12-12 * * Licensed under the MIT license. * https://github.com/zixaphir/appchan-x/blob/master/LICENSE @@ -8819,6 +8819,9 @@ return QR.captcha.destroy(); }, focusin: function() { + if ($.hasClass(QR.nodes.el, 'autohide') && !$.hasClass(QR.nodes.el, 'focus')) { + QR.captcha.setup(); + } return $.addClass(QR.nodes.el, 'focus'); }, focusout: function() { @@ -8850,7 +8853,7 @@ el.removeAttribute('style'); } if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) { - QR.captcha.setup(); + QR.captcha.setup(true); } QR.notify(el); if (d.hidden) { @@ -9007,6 +9010,9 @@ }, paste: function(e) { var blob, files, item, _i, _len, _ref; + if (!e.clipboardData.items) { + return; + } files = []; _ref = e.clipboardData.items; for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -9028,45 +9034,16 @@ QR.handleFiles(files); return $.addClass(QR.nodes.el, 'dump'); }, - handleBlob: function(urlBlob, contentType, contentDisposition, url) { - var blob, match, mime, name, _ref, _ref1, _ref2; - name = (_ref = url.match(/([^\/]+)\/*$/)) != null ? _ref[1] : void 0; - mime = (contentType != null ? contentType.match(/[^;]*/)[0] : void 0) || 'application/octet-stream'; - match = (contentDisposition != null ? (_ref1 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? _ref1[1] : void 0 : void 0) || (contentType != null ? (_ref2 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? _ref2[1] : void 0 : void 0); - if (match) { - name = match.replace(/\\"/g, '"'); - } - blob = new Blob([urlBlob], { - type: mime - }); - blob.name = name; - return QR.handleFiles([blob]); - }, handleUrl: function() { var url; - url = prompt("Enter a URL:"); + url = prompt('Enter a URL:'); if (url === null) { return; } - return GM_xmlhttpRequest({ - method: "GET", - url: url, - overrideMimeType: "text/plain; charset=x-user-defined", - onload: function(xhr) { - var contentDisposition, contentType, data, h, i, r; - r = xhr.responseText; - h = xhr.responseHeaders; - data = new Uint8Array(r.length); - i = 0; - while (i < r.length) { - data[i] = r.charCodeAt(i); - i++; - } - contentType = (h.match(/Content-Type:\s*(.*)/i) || [])[1]; - contentDisposition = (h.match(/Content-Disposition:\s*(.*)/i) || [])[1]; - return QR.handleBlob(data, contentType, contentDisposition, url); - }, - onerror: function(xhr) { + return CrossOrigin.file(url, function(blob) { + if (blob) { + return QR.handleFiles([blob]); + } else { return QR.error("Can't load image."); } }); @@ -9090,12 +9067,12 @@ } }, handleFile: function(file, index, nfiles) { - var isSingle, max, post, _ref; + var err, isSingle, max, post, _ref; isSingle = nfiles === 1; if (/^text\//.test(file.type)) { if (isSingle) { post = QR.selected; - } else if ((post = QR.posts[QR.posts.length - 1]).com) { + } else if (index !== 0 || (post = QR.posts[QR.posts.length - 1]).com) { post = new QR.post(); } post.pasteText(file); @@ -9118,7 +9095,12 @@ } else if ((post = QR.posts[QR.posts.length - 1]).file) { post = new QR.post(); } - return post.setFile(file); + try { + return post.setFile(file); + } catch (_error) { + err = _error; + return console.log(err); + } }, openFileInput: function(e) { var _ref; @@ -9126,10 +9108,12 @@ if (e.shiftKey && e.type === 'click') { return QR.selected.rmFile(); } - if (e.ctrlKey && e.type === 'click') { + if ((e.ctrlKey || e.metaKey) && e.type === 'click') { $.addClass(QR.nodes.filename, 'edit'); QR.nodes.filename.focus(); - return; + return $.on(QR.nodes.filename, 'blur', function() { + return $.rmClass(QR.nodes.filename, 'edit'); + }); } if (e.target.nodeName === 'INPUT' || (e.keyCode && ((_ref = e.keyCode) !== 32 && _ref !== 13)) || e.ctrlKey) { return; @@ -9165,7 +9149,7 @@ } }, dialog: function() { - var dialog, elm, event, i, items, name, node, nodes, prop, rules, save, setNode, _, _i, _len, _ref, _ref1, _ref2; + var dialog, elm, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode; QR.nodes = nodes = { el: dialog = UI.dialog('qr', 'top:0;right:0;', "
\uf00d
No selected fileSpoiler\uf0c1Post from URL+Dump\uf00dRemove File
") }; @@ -9197,23 +9181,20 @@ setNode('status', '[type=submit]'); setNode('fileInput', '[type=file]'); rules = $('ul.rules').textContent.trim(); - QR.min_width = QR.min_height = 1; - QR.max_width = QR.max_height = 10000; - try { - _ref = rules.match(/.+smaller than (\d+)x(\d+).+/), _ = _ref[0], QR.min_width = _ref[1], QR.min_height = _ref[2]; - _ref1 = rules.match(/.+greater than (\d+)x(\d+).+/), _ = _ref1[0], QR.max_width = _ref1[1], QR.max_height = _ref1[2]; - _ref2 = ['min_width', 'min_height', 'max_width', 'max_height']; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - prop = _ref2[_i]; - QR[prop] = parseInt(QR[prop], 10); - } - } catch (_error) { - null; - } + match_min = rules.match(/.+smaller than (\d+)x(\d+).+/); + match_max = rules.match(/.+greater than (\d+)x(\d+).+/); + QR.min_width = +(match_min != null ? match_min[1] : void 0) || 1; + QR.min_height = +(match_min != null ? match_min[2] : void 0) || 1; + QR.max_width = +(match_max != null ? match_max[1] : void 0) || 10000; + QR.max_height = +(match_max != null ? match_max[2] : void 0) || 10000; nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value; QR.max_size_video = 3145728; QR.max_width_video = QR.max_height_video = 2048; QR.max_duration_video = 120; + QR.forcedAnon = !!$('form[name="post"] input[name="name"][type="hidden"]'); + if (QR.forcedAnon) { + $.addClass(QR.nodes.el, 'forced-anon'); + } QR.spoiler = !!$('.postForm input[name=spoiler]'); if (QR.spoiler) { $.addClass(QR.nodes.el, 'has-spoiler'); @@ -9224,9 +9205,10 @@ $.after(nodes.name.parentElement, nodes.dumpList.parentElement); nodes.addPost.tabIndex = 35; } - if (g.BOARD.ID === 'f') { + if (g.BOARD.ID === 'f' && g.VIEW !== 'thread') { nodes.flashTag = $.el('select', { - name: 'filetag', + name: 'filetag' + }, { innerHTML: "\n\n\n\n\n\n" }); nodes.flashTag.dataset["default"] = '4'; @@ -9413,9 +9395,9 @@ post.lock(); formData = { resto: threadID, - name: post.name, + name: !QR.forcedAnon ? post.name : void 0, email: post.email, - sub: post.sub, + sub: !(QR.forcedAnon || threadID) ? post.sub : void 0, com: post.com, upfile: post.file, filetag: filetag, @@ -9502,9 +9484,6 @@ } QR.status(); QR.error(err); - if (QR.captcha.isEnabled) { - QR.captcha.setup(); - } return; } h1 = $('h1', resDoc); @@ -9555,7 +9534,7 @@ QR.close(); } else { post.rm(); - QR.captcha.setup(); + QR.captcha.setup(true); } QR.cooldown.set({ req: req, @@ -9607,7 +9586,7 @@ QR.captcha = { init: function() { - var container, counter, section; + var counter, root; if (d.cookie.indexOf('pass_enabled=1') >= 0) { return; } @@ -9621,80 +9600,101 @@ return QR.captcha.sync(captchas); }); $.sync('captchas', this.sync.bind(this)); - section = $.el('div', { - className: 'captcha-section' + root = $.el('div', { + className: 'captcha-root' }); - $.extend(section, { - innerHTML: "
" + $.extend(root, { + innerHTML: "
" }); - container = $('.captcha-container', section); - counter = $('.captcha-counter > a', section); + counter = $('.captcha-counter > a', root); this.nodes = { - container: container, + root: root, counter: counter }; this.count(); $.addClass(QR.nodes.el, 'has-captcha'); - $.after(QR.nodes.com.parentNode, section); - new MutationObserver(this.afterSetup.bind(this)).observe(container, { - childList: true, - subtree: true - }); + $.after(QR.nodes.com.parentNode, root); $.on(counter, 'click', this.toggle.bind(this)); - return $.on(window, 'captcha:success', this.save.bind(this)); + return $.on(window, 'captcha:success', (function(_this) { + return function() { + return _this.save(false); + }; + })(this)); }, shouldFocus: false, timeouts: {}, + postsCount: 0, needed: function() { - var captchaCount, postsCount; + var captchaCount; captchaCount = this.captchas.length; - if (this.nodes.container.dataset.widgetID && !this.timeouts.destroy) { + if (this.nodes.container && !this.timeouts.destroy) { captchaCount++; } - postsCount = QR.posts.length; - if (postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { - postsCount = 0; + 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; + }, + 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; } - return captchaCount < postsCount; }, toggle: function() { - if (this.nodes.container.dataset.widgetID && !this.timeouts.destroy) { + if (this.nodes.container && !this.timeouts.destroy) { return this.destroy(); } else { - this.shouldFocus = true; - return this.setup(true); + return this.setup(true, true); } }, - setup: function(force) { + setup: function(focus, force) { 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.dataset.widgetID) { + if (this.nodes.container) { return; } - return $.globalEval('(function() {\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})();'); + 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, _i, _j, _len, _len1, _ref; + 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 (node.nodeName === 'IFRAME') { - iframe = node; + if (iframe = $.x('./descendant-or-self::iframe', node)) { + this.setupIFrame(iframe); + } + if (textarea = $.x('./descendant-or-self::textarea', node)) { + this.setupTextArea(textarea); } } } - if (!iframe) { - return; - } + }, + 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'; @@ -9704,16 +9704,28 @@ } 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'); - $.rmAll(this.nodes.container); - return this.nodes.container.removeAttribute('data-widget-i-d'); + 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(); @@ -9729,30 +9741,39 @@ return null; } }, - save: function(e) { - var _base; - if (this.needed()) { + save: function(pasted) { + var reload, _base; + $.forceSync('captchas'); + reload = (QR.cooldown.auto || Conf['Post on Captcha Completion']) && this.needed(); + this.captchas.push({ + response: $('textarea', this.nodes.container).value, + timeout: (pasted ? this.setupTime : Date.now()) + 2 * $.MINUTE + }); + this.count(); + $.set('captchas', this.captchas); + if (reload) { this.shouldFocus = true; this.reload(); } else { - this.nodes.counter.focus(); - if ((_base = this.timeouts).destroy == null) { - _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND); + 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(); } - $.forceSync('captchas'); - this.captchas.push({ - response: e.detail, - timeout: Date.now() + 2 * $.MINUTE - }); - this.count(); - return $.set('captchas', this.captchas); }, 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) { @@ -9767,7 +9788,7 @@ this.captchas = this.captchas.slice(i); this.count(); $.set('captchas', this.captchas); - return this.setup(); + return this.setup(true); }, count: function() { this.nodes.counter.textContent = "Captchas: " + this.captchas.length; @@ -10004,7 +10025,7 @@ QR.post = (function() { function _Class(select) { this.select = __bind(this.select, this); - var el, elm, event, prev, _i, _j, _len, _len1, _ref, _ref1, _ref2; + var el, elm, event, prev, _i, _j, _len, _len1, _ref, _ref1; el = $.el('a', { className: 'qr-preview', draggable: true, @@ -10051,9 +10072,8 @@ $.on(el, event.toLowerCase(), this[event]); } this.thread = g.VIEW === 'thread' ? g.THREADID : 'new'; - _ref2 = QR.posts, prev = _ref2[_ref2.length - 1]; + prev = QR.posts[QR.posts.length - 1]; QR.posts.push(this); - QR.captcha.setup(); this.nodes.spoiler.checked = this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false; QR.persona.get((function(_this) { return function(persona) { @@ -10072,6 +10092,9 @@ this.select(); } this.unlock(); + $.queueTask(function() { + return QR.captcha.setup(); + }); } _Class.prototype.rm = function() { @@ -10162,7 +10185,7 @@ return QR.status(); case 'com': this.nodes.span.textContent = this.com; - QR.captcha.setup(); + QR.captcha.onPostChange(); QR.characterCount(); if (QR.cooldown.auto && this === QR.posts[0] && (0 < (_ref = QR.cooldown.seconds) && _ref <= 5)) { return QR.cooldown.auto = false; @@ -10202,7 +10225,7 @@ if (QR.spoiler) { this.nodes.label.hidden = false; } - QR.captcha.setup(); + QR.captcha.onPostChange(); URL.revokeObjectURL(this.URL); if (this === QR.selected) { this.showFileData(); @@ -10220,55 +10243,55 @@ var el, fileURL, isVideo; isVideo = /^video\//.test(this.file.type); el = $.el((isVideo ? 'video' : 'img')); - $.on(el, (isVideo ? 'loadeddata' : 'load'), (function(_this) { - return function() { - var cv, error, errors, height, s, width, _i, _len; - errors = _this.checkDimensions(el, isVideo); - if (errors.length) { - for (_i = 0, _len = errors.length; _i < _len; _i++) { - error = errors[_i]; - QR.error(error); - } - _this.URL = fileURL; - if ((QR.posts.length === 1) || (_this.com && _this.com.length)) { - return _this.rmFile(); - } else { - return _this.rm(); - } + $.on(el, (isVideo ? 'loadeddata' : 'load'), function() { + var cv, error, errors, height, s, width, _i, _len; + errors = this.checkDimensions(el, isVideo); + if (errors.length) { + for (_i = 0, _len = errors.length; _i < _len; _i++) { + error = errors[_i]; + QR.error(error); } - s = 90 * 2 * window.devicePixelRatio; - if (_this.file.type === 'image/gif') { - s *= 3; - } - if (isVideo) { - height = el.videoHeight; - width = el.videoWidth; + this.URL = fileURL; + if ((QR.posts.length === 1) || (this.com && this.com.length)) { + return this.rmFile(); } else { - height = el.height, width = el.width; - if (height < s || width < s) { - _this.URL = fileURL; - _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")"; - return; - } + return this.rm(); } - if (height <= width) { - width = s / height * width; - height = s; - } else { - height = s / width * height; - width = s; + } + s = 90 * 2 * window.devicePixelRatio; + if (this.file.type === 'image/gif') { + s *= 3; + } + if (isVideo) { + height = el.videoHeight; + width = el.videoWidth; + } else { + height = el.height, width = el.width; + if (height < s || width < s) { + this.URL = fileURL; + this.nodes.el.style.backgroundImage = "url(" + this.URL + ")"; + return; } - cv = $.el('canvas'); - cv.height = el.height = height; - cv.width = el.width = width; - cv.getContext('2d').drawImage(el, 0, 0, width, height); - URL.revokeObjectURL(fileURL); - return cv.toBlob(function(blob) { + } + if (height <= width) { + width = s / height * width; + height = s; + } else { + height = s / width * height; + width = s; + } + cv = $.el('canvas'); + cv.height = el.height = height; + cv.width = el.width = width; + cv.getContext('2d').drawImage(el, 0, 0, width, height); + URL.revokeObjectURL(fileURL); + return cv.toBlob((function(_this) { + return function(blob) { _this.URL = URL.createObjectURL(blob); return _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")"; - }); - }; - })(this)); + }; + })(this)); + }); fileURL = URL.createObjectURL(this.file); return el.src = fileURL; }; @@ -10326,7 +10349,7 @@ _Class.prototype.updateFilename = function() { var title; - title = "" + this.filename + " (" + this.filesize + ")\nCtrl+click to edit filename. Shift+click to clear."; + title = "" + this.filename + " (" + this.filesize + ")\nCtrl/\u2318+click to edit filename. Shift+click to clear."; this.nodes.el.title = title; if (this !== QR.selected) { return; diff --git a/builds/crx/script.js b/builds/crx/script.js index 7be3032fa..6d737ad6c 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript /* -* appchan x - Version 2.9.40 - 2014-12-09 +* appchan x - Version 2.9.40 - 2014-12-12 * * Licensed under the MIT license. * https://github.com/zixaphir/appchan-x/blob/master/LICENSE @@ -8841,6 +8841,9 @@ return QR.captcha.destroy(); }, focusin: function() { + if ($.hasClass(QR.nodes.el, 'autohide') && !$.hasClass(QR.nodes.el, 'focus')) { + QR.captcha.setup(); + } return $.addClass(QR.nodes.el, 'focus'); }, focusout: function() { @@ -8872,7 +8875,7 @@ el.removeAttribute('style'); } if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) { - QR.captcha.setup(); + QR.captcha.setup(true); } QR.notify(el); if (d.hidden) { @@ -9038,6 +9041,9 @@ }, paste: function(e) { var blob, files, item, _i, _len, _ref; + if (!e.clipboardData.items) { + return; + } files = []; _ref = e.clipboardData.items; for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -9059,42 +9065,19 @@ QR.handleFiles(files); return $.addClass(QR.nodes.el, 'dump'); }, - handleBlob: function(urlBlob, contentType, contentDisposition, url) { - var blob, match, mime, name, _ref, _ref1, _ref2; - name = (_ref = url.match(/([^\/]+)\/*$/)) != null ? _ref[1] : void 0; - mime = (contentType != null ? contentType.match(/[^;]*/)[0] : void 0) || 'application/octet-stream'; - match = (contentDisposition != null ? (_ref1 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? _ref1[1] : void 0 : void 0) || (contentType != null ? (_ref2 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? _ref2[1] : void 0 : void 0); - if (match) { - name = match.replace(/\\"/g, '"'); - } - blob = new Blob([urlBlob], { - type: mime - }); - blob.name = name; - return QR.handleFiles([blob]); - }, handleUrl: function() { - var url, xhr; - url = prompt("Enter a URL:"); + var url; + url = prompt('Enter a URL:'); if (url === null) { return; } - xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'blob'; - xhr.onload = function(e) { - var contentDisposition, contentType; - if (!(this.readyState === this.DONE && xhr.status === 200)) { + return CrossOrigin.file(url, function(blob) { + if (blob) { + return QR.handleFiles([blob]); + } else { return QR.error("Can't load image."); } - contentType = this.getResponseHeader('Content-Type'); - contentDisposition = this.getResponseHeader('Content-Disposition'); - return QR.handleBlob(this.response, contentType, contentDisposition, url); - }; - xhr.onerror = function(e) { - return QR.error("Can't load image."); - }; - return xhr.send(); + }); }, handleFiles: function(files) { var file, i, _i, _len; @@ -9115,12 +9098,12 @@ } }, handleFile: function(file, index, nfiles) { - var isSingle, max, post, _ref; + var err, isSingle, max, post, _ref; isSingle = nfiles === 1; if (/^text\//.test(file.type)) { if (isSingle) { post = QR.selected; - } else if ((post = QR.posts[QR.posts.length - 1]).com) { + } else if (index !== 0 || (post = QR.posts[QR.posts.length - 1]).com) { post = new QR.post(); } post.pasteText(file); @@ -9143,7 +9126,12 @@ } else if ((post = QR.posts[QR.posts.length - 1]).file) { post = new QR.post(); } - return post.setFile(file); + try { + return post.setFile(file); + } catch (_error) { + err = _error; + return console.log(err); + } }, openFileInput: function(e) { var _ref; @@ -9151,10 +9139,12 @@ if (e.shiftKey && e.type === 'click') { return QR.selected.rmFile(); } - if (e.ctrlKey && e.type === 'click') { + if ((e.ctrlKey || e.metaKey) && e.type === 'click') { $.addClass(QR.nodes.filename, 'edit'); QR.nodes.filename.focus(); - return; + return $.on(QR.nodes.filename, 'blur', function() { + return $.rmClass(QR.nodes.filename, 'edit'); + }); } if (e.target.nodeName === 'INPUT' || (e.keyCode && ((_ref = e.keyCode) !== 32 && _ref !== 13)) || e.ctrlKey) { return; @@ -9190,7 +9180,7 @@ } }, dialog: function() { - var dialog, elm, event, i, items, name, node, nodes, prop, rules, save, setNode, _, _i, _len, _ref, _ref1, _ref2; + var dialog, elm, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode; QR.nodes = nodes = { el: dialog = UI.dialog('qr', 'top:0;right:0;', "
\uf00d
No selected fileSpoiler\uf0c1Post from URL+Dump\uf00dRemove File
") }; @@ -9222,23 +9212,20 @@ setNode('status', '[type=submit]'); setNode('fileInput', '[type=file]'); rules = $('ul.rules').textContent.trim(); - QR.min_width = QR.min_height = 1; - QR.max_width = QR.max_height = 10000; - try { - _ref = rules.match(/.+smaller than (\d+)x(\d+).+/), _ = _ref[0], QR.min_width = _ref[1], QR.min_height = _ref[2]; - _ref1 = rules.match(/.+greater than (\d+)x(\d+).+/), _ = _ref1[0], QR.max_width = _ref1[1], QR.max_height = _ref1[2]; - _ref2 = ['min_width', 'min_height', 'max_width', 'max_height']; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - prop = _ref2[_i]; - QR[prop] = parseInt(QR[prop], 10); - } - } catch (_error) { - null; - } + match_min = rules.match(/.+smaller than (\d+)x(\d+).+/); + match_max = rules.match(/.+greater than (\d+)x(\d+).+/); + QR.min_width = +(match_min != null ? match_min[1] : void 0) || 1; + QR.min_height = +(match_min != null ? match_min[2] : void 0) || 1; + QR.max_width = +(match_max != null ? match_max[1] : void 0) || 10000; + QR.max_height = +(match_max != null ? match_max[2] : void 0) || 10000; nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value; QR.max_size_video = 3145728; QR.max_width_video = QR.max_height_video = 2048; QR.max_duration_video = 120; + QR.forcedAnon = !!$('form[name="post"] input[name="name"][type="hidden"]'); + if (QR.forcedAnon) { + $.addClass(QR.nodes.el, 'forced-anon'); + } QR.spoiler = !!$('.postForm input[name=spoiler]'); if (QR.spoiler) { $.addClass(QR.nodes.el, 'has-spoiler'); @@ -9249,9 +9236,10 @@ $.after(nodes.name.parentElement, nodes.dumpList.parentElement); nodes.addPost.tabIndex = 35; } - if (g.BOARD.ID === 'f') { + if (g.BOARD.ID === 'f' && g.VIEW !== 'thread') { nodes.flashTag = $.el('select', { - name: 'filetag', + name: 'filetag' + }, { innerHTML: "\n\n\n\n\n\n" }); nodes.flashTag.dataset["default"] = '4'; @@ -9427,9 +9415,9 @@ post.lock(); formData = { resto: threadID, - name: post.name, + name: !QR.forcedAnon ? post.name : void 0, email: post.email, - sub: post.sub, + sub: !(QR.forcedAnon || threadID) ? post.sub : void 0, com: post.com, upfile: post.file, filetag: filetag, @@ -9516,9 +9504,6 @@ } QR.status(); QR.error(err); - if (QR.captcha.isEnabled) { - QR.captcha.setup(); - } return; } h1 = $('h1', resDoc); @@ -9569,7 +9554,7 @@ QR.close(); } else { post.rm(); - QR.captcha.setup(); + QR.captcha.setup(true); } QR.cooldown.set({ req: req, @@ -9621,7 +9606,7 @@ QR.captcha = { init: function() { - var container, counter, section; + var counter, root; if (d.cookie.indexOf('pass_enabled=1') >= 0) { return; } @@ -9635,80 +9620,101 @@ return QR.captcha.sync(captchas); }); $.sync('captchas', this.sync.bind(this)); - section = $.el('div', { - className: 'captcha-section' + root = $.el('div', { + className: 'captcha-root' }); - $.extend(section, { - innerHTML: "
" + $.extend(root, { + innerHTML: "
" }); - container = $('.captcha-container', section); - counter = $('.captcha-counter > a', section); + counter = $('.captcha-counter > a', root); this.nodes = { - container: container, + root: root, counter: counter }; this.count(); $.addClass(QR.nodes.el, 'has-captcha'); - $.after(QR.nodes.com.parentNode, section); - new MutationObserver(this.afterSetup.bind(this)).observe(container, { - childList: true, - subtree: true - }); + $.after(QR.nodes.com.parentNode, root); $.on(counter, 'click', this.toggle.bind(this)); - return $.on(window, 'captcha:success', this.save.bind(this)); + return $.on(window, 'captcha:success', (function(_this) { + return function() { + return _this.save(false); + }; + })(this)); }, shouldFocus: false, timeouts: {}, + postsCount: 0, needed: function() { - var captchaCount, postsCount; + var captchaCount; captchaCount = this.captchas.length; - if (this.nodes.container.dataset.widgetID && !this.timeouts.destroy) { + if (this.nodes.container && !this.timeouts.destroy) { captchaCount++; } - postsCount = QR.posts.length; - if (postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { - postsCount = 0; + 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; + }, + 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; } - return captchaCount < postsCount; }, toggle: function() { - if (this.nodes.container.dataset.widgetID && !this.timeouts.destroy) { + if (this.nodes.container && !this.timeouts.destroy) { return this.destroy(); } else { - this.shouldFocus = true; - return this.setup(true); + return this.setup(true, true); } }, - setup: function(force) { + setup: function(focus, force) { 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.dataset.widgetID) { + if (this.nodes.container) { return; } - return $.globalEval('(function() {\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})();'); + 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, _i, _j, _len, _len1, _ref; + 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 (node.nodeName === 'IFRAME') { - iframe = node; + if (iframe = $.x('./descendant-or-self::iframe', node)) { + this.setupIFrame(iframe); + } + if (textarea = $.x('./descendant-or-self::textarea', node)) { + this.setupTextArea(textarea); } } } - if (!iframe) { - return; - } + }, + 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'; @@ -9718,16 +9724,28 @@ } 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'); - $.rmAll(this.nodes.container); - return this.nodes.container.removeAttribute('data-widget-i-d'); + 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(); @@ -9743,30 +9761,39 @@ return null; } }, - save: function(e) { - var _base; - if (this.needed()) { + save: function(pasted) { + var reload, _base; + $.forceSync('captchas'); + reload = (QR.cooldown.auto || Conf['Post on Captcha Completion']) && this.needed(); + this.captchas.push({ + response: $('textarea', this.nodes.container).value, + timeout: (pasted ? this.setupTime : Date.now()) + 2 * $.MINUTE + }); + this.count(); + $.set('captchas', this.captchas); + if (reload) { this.shouldFocus = true; this.reload(); } else { - this.nodes.counter.focus(); - if ((_base = this.timeouts).destroy == null) { - _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND); + 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(); } - $.forceSync('captchas'); - this.captchas.push({ - response: e.detail, - timeout: Date.now() + 2 * $.MINUTE - }); - this.count(); - return $.set('captchas', this.captchas); }, 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) { @@ -9781,7 +9808,7 @@ this.captchas = this.captchas.slice(i); this.count(); $.set('captchas', this.captchas); - return this.setup(); + return this.setup(true); }, count: function() { this.nodes.counter.textContent = "Captchas: " + this.captchas.length; @@ -10018,7 +10045,7 @@ QR.post = (function() { function _Class(select) { this.select = __bind(this.select, this); - var el, event, prev, _i, _len, _ref, _ref1; + var el, event, prev, _i, _len, _ref; el = $.el('a', { className: 'qr-preview', draggable: true, @@ -10059,9 +10086,8 @@ $.on(el, event.toLowerCase(), this[event]); } this.thread = g.VIEW === 'thread' ? g.THREADID : 'new'; - _ref1 = QR.posts, prev = _ref1[_ref1.length - 1]; + prev = QR.posts[QR.posts.length - 1]; QR.posts.push(this); - QR.captcha.setup(); this.nodes.spoiler.checked = this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false; QR.persona.get((function(_this) { return function(persona) { @@ -10080,6 +10106,9 @@ this.select(); } this.unlock(); + $.queueTask(function() { + return QR.captcha.setup(); + }); } _Class.prototype.rm = function() { @@ -10170,7 +10199,7 @@ return QR.status(); case 'com': this.nodes.span.textContent = this.com; - QR.captcha.setup(); + QR.captcha.onPostChange(); QR.characterCount(); if (QR.cooldown.auto && this === QR.posts[0] && (0 < (_ref = QR.cooldown.seconds) && _ref <= 5)) { return QR.cooldown.auto = false; @@ -10210,7 +10239,7 @@ if (QR.spoiler) { this.nodes.label.hidden = false; } - QR.captcha.setup(); + QR.captcha.onPostChange(); URL.revokeObjectURL(this.URL); if (this === QR.selected) { this.showFileData(); @@ -10228,55 +10257,55 @@ var el, fileURL, isVideo; isVideo = /^video\//.test(this.file.type); el = $.el((isVideo ? 'video' : 'img')); - $.on(el, (isVideo ? 'loadeddata' : 'load'), (function(_this) { - return function() { - var cv, error, errors, height, s, width, _i, _len; - errors = _this.checkDimensions(el, isVideo); - if (errors.length) { - for (_i = 0, _len = errors.length; _i < _len; _i++) { - error = errors[_i]; - QR.error(error); - } - _this.URL = fileURL; - if ((QR.posts.length === 1) || (_this.com && _this.com.length)) { - return _this.rmFile(); - } else { - return _this.rm(); - } + $.on(el, (isVideo ? 'loadeddata' : 'load'), function() { + var cv, error, errors, height, s, width, _i, _len; + errors = this.checkDimensions(el, isVideo); + if (errors.length) { + for (_i = 0, _len = errors.length; _i < _len; _i++) { + error = errors[_i]; + QR.error(error); } - s = 90 * 2 * window.devicePixelRatio; - if (_this.file.type === 'image/gif') { - s *= 3; - } - if (isVideo) { - height = el.videoHeight; - width = el.videoWidth; + this.URL = fileURL; + if ((QR.posts.length === 1) || (this.com && this.com.length)) { + return this.rmFile(); } else { - height = el.height, width = el.width; - if (height < s || width < s) { - _this.URL = fileURL; - _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")"; - return; - } + return this.rm(); } - if (height <= width) { - width = s / height * width; - height = s; - } else { - height = s / width * height; - width = s; + } + s = 90 * 2 * window.devicePixelRatio; + if (this.file.type === 'image/gif') { + s *= 3; + } + if (isVideo) { + height = el.videoHeight; + width = el.videoWidth; + } else { + height = el.height, width = el.width; + if (height < s || width < s) { + this.URL = fileURL; + this.nodes.el.style.backgroundImage = "url(" + this.URL + ")"; + return; } - cv = $.el('canvas'); - cv.height = el.height = height; - cv.width = el.width = width; - cv.getContext('2d').drawImage(el, 0, 0, width, height); - URL.revokeObjectURL(fileURL); - return cv.toBlob(function(blob) { + } + if (height <= width) { + width = s / height * width; + height = s; + } else { + height = s / width * height; + width = s; + } + cv = $.el('canvas'); + cv.height = el.height = height; + cv.width = el.width = width; + cv.getContext('2d').drawImage(el, 0, 0, width, height); + URL.revokeObjectURL(fileURL); + return cv.toBlob((function(_this) { + return function(blob) { _this.URL = URL.createObjectURL(blob); return _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")"; - }); - }; - })(this)); + }; + })(this)); + }); fileURL = URL.createObjectURL(this.file); return el.src = fileURL; }; @@ -10331,7 +10360,7 @@ _Class.prototype.updateFilename = function() { var title; - title = "" + this.filename + " (" + this.filesize + ")\nCtrl+click to edit filename. Shift+click to clear."; + title = "" + this.filename + " (" + this.filesize + ")\nCtrl/\u2318+click to edit filename. Shift+click to clear."; this.nodes.el.title = title; if (this !== QR.selected) { return; diff --git a/src/General/CrossOrigin.coffee b/src/General/CrossOrigin.coffee new file mode 100644 index 000000000..748339bda --- /dev/null +++ b/src/General/CrossOrigin.coffee @@ -0,0 +1,99 @@ +CrossOrigin = do -> + <% if (type === 'crx') { %> + eventPageRequest = do -> + callbacks = [] + chrome.runtime.onMessage.addListener (data) -> + callbacks[data.id] data + delete callbacks[data.id] + (url, responseType, cb) -> + chrome.runtime.sendMessage {url, responseType}, (id) -> + callbacks[id] = cb + <% } %> + + file: do -> + makeBlob = (urlBlob, contentType, contentDisposition, url) -> + name = url.match(/([^\/]+)\/*$/)?[1] + mime = contentType?.match(/[^;]*/)[0] or 'application/octet-stream' + match = + contentDisposition?.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)?[1] or + contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1] + if match + name = match.replace /\\"/g, '"' + blob = new Blob([urlBlob], {type: mime}) + blob.name = name + blob + + (url, cb) -> + <% if (type === 'crx') { %> + if /^https:\/\//.test(url) or location.protocol is 'http:' + $.ajax url, + responseType: 'blob' + onload: -> + return cb null unless @readyState is @DONE and @status is 200 + contentType = @getResponseHeader 'Content-Type' + contentDisposition = @getResponseHeader 'Content-Disposition' + cb (makeBlob @response, contentType, contentDisposition, url) + onerror: -> + cb null + else + eventPageRequest url, 'arraybuffer', ({response, contentType, contentDisposition, error}) -> + return cb null if error + cb (makeBlob new Uint8Array(response), contentType, contentDisposition, url) + <% } %> + <% if (type === 'userscript') { %> + GM_xmlhttpRequest + method: "GET" + url: url + overrideMimeType: "text/plain; charset=x-user-defined" + onload: (xhr) -> + r = xhr.responseText + data = new Uint8Array r.length + i = 0 + while i < r.length + data[i] = r.charCodeAt i + i++ + contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1] + contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1] + cb (makeBlob data, contentType, contentDisposition, url) + onerror: -> + cb null + <% } %> + + json: do -> + callbacks = {} + responses = {} + (url, cb) -> + <% if (type === 'crx') { %> + if /^https:\/\//.test(url) or location.protocol is 'http:' + return $.cache url, (-> cb @response), responseType: 'json' + <% } %> + if responses[url] + cb responses[url] + return + if callbacks[url] + callbacks[url].push cb + return + callbacks[url] = [cb] + <% if (type === 'userscript') { %> + GM_xmlhttpRequest + method: "GET" + url: url+'' + onload: (xhr) -> + response = JSON.parse xhr.responseText + cb response for cb in callbacks[url] + delete callbacks[url] + responses[url] = response + onerror: -> + delete callbacks[url] + onabort: -> + delete callbacks[url] + <% } %> + <% if (type === 'crx') { %> + eventPageRequest url, 'json', ({response, error}) -> + if error + delete callbacks[url] + else + cb response for cb in callbacks[url] + delete callbacks[url] + responses[url] = response + <% } %> diff --git a/src/Posting/QR.captcha.coffee b/src/Posting/QR.captcha.coffee index e098ecc74..63556d3be 100644 --- a/src/Posting/QR.captcha.coffee +++ b/src/Posting/QR.captcha.coffee @@ -8,83 +8,108 @@ QR.captcha = QR.captcha.sync captchas $.sync 'captchas', @sync.bind @ - section = $.el 'div', className: 'captcha-section' - $.extend section, <%= html( - '
' + + root = $.el 'div', className: 'captcha-root' + $.extend root, <%= html( '
' ) %> - container = $ '.captcha-container', section - counter = $ '.captcha-counter > a', section - @nodes = {container, counter} + counter = $ '.captcha-counter > a', root + @nodes = {root, counter} @count() $.addClass QR.nodes.el, 'has-captcha' - $.after QR.nodes.com.parentNode, section - - new MutationObserver(@afterSetup.bind @).observe container, - childList: true - subtree: true + $.after QR.nodes.com.parentNode, root $.on counter, 'click', @toggle.bind @ - $.on window, 'captcha:success', @save.bind @ + $.on window, 'captcha:success', => @save false shouldFocus: false timeouts: {} + postsCount: 0 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 + captchaCount++ if @nodes.container 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 + + 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.dataset.widgetID and !@timeouts.destroy + if @nodes.container and !@timeouts.destroy @destroy() else - @shouldFocus = true - @setup true + @setup true, true - setup: (force) -> + 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() - return if @nodes.container.dataset.widgetID + + return if @nodes.container + + @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() { - 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})); + 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 - iframe = node if node.nodeName is 'IFRAME' - return unless iframe + @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' - $.rmAll @nodes.container - # XXX https://github.com/greasemonkey/greasemonkey/issues/1571 - @nodes.container.removeAttribute 'data-widget-i-d' + $.rm @nodes.container if @nodes.container + delete @nodes.container - sync: (captchas) -> + sync: (captchas=[]) -> @captchas = captchas @clear() @count() @@ -98,22 +123,30 @@ QR.captcha = else null - save: (e) -> - if @needed() - @shouldFocus = true - @reload() - else - @nodes.counter.focus() - @timeouts.destroy ?= setTimeout @destroy.bind(@), 3 * $.SECOND + save: (pasted) -> $.forceSync 'captchas' + reload = (QR.cooldown.auto or Conf['Post on Captcha Completion']) and @needed() @captchas.push - response: e.detail - timeout: Date.now() + 2 * $.MINUTE + response: $('textarea', @nodes.container).value + timeout: (if pasted then @setupTime else Date.now()) + 2 * $.MINUTE @count() $.set 'captchas', @captchas + if reload + @shouldFocus = true + @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 + clear: -> return unless @captchas.length + $.forceSync 'captchas' now = Date.now() for captcha, i in @captchas break if captcha.timeout > now @@ -121,7 +154,7 @@ QR.captcha = @captchas = @captchas[i..] @count() $.set 'captchas', @captchas - @setup() + @setup true count: -> @nodes.counter.textContent = "Captchas: #{@captchas.length}" diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee index 6fd8ba687..a3ff5249e 100644 --- a/src/Posting/QR.coffee +++ b/src/Posting/QR.coffee @@ -108,6 +108,7 @@ QR = QR.captcha.destroy() focusin: -> + QR.captcha.setup() if $.hasClass(QR.nodes.el, 'autohide') and !$.hasClass(QR.nodes.el, 'focus') $.addClass QR.nodes.el, 'focus' focusout: -> @@ -136,7 +137,7 @@ QR = el = err el.removeAttribute 'style' if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent - QR.captcha.setup() + QR.captcha.setup true QR.notify el alert el.textContent if d.hidden @@ -270,6 +271,7 @@ QR = QR.handleFiles e.dataTransfer.files paste: (e) -> + return unless e.clipboardData.items files = [] for item in e.clipboardData.items when item.kind is 'file' blob = item.getAsFile() @@ -280,66 +282,15 @@ QR = QR.open() QR.handleFiles files $.addClass QR.nodes.el, 'dump' - - handleBlob: (urlBlob, contentType, contentDisposition, url) -> - name = url.match(/([^\/]+)\/*$/)?[1] - mime = contentType?.match(/[^;]*/)[0] or 'application/octet-stream' - match = - contentDisposition?.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)?[1] or - contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1] - if match - name = match.replace /\\"/g, '"' - blob = new Blob([urlBlob], {type: mime}) - blob.name = name - QR.handleFiles([blob]) - handleUrl: -> - url = prompt("Enter a URL:") + url = prompt 'Enter a URL:' return if url is null - - <% if (type === 'crx') { %> - xhr = new XMLHttpRequest(); - xhr.open('GET', url, true) - xhr.responseType = 'blob' - xhr.onload = (e) -> - return QR.error "Can't load image." unless @readyState is @DONE and xhr.status is 200 - - contentType = @getResponseHeader('Content-Type') - contentDisposition = @getResponseHeader('Content-Disposition') - QR.handleBlob @response, contentType, contentDisposition, url - - xhr.onerror = (e) -> - QR.error "Can't load image." - xhr.send() - - <% } else { %> - - GM_xmlhttpRequest - method: "GET" - url: url - - # FIXME: responseType: 'blob' - # Could do it now, but don't wanna kill off legacy GM versions yet - overrideMimeType: "text/plain; charset=x-user-defined" - onload: (xhr) -> - r = xhr.responseText - h = xhr.responseHeaders - data = new Uint8Array r.length - i = 0 - - while i < r.length - data[i] = r.charCodeAt i - i++ - - contentType = (h.match(/Content-Type:\s*(.*)/i) or [])[1] - contentDisposition = (h.match(/Content-Disposition:\s*(.*)/i) or [])[1] - QR.handleBlob data, contentType, contentDisposition, url - - onerror: (xhr) -> + CrossOrigin.file url, (blob) -> + if blob + QR.handleFiles([blob]) + else QR.error "Can't load image." - <% } %> - handleFiles: (files) -> if @ isnt QR # file input files = [@files...] @@ -355,7 +306,7 @@ QR = if /^text\//.test file.type if isSingle post = QR.selected - else if (post = QR.posts[QR.posts.length - 1]).com + else if index isnt 0 or (post = QR.posts[QR.posts.length - 1]).com post = new QR.post() post.pasteText file return @@ -371,16 +322,19 @@ QR = post = QR.selected else if (post = QR.posts[QR.posts.length - 1]).file post = new QR.post() - post.setFile file + try + post.setFile file + catch err + console.log err openFileInput: (e) -> e.stopPropagation() if e.shiftKey and e.type is 'click' return QR.selected.rmFile() - if e.ctrlKey and e.type is 'click' + if (e.ctrlKey or e.metaKey) and e.type is 'click' $.addClass QR.nodes.filename, 'edit' QR.nodes.filename.focus() - return + return $.on QR.nodes.filename, 'blur', -> $.rmClass QR.nodes.filename, 'edit' return if e.target.nodeName is 'INPUT' or (e.keyCode and e.keyCode not in [32, 13]) or e.ctrlKey e.preventDefault() QR.nodes.fileInput.click() @@ -439,15 +393,13 @@ QR = setNode 'fileInput', '[type=file]' rules = $('ul.rules').textContent.trim() - QR.min_width = QR.min_height = 1 - QR.max_width = QR.max_height = 10000 - try - [_, QR.min_width, QR.min_height] = rules.match(/.+smaller than (\d+)x(\d+).+/) - [_, QR.max_width, QR.max_height] = rules.match(/.+greater than (\d+)x(\d+).+/) - for prop in ['min_width', 'min_height', 'max_width', 'max_height'] - QR[prop] = parseInt QR[prop], 10 - catch - null + + match_min = rules.match(/.+smaller than (\d+)x(\d+).+/) + match_max = rules.match(/.+greater than (\d+)x(\d+).+/) + QR.min_width = +match_min?[1] or 1 + QR.min_height = +match_min?[2] or 1 + QR.max_width = +match_max?[1] or 10000 + QR.max_height = +match_max?[2] or 10000 nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value @@ -455,6 +407,10 @@ QR = QR.max_width_video = QR.max_height_video = 2048 QR.max_duration_video = 120 + QR.forcedAnon = !!$ 'form[name="post"] input[name="name"][type="hidden"]' + if QR.forcedAnon + $.addClass QR.nodes.el, 'forced-anon' + QR.spoiler = !!$ '.postForm input[name=spoiler]' if QR.spoiler $.addClass QR.nodes.el, 'has-spoiler' @@ -465,9 +421,8 @@ QR = $.after nodes.name.parentElement, nodes.dumpList.parentElement nodes.addPost.tabIndex = 35 - if g.BOARD.ID is 'f' - nodes.flashTag = $.el 'select', - name: 'filetag' + if g.BOARD.ID is 'f' and g.VIEW isnt 'thread' + nodes.flashTag = $.el 'select', name: 'filetag', innerHTML: """ @@ -660,9 +615,11 @@ QR = formData = resto: threadID - name: post.name + + name: post.name unless QR.forcedAnon email: post.email - sub: post.sub + + sub: post.sub unless QR.forcedAnon or threadID com: post.com upfile: post.file filetag: filetag @@ -766,7 +723,6 @@ QR = QR.cooldown.auto = false QR.status() QR.error err - QR.captcha.setup() if QR.captcha.isEnabled return h1 = $ 'h1', resDoc @@ -817,7 +773,7 @@ QR = QR.close() else post.rm() - QR.captcha.setup() + QR.captcha.setup true QR.cooldown.set {req, post, isReply, threadID} diff --git a/src/Posting/QR.post.coffee b/src/Posting/QR.post.coffee index 194385772..bde74393c 100644 --- a/src/Posting/QR.post.coffee +++ b/src/Posting/QR.post.coffee @@ -35,9 +35,8 @@ QR.post = class else 'new' - [..., prev] = QR.posts + 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 @@ -72,6 +71,8 @@ QR.post = class @load() if QR.selected is @ # load persona @select() if select @unlock() + # Post count temporarily off by 1 when called from QR.post.rm + $.queueTask -> QR.captcha.setup() rm: -> @delete() @@ -136,7 +137,7 @@ QR.post = class QR.status() when 'com' @nodes.span.textContent = @com - QR.captcha.setup() + QR.captcha.onPostChange() QR.characterCount() # Disable auto-posting if you're typing in the first post # during the last 5 seconds of the cooldown. @@ -165,7 +166,7 @@ QR.post = class @filename = file.name @filesize = $.bytesToString file.size @nodes.label.hidden = false if QR.spoiler - QR.captcha.setup() + QR.captcha.onPostChange() URL.revokeObjectURL @URL if @ is QR.selected @showFileData() @@ -181,7 +182,7 @@ QR.post = class isVideo = /^video\//.test @file.type el = $.el (if isVideo then 'video' else 'img') - $.on el, (if isVideo then 'loadeddata' else 'load'), => + $.on el, (if isVideo then 'loadeddata' else 'load'), -> # Verify element dimensions. errors = @checkDimensions el, isVideo if errors.length @@ -195,6 +196,7 @@ QR.post = class # to avoid crappy resized quality. s = 90 * 2 * window.devicePixelRatio s *= 3 if @file.type is 'image/gif' # let them animate + if isVideo height = el.videoHeight width = el.videoWidth @@ -204,12 +206,14 @@ QR.post = class @URL = fileURL @nodes.el.style.backgroundImage = "url(#{@URL})" return + if height <= width width = s / height * width height = s else height = s / width * height width = s + cv = $.el 'canvas' cv.height = el.height = height cv.width = el.width = width @@ -226,8 +230,14 @@ QR.post = class err = [] if video {videoHeight, videoWidth, duration} = el - max_height = if QR.max_height < QR.max_height_video then QR.max_height else QR.max_height_video - max_width = if QR.max_width < QR.max_width_video then QR.max_width else QR.max_width_video + max_height = if QR.max_height < QR.max_height_video + QR.max_height + else + QR.max_height_video + max_width = if QR.max_width < QR.max_width_video + QR.max_width + else + QR.max_width_video if videoHeight > max_height or videoWidth > max_width err.push "#{@file.name}: Video too large (video: #{videoHeight}x#{videoWidth}px, max: #{max_height}x#{max_width}px)" if videoHeight < QR.min_height or videoWidth < QR.min_width @@ -261,7 +271,7 @@ QR.post = class URL.revokeObjectURL @URL updateFilename: -> - title = "#{@filename} (#{@filesize})\nCtrl+click to edit filename. Shift+click to clear." + title = "#{@filename} (#{@filesize})\nCtrl/\u2318+click to edit filename. Shift+click to clear." @nodes.el.title = title return unless @ is QR.selected QR.nodes.fileContainer.title = title