| 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 |