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