Pop-Up Blocker

Simple but effective popup window blocker. Also tries to deny unsolicited redirections.

目前为 2016-12-14 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Pop-Up Blocker
  3. // @namespace HTML
  4. // @description Simple but effective popup window blocker. Also tries to deny unsolicited redirections.
  5. // @include *
  6. // @version $Id$
  7. // ==/UserScript==
  8.  
  9. (function () {
  10. var BLOCK_MODE = {'ALL': 0, // Block all popups
  11. 'CONFIRM': 2, // Confirm each popup (not recommended, but useful for testing)
  12. 'GRANT_PERIOD': 4, // Block popups that are initiated after the mouse click grant period
  13. 'INSECURE': 8} // Block popups from insecure (HTTP) sites
  14.  
  15. // Configuration
  16. var block_mode = BLOCK_MODE.GRANT_PERIOD | BLOCK_MODE.INSECURE;
  17. var grant_period = 100; // Milliseconds
  18. var debug = false; // Enable debug logging
  19.  
  20. // DO NOT CHANGE BELOW THIS POINT
  21. var allowed_elements = {'a': 1, 'button': 1, 'input': 1, 'select': 1, 'option': 1};
  22. var ts = 0, wopen = window.open, showmodaldlg = window.showModalDialog;
  23. var lastInteractedElement;
  24. var marginTop = null;
  25. var notifications = 0;
  26. var notificationOffsetTop = 0;
  27.  
  28. window.addEventListener('mousedown', function (event) {
  29. ts = Date.now();
  30. if (debug) console.info('Mouse button', event.button != null ? event.button : event.which, 'down on', event.target);
  31. setlastInteractedElement(event);
  32. //mediateEventPropagation(event);
  33. }, true);
  34.  
  35. window.addEventListener('click', function (event) {
  36. ts = Date.now();
  37. if (debug) console.info('Mouse button', event.button != null ? event.button : event.which, 'click on', event.target);
  38. setlastInteractedElement(event);
  39. //mediateEventPropagation(event);
  40. }, true);
  41.  
  42. window.addEventListener('change', function (event) {
  43. ts = Date.now();
  44. if (debug) console.info('Changed selection on', event.target);
  45. setlastInteractedElement(event);
  46. //mediateEventPropagation(event);
  47. }, true);
  48.  
  49. function setlastInteractedElement(event) {
  50. // Deal with tags nested in (e.g.) links
  51. var element = event.target;
  52. if (event instanceof MouseEvent && (event.button != null ? event.button != 0 : event.which != 1)) return;
  53. while (element.parentElement && element.tagName && !allowed_elements[element.tagName.toLowerCase()]) {
  54. element = element.parentElement;
  55. }
  56. lastInteractedElement = element;
  57. if (debug) console.info('Last interacted element', element);
  58. }
  59.  
  60. function mediateEventPropagation(event) {
  61. // Stop event propagation if element has a 'href' attribute that does not point to the current document
  62. // (prevents click hijacking)
  63. if ((block_mode & BLOCK_MODE.INSECURE ? location.protocol != 'https:' : true) &&
  64. lastInteractedElement &&
  65. lastInteractedElement.href &&
  66. boildown(lastInteractedElement.href) != boildown(window.location.href) &&
  67. lastInteractedElement.href.indexOf('javascript:') !== 0) {
  68. event.stopPropagation();
  69. console.warn('Pop-Up Blocker stopped event propagation for', event);
  70. }
  71. }
  72.  
  73. function regExpEscape(s) {
  74. return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
  75. replace(/\x08/g, '\\x08');
  76. };
  77.  
  78. var regExpProtHostPathQ = new RegExp('^(' + regExpEscape(location.protocol) + '//' +
  79. '(' + regExpEscape(location.host) +
  80. '(' + regExpEscape(location.pathname) + ')))?');
  81.  
  82. function boildown(uri) {
  83. var uri_boileddown = uri.replace(regExpProtHostPathQ, ''); // Strip current protocol + host + path
  84. uri_boileddown = uri_boileddown.replace(/#[^#]*$/, ''); // Strip any hash
  85. // Sort query vars
  86. var query = uri_boileddown.match(/\?[^?]+/);
  87. if (query)
  88. query = '?' + query[0].substr(1).split('&').sort().join('&');
  89. uri_boileddown = uri_boileddown.replace(/\?[^?]+/, query || '');
  90. return uri_boileddown;
  91. };
  92.  
  93. // Deny unsolicited redirection
  94. if (typeof window.location.watch == 'function') window.location.watch(
  95. 'href',
  96. function (id, oldval, newval) {
  97. var href_boileddown, newval_boileddown = boildown(newval);
  98. console.info('location.' + id, '->', newval);
  99. if (lastInteractedElement && lastInteractedElement.tagName &&
  100. lastInteractedElement.tagName.toLowerCase() == 'a')
  101. href_boileddown = boildown(lastInteractedElement.href);
  102. if ((block_mode & BLOCK_MODE.INSECURE ? location.protocol != 'https:' : true) &&
  103. (!lastInteractedElement ||
  104. (lastInteractedElement.tagName &&
  105. (!allowed_elements[lastInteractedElement.tagName.toLowerCase()] ||
  106. (lastInteractedElement.tagName.toLowerCase() == 'a' &&
  107. (newval_boileddown != href_boileddown ||
  108. lastInteractedElement.target == '_blank')))) ||
  109. Date.now() > ts + grant_period)) {
  110. if (debug) {
  111. console.info('Page secure?', location.protocol == 'https:');
  112. console.info('Allow insecure page?', block_mode & BLOCK_MODE.INSECURE ? false : true);
  113. console.info('Last interacted element?', lastInteractedElement);
  114. if (lastInteractedElement) {
  115. console.info('Last interacted element tag name?', lastInteractedElement.tagName);
  116. if (lastInteractedElement.tagName) {
  117. console.info('Allowed element?', !!allowed_elements[lastInteractedElement.tagName.toLowerCase()]);
  118. console.info('Last interacted element is link?', lastInteractedElement.tagName.toLowerCase() == 'a');
  119. if (lastInteractedElement.tagName.toLowerCase() == 'a') {
  120. console.info('New location (boiled down) =', newval_boileddown);
  121. console.info('Link HREF (boiled down) =', href_boileddown);
  122. console.info('New location is the same as link HREF?', newval_boileddown == href_boileddown);
  123. console.info('Link target is a new window?', lastInteractedElement.target == '_blank');
  124. }
  125. }
  126. }
  127. console.info('Grant period exceeded?', Date.now() > ts + grant_period);
  128. }
  129. notify('Denied redirection to', newval, null, 0, null, '_self');
  130. throw new Error('Pop-Up Blocker denied redirection to ' + newval);
  131. }
  132. return newval;
  133. }
  134. );
  135.  
  136. var onbeforeunload = window.onbeforeunload;
  137. window.onbeforeunload = function () {
  138. // Check if the last interacted element was a link or button, otherwise make browser ask if the user really wants to leave the page
  139. if (lastInteractedElement && lastInteractedElement.tagName &&
  140. !allowed_elements[lastInteractedElement.tagName.toLowerCase()] &&
  141. (block_mode & BLOCK_MODE.INSECURE ? location.protocol != 'https:' : true) && Date.now() <= ts + grant_period) {
  142. if (debug) {
  143. console.info('Page secure?', location.protocol == 'https:');
  144. console.info('Allow insecure page?', block_mode & BLOCK_MODE.INSECURE ? false : true);
  145. console.info('Last interacted element?', lastInteractedElement);
  146. if (lastInteractedElement) {
  147. console.info('Last interacted element tag name?', lastInteractedElement.tagName);
  148. if (lastInteractedElement.tagName) {
  149. console.info('Allowed element?', !!allowed_elements[lastInteractedElement.tagName.toLowerCase()]);
  150. }
  151. }
  152. console.info('Grant period exceeded?', Date.now() > ts + grant_period);
  153. }
  154. return 'You are possibly involuntarily being redirected to another page. Do you want to leave ' + location.href + ' or stay?';
  155. }
  156. else if (typeof onbeforeunload === 'function')
  157. return onbeforeunload.apply(window, arguments);
  158. };
  159.  
  160. function confirmPopup(msg, args) {
  161. confirm(msg + ' (' + Array.prototype.slice.apply(arguments).join(', ') + ')');
  162. }
  163.  
  164. window.open = function () {
  165. var oargs = arguments;
  166. if (debug) {
  167. console.info('Page secure?', location.protocol == 'https:');
  168. console.info('Allow insecure page?', block_mode & BLOCK_MODE.INSECURE ? false : true);
  169. console.info('Grant period exceeded?', Date.now() > ts + grant_period);
  170. }
  171. if (['_self', '_parent', '_top'].includes(arguments[1]) ||
  172. (block_mode != BLOCK_MODE.ALL &&
  173. (block_mode & BLOCK_MODE.CONFIRM
  174. ? confirmPopup('Allow popup?', arguments)
  175. : (block_mode & BLOCK_MODE.INSECURE ? location.protocol == 'https:' : true) && Date.now() <= ts + grant_period))) {
  176. console.info('Pop-Up Blocker allowed window.open', Array.prototype.slice.apply(arguments));
  177. return wopen.apply(window, arguments);
  178. }
  179. else {
  180. console.warn('Pop-Up Blocker blocked window.open', Array.prototype.slice.apply(arguments));
  181. notify('Blocked popup window', arguments[0], arguments[1], 0, function() {
  182. console.info('Pop-Up Blocker user clicked window.open', Array.prototype.slice.apply(oargs));
  183. wopen.apply(window, oargs);
  184. });
  185. }
  186. return {}
  187. };
  188.  
  189. window.showModalDialog = function () {
  190. if (debug) {
  191. console.info('Page secure?', location.protocol == 'https:');
  192. console.info('Allow insecure page?', block_mode & BLOCK_MODE.INSECURE ? false : true);
  193. console.info('Grant period exceeded?', Date.now() > ts + grant_period);
  194. }
  195. if (block_mode != BLOCK_MODE.ALL &&
  196. (block_mode & BLOCK_MODE.CONFIRM
  197. ? confirmPopup('Allow modal dialog?', arguments)
  198. : (block_mode & BLOCK_MODE.INSECURE ? location.protocol == 'https:' : true) && Date.now() <= ts + grant_period)) {
  199. console.info('Pop-Up Blocker allowed window.showModalDialog', Array.prototype.slice.apply(arguments));
  200. return showmodaldlg.apply(window, arguments);
  201. }
  202. else {
  203. console.warn('Pop-Up Blocker blocked modal showModalDialog', Array.prototype.slice.apply(arguments));
  204. notify('Blocked modal dialog', arguments[0], null, 0, function() {
  205. console.info('Pop-Up Blocker user clicked window.showModalDialog', Array.prototype.slice.apply(oargs));
  206. showmodaldlg.apply(window, oargs);
  207. });
  208. }
  209. return {}
  210. };
  211.  
  212. function notify(text, uri, title, timeout, onclick, target) {
  213. var rootElement = document.body.parentElement,
  214. notification = document.createElement('div');
  215. if (marginTop === null) marginTop = parseFloat((rootElement.currentStyle || window.getComputedStyle(rootElement)).marginTop)
  216. resetStyles(notification);
  217. notification.style.cssText = 'background: InfoBackground !important';
  218. notification.style.cssText += 'border-bottom: 1px solid WindowFrame !important';
  219. notification.style.cssText += 'box-sizing: border-box !important';
  220. notification.style.cssText += 'font: small-caption !important';
  221. notification.style.cssText += 'padding: .15em .9em !important';
  222. notification.style.cssText += 'position: fixed !important';
  223. notification.style.cssText += 'left: 0 !important';
  224. notification.style.cssText += 'line-height: 2.3 !important'; // 31px
  225. notification.style.cssText += 'right: 0 !important';
  226. notification.style.cssText += 'top: -100% !important';
  227. notification.style.cssText += 'transition: top .25s !important';
  228. notification.style.cssText += 'width: 100% !important';
  229. notification.style.cssText += 'white-space: nowrap !important';
  230. notification.style.cssText += 'z-index: 2147483647 !important';
  231. var closeButton = document.createElement('span');
  232. resetStyles(closeButton);
  233. closeButton.style.cssText += 'cursor: pointer !important';
  234. closeButton.style.cssText += 'display: inline-block !important';
  235. closeButton.style.cssText += 'float: right !important';
  236. closeButton.style.cssText += 'font: inherit !important';
  237. closeButton.style.cssText += 'line-height: 2.1 !important';
  238. closeButton.style.cssText += 'margin-left: .75em !important';
  239. closeButton.appendChild(document.createTextNode('╳'));
  240. function closeNotification(event) {
  241. if (event) event.stopPropagation();
  242. //notificationOffsetTop -= notification.offsetHeight;
  243. if (!--notifications) rootElement.style.cssText += 'margin-top: ' + marginTop + ' !important';
  244. notification.style.cssText += 'top: -' + notification.offsetHeight + 'px !important';
  245. setTimeout(function () {
  246. document.body.removeChild(notification);
  247. }, 250);
  248. return false;
  249. }
  250. closeButton.onclick = closeNotification;
  251. notification.appendChild(closeButton);
  252. notification.appendChild(document.createTextNode('? ' + text));
  253. var numLinks = target == '_self' ? 1 : 2;
  254. for (var i = 0; i < numLinks; i ++) {
  255. var popupLink = document.createElement(!i ? 'a' : 'button');
  256. resetStyles(popupLink);
  257. if (i) {
  258. popupLink.style.cssText = '-moz-appearance: button !important';
  259. popupLink.style.cssText += '-webkit-appearance: button !important';
  260. popupLink.style.cssText += 'appearance: button !important';
  261. popupLink.style.cssText += 'font: small-caption !important';
  262. }
  263. else {
  264. popupLink.style.cssText += 'color: #00e !important';
  265. popupLink.style.cssText += 'color: -moz-nativehyperlinktext !important';
  266. popupLink.style.cssText += 'display: inline-block !important';
  267. popupLink.style.cssText += 'font: inherit !important';
  268. popupLink.style.cssText += 'max-width: 50% !important';
  269. popupLink.style.cssText += 'overflow: hidden !important';
  270. popupLink.style.cssText += 'text-decoration: underline !important';
  271. popupLink.style.cssText += 'text-overflow: ellipsis !important';
  272. popupLink.style.cssText += 'vertical-align: bottom !important';
  273. popupLink.style.cssText += 'white-space: nowrap !important';
  274. popupLink.setAttribute('href', uri);
  275. popupLink.setAttribute('target', target || '_blank');
  276. }
  277. if (title) popupLink.setAttribute('title', title);
  278. var linkText = i ? 'Open in this frame' : uri;
  279. popupLink.appendChild(document.createTextNode(linkText));
  280. popupLink.onclick = function(event) {
  281. event.stopPropagation();
  282. closeNotification();
  283. if (this.tagName.toLowerCase() != 'a') {
  284. location.href = uri;
  285. }
  286. else if (onclick) {
  287. onclick(event);
  288. return false;
  289. }
  290. };
  291. notification.appendChild(document.createTextNode(' '));
  292. notification.appendChild(popupLink);
  293. }
  294. if (!notifications) rootElement.style.cssText += 'transition: margin-top .25s !important';
  295. document.body.appendChild(notification);
  296. // Note: offsetHeight is zero while the element is not part of the document DOM tree
  297. notification.style.cssText += 'top: -' + notification.offsetHeight + 'px !important';
  298. setTimeout(function() {
  299. //notificationOffsetTop += notification.offsetHeight;
  300. notification.style.cssText += 'top: ' + notificationOffsetTop + 'px !important';
  301. if (!notifications) rootElement.style.cssText += 'margin-top: ' + (marginTop + notification.offsetHeight) + 'px !important';
  302. notifications ++;
  303. }, 0)
  304. if (timeout) {
  305. setTimeout(function () {
  306. closeNotification();
  307. }, timeout);
  308. }
  309. }
  310.  
  311. function resetStyles(element) {
  312. element.style.cssText = 'background: transparent !important';
  313. element.style.cssText += 'border: none !important';
  314. element.style.cssText += 'border-radius: 0 !important';
  315. element.style.cssText += 'bottom: auto !important';
  316. element.style.cssText += 'box-shadow: none !important';
  317. element.style.cssText += 'color: WindowText !important';
  318. element.style.cssText += 'font: medium serif !important';
  319. element.style.cssText += 'line-height: normal !important';
  320. element.style.cssText += 'margin: 0 !important';
  321. element.style.cssText += 'opacity: 1 !important';
  322. element.style.cssText += 'outline: none !important';
  323. element.style.cssText += 'padding: 0 !important';
  324. element.style.cssText += 'position: static !important';
  325. element.style.cssText += 'text-align: left !important';
  326. element.style.cssText += 'text-shadow: none !important';
  327. element.style.cssText += 'left: auto !important';
  328. element.style.cssText += 'right: auto !important';
  329. element.style.cssText += 'top: auto !important';
  330. element.style.cssText += 'white-space: normal !important';
  331. element.style.cssText += 'width: auto !important';
  332. }
  333. })();