OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * This file is part of Adblock Plus <https://adblockplus.org/>, |
| 3 * Copyright (C) 2006-2016 Eyeo GmbH |
| 4 * |
| 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 |
| 7 * published by the Free Software Foundation. |
| 8 * |
| 9 * Adblock Plus is distributed in the hope that it will be useful, |
| 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 * GNU General Public License for more details. |
| 13 * |
| 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/>. |
| 16 */ |
| 17 |
| 18 "use strict"; |
| 19 |
| 20 // The page ID for the popup filter selection dialog (top frame only). |
| 21 let blockelementPopupId = null; |
| 22 |
| 23 // Element picking state (top frame only). |
| 24 let currentlyPickingElement = false; |
| 25 let lastMouseOverEvent = null; |
| 26 |
| 27 // 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. |
| 29 let currentElement = null; |
| 30 |
| 31 // Highlighting state, used by the top frame during element picking and all |
| 32 // frames when the chosen element is highlighted red. |
| 33 let highlightedElementsSelector = null; |
| 34 let highlightedElementsInterval = null; |
| 35 |
| 36 // Last right click state stored for element blocking via the context menu. |
| 37 let lastRightClickEvent = null; |
| 38 let lastRightClickEventIsMostRecent = false; |
| 39 |
| 40 |
| 41 /* Utilities */ |
| 42 |
| 43 function getFiltersForElement(element, callback) |
| 44 { |
| 45 ext.backgroundPage.sendMessage( |
| 46 { |
| 47 type: "compose-filters", |
| 48 tagName: element.localName, |
| 49 id: element.id, |
| 50 src: element.getAttribute("src"), |
| 51 style: element.getAttribute("style"), |
| 52 classes: Array.prototype.slice.call(element.classList), |
| 53 urls: getURLsFromElement(element), |
| 54 mediatype: typeMap[element.localName], |
| 55 baseURL: document.location.href |
| 56 }, |
| 57 response => |
| 58 { |
| 59 callback(response.filters, response.selectors); |
| 60 }); |
| 61 } |
| 62 |
| 63 function getBlockableElementOrAncestor(element, callback) |
| 64 { |
| 65 // We assume that the user doesn't want to block the whole page. |
| 66 // So we never consider the <html> or <body> element. |
| 67 while (element && element != document.documentElement && |
| 68 element != document.body) |
| 69 { |
| 70 // We can't handle non-HTML (like SVG) elements, as well as |
| 71 // <area> elements (see below). So fall back to the parent element. |
| 72 if (!(element instanceof HTMLElement) || element.localName == "area") |
| 73 element = element.parentElement; |
| 74 |
| 75 // If image maps are used mouse events occur for the <area> element. |
| 76 // But we have to block the image associated with the <map> element. |
| 77 else if (element.localName == "map") |
| 78 { |
| 79 let images = document.querySelectorAll("img[usemap]"); |
| 80 let image = null; |
| 81 |
| 82 for (let i = 0; i < images.length; i++) |
| 83 { |
| 84 let usemap = images[i].getAttribute("usemap"); |
| 85 let index = usemap.indexOf("#"); |
| 86 |
| 87 if (index != -1 && usemap.substr(index + 1) == element.name) |
| 88 { |
| 89 image = images[i]; |
| 90 break; |
| 91 } |
| 92 } |
| 93 |
| 94 element = image; |
| 95 } |
| 96 |
| 97 // Finally, if none of the above is true, check whether we can generate |
| 98 // any filters for this element. Otherwise fall back to its parent element. |
| 99 else |
| 100 { |
| 101 getFiltersForElement(element, filters => |
| 102 { |
| 103 if (filters.length > 0) |
| 104 callback(element); |
| 105 else |
| 106 getBlockableElementOrAncestor(element.parentElement, callback); |
| 107 }); |
| 108 |
| 109 return; |
| 110 } |
| 111 } |
| 112 |
| 113 // We reached the document root without finding a blockable element. |
| 114 callback(null); |
| 115 } |
| 116 |
| 117 |
| 118 /* Element highlighting */ |
| 119 |
| 120 // Adds an overlay to an element, which is probably a Flash object. |
| 121 function addElementOverlay(element) |
| 122 { |
| 123 let position = "absolute"; |
| 124 let offsetX = window.scrollX; |
| 125 let offsetY = window.scrollY; |
| 126 |
| 127 for (let e = element; e; e = e.parentElement) |
| 128 { |
| 129 let style = getComputedStyle(e); |
| 130 |
| 131 // If the element isn't rendered (since its or one of its ancestor's |
| 132 // "display" property is "none"), the overlay wouldn't match the element. |
| 133 if (style.display == "none") |
| 134 return null; |
| 135 |
| 136 // If the element or one of its ancestors uses fixed postioning, the overlay |
| 137 // must too. Otherwise its position might not match the element's. |
| 138 if (style.position == "fixed") |
| 139 { |
| 140 position = "fixed"; |
| 141 offsetX = offsetY = 0; |
| 142 } |
| 143 } |
| 144 |
| 145 let overlay = document.createElement("div"); |
| 146 overlay.prisoner = element; |
| 147 overlay.className = "__adblockplus__overlay"; |
| 148 overlay.setAttribute("style", "opacity:0.4; display:inline-box; " + |
| 149 "overflow:hidden; box-sizing:border-box;"); |
| 150 let rect = element.getBoundingClientRect(); |
| 151 overlay.style.width = rect.width + "px"; |
| 152 overlay.style.height = rect.height + "px"; |
| 153 overlay.style.left = (rect.left + offsetX) + "px"; |
| 154 overlay.style.top = (rect.top + offsetY) + "px"; |
| 155 overlay.style.position = position; |
| 156 overlay.style.zIndex = 0x7FFFFFFE; |
| 157 |
| 158 document.documentElement.appendChild(overlay); |
| 159 return overlay; |
| 160 } |
| 161 |
| 162 function highlightElement(element, shadowColor, backgroundColor) |
| 163 { |
| 164 unhighlightElement(element); |
| 165 |
| 166 let highlightWithOverlay = function() |
| 167 { |
| 168 let overlay = addElementOverlay(element); |
| 169 |
| 170 // If the element isn't displayed no overlay will be added. |
| 171 // Moreover, we don't need to highlight anything then. |
| 172 if (!overlay) |
| 173 return; |
| 174 |
| 175 highlightElement(overlay, shadowColor, backgroundColor); |
| 176 overlay.style.pointerEvents = "none"; |
| 177 |
| 178 element._unhighlight = () => |
| 179 { |
| 180 overlay.parentNode.removeChild(overlay); |
| 181 }; |
| 182 }; |
| 183 |
| 184 let highlightWithStyleAttribute = function() |
| 185 { |
| 186 let originalBoxShadow = element.style.getPropertyValue("box-shadow"); |
| 187 let originalBoxShadowPriority = |
| 188 element.style.getPropertyPriority("box-shadow"); |
| 189 let originalBackgroundColor = |
| 190 element.style.getPropertyValue("background-color"); |
| 191 let originalBackgroundColorPriority = |
| 192 element.style.getPropertyPriority("background-color"); |
| 193 |
| 194 element.style.setProperty("box-shadow", "inset 0px 0px 5px " + shadowColor, |
| 195 "important"); |
| 196 element.style.setProperty("background-color", backgroundColor, "important"); |
| 197 |
| 198 element._unhighlight = () => |
| 199 { |
| 200 element.style.removeProperty("box-shadow"); |
| 201 element.style.setProperty( |
| 202 "box-shadow", |
| 203 originalBoxShadow, |
| 204 originalBoxShadowPriority |
| 205 ); |
| 206 |
| 207 element.style.removeProperty("background-color"); |
| 208 element.style.setProperty( |
| 209 "background-color", |
| 210 originalBackgroundColor, |
| 211 originalBackgroundColorPriority |
| 212 ); |
| 213 }; |
| 214 }; |
| 215 |
| 216 if ("prisoner" in element) |
| 217 highlightWithStyleAttribute(); |
| 218 else |
| 219 highlightWithOverlay(); |
| 220 } |
| 221 |
| 222 function unhighlightElement(element) |
| 223 { |
| 224 if (element && "_unhighlight" in element) |
| 225 { |
| 226 element._unhighlight(); |
| 227 delete element._unhighlight; |
| 228 } |
| 229 } |
| 230 |
| 231 // Highlight elements matching the selector string red. |
| 232 // (All elements that would be blocked by the proposed filters.) |
| 233 function highlightElements(selectorString) |
| 234 { |
| 235 unhighlightElements(); |
| 236 |
| 237 let elements = Array.prototype.slice.call( |
| 238 document.querySelectorAll(selectorString) |
| 239 ); |
| 240 highlightedElementsSelector = selectorString; |
| 241 |
| 242 // Highlight elements progressively. Otherwise the page freezes |
| 243 // when a lot of elements get highlighted at the same time. |
| 244 highlightedElementsInterval = setInterval(() => |
| 245 { |
| 246 if (elements.length > 0) |
| 247 { |
| 248 let element = elements.shift(); |
| 249 if (element != currentElement) |
| 250 highlightElement(element, "#fd6738", "#f6e1e5"); |
| 251 } |
| 252 else |
| 253 { |
| 254 clearInterval(highlightedElementsInterval); |
| 255 highlightedElementsInterval = null; |
| 256 } |
| 257 }, 0); |
| 258 } |
| 259 |
| 260 // Unhighlight the elements that were highlighted by selector string previously. |
| 261 function unhighlightElements() |
| 262 { |
| 263 if (highlightedElementsInterval) |
| 264 { |
| 265 clearInterval(highlightedElementsInterval); |
| 266 highlightedElementsInterval = null; |
| 267 } |
| 268 |
| 269 if (highlightedElementsSelector) |
| 270 { |
| 271 Array.prototype.forEach.call( |
| 272 document.querySelectorAll(highlightedElementsSelector), |
| 273 unhighlightElement |
| 274 ); |
| 275 |
| 276 highlightedElementsSelector = null; |
| 277 } |
| 278 } |
| 279 |
| 280 |
| 281 /* Input event handlers */ |
| 282 |
| 283 function stopEventPropagation(event) |
| 284 { |
| 285 event.stopPropagation(); |
| 286 } |
| 287 |
| 288 // Hovering over an element so highlight it. |
| 289 function mouseOver(event) |
| 290 { |
| 291 lastMouseOverEvent = event; |
| 292 |
| 293 getBlockableElementOrAncestor(event.target, element => |
| 294 { |
| 295 if (event == lastMouseOverEvent) |
| 296 { |
| 297 lastMouseOverEvent = null; |
| 298 |
| 299 if (currentlyPickingElement) |
| 300 { |
| 301 if (currentElement) |
| 302 unhighlightElement(currentElement); |
| 303 |
| 304 if (element) |
| 305 highlightElement(element, "#d6d84b", "#f8fa47"); |
| 306 |
| 307 currentElement = element; |
| 308 } |
| 309 } |
| 310 }); |
| 311 |
| 312 event.stopPropagation(); |
| 313 } |
| 314 |
| 315 // No longer hovering over this element so unhighlight it. |
| 316 function mouseOut(event) |
| 317 { |
| 318 if (!currentlyPickingElement || currentElement != event.target) |
| 319 return; |
| 320 |
| 321 unhighlightElement(currentElement); |
| 322 event.stopPropagation(); |
| 323 } |
| 324 |
| 325 // Key events - Return selects currently hovered-over element, escape aborts. |
| 326 function keyDown(event) |
| 327 { |
| 328 if (!event.ctrlKey && !event.altKey && !event.shiftKey) |
| 329 { |
| 330 if (event.keyCode == 13) // Return |
| 331 elementPicked(event); |
| 332 else if (event.keyCode == 27) // Escape |
| 333 deactivateBlockElement(); |
| 334 } |
| 335 } |
| 336 |
| 337 |
| 338 /* Element selection */ |
| 339 |
| 340 // Start highlighting elements yellow as the mouse moves over them, when one is |
| 341 // chosen launch the popup dialog for the user to confirm the generated filters. |
| 342 function startPickingElement() |
| 343 { |
| 344 currentlyPickingElement = true; |
| 345 |
| 346 // Add overlays for blockable elements that don't emit mouse events, |
| 347 // so that they can still be selected. |
| 348 Array.prototype.forEach.call( |
| 349 document.querySelectorAll("object,embed,iframe,frame"), |
| 350 element => |
| 351 { |
| 352 getFiltersForElement(element, filters => |
| 353 { |
| 354 if (filters.length > 0) |
| 355 addElementOverlay(element); |
| 356 }); |
| 357 } |
| 358 ); |
| 359 |
| 360 document.addEventListener("mousedown", stopEventPropagation, true); |
| 361 document.addEventListener("mouseup", stopEventPropagation, true); |
| 362 document.addEventListener("mouseenter", stopEventPropagation, true); |
| 363 document.addEventListener("mouseleave", stopEventPropagation, true); |
| 364 document.addEventListener("mouseover", mouseOver, true); |
| 365 document.addEventListener("mouseout", mouseOut, true); |
| 366 document.addEventListener("click", elementPicked, true); |
| 367 document.addEventListener("contextmenu", elementPicked, true); |
| 368 document.addEventListener("keydown", keyDown, true); |
| 369 |
| 370 ext.onExtensionUnloaded.addListener(deactivateBlockElement); |
| 371 } |
| 372 |
| 373 // The user has picked an element - currentElement. Highlight it red, generate |
| 374 // filters for it and open a popup dialog so that the user can confirm. |
| 375 function elementPicked(event) |
| 376 { |
| 377 if (!currentElement) |
| 378 return; |
| 379 |
| 380 let element = currentElement.prisoner || currentElement; |
| 381 getFiltersForElement(element, (filters, selectors) => |
| 382 { |
| 383 if (currentlyPickingElement) |
| 384 stopPickingElement(); |
| 385 |
| 386 ext.backgroundPage.sendMessage( |
| 387 { |
| 388 type: "blockelement-open-popup" |
| 389 }, |
| 390 response => |
| 391 { |
| 392 blockelementPopupId = response; |
| 393 ext.backgroundPage.sendMessage( |
| 394 { |
| 395 type: "forward", |
| 396 targetPageId: blockelementPopupId, |
| 397 payload: |
| 398 { |
| 399 type: "blockelement-popup-init", |
| 400 filters: filters |
| 401 } |
| 402 }); |
| 403 }); |
| 404 |
| 405 if (selectors.length > 0) |
| 406 highlightElements(selectors.join(",")); |
| 407 |
| 408 highlightElement(currentElement, "#fd1708", "#f6a1b5"); |
| 409 }); |
| 410 |
| 411 event.preventDefault(); |
| 412 event.stopPropagation(); |
| 413 } |
| 414 |
| 415 function stopPickingElement() |
| 416 { |
| 417 currentlyPickingElement = false; |
| 418 |
| 419 document.removeEventListener("mousedown", stopEventPropagation, true); |
| 420 document.removeEventListener("mouseup", stopEventPropagation, true); |
| 421 document.removeEventListener("mouseenter", stopEventPropagation, true); |
| 422 document.removeEventListener("mouseleave", stopEventPropagation, true); |
| 423 document.removeEventListener("mouseover", mouseOver, true); |
| 424 document.removeEventListener("mouseout", mouseOut, true); |
| 425 document.removeEventListener("click", elementPicked, true); |
| 426 document.removeEventListener("contextmenu", elementPicked, true); |
| 427 document.removeEventListener("keydown", keyDown, true); |
| 428 } |
| 429 |
| 430 |
| 431 /* Core logic */ |
| 432 |
| 433 // We're done with the block element feature for now, tidy everything up. |
| 434 function deactivateBlockElement() |
| 435 { |
| 436 if (currentlyPickingElement) |
| 437 stopPickingElement(); |
| 438 |
| 439 if (blockelementPopupId != null) |
| 440 { |
| 441 ext.backgroundPage.sendMessage( |
| 442 { |
| 443 type: "forward", |
| 444 targetPageId: blockelementPopupId, |
| 445 payload: |
| 446 { |
| 447 type: "blockelement-close-popup" |
| 448 } |
| 449 }); |
| 450 |
| 451 blockelementPopupId = null; |
| 452 } |
| 453 |
| 454 lastRightClickEvent = null; |
| 455 |
| 456 if (currentElement) |
| 457 { |
| 458 unhighlightElement(currentElement); |
| 459 currentElement = null; |
| 460 } |
| 461 unhighlightElements(); |
| 462 |
| 463 let overlays = document.getElementsByClassName("__adblockplus__overlay"); |
| 464 while (overlays.length > 0) |
| 465 overlays[0].parentNode.removeChild(overlays[0]); |
| 466 |
| 467 ext.onExtensionUnloaded.removeListener(deactivateBlockElement); |
| 468 } |
| 469 |
| 470 // In Chrome 37-40, the document_end content script (this one) runs properly, |
| 471 // while the document_start content scripts (that defines ext) might not. Check |
| 472 // whether variable ext exists before continuing to avoid |
| 473 // "Uncaught ReferenceError: ext is not defined". See https://crbug.com/416907 |
| 474 if ("ext" in window && document instanceof HTMLDocument) |
| 475 { |
| 476 // Use a contextmenu handler to save the last element the user right-clicked |
| 477 // on. To make things easier, we actually save the DOM event. We have to do |
| 478 // this because the contextMenu API only provides a URL, not the actual DOM |
| 479 // element. |
| 480 // We also need to make sure that the previous right click event, |
| 481 // if there is one, is removed. We don't know which frame it is in so we must |
| 482 // send a message to the other frames to clear their old right click events. |
| 483 document.addEventListener("contextmenu", event => |
| 484 { |
| 485 lastRightClickEvent = event; |
| 486 lastRightClickEventIsMostRecent = true; |
| 487 |
| 488 ext.backgroundPage.sendMessage( |
| 489 { |
| 490 type: "forward", |
| 491 payload: |
| 492 { |
| 493 type: "blockelement-clear-previous-right-click-event" |
| 494 } |
| 495 }); |
| 496 }, true); |
| 497 |
| 498 ext.onMessage.addListener((msg, sender, sendResponse) => |
| 499 { |
| 500 switch (msg.type) |
| 501 { |
| 502 case "blockelement-get-state": |
| 503 if (window == window.top) |
| 504 sendResponse({ |
| 505 active: currentlyPickingElement || blockelementPopupId != null |
| 506 }); |
| 507 break; |
| 508 case "blockelement-start-picking-element": |
| 509 if (window == window.top) |
| 510 startPickingElement(); |
| 511 break; |
| 512 case "blockelement-context-menu-clicked": |
| 513 let event = lastRightClickEvent; |
| 514 deactivateBlockElement(); |
| 515 if (event) |
| 516 { |
| 517 getBlockableElementOrAncestor(event.target, element => |
| 518 { |
| 519 if (element) |
| 520 { |
| 521 currentElement = element; |
| 522 elementPicked(event); |
| 523 } |
| 524 }); |
| 525 } |
| 526 break; |
| 527 case "blockelement-finished": |
| 528 if (currentElement && msg.remove) |
| 529 { |
| 530 // Hide the selected element itself if an added blocking |
| 531 // filter is causing it to collapse. Note that this |
| 532 // behavior is incomplete, but the best we can do here, |
| 533 // e.g. if an added blocking filter matches other elements, |
| 534 // the effect won't be visible until the page is is reloaded. |
| 535 checkCollapse(currentElement.prisoner || currentElement); |
| 536 |
| 537 // Apply added element hiding filters. |
| 538 updateStylesheet(); |
| 539 } |
| 540 deactivateBlockElement(); |
| 541 break; |
| 542 case "blockelement-clear-previous-right-click-event": |
| 543 if (!lastRightClickEventIsMostRecent) |
| 544 lastRightClickEvent = null; |
| 545 lastRightClickEventIsMostRecent = false; |
| 546 break; |
| 547 case "blockelement-popup-closed": |
| 548 // The onRemoved hook for the popup can create a race condition, so we |
| 549 // to be careful here. (This is not perfect, but best we can do.) |
| 550 if (window == window.top && blockelementPopupId == msg.popupId) |
| 551 { |
| 552 ext.backgroundPage.sendMessage( |
| 553 { |
| 554 type: "forward", |
| 555 payload: |
| 556 { |
| 557 type: "blockelement-finished" |
| 558 } |
| 559 }); |
| 560 } |
| 561 break; |
| 562 } |
| 563 }); |
| 564 |
| 565 if (window == window.top) |
| 566 ext.backgroundPage.sendMessage({type: "report-html-page"}); |
| 567 } |
OLD | NEW |