Pop-Up Blocker

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

目前为 2018-06-02 提交的版本。查看 最新版本

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