OLD | NEW |
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-2017 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 (function() |
20 { | 21 { |
21 let nonEmptyPageMaps = Object.create(null); | 22 let nonEmptyPageMaps = new Set(); |
22 let pageMapCounter = 0; | |
23 | 23 |
24 let PageMap = ext.PageMap = function() | 24 let PageMap = ext.PageMap = function() |
25 { | 25 { |
26 this._map = Object.create(null); | 26 this._map = new Map(); |
27 this._id = ++pageMapCounter; | |
28 }; | 27 }; |
29 PageMap.prototype = { | 28 PageMap.prototype = { |
30 _delete(id) | 29 _delete(id) |
31 { | 30 { |
32 delete this._map[id]; | 31 this._map.delete(id); |
33 | 32 |
34 if (Object.keys(this._map).length == 0) | 33 if (this._map.size == 0) |
35 delete nonEmptyPageMaps[this._id]; | 34 nonEmptyPageMaps.delete(this); |
36 }, | 35 }, |
37 keys() | 36 keys() |
38 { | 37 { |
39 return Object.keys(this._map).map(ext.getPage); | 38 return Array.from(this._map.keys()).map(ext.getPage); |
40 }, | 39 }, |
41 get(page) | 40 get(page) |
42 { | 41 { |
43 return this._map[page.id]; | 42 return this._map.get(page.id); |
44 }, | 43 }, |
45 set(page, value) | 44 set(page, value) |
46 { | 45 { |
47 this._map[page.id] = value; | 46 this._map.set(page.id, value); |
48 nonEmptyPageMaps[this._id] = this; | 47 nonEmptyPageMaps.add(this); |
49 }, | 48 }, |
50 has(page) | 49 has(page) |
51 { | 50 { |
52 return page.id in this._map; | 51 return this._map.has(page.id); |
53 }, | 52 }, |
54 clear() | 53 clear() |
55 { | 54 { |
56 for (let id in this._map) | 55 this._map.clear(); |
57 this._delete(id); | 56 nonEmptyPageMaps.delete(this); |
58 }, | 57 }, |
59 delete(page) | 58 delete(page) |
60 { | 59 { |
61 this._delete(page.id); | 60 this._delete(page.id); |
62 } | 61 } |
63 }; | 62 }; |
64 | 63 |
65 ext._removeFromAllPageMaps = pageId => | 64 ext._removeFromAllPageMaps = pageId => |
66 { | 65 { |
67 for (let pageMapId in nonEmptyPageMaps) | 66 for (let pageMap of nonEmptyPageMaps) |
68 nonEmptyPageMaps[pageMapId]._delete(pageId); | 67 pageMap._delete(pageId); |
69 }; | 68 }; |
70 } | 69 |
| 70 /* Pages */ |
| 71 |
| 72 let Page = ext.Page = function(tab) |
| 73 { |
| 74 this.id = tab.id; |
| 75 this._url = tab.url && new URL(tab.url); |
| 76 |
| 77 this.browserAction = new BrowserAction(tab.id); |
| 78 this.contextMenus = new ContextMenus(this); |
| 79 }; |
| 80 Page.prototype = { |
| 81 get url() |
| 82 { |
| 83 // usually our Page objects are created from Chrome's Tab objects, which |
| 84 // provide the url. So we can return the url given in the constructor. |
| 85 if (this._url) |
| 86 return this._url; |
| 87 |
| 88 // but sometimes we only have the tab id when we create a Page object. |
| 89 // In that case we get the url from top frame of the tab, recorded by |
| 90 // the onBeforeRequest handler. |
| 91 let frames = framesOfTabs.get(this.id); |
| 92 if (frames) |
| 93 { |
| 94 let frame = frames.get(0); |
| 95 if (frame) |
| 96 return frame.url; |
| 97 } |
| 98 }, |
| 99 sendMessage(message, responseCallback) |
| 100 { |
| 101 chrome.tabs.sendMessage(this.id, message, responseCallback); |
| 102 } |
| 103 }; |
| 104 |
| 105 ext.getPage = id => new Page({id: parseInt(id, 10)}); |
| 106 |
| 107 function afterTabLoaded(callback) |
| 108 { |
| 109 return openedTab => |
| 110 { |
| 111 let onUpdated = (tabId, changeInfo, tab) => |
| 112 { |
| 113 if (tabId == openedTab.id && changeInfo.status == "complete") |
| 114 { |
| 115 chrome.tabs.onUpdated.removeListener(onUpdated); |
| 116 callback(new Page(openedTab)); |
| 117 } |
| 118 }; |
| 119 chrome.tabs.onUpdated.addListener(onUpdated); |
| 120 }; |
| 121 } |
| 122 |
| 123 ext.pages = { |
| 124 open(url, callback) |
| 125 { |
| 126 if (typeof callback !== "undefined" && typeof callback !== "null") |
| 127 { |
| 128 chrome.tabs.create({url: url}, callback && afterTabLoaded(callback)); |
| 129 } |
| 130 else |
| 131 { |
| 132 chrome.tabs.create({url: url}); |
| 133 } |
| 134 }, |
| 135 query(info, callback) |
| 136 { |
| 137 let rawInfo = {}; |
| 138 for (let property in info) |
| 139 { |
| 140 switch (property) |
| 141 { |
| 142 case "active": |
| 143 case "lastFocusedWindow": |
| 144 rawInfo[property] = info[property]; |
| 145 } |
| 146 } |
| 147 |
| 148 chrome.tabs.query(rawInfo, tabs => |
| 149 { |
| 150 callback(tabs.map(tab => new Page(tab))); |
| 151 }); |
| 152 }, |
| 153 onLoading: new ext._EventTarget(), |
| 154 onActivated: new ext._EventTarget(), |
| 155 onRemoved: new ext._EventTarget() |
| 156 }; |
| 157 |
| 158 chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => |
| 159 { |
| 160 if (changeInfo.status == "loading") |
| 161 ext.pages.onLoading._dispatch(new Page(tab)); |
| 162 }); |
| 163 |
| 164 function createFrame(tabId, frameId) |
| 165 { |
| 166 let frames = framesOfTabs.get(tabId); |
| 167 if (!frames) |
| 168 { |
| 169 frames = new Map(); |
| 170 framesOfTabs.set(tabId, frames); |
| 171 } |
| 172 |
| 173 let frame = frames.get(frameId); |
| 174 if (!frame) |
| 175 { |
| 176 frame = {}; |
| 177 frames.set(frameId, frame); |
| 178 } |
| 179 |
| 180 return frame; |
| 181 } |
| 182 |
| 183 function updatePageFrameStructure(frameId, tabId, url, parentFrameId) |
| 184 { |
| 185 if (frameId == 0) |
| 186 { |
| 187 let page = new Page({id: tabId, url}); |
| 188 |
| 189 ext._removeFromAllPageMaps(tabId); |
| 190 |
| 191 chrome.tabs.get(tabId, () => |
| 192 { |
| 193 // If the tab is prerendered, chrome.tabs.get() sets |
| 194 // chrome.runtime.lastError and we have to dispatch the onLoading event, |
| 195 // since the onUpdated event isn't dispatched for prerendered tabs. |
| 196 // However, we have to keep relying on the unUpdated event for tabs that |
| 197 // are already visible. Otherwise browser action changes get overridden |
| 198 // when Chrome automatically resets them on navigation. |
| 199 if (chrome.runtime.lastError) |
| 200 ext.pages.onLoading._dispatch(page); |
| 201 }); |
| 202 } |
| 203 |
| 204 // Update frame URL and parent in frame structure |
| 205 let frame = createFrame(tabId, frameId); |
| 206 frame.url = new URL(url); |
| 207 |
| 208 let parentFrame = framesOfTabs.get(tabId).get(parentFrameId); |
| 209 if (parentFrame) |
| 210 frame.parent = parentFrame; |
| 211 } |
| 212 |
| 213 chrome.webRequest.onHeadersReceived.addListener(details => |
| 214 { |
| 215 // We have to update the frame structure when switching to a new |
| 216 // document, so that we process any further requests made by that |
| 217 // document in the right context. Unfortunately, we cannot rely |
| 218 // on webNavigation.onCommitted since it isn't guaranteed to fire |
| 219 // before any subresources start downloading[1]. As an |
| 220 // alternative we use webRequest.onHeadersReceived for HTTP(S) |
| 221 // URLs, being careful to ignore any responses that won't cause |
| 222 // the document to be replaced. |
| 223 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=665843 |
| 224 |
| 225 // The request has been processed without replacing the document. |
| 226 // https://chromium.googlesource.com/chromium/src/+/02d3f50b/content/browser
/frame_host/navigation_request.cc#473 |
| 227 if (details.statusCode == 204 || details.statusCode == 205) |
| 228 return; |
| 229 |
| 230 for (let header of details.responseHeaders) |
| 231 { |
| 232 let headerName = header.name.toLowerCase(); |
| 233 |
| 234 // For redirects we must wait for the next response in order |
| 235 // to know if the document will be replaced. Note: Chrome |
| 236 // performs a redirect only if there is a "Location" header with |
| 237 // a non-empty value and a known redirect status code. |
| 238 // https://chromium.googlesource.com/chromium/src/+/39a7d96/net/http/http_
response_headers.cc#929 |
| 239 if (headerName == "location" && header.value && |
| 240 (details.statusCode == 301 || details.statusCode == 302 || |
| 241 details.statusCode == 303 || details.statusCode == 307 || |
| 242 details.statusCode == 308)) |
| 243 return; |
| 244 |
| 245 // If the response initiates a download the document won't be |
| 246 // replaced. Chrome initiates a download if there is a |
| 247 // "Content-Disposition" with a valid and non-empty value other |
| 248 // than "inline". |
| 249 // https://chromium.googlesource.com/chromium/src/+/02d3f50b/content/brows
er/loader/mime_sniffing_resource_handler.cc#534 |
| 250 // https://chromium.googlesource.com/chromium/src/+/02d3f50b/net/http/http
_content_disposition.cc#374 |
| 251 // https://chromium.googlesource.com/chromium/src/+/16e2688e/net/http/http
_util.cc#431 |
| 252 if (headerName == "content-disposition") |
| 253 { |
| 254 let disposition = header.value.split(";")[0].replace(/[ \t]+$/, ""); |
| 255 if (disposition.toLowerCase() != "inline" && |
| 256 /^[\x21-\x7E]+$/.test(disposition) && |
| 257 !/[()<>@,;:\\"/[\]?={}]/.test(disposition)) |
| 258 return; |
| 259 } |
| 260 |
| 261 // The value of the "Content-Type" header also determines if Chrome will |
| 262 // initiate a download, or otherwise how the response will be rendered. |
| 263 // We only need to consider responses which will result in a navigation |
| 264 // and be rendered as HTML or similar. |
| 265 // Note: Chrome might render the response as HTML if the "Content-Type" |
| 266 // header is missing, invalid or unknown. |
| 267 // https://chromium.googlesource.com/chromium/src/+/99f41af9/net/http/http
_util.cc#66 |
| 268 // https://chromium.googlesource.com/chromium/src/+/3130418a/net/base/mime
_sniffer.cc#667 |
| 269 if (headerName == "content-type") |
| 270 { |
| 271 let mediaType = header.value.split(/[ \t;(]/)[0].toLowerCase(); |
| 272 if (mediaType.includes("/") && |
| 273 mediaType != "*/*" && |
| 274 mediaType != "application/unknown" && |
| 275 mediaType != "unknown/unknown" && |
| 276 mediaType != "text/html" && |
| 277 mediaType != "text/xml" && |
| 278 mediaType != "application/xml" && |
| 279 mediaType != "application/xhtml+xml" && |
| 280 mediaType != "image/svg+xml") |
| 281 return; |
| 282 } |
| 283 } |
| 284 |
| 285 updatePageFrameStructure(details.frameId, details.tabId, details.url, |
| 286 details.parentFrameId); |
| 287 }, |
| 288 {types: ["main_frame", "sub_frame"], urls: ["http://*/*", "https://*/*"]}, |
| 289 ["responseHeaders"]); |
| 290 |
| 291 chrome.webNavigation.onBeforeNavigate.addListener(details => |
| 292 { |
| 293 // Since we can only listen for HTTP(S) responses using |
| 294 // webRequest.onHeadersReceived we must update the page structure here for |
| 295 // other navigations. |
| 296 let url = new URL(details.url); |
| 297 if (url.protocol != "http:" && url.protocol != "https:") |
| 298 { |
| 299 updatePageFrameStructure(details.frameId, details.tabId, details.url, |
| 300 details.parentFrameId); |
| 301 } |
| 302 }); |
| 303 |
| 304 function forgetTab(tabId) |
| 305 { |
| 306 ext.pages.onRemoved._dispatch(tabId); |
| 307 |
| 308 ext._removeFromAllPageMaps(tabId); |
| 309 framesOfTabs.delete(tabId); |
| 310 } |
| 311 |
| 312 chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => |
| 313 { |
| 314 forgetTab(removedTabId); |
| 315 }); |
| 316 |
| 317 chrome.tabs.onRemoved.addListener(forgetTab); |
| 318 |
| 319 chrome.tabs.onActivated.addListener(details => |
| 320 { |
| 321 ext.pages.onActivated._dispatch(new Page({id: details.tabId})); |
| 322 }); |
| 323 |
| 324 |
| 325 /* Browser actions */ |
| 326 |
| 327 let BrowserAction = function(tabId) |
| 328 { |
| 329 this._tabId = tabId; |
| 330 this._changes = null; |
| 331 }; |
| 332 BrowserAction.prototype = { |
| 333 _applyChanges() |
| 334 { |
| 335 if ("iconPath" in this._changes) |
| 336 { |
| 337 chrome.browserAction.setIcon({ |
| 338 tabId: this._tabId, |
| 339 path: { |
| 340 16: this._changes.iconPath.replace("$size", "16"), |
| 341 19: this._changes.iconPath.replace("$size", "19"), |
| 342 20: this._changes.iconPath.replace("$size", "20"), |
| 343 32: this._changes.iconPath.replace("$size", "32"), |
| 344 38: this._changes.iconPath.replace("$size", "38"), |
| 345 40: this._changes.iconPath.replace("$size", "40") |
| 346 } |
| 347 }); |
| 348 } |
| 349 |
| 350 if ("badgeText" in this._changes) |
| 351 { |
| 352 chrome.browserAction.setBadgeText({ |
| 353 tabId: this._tabId, |
| 354 text: this._changes.badgeText |
| 355 }); |
| 356 } |
| 357 |
| 358 if ("badgeColor" in this._changes) |
| 359 { |
| 360 chrome.browserAction.setBadgeBackgroundColor({ |
| 361 tabId: this._tabId, |
| 362 color: this._changes.badgeColor |
| 363 }); |
| 364 } |
| 365 |
| 366 this._changes = null; |
| 367 }, |
| 368 _queueChanges() |
| 369 { |
| 370 chrome.tabs.get(this._tabId, () => |
| 371 { |
| 372 // If the tab is prerendered, chrome.tabs.get() sets |
| 373 // chrome.runtime.lastError and we have to delay our changes |
| 374 // until the currently visible tab is replaced with the |
| 375 // prerendered tab. Otherwise chrome.browserAction.set* fails. |
| 376 if (chrome.runtime.lastError) |
| 377 { |
| 378 let onReplaced = (addedTabId, removedTabId) => |
| 379 { |
| 380 if (addedTabId == this._tabId) |
| 381 { |
| 382 chrome.tabs.onReplaced.removeListener(onReplaced); |
| 383 this._applyChanges(); |
| 384 } |
| 385 }; |
| 386 chrome.tabs.onReplaced.addListener(onReplaced); |
| 387 } |
| 388 else |
| 389 { |
| 390 this._applyChanges(); |
| 391 } |
| 392 }); |
| 393 }, |
| 394 _addChange(name, value) |
| 395 { |
| 396 if (!this._changes) |
| 397 { |
| 398 this._changes = {}; |
| 399 this._queueChanges(); |
| 400 } |
| 401 |
| 402 this._changes[name] = value; |
| 403 }, |
| 404 setIcon(path) |
| 405 { |
| 406 this._addChange("iconPath", path); |
| 407 }, |
| 408 setBadge(badge) |
| 409 { |
| 410 if (!badge) |
| 411 { |
| 412 this._addChange("badgeText", ""); |
| 413 } |
| 414 else |
| 415 { |
| 416 if ("number" in badge) |
| 417 this._addChange("badgeText", badge.number.toString()); |
| 418 |
| 419 if ("color" in badge) |
| 420 this._addChange("badgeColor", badge.color); |
| 421 } |
| 422 } |
| 423 }; |
| 424 |
| 425 |
| 426 /* Context menus */ |
| 427 |
| 428 let contextMenuItems = new ext.PageMap(); |
| 429 let contextMenuUpdating = false; |
| 430 |
| 431 let updateContextMenu = () => |
| 432 { |
| 433 if (contextMenuUpdating) |
| 434 return; |
| 435 |
| 436 contextMenuUpdating = true; |
| 437 |
| 438 chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs => |
| 439 { |
| 440 chrome.contextMenus.removeAll(() => |
| 441 { |
| 442 contextMenuUpdating = false; |
| 443 |
| 444 if (tabs.length == 0) |
| 445 return; |
| 446 |
| 447 let items = contextMenuItems.get({id: tabs[0].id}); |
| 448 |
| 449 if (!items) |
| 450 return; |
| 451 |
| 452 items.forEach(item => |
| 453 { |
| 454 chrome.contextMenus.create({ |
| 455 title: item.title, |
| 456 contexts: item.contexts, |
| 457 onclick(info, tab) |
| 458 { |
| 459 item.onclick(new Page(tab)); |
| 460 } |
| 461 }); |
| 462 }); |
| 463 }); |
| 464 }); |
| 465 }; |
| 466 |
| 467 let ContextMenus = function(page) |
| 468 { |
| 469 this._page = page; |
| 470 }; |
| 471 ContextMenus.prototype = { |
| 472 create(item) |
| 473 { |
| 474 let items = contextMenuItems.get(this._page); |
| 475 if (!items) |
| 476 contextMenuItems.set(this._page, items = []); |
| 477 |
| 478 items.push(item); |
| 479 updateContextMenu(); |
| 480 }, |
| 481 remove(item) |
| 482 { |
| 483 let items = contextMenuItems.get(this._page); |
| 484 if (items) |
| 485 { |
| 486 let index = items.indexOf(item); |
| 487 if (index != -1) |
| 488 { |
| 489 items.splice(index, 1); |
| 490 updateContextMenu(); |
| 491 } |
| 492 } |
| 493 } |
| 494 }; |
| 495 |
| 496 chrome.tabs.onActivated.addListener(updateContextMenu); |
| 497 |
| 498 chrome.windows.onFocusChanged.addListener(windowId => |
| 499 { |
| 500 if (windowId != chrome.windows.WINDOW_ID_NONE) |
| 501 updateContextMenu(); |
| 502 }); |
| 503 |
| 504 |
| 505 /* Web requests */ |
| 506 |
| 507 let framesOfTabs = new Map(); |
| 508 |
| 509 ext.getFrame = (tabId, frameId) => |
| 510 { |
| 511 let frames = framesOfTabs.get(tabId); |
| 512 return frames && frames.get(frameId); |
| 513 }; |
| 514 |
| 515 let handlerBehaviorChangedQuota = |
| 516 chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES; |
| 517 |
| 518 function propagateHandlerBehaviorChange() |
| 519 { |
| 520 // Make sure to not call handlerBehaviorChanged() more often than allowed |
| 521 // by chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES. |
| 522 // Otherwise Chrome notifies the user that this extension is causing issues. |
| 523 if (handlerBehaviorChangedQuota > 0) |
| 524 { |
| 525 chrome.webNavigation.onBeforeNavigate.removeListener( |
| 526 propagateHandlerBehaviorChange |
| 527 ); |
| 528 chrome.webRequest.handlerBehaviorChanged(); |
| 529 |
| 530 handlerBehaviorChangedQuota--; |
| 531 setTimeout(() => { handlerBehaviorChangedQuota++; }, 600000); |
| 532 } |
| 533 } |
| 534 |
| 535 ext.webRequest = { |
| 536 onBeforeRequest: new ext._EventTarget(), |
| 537 handlerBehaviorChanged() |
| 538 { |
| 539 // Defer handlerBehaviorChanged() until navigation occurs. |
| 540 // There wouldn't be any visible effect when calling it earlier, |
| 541 // but it's an expensive operation and that way we avoid to call |
| 542 // it multiple times, if multiple filters are added/removed. |
| 543 let {onBeforeNavigate} = chrome.webNavigation; |
| 544 if (!onBeforeNavigate.hasListener(propagateHandlerBehaviorChange)) |
| 545 onBeforeNavigate.addListener(propagateHandlerBehaviorChange); |
| 546 } |
| 547 }; |
| 548 |
| 549 chrome.tabs.query({}, tabs => |
| 550 { |
| 551 tabs.forEach(tab => |
| 552 { |
| 553 chrome.webNavigation.getAllFrames({tabId: tab.id}, details => |
| 554 { |
| 555 if (details && details.length > 0) |
| 556 { |
| 557 let frames = new Map(); |
| 558 framesOfTabs.set(tab.id, frames); |
| 559 |
| 560 for (let detail of details) |
| 561 { |
| 562 let frame = {url: new URL(detail.url)}; |
| 563 frames.set(detail.frameId, frame); |
| 564 |
| 565 if (detail.parentFrameId != -1) |
| 566 frame.parent = frames.get(detail.parentFrameId); |
| 567 } |
| 568 } |
| 569 }); |
| 570 }); |
| 571 }); |
| 572 |
| 573 chrome.webRequest.onBeforeRequest.addListener(details => |
| 574 { |
| 575 // The high-level code isn't interested in requests that aren't |
| 576 // related to a tab or requests loading a top-level document, |
| 577 // those should never be blocked. |
| 578 if (details.tabId == -1 || details.type == "main_frame") |
| 579 return; |
| 580 |
| 581 // Filter out requests from non web protocols. Ideally, we'd explicitly |
| 582 // specify the protocols we are interested in (i.e. http://, https://, |
| 583 // ws:// and wss://) with the url patterns, given below, when adding this |
| 584 // listener. But unfortunately, Chrome <=57 doesn't support the WebSocket |
| 585 // protocol and is causing an error if it is given. |
| 586 let url = new URL(details.url); |
| 587 if (url.protocol != "http:" && url.protocol != "https:" && |
| 588 url.protocol != "ws:" && url.protocol != "wss:") |
| 589 return; |
| 590 |
| 591 // We are looking for the frame that contains the element which |
| 592 // has triggered this request. For most requests (e.g. images) we |
| 593 // can just use the request's frame ID, but for subdocument requests |
| 594 // (e.g. iframes) we must instead use the request's parent frame ID. |
| 595 let {frameId, type} = details; |
| 596 if (type == "sub_frame") |
| 597 frameId = details.parentFrameId; |
| 598 |
| 599 let frame = ext.getFrame(details.tabId, frameId); |
| 600 if (frame) |
| 601 { |
| 602 let results = ext.webRequest.onBeforeRequest._dispatch( |
| 603 url, type, new Page({id: details.tabId}), frame |
| 604 ); |
| 605 |
| 606 if (results.indexOf(false) != -1) |
| 607 return {cancel: true}; |
| 608 } |
| 609 }, {urls: ["<all_urls>"]}, ["blocking"]); |
| 610 |
| 611 |
| 612 /* Message passing */ |
| 613 |
| 614 chrome.runtime.onMessage.addListener((message, rawSender, sendResponse) => |
| 615 { |
| 616 let sender = {}; |
| 617 |
| 618 // Add "page" and "frame" if the message was sent by a content script. |
| 619 // If sent by popup or the background page itself, there is no "tab". |
| 620 if ("tab" in rawSender) |
| 621 { |
| 622 sender.page = new Page(rawSender.tab); |
| 623 sender.frame = { |
| 624 // In Edge requests from internal extension pages |
| 625 // (protocol ms-browser-extension://) do no have a sender URL. |
| 626 url: rawSender.url ? new URL(rawSender.url) : null, |
| 627 get parent() |
| 628 { |
| 629 let frames = framesOfTabs.get(rawSender.tab.id); |
| 630 |
| 631 if (!frames) |
| 632 return null; |
| 633 |
| 634 let frame = frames.get(rawSender.frameId); |
| 635 if (frame) |
| 636 return frame.parent || null; |
| 637 |
| 638 return frames.get(0) || null; |
| 639 } |
| 640 }; |
| 641 } |
| 642 |
| 643 return ext.onMessage._dispatch( |
| 644 message, sender, sendResponse |
| 645 ).indexOf(true) != -1; |
| 646 }); |
| 647 |
| 648 |
| 649 /* Storage */ |
| 650 |
| 651 ext.storage = { |
| 652 get(keys, callback) |
| 653 { |
| 654 chrome.storage.local.get(keys, callback); |
| 655 }, |
| 656 set(key, value, callback) |
| 657 { |
| 658 let items = {}; |
| 659 items[key] = value; |
| 660 chrome.storage.local.set(items, callback); |
| 661 }, |
| 662 remove(key, callback) |
| 663 { |
| 664 chrome.storage.local.remove(key, callback); |
| 665 }, |
| 666 onChanged: chrome.storage.onChanged |
| 667 }; |
| 668 |
| 669 /* Options */ |
| 670 |
| 671 if ("openOptionsPage" in chrome.runtime) |
| 672 { |
| 673 ext.showOptions = callback => |
| 674 { |
| 675 if (!callback) |
| 676 { |
| 677 chrome.runtime.openOptionsPage(); |
| 678 } |
| 679 else |
| 680 { |
| 681 chrome.runtime.openOptionsPage(() => |
| 682 { |
| 683 if (chrome.runtime.lastError) |
| 684 return; |
| 685 |
| 686 chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs => |
| 687 { |
| 688 if (tabs.length > 0) |
| 689 { |
| 690 if (tabs[0].status == "complete") |
| 691 callback(new Page(tabs[0])); |
| 692 else |
| 693 afterTabLoaded(callback)(tabs[0]); |
| 694 } |
| 695 }); |
| 696 }); |
| 697 } |
| 698 }; |
| 699 } |
| 700 else |
| 701 { |
| 702 // Edge does not yet support runtime.openOptionsPage (tested version 38) |
| 703 // and so this workaround needs to stay for now. |
| 704 ext.showOptions = callback => |
| 705 { |
| 706 chrome.windows.getLastFocused(win => |
| 707 { |
| 708 let optionsUrl = chrome.extension.getURL("options.html"); |
| 709 let queryInfo = {url: optionsUrl}; |
| 710 |
| 711 // extension pages can't be accessed in incognito windows. In order to |
| 712 // correctly mimic the way in which Chrome opens extension options, |
| 713 // we have to focus the options page in any other window. |
| 714 if (!win.incognito) |
| 715 queryInfo.windowId = win.id; |
| 716 |
| 717 chrome.tabs.query(queryInfo, tabs => |
| 718 { |
| 719 if (tabs.length > 0) |
| 720 { |
| 721 let tab = tabs[0]; |
| 722 |
| 723 chrome.windows.update(tab.windowId, {focused: true}); |
| 724 chrome.tabs.update(tab.id, {active: true}); |
| 725 |
| 726 if (callback) |
| 727 callback(new Page(tab)); |
| 728 } |
| 729 else |
| 730 { |
| 731 ext.pages.open(optionsUrl, callback); |
| 732 } |
| 733 }); |
| 734 }); |
| 735 }; |
| 736 } |
| 737 |
| 738 /* Windows */ |
| 739 ext.windows = { |
| 740 create(createData, callback) |
| 741 { |
| 742 chrome.windows.create(createData, createdWindow => |
| 743 { |
| 744 afterTabLoaded(callback)(createdWindow.tabs[0]); |
| 745 }); |
| 746 } |
| 747 }; |
| 748 }()); |
OLD | NEW |