diff --git a/CHANGELOG.md b/CHANGELOG.md index db3114f7e..953f9c49f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +### v1.7.52 +*2014-06-02* + +**ccd0** +- Add workaround for downloading with the original filename in Firefox. + ### v1.7.51 *2014-06-02* diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 62aa4eaf4..9fcd25d93 100755 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -27,6 +27,7 @@ module.exports = (grunt) -> 'src/General/Get.coffee' 'src/General/UI.coffee' 'src/General/Notice.coffee' + 'src/General/CrossOrigin.coffee' 'src/Filtering/**/*.coffee' 'src/Quotelinks/**/*.coffee' 'src/Posting/QR.coffee' diff --git a/LICENSE b/LICENSE index 3081293f9..cac8eb81a 100755 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ /* -* 4chan X - Version 1.7.51 - 2014-06-02 +* 4chan X - Version 1.7.52 - 2014-06-02 * * Licensed under the MIT license. * https://github.com/ccd0/4chan-x/blob/master/LICENSE diff --git a/builds/4chan-X.meta.js b/builds/4chan-X.meta.js index 404368fc2..3515df658 100755 --- a/builds/4chan-X.meta.js +++ b/builds/4chan-X.meta.js @@ -1,6 +1,6 @@ // ==UserScript== // @name 4chan X -// @version 1.7.51 +// @version 1.7.52 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X diff --git a/builds/4chan-X.user.js b/builds/4chan-X.user.js index 4f5c93cf4..d80b1a06c 100644 --- a/builds/4chan-X.user.js +++ b/builds/4chan-X.user.js @@ -1,7 +1,7 @@ // Generated by CoffeeScript // ==UserScript== // @name 4chan X -// @version 1.7.51 +// @version 1.7.52 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X @@ -24,7 +24,7 @@ // ==/UserScript== /* -* 4chan X - Version 1.7.51 - 2014-06-02 +* 4chan X - Version 1.7.52 - 2014-06-02 * * Licensed under the MIT license. * https://github.com/ccd0/4chan-x/blob/master/LICENSE @@ -106,7 +106,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, Callbacks, CatalogLinks, Clone, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, g, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, Callbacks, CatalogLinks, Clone, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, g, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -194,6 +194,7 @@ 'Thread Hiding Link': [true, 'Add a link to hide entire threads.'], 'Reply Hiding Link': [true, 'Add a link to hide single replies.'], 'Delete Link': [true, 'Add post and image deletion links to the menu.'], + 'Download Link': [true, 'Add a download with original filename link to the menu.'], 'Archive Link': [true, 'Add an archive link to the menu.'] }, 'Monitoring': { @@ -372,7 +373,7 @@ doc = d.documentElement; g = { - VERSION: '1.7.51', + VERSION: '1.7.52', NAMESPACE: '4chan X.', boards: {} }; @@ -4016,6 +4017,50 @@ }; })(); + CrossOrigin = (function() { + var handleBlob, handleUrl; + handleBlob = function(urlBlob, contentType, contentDisposition, url, cb) { + 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 cb(blob); + }; + handleUrl = function(url, cb) { + return GM_xmlhttpRequest({ + method: "GET", + url: url, + overrideMimeType: "text/plain; charset=x-user-defined", + onload: function(xhr) { + var contentDisposition, contentType, data, i, r, _ref, _ref1; + r = xhr.responseText; + data = new Uint8Array(r.length); + i = 0; + while (i < r.length) { + data[i] = r.charCodeAt(i); + i++; + } + contentType = (_ref = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)) != null ? _ref[1] : void 0; + contentDisposition = (_ref1 = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)) != null ? _ref1[1] : void 0; + return handleBlob(data, contentType, contentDisposition, url, cb); + }, + onerror: function(xhr) { + return cb(null); + } + }); + }; + return { + request: handleUrl + }; + })(); + Anonymize = { init: function() { if (g.VIEW === 'catalog' || !Conf['Anonymize']) { @@ -6052,44 +6097,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("Insert an 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, i, r, _ref, _ref1; - r = xhr.responseText; - data = new Uint8Array(r.length); - i = 0; - while (i < r.length) { - data[i] = r.charCodeAt(i); - i++; - } - contentType = (_ref = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)) != null ? _ref[1] : void 0; - contentDisposition = (_ref1 = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)) != null ? _ref1[1] : void 0; - return QR.handleBlob(data, contentType, contentDisposition, url); - }, - onerror: function(xhr) { + return CrossOrigin.request(url, function(blob) { + if (blob) { + return QR.handleFiles([blob]); + } else { return QR.error("Can't load image."); } }); @@ -9320,6 +9337,24 @@ className: 'download-link', textContent: 'Download file' }); + if (typeof chrome === "undefined" || chrome === null) { + $.on(a, 'click', function(e) { + if (this.protocol === 'blob:') { + return true; + } + e.preventDefault(); + return CrossOrigin.request(this.href, (function(_this) { + return function(blob) { + if (blob) { + _this.href = URL.createObjectURL(blob); + return _this.click(); + } else { + return new Notice('error', "Could not download " + file.URL, 30); + } + }; + })(this)); + }); + } return $.event('AddMenuEntry', { type: 'post', el: a, @@ -12444,7 +12479,7 @@ Settings.dialog = dialog = $.el('div', { id: 'fourchanx-settings', className: 'dialog', - innerHTML: '
' + innerHTML: '
' }); $.on($('.export', Settings.dialog), 'click', Settings["export"]); $.on($('.import', Settings.dialog), 'click', Settings["import"]); @@ -13289,7 +13324,7 @@ } if (previousversion) { el = $.el('span', { - innerHTML: '4chan X has been updated to version 1.7.51.' + innerHTML: '4chan X has been updated to version 1.7.52.' }); new Notice('info', el, 15); } else { diff --git a/builds/4chan-X.zip b/builds/4chan-X.zip index ab9aee252..77a5cef89 100644 Binary files a/builds/4chan-X.zip and b/builds/4chan-X.zip differ diff --git a/builds/crx.crx b/builds/crx.crx index aade6da8a..87f8e8e62 100644 Binary files a/builds/crx.crx and b/builds/crx.crx differ diff --git a/builds/crx/manifest.json b/builds/crx/manifest.json index 4d0e7d248..a7f853f0a 100755 --- a/builds/crx/manifest.json +++ b/builds/crx/manifest.json @@ -1,6 +1,6 @@ { "name": "4chan X", - "version": "1.7.51", + "version": "1.7.52", "manifest_version": 2, "description": "Cross-browser userscript for maximum lurking on 4chan.", "icons": { diff --git a/builds/crx/script.js b/builds/crx/script.js index 14457ad3c..91c21a80f 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript /* -* 4chan X - Version 1.7.51 - 2014-06-02 +* 4chan X - Version 1.7.52 - 2014-06-02 * * Licensed under the MIT license. * https://github.com/ccd0/4chan-x/blob/master/LICENSE @@ -82,7 +82,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, Callbacks, CatalogLinks, Clone, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, g, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, Callbacks, CatalogLinks, Clone, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, g, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -170,7 +170,7 @@ 'Thread Hiding Link': [true, 'Add a link to hide entire threads.'], 'Reply Hiding Link': [true, 'Add a link to hide single replies.'], 'Delete Link': [true, 'Add post and image deletion links to the menu.'], - 'Download Link': [true, 'Add a download with original filename link to the menu. Chrome-only currently.'], + 'Download Link': [true, 'Add a download with original filename link to the menu.'], 'Archive Link': [true, 'Add an archive link to the menu.'] }, 'Monitoring': { @@ -348,7 +348,7 @@ doc = d.documentElement; g = { - VERSION: '1.7.51', + VERSION: '1.7.52', NAMESPACE: '4chan X.', boards: {} }; @@ -4045,6 +4045,47 @@ }; })(); + CrossOrigin = (function() { + var handleBlob, handleUrl; + handleBlob = function(urlBlob, contentType, contentDisposition, url, cb) { + 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 cb(blob); + }; + handleUrl = function(url, cb) { + var xhr; + 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) { + contentType = this.getResponseHeader('Content-Type'); + contentDisposition = this.getResponseHeader('Content-Disposition'); + return handleBlob(this.response, contentType, contentDisposition, url, cb); + } else { + return cb(null); + } + }; + xhr.onerror = function(e) { + return cb(null); + }; + return xhr.send(); + }; + return { + request: handleUrl + }; + })(); + Anonymize = { init: function() { if (g.VIEW === 'catalog' || !Conf['Anonymize']) { @@ -6090,43 +6131,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; + var url; url = prompt("Insert an 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) { - contentType = this.getResponseHeader('Content-Type'); - contentDisposition = this.getResponseHeader('Content-Disposition'); - return QR.handleBlob(this.response, contentType, contentDisposition, url); + return CrossOrigin.request(url, function(blob) { + if (blob) { + return QR.handleFiles([blob]); } else { return QR.error("Can't load image."); } - }; - xhr.onerror = function(e) { - return QR.error("Can't load image."); - }; - return xhr.send(); + }); }, handleFiles: function(files) { var file, i, _i, _len; @@ -12438,7 +12455,7 @@ Settings.dialog = dialog = $.el('div', { id: 'fourchanx-settings', className: 'dialog', - innerHTML: '
' + innerHTML: '
' }); $.on($('.export', Settings.dialog), 'click', Settings["export"]); $.on($('.import', Settings.dialog), 'click', Settings["import"]); @@ -13270,7 +13287,7 @@ } if (previousversion) { el = $.el('span', { - innerHTML: '4chan X has been updated to version 1.7.51.' + innerHTML: '4chan X has been updated to version 1.7.52.' }); new Notice('info', el, 15); } else { diff --git a/builds/updates.xml b/builds/updates.xml index 272a860f6..8e776a68d 100644 --- a/builds/updates.xml +++ b/builds/updates.xml @@ -1,7 +1,7 @@ - + diff --git a/builds/wcrx/manifest.json b/builds/wcrx/manifest.json index 1034923ab..636735ed6 100644 --- a/builds/wcrx/manifest.json +++ b/builds/wcrx/manifest.json @@ -1,6 +1,6 @@ { "name": "4chan X", - "version": "1.7.51", + "version": "1.7.52", "manifest_version": 2, "description": "Cross-browser userscript for maximum lurking on 4chan.", "icons": { diff --git a/builds/wcrx/script.js b/builds/wcrx/script.js index 14457ad3c..91c21a80f 100644 --- a/builds/wcrx/script.js +++ b/builds/wcrx/script.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript /* -* 4chan X - Version 1.7.51 - 2014-06-02 +* 4chan X - Version 1.7.52 - 2014-06-02 * * Licensed under the MIT license. * https://github.com/ccd0/4chan-x/blob/master/LICENSE @@ -82,7 +82,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, Callbacks, CatalogLinks, Clone, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, g, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, Callbacks, CatalogLinks, Clone, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, g, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -170,7 +170,7 @@ 'Thread Hiding Link': [true, 'Add a link to hide entire threads.'], 'Reply Hiding Link': [true, 'Add a link to hide single replies.'], 'Delete Link': [true, 'Add post and image deletion links to the menu.'], - 'Download Link': [true, 'Add a download with original filename link to the menu. Chrome-only currently.'], + 'Download Link': [true, 'Add a download with original filename link to the menu.'], 'Archive Link': [true, 'Add an archive link to the menu.'] }, 'Monitoring': { @@ -348,7 +348,7 @@ doc = d.documentElement; g = { - VERSION: '1.7.51', + VERSION: '1.7.52', NAMESPACE: '4chan X.', boards: {} }; @@ -4045,6 +4045,47 @@ }; })(); + CrossOrigin = (function() { + var handleBlob, handleUrl; + handleBlob = function(urlBlob, contentType, contentDisposition, url, cb) { + 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 cb(blob); + }; + handleUrl = function(url, cb) { + var xhr; + 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) { + contentType = this.getResponseHeader('Content-Type'); + contentDisposition = this.getResponseHeader('Content-Disposition'); + return handleBlob(this.response, contentType, contentDisposition, url, cb); + } else { + return cb(null); + } + }; + xhr.onerror = function(e) { + return cb(null); + }; + return xhr.send(); + }; + return { + request: handleUrl + }; + })(); + Anonymize = { init: function() { if (g.VIEW === 'catalog' || !Conf['Anonymize']) { @@ -6090,43 +6131,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; + var url; url = prompt("Insert an 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) { - contentType = this.getResponseHeader('Content-Type'); - contentDisposition = this.getResponseHeader('Content-Disposition'); - return QR.handleBlob(this.response, contentType, contentDisposition, url); + return CrossOrigin.request(url, function(blob) { + if (blob) { + return QR.handleFiles([blob]); } else { return QR.error("Can't load image."); } - }; - xhr.onerror = function(e) { - return QR.error("Can't load image."); - }; - return xhr.send(); + }); }, handleFiles: function(files) { var file, i, _i, _len; @@ -12438,7 +12455,7 @@ Settings.dialog = dialog = $.el('div', { id: 'fourchanx-settings', className: 'dialog', - innerHTML: '
' + innerHTML: '
' }); $.on($('.export', Settings.dialog), 'click', Settings["export"]); $.on($('.import', Settings.dialog), 'click', Settings["import"]); @@ -13270,7 +13287,7 @@ } if (previousversion) { el = $.el('span', { - innerHTML: '4chan X has been updated to version 1.7.51.' + innerHTML: '4chan X has been updated to version 1.7.52.' }); new Notice('info', el, 15); } else { diff --git a/package.json b/package.json index a696651ae..d82ead39b 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "4chan-X", - "version": "1.7.51", + "version": "1.7.52", "description": "Cross-browser userscript for maximum lurking on 4chan.", "meta": { "name": "4chan X", diff --git a/src/General/Config.coffee b/src/General/Config.coffee index a5b4f206a..8086abf7d 100755 --- a/src/General/Config.coffee +++ b/src/General/Config.coffee @@ -229,12 +229,10 @@ Config = true 'Add post and image deletion links to the menu.' ] - <% if (type === 'crx') { %> 'Download Link': [ true - 'Add a download with original filename link to the menu. Chrome-only currently.' + 'Add a download with original filename link to the menu.' ] - <% } %> 'Archive Link': [ true 'Add an archive link to the menu.' diff --git a/src/General/CrossOrigin.coffee b/src/General/CrossOrigin.coffee new file mode 100644 index 000000000..a712cc9f5 --- /dev/null +++ b/src/General/CrossOrigin.coffee @@ -0,0 +1,52 @@ +CrossOrigin = do -> + + handleBlob = (urlBlob, contentType, contentDisposition, url, cb) -> + 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 + cb blob + + handleUrl = (url, cb) -> + <% if (type === 'crx') { %> + xhr = new XMLHttpRequest(); + xhr.open('GET', url, true) + xhr.responseType = 'blob' + xhr.onload = (e) -> + if @readyState is @DONE && xhr.status is 200 + contentType = @getResponseHeader('Content-Type') + contentDisposition = @getResponseHeader('Content-Disposition') + handleBlob @response, contentType, contentDisposition, url, cb + else + cb null + xhr.onerror = (e) -> + cb null + xhr.send() + <% } %> + + <% 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] + handleBlob data, contentType, contentDisposition, url, cb + onerror: (xhr) -> + cb null + <% } %> + + return {request: handleUrl} + diff --git a/src/Menu/DownloadLink.coffee b/src/Menu/DownloadLink.coffee index c3eda1620..8da1cb882 100755 --- a/src/Menu/DownloadLink.coffee +++ b/src/Menu/DownloadLink.coffee @@ -5,6 +5,21 @@ DownloadLink = a = $.el 'a', className: 'download-link' textContent: 'Download file' + + <% if (type === 'userscript') { %> + unless chrome? + # Firefox places same-origin restrictions on links with the download attribute. + $.on a, 'click', (e) -> + return true if @protocol is 'blob:' + e.preventDefault() + CrossOrigin.request @href, (blob) => + if blob + @href = URL.createObjectURL blob + @click() + else + new Notice 'error', "Could not download #{file.URL}", 30 + <% } %> + $.event 'AddMenuEntry', type: 'post' el: a diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee index cef172f12..9447ab7c4 100644 --- a/src/Posting/QR.coffee +++ b/src/Posting/QR.coffee @@ -293,56 +293,14 @@ QR = 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("Insert an url:") return if url is null - - <% if (type === 'crx') { %> - xhr = new XMLHttpRequest(); - xhr.open('GET', url, true) - xhr.responseType = 'blob' - xhr.onload = (e) -> - if @readyState is @DONE && xhr.status is 200 - contentType = @getResponseHeader('Content-Type') - contentDisposition = @getResponseHeader('Content-Disposition') - QR.handleBlob @response, contentType, contentDisposition, url + CrossOrigin.request url, (blob) -> + if blob + QR.handleFiles([blob]) else QR.error "Can't load image." - xhr.onerror = (e) -> - QR.error "Can't load image." - xhr.send() - <% } %> - - <% 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] - QR.handleBlob data, contentType, contentDisposition, url - onerror: (xhr) -> - QR.error "Can't load image." - <% } %> handleFiles: (files) -> if @ isnt QR # file input