OLD | NEW |
| (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 }()); | |
OLD | NEW |