mirror of https://github.com/gorhill/uBlock.git
Implement pop-up blocking for Safari
It works similarly to the xhr intercepting, except here the window.open global function is being overridden. Note that it could only work if the site's Content Security Policy allows inline scripts, and the script on the webpage doesn't have a copy of the original window.open function (it can happen only if the page has an inline script in its head element, where the reference to the original function can be obtained - likely this cannot be prevented in Safari).
This commit is contained in:
parent
ac272afb4b
commit
f6f85ec793
|
@ -5,7 +5,7 @@
|
||||||
"update_url": "https://clients2.google.com/service/update2/crx",
|
"update_url": "https://clients2.google.com/service/update2/crx",
|
||||||
|
|
||||||
"version": "{version}",
|
"version": "{version}",
|
||||||
"name": "__MSG_extName__",
|
"name": "{name}",
|
||||||
"description": "__MSG_extShortDesc__",
|
"description": "__MSG_extShortDesc__",
|
||||||
"homepage_url": "{url}",
|
"homepage_url": "{url}",
|
||||||
"author": "{author}",
|
"author": "{author}",
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.7.0.7</string>
|
<string>0.7.0.7</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1452580</string>
|
<string>1453267</string>
|
||||||
<key>Chrome</key>
|
<key>Chrome</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>Database Quota</key>
|
<key>Database Quota</key>
|
||||||
|
|
|
@ -610,6 +610,11 @@
|
||||||
}, scriptDone);
|
}, scriptDone);
|
||||||
};
|
};
|
||||||
var scriptStart = function(tabId) {
|
var scriptStart = function(tabId) {
|
||||||
|
vAPI.tabs.injectScript(tabId, {
|
||||||
|
file: 'js/vapi-client.js',
|
||||||
|
allFrames: true,
|
||||||
|
runAt: 'document_start'
|
||||||
|
}, function(){ });
|
||||||
vAPI.tabs.injectScript(tabId, {
|
vAPI.tabs.injectScript(tabId, {
|
||||||
file: 'js/contentscript-start.js',
|
file: 'js/contentscript-start.js',
|
||||||
allFrames: true,
|
allFrames: true,
|
||||||
|
|
|
@ -79,6 +79,12 @@ vAPI.tabs.onPopup = function(details) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blocked
|
// Blocked
|
||||||
|
|
||||||
|
// Safari blocks before the pop-up opens, so there is no window to remove.
|
||||||
|
if (vAPI.safari) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// It is a popup, block and remove the tab.
|
// It is a popup, block and remove the tab.
|
||||||
µBlock.unbindTabFromPageStats(details.tabId);
|
µBlock.unbindTabFromPageStats(details.tabId);
|
||||||
µBlock.XAL.destroyTab(details.tabId);
|
µBlock.XAL.destroyTab(details.tabId);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* global SafariBrowserTab, Services, XPCOMUtils */
|
||||||
// for background page only
|
// for background page only
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
|
@ -392,16 +393,11 @@ if (window.chrome) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ??
|
// ??
|
||||||
/*if (typeof onUpdated === 'function') {
|
/* if (typeof this.onUpdated === 'function') { } */
|
||||||
chrome.tabs.onUpdated.addListener(this.onUpdated);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// onClosed handled in the main tab-close event
|
// onClosed handled in the main tab-close event
|
||||||
|
// onPopup is handled in window.open on web-pages?
|
||||||
// maybe intercept window.open on web-pages?
|
/* if (typeof onPopup === 'function') { } */
|
||||||
/*if (typeof onPopup === 'function') {
|
|
||||||
chrome.webNavigation.onCreatedNavigationTarget.addListener(this.onPopup);
|
|
||||||
}*/
|
|
||||||
},
|
},
|
||||||
getTabId: function(tab) {
|
getTabId: function(tab) {
|
||||||
for (var i in vAPI.tabs.stack) {
|
for (var i in vAPI.tabs.stack) {
|
||||||
|
@ -653,10 +649,6 @@ if (window.chrome) {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connector = function(request) {
|
this.connector = function(request) {
|
||||||
if (request.name === 'canLoad') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var callback = function(response) {
|
var callback = function(response) {
|
||||||
if (request.message.requestId && response !== undefined) {
|
if (request.message.requestId && response !== undefined) {
|
||||||
request.target.page.dispatchMessage(
|
request.target.page.dispatchMessage(
|
||||||
|
@ -689,6 +681,9 @@ if (window.chrome) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// the third parameter must stay false (bubbling), so later
|
||||||
|
// onBeforeRequest will use true (capturing), where we can invoke
|
||||||
|
// stopPropagation() (this way this.connector won't be fired)
|
||||||
safari.application.addEventListener('message', this.connector, false);
|
safari.application.addEventListener('message', this.connector, false);
|
||||||
},
|
},
|
||||||
broadcast: function(message) {
|
broadcast: function(message) {
|
||||||
|
@ -705,8 +700,6 @@ if (window.chrome) {
|
||||||
|
|
||||||
vAPI.net = {
|
vAPI.net = {
|
||||||
registerListeners: function() {
|
registerListeners: function() {
|
||||||
// onBeforeRequest is used in the messaging above, in the connector method
|
|
||||||
// in order to use only one listener
|
|
||||||
var onBeforeRequest = this.onBeforeRequest;
|
var onBeforeRequest = this.onBeforeRequest;
|
||||||
|
|
||||||
if (typeof onBeforeRequest.callback === 'function') {
|
if (typeof onBeforeRequest.callback === 'function') {
|
||||||
|
@ -715,53 +708,67 @@ if (window.chrome) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeRequest = onBeforeRequest.callback;
|
onBeforeRequest = onBeforeRequest.callback;
|
||||||
this.onBeforeRequest.callback = function(request) {
|
this.onBeforeRequest.callback = function(e) {
|
||||||
if (request.name !== 'canLoad') {
|
if (e.name !== 'canLoad') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no stopPropagation if it was called from beforeNavigate event
|
// no stopPropagation if it was called from beforeNavigate event
|
||||||
if (request.stopPropagation) {
|
if (e.stopPropagation) {
|
||||||
request.stopPropagation();
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// blocking unwanted pop-ups
|
||||||
|
if (e.message.type === 'popup') {
|
||||||
|
if (typeof vAPI.tabs.onPopup === 'function') {
|
||||||
|
e.message.type = 'main_frame';
|
||||||
|
e.message.sourceTabId = vAPI.tabs.getTabId(e.target);
|
||||||
|
|
||||||
|
if (vAPI.tabs.onPopup(e.message)) {
|
||||||
|
e.message = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.message = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var block = vAPI.net.onBeforeRequest;
|
var block = vAPI.net.onBeforeRequest;
|
||||||
|
|
||||||
if (block.types.indexOf(request.message.type) < 0) {
|
if (block.types.indexOf(e.message.type) < 0) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
request.message.tabId = vAPI.tabs.getTabId(request.target);
|
e.message.tabId = vAPI.tabs.getTabId(e.target);
|
||||||
block = onBeforeRequest(request.message);
|
block = onBeforeRequest(e.message);
|
||||||
|
|
||||||
// truthy return value will allow the request,
|
// truthy return value will allow the request,
|
||||||
// except when redirectUrl is present
|
// except when redirectUrl is present
|
||||||
if (block && typeof block === 'object') {
|
if (block && typeof block === 'object') {
|
||||||
if (block.cancel) {
|
if (block.cancel) {
|
||||||
request.message = false;
|
e.message = false;
|
||||||
}
|
}
|
||||||
else if (typeof block.redirectUrl === "string") {
|
else if (e.message.type === 'script'
|
||||||
request.message = block.redirectUrl;
|
&& typeof block.redirectUrl === "string") {
|
||||||
|
e.message = block.redirectUrl;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
request.message = true;
|
e.message = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
request.message = true;
|
e.message = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return request.message;
|
return e.message;
|
||||||
};
|
};
|
||||||
safari.application.addEventListener('message', this.onBeforeRequest.callback, true);
|
safari.application.addEventListener('message', this.onBeforeRequest.callback, true);
|
||||||
|
|
||||||
// 'main_frame' simulation, since this isn't available in beforeload
|
// 'main_frame' simulation, since this isn't available in beforeload
|
||||||
safari.application.addEventListener('beforeNavigate', function(e) {
|
safari.application.addEventListener('beforeNavigate', function(e) {
|
||||||
// e.url is not present for local files or data URIs
|
// e.url is not present for local files or data URIs
|
||||||
if (!e.url) {
|
if (e.url) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
vAPI.net.onBeforeRequest.callback({
|
vAPI.net.onBeforeRequest.callback({
|
||||||
name: 'canLoad',
|
name: 'canLoad',
|
||||||
target: e.target,
|
target: e.target,
|
||||||
|
@ -773,6 +780,7 @@ if (window.chrome) {
|
||||||
timeStamp: e.timeStamp
|
timeStamp: e.timeStamp
|
||||||
}
|
}
|
||||||
}) || e.preventDefault();
|
}) || e.preventDefault();
|
||||||
|
}
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -861,7 +869,7 @@ if (window.chrome) {
|
||||||
safari.application.addEventListener('contextmenu', this.onContextMenu);
|
safari.application.addEventListener('contextmenu', this.onContextMenu);
|
||||||
safari.application.addEventListener("command", this.onContextMenuCommand);
|
safari.application.addEventListener("command", this.onContextMenuCommand);
|
||||||
},
|
},
|
||||||
remove: function(argument) {
|
remove: function() {
|
||||||
safari.application.removeEventListener('contextmenu', this.onContextMenu);
|
safari.application.removeEventListener('contextmenu', this.onContextMenu);
|
||||||
safari.application.removeEventListener("command", this.onContextMenuCommand);
|
safari.application.removeEventListener("command", this.onContextMenuCommand);
|
||||||
this.onContextMenu = null;
|
this.onContextMenu = null;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* global addMessageListener, removeMessageListener, sendAsyncMessage */
|
||||||
// for non background pages
|
// for non background pages
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
|
@ -200,7 +201,6 @@ if (window.chrome) {
|
||||||
|
|
||||||
if (details) {
|
if (details) {
|
||||||
details.url = linkHelper.href;
|
details.url = linkHelper.href;
|
||||||
details.type = 'xmlhttprequest';
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
details = {
|
details = {
|
||||||
|
@ -237,6 +237,11 @@ if (window.chrome) {
|
||||||
default:
|
default:
|
||||||
details.type = 'other';
|
details.type = 'other';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This can run even before the first DOMSubtreeModified event fired
|
||||||
|
if (firstMutation) {
|
||||||
|
firstMutation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tabId is determined in the background script
|
// tabId is determined in the background script
|
||||||
|
@ -252,25 +257,22 @@ if (window.chrome) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// local mirroring, response is a data: URL here
|
// local mirroring, response is a data: URL here
|
||||||
else if (typeof response === 'string') {
|
else if (typeof response === 'string' && details.type === 'script') {
|
||||||
if (details.type === 'script') {
|
// Content Security Policy with disallowed inline scripts may break things
|
||||||
e.preventDefault();
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
else if (details.type === 'script') {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
details = document.createElement('script');
|
details = document.createElement('script');
|
||||||
details.textContent = atob(response.slice(35));
|
details.textContent = atob(response.slice(response.indexOf(',', 20) + 1));
|
||||||
e.target.parentNode.insertBefore(details, e.target);
|
e.target.parentNode.insertBefore(details, e.target);
|
||||||
details.parentNode.removeChild(details);
|
details.parentNode.removeChild(details);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('beforeload', onBeforeLoad, true);
|
document.addEventListener('beforeload', onBeforeLoad, true);
|
||||||
|
|
||||||
// intercepting xhr requests
|
// blocking pop-ups and intercepting xhr requests
|
||||||
setTimeout(function() {
|
var firstMutation = function() {
|
||||||
|
document.removeEventListener('DOMSubtreeModified', firstMutation, true);
|
||||||
|
firstMutation = null;
|
||||||
var randomEventName = parseInt(Math.random() * 1e15, 10).toString(36);
|
var randomEventName = parseInt(Math.random() * 1e15, 10).toString(36);
|
||||||
var beforeLoadEvent = document.createEvent('Event');
|
var beforeLoadEvent = document.createEvent('Event');
|
||||||
beforeLoadEvent.initEvent('beforeload');
|
beforeLoadEvent.initEvent('beforeload');
|
||||||
|
@ -278,41 +280,35 @@ if (window.chrome) {
|
||||||
window.addEventListener(randomEventName, function(e) {
|
window.addEventListener(randomEventName, function(e) {
|
||||||
var result = onBeforeLoad(beforeLoadEvent, e.detail);
|
var result = onBeforeLoad(beforeLoadEvent, e.detail);
|
||||||
|
|
||||||
if (onBeforeLoad(beforeLoadEvent, e.detail) === false) {
|
if (result === false) {
|
||||||
e.detail.url = false;
|
e.detail.url = false;
|
||||||
}
|
}
|
||||||
else if (typeof result === 'string') {
|
|
||||||
e.detail.url = result;
|
|
||||||
}
|
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// since the extension context is unable to reach the page context
|
// the extension context is unable to reach the page context,
|
||||||
var tempScript = document.createElement('script');
|
// also this only works when Content Security Policy allows inline scripts
|
||||||
tempScript.onload = function() {
|
var tmpJS = document.createElement('script');
|
||||||
this.parentNode.removeChild(this);
|
tmpJS.textContent = ["(function() {",
|
||||||
};
|
"var block = function(u, t) {",
|
||||||
document.head.appendChild(tempScript).src = "data:application/x-javascript;base64," + btoa(["(function() {",
|
"var e = document.createEvent('CustomEvent'),",
|
||||||
"var xhr_open = XMLHttpRequest.prototype.open;",
|
"d = {url: u, type: t};",
|
||||||
|
"e.initCustomEvent(",
|
||||||
"XMLHttpRequest.prototype.open = function(method, url, async, u, p) {",
|
"'" + randomEventName + "', !1, !1, d",
|
||||||
"var ev = document.createEvent('CustomEvent');",
|
|
||||||
"var detail = {url: url};",
|
|
||||||
"ev.initCustomEvent(",
|
|
||||||
"'" + randomEventName + "',",
|
|
||||||
"false, false,",
|
|
||||||
"detail",
|
|
||||||
");",
|
");",
|
||||||
"window.dispatchEvent(ev);",
|
"dispatchEvent(e);",
|
||||||
"if (detail.url === false) {",
|
"return d.url === !1;",
|
||||||
"throw Error;",
|
"}, wo = open, xo = XMLHttpRequest.prototype.open;",
|
||||||
"}",
|
"open = function(u) {",
|
||||||
"else if (typeof detail.url === 'string') {",
|
"return block(u, 'popup') ? null : wo.apply(this, [].slice.call(arguments));",
|
||||||
"url = detail.url;",
|
|
||||||
"}",
|
|
||||||
"return xhr_open.call(this, method, url, async, u, p);",
|
|
||||||
"};",
|
"};",
|
||||||
"})();"].join(''));
|
"XMLHttpRequest.prototype.open = function(m, u) {",
|
||||||
}, 0);
|
"return block(u, 'xmlhttprequest') ? null : xo.apply(this, [].slice.call(arguments));",
|
||||||
|
"};",
|
||||||
|
"})();"].join('');
|
||||||
|
document.head.removeChild(document.head.appendChild(tmpJS));
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMSubtreeModified', firstMutation, true);
|
||||||
|
|
||||||
var onContextMenu = function(e) {
|
var onContextMenu = function(e) {
|
||||||
var details = {
|
var details = {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"update_url": "https://clients2.google.com/service/update2/crx",
|
"update_url": "https://clients2.google.com/service/update2/crx",
|
||||||
|
|
||||||
"version": "0.7.0.10",
|
"version": "0.7.0.10",
|
||||||
"name": "__MSG_extName__",
|
"name": "µBlock",
|
||||||
"description": "__MSG_extShortDesc__",
|
"description": "__MSG_extShortDesc__",
|
||||||
"homepage_url": "https://github.com/gorhill/uBlock",
|
"homepage_url": "https://github.com/gorhill/uBlock",
|
||||||
"author": "Raymond Hill",
|
"author": "Raymond Hill",
|
||||||
|
|
Loading…
Reference in New Issue