Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: chrome/ext/background.js

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

Powered by Google App Engine
This is Rietveld