LEFT | RIGHT |
1 /* | 1 /* |
2 * This file is part of Adblock Plus <https://adblockplus.org/>, | 2 * This file is part of Adblock Plus <https://adblockplus.org/>, |
3 * Copyright (C) 2006-2016 Eyeo GmbH | 3 * Copyright (C) 2006-present eyeo GmbH |
4 * | 4 * |
5 * Adblock Plus is free software: you can redistribute it and/or modify | 5 * Adblock Plus is free software: you can redistribute it and/or modify |
6 * it under the terms of the GNU General Public License version 3 as | 6 * it under the terms of the GNU General Public License version 3 as |
7 * published by the Free Software Foundation. | 7 * published by the Free Software Foundation. |
8 * | 8 * |
9 * Adblock Plus is distributed in the hope that it will be useful, | 9 * Adblock Plus is distributed in the hope that it will be useful, |
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 * GNU General Public License for more details. | 12 * GNU General Public License for more details. |
13 * | 13 * |
14 * You should have received a copy of the GNU General Public License | 14 * You should have received a copy of the GNU General Public License |
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
16 */ | 16 */ |
17 | 17 |
18 "use strict"; | 18 "use strict"; |
19 | 19 |
| 20 const {checkCollapse, elemhide, |
| 21 getURLsFromElement, typeMap} = require("./include.preload"); |
| 22 |
20 // The page ID for the popup filter selection dialog (top frame only). | 23 // The page ID for the popup filter selection dialog (top frame only). |
21 let blockelementPopupId = null; | 24 let blockelementPopupId = null; |
22 | 25 |
23 // Element picking state (top frame only). | 26 // Element picking state (top frame only). |
24 let currentlyPickingElement = false; | 27 let currentlyPickingElement = false; |
25 let lastMouseOverEvent = null; | 28 let lastMouseOverEvent = null; |
26 | 29 |
27 // During element picking this is the currently highlighted element. When | 30 // During element picking this is the currently highlighted element. When |
28 // element has been picked this is the element that is due to be blocked. | 31 // element has been picked this is the element that is due to be blocked. |
29 let currentElement = null; | 32 let currentElement = null; |
30 | 33 |
31 // Highlighting state, used by the top frame during element picking and all | 34 // Highlighting state, used by the top frame during element picking and all |
32 // frames when the chosen element is highlighted red. | 35 // frames when the chosen element is highlighted red. |
33 let highlightedElementsSelector = null; | 36 let highlightedElementsSelector = null; |
34 let highlightedElementsInterval = null; | 37 let highlightedElementsInterval = null; |
35 | 38 |
36 // Last right click state stored for element blocking via the context menu. | 39 // Last right click state stored for element blocking via the context menu. |
37 let lastRightClickEvent = null; | 40 let lastRightClickEvent = null; |
38 let lastRightClickEventIsMostRecent = false; | 41 let lastRightClickEventIsMostRecent = false; |
39 | 42 |
| 43 let perFrameMessagingSupported = false; |
| 44 browser.runtime.sendMessage( |
| 45 {type: "app.get", what: "application"}, |
| 46 application => { perFrameMessagingSupported = application != "edge"; } |
| 47 ); |
40 | 48 |
41 /* Utilities */ | 49 /* Utilities */ |
42 | 50 |
43 function getFiltersForElement(element, callback) | 51 function getFiltersForElement(element, callback) |
44 { | 52 { |
45 let src = element.getAttribute("src"); | 53 let src = element.getAttribute("src"); |
46 ext.backgroundPage.sendMessage( | 54 browser.runtime.sendMessage({ |
47 { | |
48 type: "composer.getFilters", | 55 type: "composer.getFilters", |
49 tagName: element.localName, | 56 tagName: element.localName, |
50 id: element.id, | 57 id: element.id, |
51 src: src && src.length <= 1000 ? src : null, | 58 src: src && src.length <= 1000 ? src : null, |
52 style: element.getAttribute("style"), | 59 style: element.getAttribute("style"), |
53 classes: Array.prototype.slice.call(element.classList), | 60 classes: Array.prototype.slice.call(element.classList), |
54 urls: getURLsFromElement(element), | 61 urls: getURLsFromElement(element), |
55 mediatype: typeMap[element.localName], | 62 mediatype: typeMap.get(element.localName), |
56 baseURL: document.location.href | 63 baseURL: document.location.href |
57 }, | 64 }, |
58 response => | 65 response => |
59 { | 66 { |
60 callback(response.filters, response.selectors); | 67 callback(response.filters, response.selectors); |
61 }); | 68 }); |
62 } | 69 } |
63 | 70 |
64 function getBlockableElementOrAncestor(element, callback) | 71 function getBlockableElementOrAncestor(element, callback) |
65 { | 72 { |
(...skipping 12 matching lines...) Expand all Loading... |
78 if (!(element instanceof HTMLElement) || element.localName == "area") | 85 if (!(element instanceof HTMLElement) || element.localName == "area") |
79 element = element.parentElement; | 86 element = element.parentElement; |
80 | 87 |
81 // If image maps are used mouse events occur for the <area> element. | 88 // If image maps are used mouse events occur for the <area> element. |
82 // But we have to block the image associated with the <map> element. | 89 // But we have to block the image associated with the <map> element. |
83 else if (element.localName == "map") | 90 else if (element.localName == "map") |
84 { | 91 { |
85 let images = document.querySelectorAll("img[usemap]"); | 92 let images = document.querySelectorAll("img[usemap]"); |
86 let image = null; | 93 let image = null; |
87 | 94 |
88 for (let i = 0; i < images.length; i++) | 95 for (let currentImage of images) |
89 { | 96 { |
90 let usemap = images[i].getAttribute("usemap"); | 97 let usemap = currentImage.getAttribute("usemap"); |
91 let index = usemap.indexOf("#"); | 98 let index = usemap.indexOf("#"); |
92 | 99 |
93 if (index != -1 && usemap.substr(index + 1) == element.name) | 100 if (index != -1 && usemap.substr(index + 1) == element.name) |
94 { | 101 { |
95 image = images[i]; | 102 image = currentImage; |
96 break; | 103 break; |
97 } | 104 } |
98 } | 105 } |
99 | 106 |
100 element = image; | 107 element = image; |
101 } | 108 } |
102 | 109 |
103 // Finally, if none of the above is true, check whether we can generate | 110 // Finally, if none of the above is true, check whether we can generate |
104 // any filters for this element. Otherwise fall back to its parent element. | 111 // any filters for this element. Otherwise fall back to its parent element. |
105 else | 112 else |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
165 overlay.style.zIndex = 0x7FFFFFFE; | 172 overlay.style.zIndex = 0x7FFFFFFE; |
166 | 173 |
167 document.documentElement.appendChild(overlay); | 174 document.documentElement.appendChild(overlay); |
168 return overlay; | 175 return overlay; |
169 } | 176 } |
170 | 177 |
171 function highlightElement(element, shadowColor, backgroundColor) | 178 function highlightElement(element, shadowColor, backgroundColor) |
172 { | 179 { |
173 unhighlightElement(element); | 180 unhighlightElement(element); |
174 | 181 |
175 let highlightWithOverlay = function() | 182 let highlightWithOverlay = () => |
176 { | 183 { |
177 let overlay = addElementOverlay(element); | 184 let overlay = addElementOverlay(element); |
178 | 185 |
179 // If the element isn't displayed no overlay will be added. | 186 // If the element isn't displayed no overlay will be added. |
180 // Moreover, we don't need to highlight anything then. | 187 // Moreover, we don't need to highlight anything then. |
181 if (!overlay) | 188 if (!overlay) |
182 return; | 189 return; |
183 | 190 |
184 highlightElement(overlay, shadowColor, backgroundColor); | 191 highlightElement(overlay, shadowColor, backgroundColor); |
185 overlay.style.pointerEvents = "none"; | 192 overlay.style.pointerEvents = "none"; |
186 | 193 |
187 element._unhighlight = () => | 194 element._unhighlight = () => |
188 { | 195 { |
189 overlay.parentNode.removeChild(overlay); | 196 overlay.parentNode.removeChild(overlay); |
190 }; | 197 }; |
191 }; | 198 }; |
192 | 199 |
193 let highlightWithStyleAttribute = function() | 200 let highlightWithStyleAttribute = () => |
194 { | 201 { |
195 let originalBoxShadow = element.style.getPropertyValue("box-shadow"); | 202 let originalBoxShadow = element.style.getPropertyValue("box-shadow"); |
196 let originalBoxShadowPriority = | 203 let originalBoxShadowPriority = |
197 element.style.getPropertyPriority("box-shadow"); | 204 element.style.getPropertyPriority("box-shadow"); |
198 let originalBackgroundColor = | 205 let originalBackgroundColor = |
199 element.style.getPropertyValue("background-color"); | 206 element.style.getPropertyValue("background-color"); |
200 let originalBackgroundColorPriority = | 207 let originalBackgroundColorPriority = |
201 element.style.getPropertyPriority("background-color"); | 208 element.style.getPropertyPriority("background-color"); |
202 | 209 |
203 element.style.setProperty("box-shadow", "inset 0px 0px 5px " + shadowColor, | 210 element.style.setProperty("box-shadow", "inset 0px 0px 5px " + shadowColor, |
(...skipping 184 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
388 { | 395 { |
389 if (!currentElement) | 396 if (!currentElement) |
390 return; | 397 return; |
391 | 398 |
392 let element = currentElement.prisoner || currentElement; | 399 let element = currentElement.prisoner || currentElement; |
393 getFiltersForElement(element, (filters, selectors) => | 400 getFiltersForElement(element, (filters, selectors) => |
394 { | 401 { |
395 if (currentlyPickingElement) | 402 if (currentlyPickingElement) |
396 stopPickingElement(); | 403 stopPickingElement(); |
397 | 404 |
398 ext.backgroundPage.sendMessage( | 405 browser.runtime.sendMessage({ |
399 { | |
400 type: "composer.openDialog" | 406 type: "composer.openDialog" |
401 }, | 407 }, |
402 popupId => | 408 popupId => |
403 { | 409 { |
404 ext.backgroundPage.sendMessage( | 410 browser.runtime.sendMessage({ |
405 { | |
406 type: "forward", | 411 type: "forward", |
407 targetPageId: popupId, | 412 targetPageId: popupId, |
408 payload: | 413 payload: {type: "composer.dialog.init", filters} |
409 { | |
410 type: "composer.dialog.init", | |
411 filters: filters | |
412 } | |
413 }); | 414 }); |
414 | 415 |
415 // Only the top frame keeps a record of the popup window's ID, | 416 // Only the top frame keeps a record of the popup window's ID, |
416 // so if this isn't the top frame we need to pass the ID on. | 417 // so if this isn't the top frame we need to pass the ID on. |
417 if (window == window.top) | 418 if (window == window.top) |
418 { | 419 { |
419 blockelementPopupId = popupId; | 420 blockelementPopupId = popupId; |
420 } | 421 } |
421 else | 422 else |
422 { | 423 { |
423 ext.backgroundPage.sendMessage( | 424 browser.runtime.sendMessage({ |
424 { | |
425 type: "forward", | 425 type: "forward", |
426 payload: | 426 payload: {type: "composer.content.dialogOpened", popupId} |
427 { | |
428 type: "composer.content.dialogOpened", | |
429 popupId: popupId | |
430 } | |
431 }); | 427 }); |
432 } | 428 } |
433 }); | 429 }); |
434 | 430 |
435 if (selectors.length > 0) | 431 if (selectors.length > 0) |
436 highlightElements(selectors.join(",")); | 432 highlightElements(selectors.join(",")); |
437 | 433 |
438 highlightElement(currentElement, "#fd1708", "#f6a1b5"); | 434 highlightElement(currentElement, "#fd1708", "#f6a1b5"); |
439 }); | 435 }); |
440 | 436 |
(...skipping 23 matching lines...) Expand all Loading... |
464 /* Core logic */ | 460 /* Core logic */ |
465 | 461 |
466 // We're done with the block element feature for now, tidy everything up. | 462 // We're done with the block element feature for now, tidy everything up. |
467 function deactivateBlockElement() | 463 function deactivateBlockElement() |
468 { | 464 { |
469 if (currentlyPickingElement) | 465 if (currentlyPickingElement) |
470 stopPickingElement(); | 466 stopPickingElement(); |
471 | 467 |
472 if (blockelementPopupId != null) | 468 if (blockelementPopupId != null) |
473 { | 469 { |
474 ext.backgroundPage.sendMessage( | 470 browser.runtime.sendMessage({ |
475 { | |
476 type: "forward", | 471 type: "forward", |
477 targetPageId: blockelementPopupId, | 472 targetPageId: blockelementPopupId, |
478 payload: | 473 payload: |
479 { | 474 { |
480 type: "composer.dialog.close" | 475 type: "composer.dialog.close" |
481 } | 476 } |
482 }); | 477 }); |
483 | 478 |
484 blockelementPopupId = null; | 479 blockelementPopupId = null; |
485 } | 480 } |
486 | 481 |
487 lastRightClickEvent = null; | 482 lastRightClickEvent = null; |
488 | 483 |
489 if (currentElement) | 484 if (currentElement) |
490 { | 485 { |
491 unhighlightElement(currentElement); | 486 unhighlightElement(currentElement); |
492 currentElement = null; | 487 currentElement = null; |
493 } | 488 } |
494 unhighlightElements(); | 489 unhighlightElements(); |
495 | 490 |
496 let overlays = document.getElementsByClassName("__adblockplus__overlay"); | 491 let overlays = document.getElementsByClassName("__adblockplus__overlay"); |
497 while (overlays.length > 0) | 492 while (overlays.length > 0) |
498 overlays[0].parentNode.removeChild(overlays[0]); | 493 overlays[0].parentNode.removeChild(overlays[0]); |
499 | 494 |
500 ext.onExtensionUnloaded.removeListener(deactivateBlockElement); | 495 ext.onExtensionUnloaded.removeListener(deactivateBlockElement); |
501 } | 496 } |
502 | 497 |
503 if (document instanceof HTMLDocument) | 498 function initializeComposer() |
504 { | 499 { |
| 500 if (typeof ext == "undefined") |
| 501 return false; |
| 502 |
505 // Use a contextmenu handler to save the last element the user right-clicked | 503 // Use a contextmenu handler to save the last element the user right-clicked |
506 // on. To make things easier, we actually save the DOM event. We have to do | 504 // on. To make things easier, we actually save the DOM event. We have to do |
507 // this because the contextMenu API only provides a URL, not the actual DOM | 505 // this because the contextMenu API only provides a URL, not the actual DOM |
508 // element. | 506 // element. |
509 // We also need to make sure that the previous right click event, | 507 // We also need to make sure that the previous right click event, |
510 // if there is one, is removed. We don't know which frame it is in so we must | 508 // if there is one, is removed. We don't know which frame it is in so we must |
511 // send a message to the other frames to clear their old right click events. | 509 // send a message to the other frames to clear their old right click events. |
512 document.addEventListener("contextmenu", event => | 510 document.addEventListener("contextmenu", event => |
513 { | 511 { |
514 lastRightClickEvent = event; | 512 lastRightClickEvent = event; |
515 lastRightClickEventIsMostRecent = true; | 513 lastRightClickEventIsMostRecent = true; |
516 | 514 |
517 ext.backgroundPage.sendMessage( | 515 browser.runtime.sendMessage({ |
518 { | |
519 type: "forward", | 516 type: "forward", |
520 payload: | 517 payload: |
521 { | 518 { |
522 type: "composer.content.clearPreviousRightClickEvent" | 519 type: "composer.content.clearPreviousRightClickEvent" |
523 } | 520 } |
524 }); | 521 }); |
525 }, true); | 522 }, true); |
526 | 523 |
527 ext.onMessage.addListener((msg, sender, sendResponse) => | 524 ext.onMessage.addListener((message, sender, sendResponse) => |
528 { | 525 { |
529 switch (msg.type) | 526 switch (message.type) |
530 { | 527 { |
531 case "composer.content.getState": | 528 case "composer.content.getState": |
532 if (window == window.top) | 529 if (window == window.top) |
| 530 { |
533 sendResponse({ | 531 sendResponse({ |
534 active: currentlyPickingElement || blockelementPopupId != null | 532 active: currentlyPickingElement || blockelementPopupId != null |
535 }); | 533 }); |
| 534 } |
536 break; | 535 break; |
537 case "composer.content.startPickingElement": | 536 case "composer.content.startPickingElement": |
538 if (window == window.top) | 537 if (window == window.top) |
539 startPickingElement(); | 538 startPickingElement(); |
540 break; | 539 break; |
541 case "composer.content.contextMenuClicked": | 540 case "composer.content.contextMenuClicked": |
542 let event = lastRightClickEvent; | 541 let event = lastRightClickEvent; |
543 let target = event && event.target; | 542 let target = event && event.target; |
544 | 543 |
545 // When the user attempts to block an element inside an iframe for which | 544 // When the user attempts to block an element inside an iframe for which |
546 // our right click event listener was trashed the best we can do is to | 545 // our right click event listener was trashed the best we can do is to |
547 // offer to block the entire iframe. This of course only works if the | 546 // offer to block the entire iframe. This doesn't work for cross origin |
548 // parent frame is considered to be of the same origin. | 547 // frames, neither on Edge where per-frame messaging isn't supported |
549 // Note: Since Edge doesn't yet support per-frame messaging[1] we | 548 // yet[1], but it's the best we can do. |
550 // can't use this workaround there yet. (The contextMenuClicked message | |
551 // will be sent to all of the page's frames.) | |
552 // [1] - https://developer.microsoft.com/en-us/microsoft-edge/platform/d
ocumentation/extensions/api-support/supported-apis/ | 549 // [1] - https://developer.microsoft.com/en-us/microsoft-edge/platform/d
ocumentation/extensions/api-support/supported-apis/ |
553 if (!target && window.frameElement && typeof chrome != "undefined") | 550 if (!target && window.frameElement && perFrameMessagingSupported) |
554 target = addElementOverlay(window.frameElement); | 551 target = addElementOverlay(window.frameElement); |
555 | 552 |
556 deactivateBlockElement(); | 553 deactivateBlockElement(); |
557 if (target) | 554 if (target) |
558 { | 555 { |
559 getBlockableElementOrAncestor(target, element => | 556 getBlockableElementOrAncestor(target, element => |
560 { | 557 { |
561 if (element) | 558 if (element) |
562 { | 559 { |
563 currentElement = element; | 560 currentElement = element; |
564 elementPicked(event); | 561 elementPicked(event); |
565 } | 562 } |
566 }); | 563 }); |
567 } | 564 } |
568 break; | 565 break; |
569 case "composer.content.finished": | 566 case "composer.content.finished": |
570 if (currentElement && msg.remove) | 567 if (currentElement && message.remove) |
571 { | 568 { |
572 // Hide the selected element itself if an added blocking | 569 // Hide the selected element itself if an added blocking |
573 // filter is causing it to collapse. Note that this | 570 // filter is causing it to collapse. Note that this |
574 // behavior is incomplete, but the best we can do here, | 571 // behavior is incomplete, but the best we can do here, |
575 // e.g. if an added blocking filter matches other elements, | 572 // e.g. if an added blocking filter matches other elements, |
576 // the effect won't be visible until the page is is reloaded. | 573 // the effect won't be visible until the page is is reloaded. |
577 checkCollapse(currentElement.prisoner || currentElement); | 574 checkCollapse(currentElement.prisoner || currentElement); |
578 | 575 |
579 // Apply added element hiding filters. | 576 // Apply added element hiding filters. |
580 elemhide.apply(); | 577 elemhide.apply(); |
581 } | 578 } |
582 deactivateBlockElement(); | 579 deactivateBlockElement(); |
583 break; | 580 break; |
584 case "composer.content.clearPreviousRightClickEvent": | 581 case "composer.content.clearPreviousRightClickEvent": |
585 if (!lastRightClickEventIsMostRecent) | 582 if (!lastRightClickEventIsMostRecent) |
586 lastRightClickEvent = null; | 583 lastRightClickEvent = null; |
587 lastRightClickEventIsMostRecent = false; | 584 lastRightClickEventIsMostRecent = false; |
588 break; | 585 break; |
589 case "composer.content.dialogOpened": | 586 case "composer.content.dialogOpened": |
590 if (window == window.top) | 587 if (window == window.top) |
591 blockelementPopupId = msg.popupId; | 588 blockelementPopupId = message.popupId; |
592 break; | 589 break; |
593 case "composer.content.dialogClosed": | 590 case "composer.content.dialogClosed": |
594 // The onRemoved hook for the popup can create a race condition, so we | 591 // The onRemoved hook for the popup can create a race condition, so we |
595 // to be careful here. (This is not perfect, but best we can do.) | 592 // to be careful here. (This is not perfect, but best we can do.) |
596 if (window == window.top && blockelementPopupId == msg.popupId) | 593 if (window == window.top && blockelementPopupId == message.popupId) |
597 { | 594 { |
598 ext.backgroundPage.sendMessage( | 595 browser.runtime.sendMessage({ |
599 { | |
600 type: "forward", | 596 type: "forward", |
601 payload: | 597 payload: |
602 { | 598 { |
603 type: "composer.content.finished" | 599 type: "composer.content.finished" |
604 } | 600 } |
605 }); | 601 }); |
606 } | 602 } |
607 break; | 603 break; |
608 } | 604 } |
609 }); | 605 }); |
610 | 606 |
611 if (window == window.top) | 607 if (window == window.top) |
612 ext.backgroundPage.sendMessage({type: "composer.ready"}); | 608 browser.runtime.sendMessage({type: "composer.ready"}); |
613 } | 609 |
| 610 return true; |
| 611 } |
| 612 |
| 613 if (document instanceof HTMLDocument) |
| 614 { |
| 615 // There's a bug in Firefox that causes document_end content scripts to run |
| 616 // before document_start content scripts on extension startup. In this case |
| 617 // the ext object is undefined, we fail to initialize, and initializeComposer |
| 618 // returns false. As a workaround, try again after a timeout. |
| 619 // https://bugzilla.mozilla.org/show_bug.cgi?id=1395287 |
| 620 if (!initializeComposer()) |
| 621 setTimeout(initializeComposer, 2000); |
| 622 } |
LEFT | RIGHT |