From d114bf21e7a731600f4469bf3e1cd529534e0f4a Mon Sep 17 00:00:00 2001 From: Deathamns Date: Sun, 2 Nov 2014 17:20:06 +0100 Subject: Site-patching possibility for Safari Safari's extension API doesn't provide a way to intercept requests initiated by plugins, so those cases need special care (or at least the popular sites). This commit adds a new JS file (sitepatch-safari.js), which will store the patches (if it's possible to create one) for specific sites. As an example, this commit includes a technique for removing in-video ads from YouTube videos. --- src/.jshintrc | 1 + src/js/element-picker.js | 2 +- src/js/sitepatch-safari.js | 91 ++++++++++++++++++++++++++++++++++++++++++++++ src/js/vapi-appinfo.js | 2 +- src/js/vapi-background.js | 36 ++++++++++++++---- src/js/vapi-client.js | 36 ++++++++++-------- src/js/vapi-common.js | 8 ++-- 7 files changed, 148 insertions(+), 28 deletions(-) create mode 100644 src/js/sitepatch-safari.js diff --git a/src/.jshintrc b/src/.jshintrc index b3ee961..618c34f 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -13,6 +13,7 @@ "newcap": false, "-W058": true, // suppress "Missing '()' invoking a constructor" message "globals": { + "self": false, "vAPI": false, "chrome": false, "safari": false, diff --git a/src/js/element-picker.js b/src/js/element-picker.js index 9700a27..fa29f6a 100644 --- a/src/js/element-picker.js +++ b/src/js/element-picker.js @@ -108,7 +108,7 @@ }; } -}(this)); +}(self)); /******************************************************************************/ /******************************************************************************/ diff --git a/src/js/sitepatch-safari.js b/src/js/sitepatch-safari.js new file mode 100644 index 0000000..e4eee90 --- /dev/null +++ b/src/js/sitepatch-safari.js @@ -0,0 +1,91 @@ +// Only for Safari +// Adding new URL requires to whitelist it in the background script too (addContentScriptFromURL) +// Note that the sitePach function will be converted to a string, and injected +// into the web-page in order to run in that scope. Because of this, variables +// from the extension scope won't be accessible in the sitePatch function. +'use strict'; + +self.vAPI = self.vAPI || {}; + +if (/^www\.youtube(-nocookie)?\.com/.test(location.host)) { + vAPI.sitePatch = function() { + window.addEventListener('load', function onWindowLoad() { + this.removeEventListener('load', onWindowLoad, true); + var spf = this._spf_state; + + if (spf && (spf = spf.config)) { + spf['navigate-limit'] = 0; + spf['navigate-part-received-callback'] = function(url) { + window.location.href = url; + }; + } + }, true); + + + // based on ExtendTube's ad removing solution + var p, yt = {}, config_ = {}, ytplayer = {}, playerConfig = { args: {} }; + + Object.defineProperties(yt, { + 'playerConfig': { + get: function() { return playerConfig; }, + set: function(data) { + if (data && typeof data === 'object' + && data.args && typeof data.args === 'object') { + var nope = /ad\d?_|afv|watermark|adsense|xfp/; + + for (var prop in data.args) { + if (nope.test(prop) && !/policy/.test(prop)) { + delete data.args[prop]; + } + } + } + + playerConfig = data; + + var playerRoot = document.querySelector('[data-swf-config]'); + if (playerRoot) + playerRoot.dataset.swfConfig = JSON.stringify(yt.playerConfig); + } + }, + 'config_': { + get: function() { return config_; }, + set: function(value) { config_ = value; } + } + }); + + Object.defineProperty(config_, 'PLAYER_CONFIG', { + get: function() { return yt.playerConfig; }, + set: function(value) { yt.playerConfig = value; } + }); + + Object.defineProperty(ytplayer, 'config', { + get: function() { return playerConfig; }, + set: function(value) { yt.playerConfig = value; } + }); + + if (window.yt) { + for (p in window.yt) { yt[p] = window.yt[p]; } + window.yt = yt; + } + else { + Object.defineProperty(window, 'yt', { + get: function() { return yt; }, + set: function() {} + }); + } + + if (window.ytplayer) { + for (p in window.ytplayer) { ytplayer[p] = window.ytplayer[p]; } + window.ytplayer = ytplayer; + } + else { + Object.defineProperty(window, 'ytplayer', { + get: function() { return ytplayer; }, + set: function() {} + }); + } + }; +} +/*else if (check url) { + vAPI.sitePatch do something +}*/ diff --git a/src/js/vapi-appinfo.js b/src/js/vapi-appinfo.js index dc4684d..2dea72c 100644 --- a/src/js/vapi-appinfo.js +++ b/src/js/vapi-appinfo.js @@ -1,7 +1,7 @@ // can be included anywhere if it's needed 'use strict'; -window.vAPI = window.vAPI || {}; +self.vAPI = self.vAPI || {}; vAPI.app = { /**/name: 'µBlock', diff --git a/src/js/vapi-background.js b/src/js/vapi-background.js index 4344530..c56d806 100644 --- a/src/js/vapi-background.js +++ b/src/js/vapi-background.js @@ -4,10 +4,10 @@ (function() { 'use strict'; -window.vAPI = window.vAPI || {}; +self.vAPI = self.vAPI || {}; -if (window.chrome) { - var chrome = window.chrome; +if (self.chrome) { + var chrome = self.chrome; vAPI.chrome = true; @@ -272,9 +272,22 @@ if (window.chrome) { chrome.contextMenus.remove(this.menuId); } }; -} else if (window.safari) { +} else if (self.safari) { vAPI.safari = true; + // addContentScriptFromURL allows whitelisting, + // so load sitepaching this way, instead of adding it to the Info.plist + safari.extension.addContentScriptFromURL( + safari.extension.baseURI + 'js/sitepatch-safari.js', + [ + 'http://www.youtube.com/*', + 'https://www.youtube.com/*', + 'http://www.youtube-nocookie.com/*', + 'https://www.youtube-nocookie.com/*' + ] + ); + + vAPI.storage = { _storage: safari.extension.settings, QUOTA_BYTES: 52428800, // copied from Info.plist @@ -709,6 +722,8 @@ if (window.chrome) { onBeforeRequest = onBeforeRequest.callback; this.onBeforeRequest.callback = function(e) { + var block; + if (e.name !== 'canLoad') { return; } @@ -718,6 +733,13 @@ if (window.chrome) { e.stopPropagation(); } + if (e.message.isWhiteListed) { + block = µBlock.URI.hostnameFromURI(e.message.isWhiteListed); + block = µBlock.URI.domainFromHostname(block) || block; + e.message = !!µBlock.netWhitelist[block]; + return e.message; + } + if (e.message.middleClickURL) { vAPI.lastMiddleClick = e.message; return; @@ -739,7 +761,7 @@ if (window.chrome) { return; } - var block = vAPI.net.onBeforeRequest; + block = vAPI.net.onBeforeRequest; if (block.types.indexOf(e.message.type) < 0) { return true; @@ -897,7 +919,7 @@ if (window.chrome) { }; } -if (!window.chrome) { - window.chrome = { runtime: { lastError: null } }; +if (!self.chrome) { + self.chrome = { runtime: { lastError: null } }; } })(); diff --git a/src/js/vapi-client.js b/src/js/vapi-client.js index 1fc4535..135a95d 100644 --- a/src/js/vapi-client.js +++ b/src/js/vapi-client.js @@ -4,7 +4,7 @@ (function() { 'use strict'; -window.vAPI = window.vAPI || {}; +self.vAPI = self.vAPI || {}; // since this is common across vendors var messagingConnector = function(response) { @@ -48,7 +48,7 @@ var messagingConnector = function(response) { } }; -if (window.chrome) { +if (self.chrome) { vAPI.chrome = true; vAPI.messaging = { port: null, @@ -102,7 +102,7 @@ if (window.chrome) { return this.channels[name]; } }; -} else if (window.safari) { +} else if (self.safari) { vAPI.safari = true; // relevant? @@ -121,7 +121,7 @@ if (window.chrome) { this.channels['vAPI'] = { listener: function(msg) { if (msg.cmd === 'runScript' && msg.details.code) { - Function(msg.details.code).call(window); + Function(msg.details.code).call(self); } } }; @@ -272,13 +272,13 @@ if (window.chrome) { document.addEventListener('beforeload', onBeforeLoad, true); - // blocking pop-ups and intercepting xhr requests + // block pop-ups, intercept xhr requests, and apply site patches var firstMutation = function() { document.removeEventListener('DOMSubtreeModified', firstMutation, true); firstMutation = null; - var randomEventName = parseInt(Math.random() * 1e15, 10).toString(36); + var randEventName = parseInt(Math.random() * 1e15, 10).toString(36); - window.addEventListener(randomEventName, function(e) { + window.addEventListener(randEventName, function(e) { var result = onBeforeLoad(beforeLoadEvent, e.detail); if (result === false) { @@ -289,13 +289,11 @@ if (window.chrome) { // the extension context is unable to reach the page context, // also this only works when Content Security Policy allows inline scripts var tmpJS = document.createElement('script'); - tmpJS.textContent = ["(function() {", + var tmpScript = ["(function() {", "var block = function(u, t) {", "var e = document.createEvent('CustomEvent'),", "d = {url: u, type: t};", - "e.initCustomEvent(", - "'" + randomEventName + "', !1, !1, d", - ");", + "e.initCustomEvent('" + randEventName + "', !1, !1, d);", "dispatchEvent(e);", "return d.url === !1;", "}, wo = open, xo = XMLHttpRequest.prototype.open;", @@ -304,8 +302,16 @@ if (window.chrome) { "};", "XMLHttpRequest.prototype.open = function(m, u) {", "return block(u, 'xmlhttprequest') ? null : xo.apply(this, [].slice.call(arguments));", - "};", - "})();"].join(''); + "};" + ]; + + if (vAPI.sitePatch + && !safari.self.tab.canLoad(beforeLoadEvent, {isWhiteListed: location.href})) { + tmpScript.push('(' + vAPI.sitePatch + ')();'); + } + + tmpScript.push("})();"); + tmpJS.textContent = tmpScript.join(''); document.documentElement.removeChild(document.documentElement.appendChild(tmpJS)); }; @@ -342,9 +348,9 @@ if (window.chrome) { safari.self.tab.setContextMenuEventUserInfo(e, details); }; - window.addEventListener('contextmenu', onContextMenu, true); + self.addEventListener('contextmenu', onContextMenu, true); - window.addEventListener('mouseup', function(e) { + self.addEventListener('mouseup', function(e) { if (e.button !== 1) { return; } diff --git a/src/js/vapi-common.js b/src/js/vapi-common.js index 3ece2ad..a44aa74 100644 --- a/src/js/vapi-common.js +++ b/src/js/vapi-common.js @@ -3,7 +3,7 @@ (function() { 'use strict'; -window.vAPI = window.vAPI || {}; +self.vAPI = self.vAPI || {}; // http://www.w3.org/International/questions/qa-scripts#directions var setScriptDirection = function(langugae) { @@ -38,8 +38,8 @@ vAPI.download = function(details) { } }; -if (window.chrome) { - var chrome = window.chrome; +if (self.chrome) { + var chrome = self.chrome; vAPI.getURL = function(path) { return chrome.runtime.getURL(path); @@ -50,7 +50,7 @@ if (window.chrome) { }; setScriptDirection(vAPI.i18n('@@ui_locale')); -} else if (window.safari) { +} else if (self.safari) { vAPI.getURL = function(path) { return safari.extension.baseURI + path; }; -- cgit v1.1