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