| 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 { | |
| 21 /* Pages */ | |
| 22 | |
| 23 let Page = ext.Page = function(tab) | |
| 24 { | |
| 25 this.id = tab.id; | |
| 26 this._url = tab.url && new URL(tab.url); | |
| 27 | |
| 28 this.browserAction = new BrowserAction(tab.id); | |
| 29 this.contextMenus = new ContextMenus(this); | |
| 30 }; | |
| 31 Page.prototype = { | |
| 32 get url() | |
| 33 { | |
| 34 // usually our Page objects are created from Chrome's Tab objects, which | |
| 35 // provide the url. So we can return the url given in the constructor. | |
| 36 if (this._url) | |
| 37 return this._url; | |
| 38 | |
| 39 // but sometimes we only have the tab id when we create a Page object. | |
| 40 // In that case we get the url from top frame of the tab, recorded by | |
| 41 // the onBeforeRequest handler. | |
| 42 let frames = framesOfTabs[this.id]; | |
| 43 if (frames) | |
| 44 { | |
| 45 let frame = frames[0]; | |
| 46 if (frame) | |
| 47 return frame.url; | |
| 48 } | |
| 49 }, | |
| 50 sendMessage(message, responseCallback) | |
| 51 { | |
| 52 chrome.tabs.sendMessage(this.id, message, responseCallback); | |
| 53 } | |
| 54 }; | |
| 55 | |
| 56 ext.getPage = id => new Page({id: parseInt(id, 10)}); | |
| 57 | |
| 58 function afterTabLoaded(callback) | |
| 59 { | |
| 60 return openedTab => | |
| 61 { | |
| 62 let onUpdated = (tabId, changeInfo, tab) => | |
| 63 { | |
| 64 if (tabId == openedTab.id && changeInfo.status == "complete") | |
| 65 { | |
| 66 chrome.tabs.onUpdated.removeListener(onUpdated); | |
| 67 callback(new Page(openedTab)); | |
| 68 } | |
| 69 }; | |
| 70 chrome.tabs.onUpdated.addListener(onUpdated); | |
| 71 }; | |
| 72 } | |
| 73 | |
| 74 ext.pages = { | |
| 75 open(url, callback) | |
| 76 { | |
| 77 chrome.tabs.create({url: url}, callback && afterTabLoaded(callback)); | |
| 78 }, | |
| 79 query(info, callback) | |
| 80 { | |
| 81 let rawInfo = {}; | |
| 82 for (let property in info) | |
| 83 { | |
| 84 switch (property) | |
| 85 { | |
| 86 case "active": | |
| 87 case "lastFocusedWindow": | |
| 88 rawInfo[property] = info[property]; | |
| 89 } | |
| 90 } | |
| 91 | |
| 92 chrome.tabs.query(rawInfo, tabs => | |
| 93 { | |
| 94 callback(tabs.map(tab => new Page(tab))); | |
| 95 }); | |
| 96 }, | |
| 97 onLoading: new ext._EventTarget(), | |
| 98 onActivated: new ext._EventTarget(), | |
| 99 onRemoved: new ext._EventTarget() | |
| 100 }; | |
| 101 | |
| 102 chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => | |
| 103 { | |
| 104 if (changeInfo.status == "loading") | |
| 105 ext.pages.onLoading._dispatch(new Page(tab)); | |
| 106 }); | |
| 107 | |
| 108 function createFrame(tabId, frameId) | |
| 109 { | |
| 110 let frames = framesOfTabs[tabId]; | |
| 111 if (!frames) | |
| 112 frames = framesOfTabs[tabId] = Object.create(null); | |
| 113 | |
| 114 let frame = frames[frameId]; | |
| 115 if (!frame) | |
| 116 frame = frames[frameId] = {}; | |
| 117 | |
| 118 return frame; | |
| 119 } | |
| 120 | |
| 121 chrome.webNavigation.onBeforeNavigate.addListener(details => | |
| 122 { | |
| 123 // Capture parent frame here because onCommitted doesn't get this info. | |
| 124 let frame = createFrame(details.tabId, details.frameId); | |
| 125 frame.parent = framesOfTabs[details.tabId][details.parentFrameId] || null; | |
| 126 }); | |
| 127 | |
| 128 let eagerlyUpdatedPages = new ext.PageMap(); | |
| 129 | |
| 130 ext._updatePageFrameStructure = (frameId, tabId, url, eager) => | |
| 131 { | |
| 132 if (frameId == 0) | |
| 133 { | |
| 134 let page = new Page({id: tabId, url: url}); | |
| 135 | |
| 136 if (eagerlyUpdatedPages.get(page) != url) | |
| 137 { | |
| 138 ext._removeFromAllPageMaps(tabId); | |
| 139 | |
| 140 // When a sitekey header is received we must immediately update the page | |
| 141 // structure in order to record and use the key. We want to avoid | |
| 142 // trashing the page structure if the onCommitted event is then fired | |
| 143 // for the page. | |
| 144 if (eager) | |
| 145 eagerlyUpdatedPages.set(page, url); | |
| 146 | |
| 147 chrome.tabs.get(tabId, () => | |
| 148 { | |
| 149 // If the tab is prerendered, chrome.tabs.get() sets | |
| 150 // chrome.runtime.lastError and we have to dispatch the onLoading even
t, | |
| 151 // since the onUpdated event isn't dispatched for prerendered tabs. | |
| 152 // However, we have to keep relying on the unUpdated event for tabs th
at | |
| 153 // are already visible. Otherwise browser action changes get overridde
n | |
| 154 // when Chrome automatically resets them on navigation. | |
| 155 if (chrome.runtime.lastError) | |
| 156 ext.pages.onLoading._dispatch(page); | |
| 157 }); | |
| 158 } | |
| 159 } | |
| 160 | |
| 161 // Update frame URL in frame structure | |
| 162 let frame = createFrame(tabId, frameId); | |
| 163 frame.url = new URL(url); | |
| 164 }; | |
| 165 | |
| 166 chrome.webNavigation.onCommitted.addListener(details => | |
| 167 { | |
| 168 ext._updatePageFrameStructure(details.frameId, details.tabId, details.url); | |
| 169 }); | |
| 170 | |
| 171 function forgetTab(tabId) | |
| 172 { | |
| 173 ext.pages.onRemoved._dispatch(tabId); | |
| 174 | |
| 175 ext._removeFromAllPageMaps(tabId); | |
| 176 delete framesOfTabs[tabId]; | |
| 177 } | |
| 178 | |
| 179 chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => | |
| 180 { | |
| 181 forgetTab(removedTabId); | |
| 182 }); | |
| 183 | |
| 184 chrome.tabs.onRemoved.addListener(forgetTab); | |
| 185 | |
| 186 chrome.tabs.onActivated.addListener(details => | |
| 187 { | |
| 188 ext.pages.onActivated._dispatch(new Page({id: details.tabId})); | |
| 189 }); | |
| 190 | |
| 191 | |
| 192 /* Browser actions */ | |
| 193 | |
| 194 let BrowserAction = function(tabId) | |
| 195 { | |
| 196 this._tabId = tabId; | |
| 197 this._changes = null; | |
| 198 }; | |
| 199 BrowserAction.prototype = { | |
| 200 _applyChanges() | |
| 201 { | |
| 202 if ("iconPath" in this._changes) | |
| 203 { | |
| 204 chrome.browserAction.setIcon({ | |
| 205 tabId: this._tabId, | |
| 206 path: { | |
| 207 16: this._changes.iconPath.replace("$size", "16"), | |
| 208 19: this._changes.iconPath.replace("$size", "19"), | |
| 209 20: this._changes.iconPath.replace("$size", "20"), | |
| 210 32: this._changes.iconPath.replace("$size", "32"), | |
| 211 38: this._changes.iconPath.replace("$size", "38"), | |
| 212 40: this._changes.iconPath.replace("$size", "40") | |
| 213 } | |
| 214 }); | |
| 215 } | |
| 216 | |
| 217 if ("badgeText" in this._changes) | |
| 218 { | |
| 219 chrome.browserAction.setBadgeText({ | |
| 220 tabId: this._tabId, | |
| 221 text: this._changes.badgeText | |
| 222 }); | |
| 223 } | |
| 224 | |
| 225 if ("badgeColor" in this._changes) | |
| 226 { | |
| 227 chrome.browserAction.setBadgeBackgroundColor({ | |
| 228 tabId: this._tabId, | |
| 229 color: this._changes.badgeColor | |
| 230 }); | |
| 231 } | |
| 232 | |
| 233 this._changes = null; | |
| 234 }, | |
| 235 _queueChanges() | |
| 236 { | |
| 237 chrome.tabs.get(this._tabId, function() | |
| 238 { | |
| 239 // If the tab is prerendered, chrome.tabs.get() sets | |
| 240 // chrome.runtime.lastError and we have to delay our changes | |
| 241 // until the currently visible tab is replaced with the | |
| 242 // prerendered tab. Otherwise chrome.browserAction.set* fails. | |
| 243 if (chrome.runtime.lastError) | |
| 244 { | |
| 245 let onReplaced = (addedTabId, removedTabId) => | |
| 246 { | |
| 247 if (addedTabId == this._tabId) | |
| 248 { | |
| 249 chrome.tabs.onReplaced.removeListener(onReplaced); | |
| 250 this._applyChanges(); | |
| 251 } | |
| 252 }; | |
| 253 chrome.tabs.onReplaced.addListener(onReplaced); | |
| 254 } | |
| 255 else | |
| 256 { | |
| 257 this._applyChanges(); | |
| 258 } | |
| 259 }.bind(this)); | |
| 260 }, | |
| 261 _addChange(name, value) | |
| 262 { | |
| 263 if (!this._changes) | |
| 264 { | |
| 265 this._changes = {}; | |
| 266 this._queueChanges(); | |
| 267 } | |
| 268 | |
| 269 this._changes[name] = value; | |
| 270 }, | |
| 271 setIcon(path) | |
| 272 { | |
| 273 this._addChange("iconPath", path); | |
| 274 }, | |
| 275 setBadge(badge) | |
| 276 { | |
| 277 if (!badge) | |
| 278 { | |
| 279 this._addChange("badgeText", ""); | |
| 280 } | |
| 281 else | |
| 282 { | |
| 283 if ("number" in badge) | |
| 284 this._addChange("badgeText", badge.number.toString()); | |
| 285 | |
| 286 if ("color" in badge) | |
| 287 this._addChange("badgeColor", badge.color); | |
| 288 } | |
| 289 } | |
| 290 }; | |
| 291 | |
| 292 | |
| 293 /* Context menus */ | |
| 294 | |
| 295 let contextMenuItems = new ext.PageMap(); | |
| 296 let contextMenuUpdating = false; | |
| 297 | |
| 298 let updateContextMenu = () => | |
| 299 { | |
| 300 if (contextMenuUpdating) | |
| 301 return; | |
| 302 | |
| 303 contextMenuUpdating = true; | |
| 304 | |
| 305 chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs => | |
| 306 { | |
| 307 chrome.contextMenus.removeAll(() => | |
| 308 { | |
| 309 contextMenuUpdating = false; | |
| 310 | |
| 311 if (tabs.length == 0) | |
| 312 return; | |
| 313 | |
| 314 let items = contextMenuItems.get({id: tabs[0].id}); | |
| 315 | |
| 316 if (!items) | |
| 317 return; | |
| 318 | |
| 319 items.forEach(item => | |
| 320 { | |
| 321 chrome.contextMenus.create({ | |
| 322 title: item.title, | |
| 323 contexts: item.contexts, | |
| 324 onclick(info, tab) | |
| 325 { | |
| 326 item.onclick(new Page(tab)); | |
| 327 } | |
| 328 }); | |
| 329 }); | |
| 330 }); | |
| 331 }); | |
| 332 }; | |
| 333 | |
| 334 let ContextMenus = function(page) | |
| 335 { | |
| 336 this._page = page; | |
| 337 }; | |
| 338 ContextMenus.prototype = { | |
| 339 create(item) | |
| 340 { | |
| 341 let items = contextMenuItems.get(this._page); | |
| 342 if (!items) | |
| 343 contextMenuItems.set(this._page, items = []); | |
| 344 | |
| 345 items.push(item); | |
| 346 updateContextMenu(); | |
| 347 }, | |
| 348 remove(item) | |
| 349 { | |
| 350 let items = contextMenuItems.get(this._page); | |
| 351 if (items) | |
| 352 { | |
| 353 let index = items.indexOf(item); | |
| 354 if (index != -1) | |
| 355 { | |
| 356 items.splice(index, 1); | |
| 357 updateContextMenu(); | |
| 358 } | |
| 359 } | |
| 360 } | |
| 361 }; | |
| 362 | |
| 363 chrome.tabs.onActivated.addListener(updateContextMenu); | |
| 364 | |
| 365 chrome.windows.onFocusChanged.addListener(windowId => | |
| 366 { | |
| 367 if (windowId != chrome.windows.WINDOW_ID_NONE) | |
| 368 updateContextMenu(); | |
| 369 }); | |
| 370 | |
| 371 | |
| 372 /* Web requests */ | |
| 373 | |
| 374 let framesOfTabs = Object.create(null); | |
| 375 | |
| 376 ext.getFrame = (tabId, frameId) => | |
| 377 { | |
| 378 return (framesOfTabs[tabId] || {})[frameId]; | |
| 379 }; | |
| 380 | |
| 381 let handlerBehaviorChangedQuota = chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANG
ED_CALLS_PER_10_MINUTES; | |
| 382 | |
| 383 function propagateHandlerBehaviorChange() | |
| 384 { | |
| 385 // Make sure to not call handlerBehaviorChanged() more often than allowed | |
| 386 // by chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES. | |
| 387 // Otherwise Chrome notifies the user that this extension is causing issues. | |
| 388 if (handlerBehaviorChangedQuota > 0) | |
| 389 { | |
| 390 chrome.webNavigation.onBeforeNavigate.removeListener(propagateHandlerBehav
iorChange); | |
| 391 chrome.webRequest.handlerBehaviorChanged(); | |
| 392 | |
| 393 handlerBehaviorChangedQuota--; | |
| 394 setTimeout(() => { handlerBehaviorChangedQuota++; }, 600000); | |
| 395 } | |
| 396 } | |
| 397 | |
| 398 ext.webRequest = { | |
| 399 onBeforeRequest: new ext._EventTarget(), | |
| 400 handlerBehaviorChanged() | |
| 401 { | |
| 402 // Defer handlerBehaviorChanged() until navigation occurs. | |
| 403 // There wouldn't be any visible effect when calling it earlier, | |
| 404 // but it's an expensive operation and that way we avoid to call | |
| 405 // it multiple times, if multiple filters are added/removed. | |
| 406 let onBeforeNavigate = chrome.webNavigation.onBeforeNavigate; | |
| 407 if (!onBeforeNavigate.hasListener(propagateHandlerBehaviorChange)) | |
| 408 onBeforeNavigate.addListener(propagateHandlerBehaviorChange); | |
| 409 } | |
| 410 }; | |
| 411 | |
| 412 chrome.tabs.query({}, tabs => | |
| 413 { | |
| 414 tabs.forEach(tab => | |
| 415 { | |
| 416 chrome.webNavigation.getAllFrames({tabId: tab.id}, details => | |
| 417 { | |
| 418 if (details && details.length > 0) | |
| 419 { | |
| 420 let frames = framesOfTabs[tab.id] = Object.create(null); | |
| 421 | |
| 422 for (let i = 0; i < details.length; i++) | |
| 423 frames[details[i].frameId] = {url: new URL(details[i].url), parent:
null}; | |
| 424 | |
| 425 for (let i = 0; i < details.length; i++) | |
| 426 { | |
| 427 let parentFrameId = details[i].parentFrameId; | |
| 428 | |
| 429 if (parentFrameId != -1) | |
| 430 frames[details[i].frameId].parent = frames[parentFrameId]; | |
| 431 } | |
| 432 } | |
| 433 }); | |
| 434 }); | |
| 435 }); | |
| 436 | |
| 437 chrome.webRequest.onBeforeRequest.addListener(details => | |
| 438 { | |
| 439 // The high-level code isn't interested in requests that aren't | |
| 440 // related to a tab or requests loading a top-level document, | |
| 441 // those should never be blocked. | |
| 442 if (details.tabId == -1 || details.type == "main_frame") | |
| 443 return; | |
| 444 | |
| 445 // We are looking for the frame that contains the element which | |
| 446 // has triggered this request. For most requests (e.g. images) we | |
| 447 // can just use the request's frame ID, but for subdocument requests | |
| 448 // (e.g. iframes) we must instead use the request's parent frame ID. | |
| 449 let frameId; | |
| 450 let requestType; | |
| 451 if (details.type == "sub_frame") | |
| 452 { | |
| 453 frameId = details.parentFrameId; | |
| 454 requestType = "SUBDOCUMENT"; | |
| 455 } | |
| 456 else | |
| 457 { | |
| 458 frameId = details.frameId; | |
| 459 requestType = details.type.toUpperCase(); | |
| 460 } | |
| 461 | |
| 462 let frame = ext.getFrame(details.tabId, frameId); | |
| 463 if (frame) | |
| 464 { | |
| 465 let results = ext.webRequest.onBeforeRequest._dispatch( | |
| 466 new URL(details.url), | |
| 467 requestType, | |
| 468 new Page({id: details.tabId}), | |
| 469 frame | |
| 470 ); | |
| 471 | |
| 472 if (results.indexOf(false) != -1) | |
| 473 return {cancel: true}; | |
| 474 } | |
| 475 }, {urls: ["http://*/*", "https://*/*"]}, ["blocking"]); | |
| 476 | |
| 477 | |
| 478 /* Message passing */ | |
| 479 | |
| 480 chrome.runtime.onMessage.addListener((message, rawSender, sendResponse) => | |
| 481 { | |
| 482 let sender = {}; | |
| 483 | |
| 484 // Add "page" and "frame" if the message was sent by a content script. | |
| 485 // If sent by popup or the background page itself, there is no "tab". | |
| 486 if ("tab" in rawSender) | |
| 487 { | |
| 488 sender.page = new Page(rawSender.tab); | |
| 489 sender.frame = { | |
| 490 // In Edge requests from internal extension pages | |
| 491 // (protocol ms-browser-extension://) do no have a sender URL. | |
| 492 url: rawSender.url ? new URL(rawSender.url) : null, | |
| 493 get parent() | |
| 494 { | |
| 495 let frames = framesOfTabs[rawSender.tab.id]; | |
| 496 | |
| 497 if (!frames) | |
| 498 return null; | |
| 499 | |
| 500 let frame = frames[rawSender.frameId]; | |
| 501 if (frame) | |
| 502 return frame.parent; | |
| 503 | |
| 504 return frames[0]; | |
| 505 } | |
| 506 }; | |
| 507 } | |
| 508 | |
| 509 return ext.onMessage._dispatch(message, sender, sendResponse).indexOf(true)
!= -1; | |
| 510 }); | |
| 511 | |
| 512 | |
| 513 /* Storage */ | |
| 514 | |
| 515 ext.storage = { | |
| 516 get(keys, callback) | |
| 517 { | |
| 518 chrome.storage.local.get(keys, callback); | |
| 519 }, | |
| 520 set(key, value, callback) | |
| 521 { | |
| 522 let items = {}; | |
| 523 items[key] = value; | |
| 524 chrome.storage.local.set(items, callback); | |
| 525 }, | |
| 526 remove(key, callback) | |
| 527 { | |
| 528 chrome.storage.local.remove(key, callback); | |
| 529 }, | |
| 530 onChanged: chrome.storage.onChanged | |
| 531 }; | |
| 532 | |
| 533 /* Options */ | |
| 534 | |
| 535 if ("openOptionsPage" in chrome.runtime) | |
| 536 { | |
| 537 ext.showOptions = callback => | |
| 538 { | |
| 539 if (!callback) | |
| 540 { | |
| 541 chrome.runtime.openOptionsPage(); | |
| 542 } | |
| 543 else | |
| 544 { | |
| 545 chrome.runtime.openOptionsPage(() => | |
| 546 { | |
| 547 if (chrome.runtime.lastError) | |
| 548 return; | |
| 549 | |
| 550 chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs => | |
| 551 { | |
| 552 if (tabs.length > 0) | |
| 553 { | |
| 554 window.setTimeout(() => | |
| 555 { | |
| 556 callback(new Page(tabs[0])); | |
| 557 }); | |
| 558 } | |
| 559 }); | |
| 560 }); | |
| 561 } | |
| 562 }; | |
| 563 } | |
| 564 else | |
| 565 { | |
| 566 // Edge does not yet support runtime.openOptionsPage (tested version 38) | |
| 567 // and so this workaround needs to stay for now. | |
| 568 ext.showOptions = callback => | |
| 569 { | |
| 570 chrome.windows.getLastFocused(win => | |
| 571 { | |
| 572 let optionsUrl = chrome.extension.getURL("options.html"); | |
| 573 let queryInfo = {url: optionsUrl}; | |
| 574 | |
| 575 // extension pages can't be accessed in incognito windows. In order to | |
| 576 // correctly mimic the way in which Chrome opens extension options, | |
| 577 // we have to focus the options page in any other window. | |
| 578 if (!win.incognito) | |
| 579 queryInfo.windowId = win.id; | |
| 580 | |
| 581 chrome.tabs.query(queryInfo, tabs => | |
| 582 { | |
| 583 if (tabs.length > 0) | |
| 584 { | |
| 585 let tab = tabs[0]; | |
| 586 | |
| 587 chrome.windows.update(tab.windowId, {focused: true}); | |
| 588 chrome.tabs.update(tab.id, {active: true}); | |
| 589 | |
| 590 if (callback) | |
| 591 callback(new Page(tab)); | |
| 592 } | |
| 593 else | |
| 594 { | |
| 595 ext.pages.open(optionsUrl, callback); | |
| 596 } | |
| 597 }); | |
| 598 }); | |
| 599 }; | |
| 600 } | |
| 601 | |
| 602 /* Windows */ | |
| 603 ext.windows = { | |
| 604 create(createData, callback) | |
| 605 { | |
| 606 if ("create" in chrome.windows){ | |
| 607 chrome.windows.create(createData, createdWindow => | |
| 608 { | |
| 609 afterTabLoaded(callback)(createdWindow.tabs[0]); | |
| 610 }); | |
| 611 } | |
| 612 else | |
| 613 { | |
| 614 ext.pages.open(createData.url, callback); | |
| 615 } | |
| 616 } | |
| 617 }; | |
| 618 } | |
| OLD | NEW |