OLD | NEW |
| (Empty) |
1 /* | |
2 * This file is part of Adblock Plus <https://adblockplus.org/>, | |
3 * Copyright (C) 2006-2016 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 { | |
21 /* Pages */ | |
22 | |
23 let Page = ext.Page = function(tab) | |
24 { | |
25 this.id = tab.id; | |
26 this._url = tab.url && new URL(tab.url); | |
27 | |
28 this.browserAction = new BrowserAction(tab.id); | |
29 this.contextMenus = new ContextMenus(this); | |
30 }; | |
31 Page.prototype = { | |
32 get url() | |
33 { | |
34 // usually our Page objects are created from Chrome's Tab objects, which | |
35 // provide the url. So we can return the url given in the constructor. | |
36 if (this._url) | |
37 return this._url; | |
38 | |
39 // but sometimes we only have the tab id when we create a Page object. | |
40 // In that case we get the url from top frame of the tab, recorded by | |
41 // the onBeforeRequest handler. | |
42 let frames = framesOfTabs[this.id]; | |
43 if (frames) | |
44 { | |
45 let frame = frames[0]; | |
46 if (frame) | |
47 return frame.url; | |
48 } | |
49 }, | |
50 sendMessage(message, responseCallback) | |
51 { | |
52 chrome.tabs.sendMessage(this.id, message, responseCallback); | |
53 } | |
54 }; | |
55 | |
56 ext.getPage = id => new Page({id: parseInt(id, 10)}); | |
57 | |
58 function afterTabLoaded(callback) | |
59 { | |
60 return openedTab => | |
61 { | |
62 let onUpdated = (tabId, changeInfo, tab) => | |
63 { | |
64 if (tabId == openedTab.id && changeInfo.status == "complete") | |
65 { | |
66 chrome.tabs.onUpdated.removeListener(onUpdated); | |
67 callback(new Page(openedTab)); | |
68 } | |
69 }; | |
70 chrome.tabs.onUpdated.addListener(onUpdated); | |
71 }; | |
72 } | |
73 | |
74 ext.pages = { | |
75 open(url, callback) | |
76 { | |
77 chrome.tabs.create({url: url}, callback && afterTabLoaded(callback)); | |
78 }, | |
79 query(info, callback) | |
80 { | |
81 let rawInfo = {}; | |
82 for (let property in info) | |
83 { | |
84 switch (property) | |
85 { | |
86 case "active": | |
87 case "lastFocusedWindow": | |
88 rawInfo[property] = info[property]; | |
89 } | |
90 } | |
91 | |
92 chrome.tabs.query(rawInfo, tabs => | |
93 { | |
94 callback(tabs.map(tab => new Page(tab))); | |
95 }); | |
96 }, | |
97 onLoading: new ext._EventTarget(), | |
98 onActivated: new ext._EventTarget(), | |
99 onRemoved: new ext._EventTarget() | |
100 }; | |
101 | |
102 chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => | |
103 { | |
104 if (changeInfo.status == "loading") | |
105 ext.pages.onLoading._dispatch(new Page(tab)); | |
106 }); | |
107 | |
108 function createFrame(tabId, frameId) | |
109 { | |
110 let frames = framesOfTabs[tabId]; | |
111 if (!frames) | |
112 frames = framesOfTabs[tabId] = Object.create(null); | |
113 | |
114 let frame = frames[frameId]; | |
115 if (!frame) | |
116 frame = frames[frameId] = {}; | |
117 | |
118 return frame; | |
119 } | |
120 | |
121 chrome.webNavigation.onBeforeNavigate.addListener(details => | |
122 { | |
123 // Capture parent frame here because onCommitted doesn't get this info. | |
124 let frame = createFrame(details.tabId, details.frameId); | |
125 frame.parent = framesOfTabs[details.tabId][details.parentFrameId] || null; | |
126 }); | |
127 | |
128 let eagerlyUpdatedPages = new ext.PageMap(); | |
129 | |
130 ext._updatePageFrameStructure = (frameId, tabId, url, eager) => | |
131 { | |
132 if (frameId == 0) | |
133 { | |
134 let page = new Page({id: tabId, url: url}); | |
135 | |
136 if (eagerlyUpdatedPages.get(page) != url) | |
137 { | |
138 ext._removeFromAllPageMaps(tabId); | |
139 | |
140 // When a sitekey header is received we must immediately update the page | |
141 // structure in order to record and use the key. We want to avoid | |
142 // trashing the page structure if the onCommitted event is then fired | |
143 // for the page. | |
144 if (eager) | |
145 eagerlyUpdatedPages.set(page, url); | |
146 | |
147 chrome.tabs.get(tabId, () => | |
148 { | |
149 // If the tab is prerendered, chrome.tabs.get() sets | |
150 // chrome.runtime.lastError and we have to dispatch the onLoading even
t, | |
151 // since the onUpdated event isn't dispatched for prerendered tabs. | |
152 // However, we have to keep relying on the unUpdated event for tabs th
at | |
153 // are already visible. Otherwise browser action changes get overridde
n | |
154 // when Chrome automatically resets them on navigation. | |
155 if (chrome.runtime.lastError) | |
156 ext.pages.onLoading._dispatch(page); | |
157 }); | |
158 } | |
159 } | |
160 | |
161 // Update frame URL in frame structure | |
162 let frame = createFrame(tabId, frameId); | |
163 frame.url = new URL(url); | |
164 }; | |
165 | |
166 chrome.webNavigation.onCommitted.addListener(details => | |
167 { | |
168 ext._updatePageFrameStructure(details.frameId, details.tabId, details.url); | |
169 }); | |
170 | |
171 function forgetTab(tabId) | |
172 { | |
173 ext.pages.onRemoved._dispatch(tabId); | |
174 | |
175 ext._removeFromAllPageMaps(tabId); | |
176 delete framesOfTabs[tabId]; | |
177 } | |
178 | |
179 chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => | |
180 { | |
181 forgetTab(removedTabId); | |
182 }); | |
183 | |
184 chrome.tabs.onRemoved.addListener(forgetTab); | |
185 | |
186 chrome.tabs.onActivated.addListener(details => | |
187 { | |
188 ext.pages.onActivated._dispatch(new Page({id: details.tabId})); | |
189 }); | |
190 | |
191 | |
192 /* Browser actions */ | |
193 | |
194 let BrowserAction = function(tabId) | |
195 { | |
196 this._tabId = tabId; | |
197 this._changes = null; | |
198 }; | |
199 BrowserAction.prototype = { | |
200 _applyChanges() | |
201 { | |
202 if ("iconPath" in this._changes) | |
203 { | |
204 chrome.browserAction.setIcon({ | |
205 tabId: this._tabId, | |
206 path: { | |
207 16: this._changes.iconPath.replace("$size", "16"), | |
208 19: this._changes.iconPath.replace("$size", "19"), | |
209 20: this._changes.iconPath.replace("$size", "20"), | |
210 32: this._changes.iconPath.replace("$size", "32"), | |
211 38: this._changes.iconPath.replace("$size", "38"), | |
212 40: this._changes.iconPath.replace("$size", "40") | |
213 } | |
214 }); | |
215 } | |
216 | |
217 if ("badgeText" in this._changes) | |
218 { | |
219 chrome.browserAction.setBadgeText({ | |
220 tabId: this._tabId, | |
221 text: this._changes.badgeText | |
222 }); | |
223 } | |
224 | |
225 if ("badgeColor" in this._changes) | |
226 { | |
227 chrome.browserAction.setBadgeBackgroundColor({ | |
228 tabId: this._tabId, | |
229 color: this._changes.badgeColor | |
230 }); | |
231 } | |
232 | |
233 this._changes = null; | |
234 }, | |
235 _queueChanges() | |
236 { | |
237 chrome.tabs.get(this._tabId, function() | |
238 { | |
239 // If the tab is prerendered, chrome.tabs.get() sets | |
240 // chrome.runtime.lastError and we have to delay our changes | |
241 // until the currently visible tab is replaced with the | |
242 // prerendered tab. Otherwise chrome.browserAction.set* fails. | |
243 if (chrome.runtime.lastError) | |
244 { | |
245 let onReplaced = (addedTabId, removedTabId) => | |
246 { | |
247 if (addedTabId == this._tabId) | |
248 { | |
249 chrome.tabs.onReplaced.removeListener(onReplaced); | |
250 this._applyChanges(); | |
251 } | |
252 }; | |
253 chrome.tabs.onReplaced.addListener(onReplaced); | |
254 } | |
255 else | |
256 { | |
257 this._applyChanges(); | |
258 } | |
259 }.bind(this)); | |
260 }, | |
261 _addChange(name, value) | |
262 { | |
263 if (!this._changes) | |
264 { | |
265 this._changes = {}; | |
266 this._queueChanges(); | |
267 } | |
268 | |
269 this._changes[name] = value; | |
270 }, | |
271 setIcon(path) | |
272 { | |
273 this._addChange("iconPath", path); | |
274 }, | |
275 setBadge(badge) | |
276 { | |
277 if (!badge) | |
278 { | |
279 this._addChange("badgeText", ""); | |
280 } | |
281 else | |
282 { | |
283 if ("number" in badge) | |
284 this._addChange("badgeText", badge.number.toString()); | |
285 | |
286 if ("color" in badge) | |
287 this._addChange("badgeColor", badge.color); | |
288 } | |
289 } | |
290 }; | |
291 | |
292 | |
293 /* Context menus */ | |
294 | |
295 let contextMenuItems = new ext.PageMap(); | |
296 let contextMenuUpdating = false; | |
297 | |
298 let updateContextMenu = () => | |
299 { | |
300 if (contextMenuUpdating) | |
301 return; | |
302 | |
303 contextMenuUpdating = true; | |
304 | |
305 chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs => | |
306 { | |
307 chrome.contextMenus.removeAll(() => | |
308 { | |
309 contextMenuUpdating = false; | |
310 | |
311 if (tabs.length == 0) | |
312 return; | |
313 | |
314 let items = contextMenuItems.get({id: tabs[0].id}); | |
315 | |
316 if (!items) | |
317 return; | |
318 | |
319 items.forEach(item => | |
320 { | |
321 chrome.contextMenus.create({ | |
322 title: item.title, | |
323 contexts: item.contexts, | |
324 onclick(info, tab) | |
325 { | |
326 item.onclick(new Page(tab)); | |
327 } | |
328 }); | |
329 }); | |
330 }); | |
331 }); | |
332 }; | |
333 | |
334 let ContextMenus = function(page) | |
335 { | |
336 this._page = page; | |
337 }; | |
338 ContextMenus.prototype = { | |
339 create(item) | |
340 { | |
341 let items = contextMenuItems.get(this._page); | |
342 if (!items) | |
343 contextMenuItems.set(this._page, items = []); | |
344 | |
345 items.push(item); | |
346 updateContextMenu(); | |
347 }, | |
348 remove(item) | |
349 { | |
350 let items = contextMenuItems.get(this._page); | |
351 if (items) | |
352 { | |
353 let index = items.indexOf(item); | |
354 if (index != -1) | |
355 { | |
356 items.splice(index, 1); | |
357 updateContextMenu(); | |
358 } | |
359 } | |
360 } | |
361 }; | |
362 | |
363 chrome.tabs.onActivated.addListener(updateContextMenu); | |
364 | |
365 chrome.windows.onFocusChanged.addListener(windowId => | |
366 { | |
367 if (windowId != chrome.windows.WINDOW_ID_NONE) | |
368 updateContextMenu(); | |
369 }); | |
370 | |
371 | |
372 /* Web requests */ | |
373 | |
374 let framesOfTabs = Object.create(null); | |
375 | |
376 ext.getFrame = (tabId, frameId) => | |
377 { | |
378 return (framesOfTabs[tabId] || {})[frameId]; | |
379 }; | |
380 | |
381 let handlerBehaviorChangedQuota = chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANG
ED_CALLS_PER_10_MINUTES; | |
382 | |
383 function propagateHandlerBehaviorChange() | |
384 { | |
385 // Make sure to not call handlerBehaviorChanged() more often than allowed | |
386 // by chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES. | |
387 // Otherwise Chrome notifies the user that this extension is causing issues. | |
388 if (handlerBehaviorChangedQuota > 0) | |
389 { | |
390 chrome.webNavigation.onBeforeNavigate.removeListener(propagateHandlerBehav
iorChange); | |
391 chrome.webRequest.handlerBehaviorChanged(); | |
392 | |
393 handlerBehaviorChangedQuota--; | |
394 setTimeout(() => { handlerBehaviorChangedQuota++; }, 600000); | |
395 } | |
396 } | |
397 | |
398 ext.webRequest = { | |
399 onBeforeRequest: new ext._EventTarget(), | |
400 handlerBehaviorChanged() | |
401 { | |
402 // Defer handlerBehaviorChanged() until navigation occurs. | |
403 // There wouldn't be any visible effect when calling it earlier, | |
404 // but it's an expensive operation and that way we avoid to call | |
405 // it multiple times, if multiple filters are added/removed. | |
406 let onBeforeNavigate = chrome.webNavigation.onBeforeNavigate; | |
407 if (!onBeforeNavigate.hasListener(propagateHandlerBehaviorChange)) | |
408 onBeforeNavigate.addListener(propagateHandlerBehaviorChange); | |
409 } | |
410 }; | |
411 | |
412 chrome.tabs.query({}, tabs => | |
413 { | |
414 tabs.forEach(tab => | |
415 { | |
416 chrome.webNavigation.getAllFrames({tabId: tab.id}, details => | |
417 { | |
418 if (details && details.length > 0) | |
419 { | |
420 let frames = framesOfTabs[tab.id] = Object.create(null); | |
421 | |
422 for (let i = 0; i < details.length; i++) | |
423 frames[details[i].frameId] = {url: new URL(details[i].url), parent:
null}; | |
424 | |
425 for (let i = 0; i < details.length; i++) | |
426 { | |
427 let parentFrameId = details[i].parentFrameId; | |
428 | |
429 if (parentFrameId != -1) | |
430 frames[details[i].frameId].parent = frames[parentFrameId]; | |
431 } | |
432 } | |
433 }); | |
434 }); | |
435 }); | |
436 | |
437 chrome.webRequest.onBeforeRequest.addListener(details => | |
438 { | |
439 // The high-level code isn't interested in requests that aren't | |
440 // related to a tab or requests loading a top-level document, | |
441 // those should never be blocked. | |
442 if (details.tabId == -1 || details.type == "main_frame") | |
443 return; | |
444 | |
445 // We are looking for the frame that contains the element which | |
446 // has triggered this request. For most requests (e.g. images) we | |
447 // can just use the request's frame ID, but for subdocument requests | |
448 // (e.g. iframes) we must instead use the request's parent frame ID. | |
449 let frameId; | |
450 let requestType; | |
451 if (details.type == "sub_frame") | |
452 { | |
453 frameId = details.parentFrameId; | |
454 requestType = "SUBDOCUMENT"; | |
455 } | |
456 else | |
457 { | |
458 frameId = details.frameId; | |
459 requestType = details.type.toUpperCase(); | |
460 } | |
461 | |
462 let frame = ext.getFrame(details.tabId, frameId); | |
463 if (frame) | |
464 { | |
465 let results = ext.webRequest.onBeforeRequest._dispatch( | |
466 new URL(details.url), | |
467 requestType, | |
468 new Page({id: details.tabId}), | |
469 frame | |
470 ); | |
471 | |
472 if (results.indexOf(false) != -1) | |
473 return {cancel: true}; | |
474 } | |
475 }, {urls: ["http://*/*", "https://*/*"]}, ["blocking"]); | |
476 | |
477 | |
478 /* Message passing */ | |
479 | |
480 chrome.runtime.onMessage.addListener((message, rawSender, sendResponse) => | |
481 { | |
482 let sender = {}; | |
483 | |
484 // Add "page" and "frame" if the message was sent by a content script. | |
485 // If sent by popup or the background page itself, there is no "tab". | |
486 if ("tab" in rawSender) | |
487 { | |
488 sender.page = new Page(rawSender.tab); | |
489 sender.frame = { | |
490 // In Edge requests from internal extension pages | |
491 // (protocol ms-browser-extension://) do no have a sender URL. | |
492 url: rawSender.url ? new URL(rawSender.url) : null, | |
493 get parent() | |
494 { | |
495 let frames = framesOfTabs[rawSender.tab.id]; | |
496 | |
497 if (!frames) | |
498 return null; | |
499 | |
500 let frame = frames[rawSender.frameId]; | |
501 if (frame) | |
502 return frame.parent; | |
503 | |
504 return frames[0]; | |
505 } | |
506 }; | |
507 } | |
508 | |
509 return ext.onMessage._dispatch(message, sender, sendResponse).indexOf(true)
!= -1; | |
510 }); | |
511 | |
512 | |
513 /* Storage */ | |
514 | |
515 ext.storage = { | |
516 get(keys, callback) | |
517 { | |
518 chrome.storage.local.get(keys, callback); | |
519 }, | |
520 set(key, value, callback) | |
521 { | |
522 let items = {}; | |
523 items[key] = value; | |
524 chrome.storage.local.set(items, callback); | |
525 }, | |
526 remove(key, callback) | |
527 { | |
528 chrome.storage.local.remove(key, callback); | |
529 }, | |
530 onChanged: chrome.storage.onChanged | |
531 }; | |
532 | |
533 /* Options */ | |
534 | |
535 if ("openOptionsPage" in chrome.runtime) | |
536 { | |
537 ext.showOptions = callback => | |
538 { | |
539 if (!callback) | |
540 { | |
541 chrome.runtime.openOptionsPage(); | |
542 } | |
543 else | |
544 { | |
545 chrome.runtime.openOptionsPage(() => | |
546 { | |
547 if (chrome.runtime.lastError) | |
548 return; | |
549 | |
550 chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs => | |
551 { | |
552 if (tabs.length > 0) | |
553 { | |
554 window.setTimeout(() => | |
555 { | |
556 callback(new Page(tabs[0])); | |
557 }); | |
558 } | |
559 }); | |
560 }); | |
561 } | |
562 }; | |
563 } | |
564 else | |
565 { | |
566 // Edge does not yet support runtime.openOptionsPage (tested version 38) | |
567 // and so this workaround needs to stay for now. | |
568 ext.showOptions = callback => | |
569 { | |
570 chrome.windows.getLastFocused(win => | |
571 { | |
572 let optionsUrl = chrome.extension.getURL("options.html"); | |
573 let queryInfo = {url: optionsUrl}; | |
574 | |
575 // extension pages can't be accessed in incognito windows. In order to | |
576 // correctly mimic the way in which Chrome opens extension options, | |
577 // we have to focus the options page in any other window. | |
578 if (!win.incognito) | |
579 queryInfo.windowId = win.id; | |
580 | |
581 chrome.tabs.query(queryInfo, tabs => | |
582 { | |
583 if (tabs.length > 0) | |
584 { | |
585 let tab = tabs[0]; | |
586 | |
587 chrome.windows.update(tab.windowId, {focused: true}); | |
588 chrome.tabs.update(tab.id, {active: true}); | |
589 | |
590 if (callback) | |
591 callback(new Page(tab)); | |
592 } | |
593 else | |
594 { | |
595 ext.pages.open(optionsUrl, callback); | |
596 } | |
597 }); | |
598 }); | |
599 }; | |
600 } | |
601 | |
602 /* Windows */ | |
603 ext.windows = { | |
604 create(createData, callback) | |
605 { | |
606 if ("create" in chrome.windows){ | |
607 chrome.windows.create(createData, createdWindow => | |
608 { | |
609 afterTabLoaded(callback)(createdWindow.tabs[0]); | |
610 }); | |
611 } | |
612 else | |
613 { | |
614 ext.pages.open(createData.url, callback); | |
615 } | |
616 } | |
617 }; | |
618 } | |
OLD | NEW |