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 (function() | |
19 { | |
20 /* Context menus */ | |
21 | |
22 var contextMenuItems = new ext.PageMap(); | |
23 var lastContextMenuTab; | |
24 | |
25 var ContextMenus = function(page) | |
26 { | |
27 this._page = page; | |
28 }; | |
29 ContextMenus.prototype = { | |
30 create: function(item) | |
31 { | |
32 var items = contextMenuItems.get(this._page); | |
33 if (!items) | |
34 contextMenuItems.set(this._page, items = []); | |
35 | |
36 items.push(item); | |
37 }, | |
38 remove: function(item) | |
39 { | |
40 let items = contextMenuItems.get(this._page); | |
41 if (items) | |
42 { | |
43 let index = items.indexOf(item); | |
44 if (index != -1) | |
45 items.splice(index, 1); | |
46 } | |
47 } | |
48 }; | |
49 | |
50 safari.application.addEventListener("contextmenu", function(event) | |
51 { | |
52 lastContextMenuTab = event.target; | |
53 | |
54 if (!event.userInfo) | |
55 return; | |
56 | |
57 var documentId = event.userInfo.documentId; | |
58 if (!documentId) | |
59 return; | |
60 | |
61 var page = pages[event.target._documentLookup[documentId].pageId]; | |
62 var items = contextMenuItems.get(page); | |
63 if (!items) | |
64 return; | |
65 | |
66 var context = event.userInfo.tagName; | |
67 if (context == "img") | |
68 context = "image"; | |
69 | |
70 for (var i = 0; i < items.length; i++) | |
71 { | |
72 // Supported contexts are: all, audio, image, video | |
73 var menuItem = items[i]; | |
74 if (menuItem.contexts.indexOf("all") == -1 && menuItem.contexts.indexOf(co
ntext) == -1) | |
75 continue; | |
76 | |
77 event.contextMenu.appendContextMenuItem(i, menuItem.title); | |
78 } | |
79 }); | |
80 | |
81 safari.application.addEventListener("command", function(event) | |
82 { | |
83 var documentId = event.userInfo.documentId; | |
84 var page = pages[lastContextMenuTab._documentLookup[documentId].pageId]; | |
85 var items = contextMenuItems.get(page); | |
86 | |
87 items[event.command].onclick(page); | |
88 }); | |
89 | |
90 | |
91 /* Browser actions */ | |
92 | |
93 var toolbarItemProperties = {}; | |
94 | |
95 var getToolbarItemForWindow = function(win) | |
96 { | |
97 for (var i = 0; i < safari.extension.toolbarItems.length; i++) | |
98 { | |
99 var toolbarItem = safari.extension.toolbarItems[i]; | |
100 | |
101 if (toolbarItem.browserWindow == win) | |
102 return toolbarItem; | |
103 } | |
104 | |
105 return null; | |
106 }; | |
107 | |
108 var updateToolbarItemForPage = function(page, win) { | |
109 var toolbarItem = getToolbarItemForWindow(win); | |
110 if (!toolbarItem) | |
111 return; | |
112 | |
113 for (var name in toolbarItemProperties) | |
114 { | |
115 var property = toolbarItemProperties[name]; | |
116 | |
117 if (page && property.pages.has(page)) | |
118 toolbarItem[name] = property.pages.get(page); | |
119 else | |
120 toolbarItem[name] = property.global; | |
121 } | |
122 }; | |
123 | |
124 var BrowserAction = function(page) | |
125 { | |
126 this._page = page; | |
127 }; | |
128 BrowserAction.prototype = { | |
129 _set: function(name, value) | |
130 { | |
131 var toolbarItem = getToolbarItemForWindow(this._page._tab.browserWindow); | |
132 if (!toolbarItem) | |
133 return; | |
134 | |
135 var property = toolbarItemProperties[name]; | |
136 if (!property) | |
137 property = toolbarItemProperties[name] = { | |
138 pages: new ext.PageMap(), | |
139 global: toolbarItem[name] | |
140 }; | |
141 | |
142 property.pages.set(this._page, value); | |
143 | |
144 if (isPageActive(this._page)) | |
145 toolbarItem[name] = value; | |
146 }, | |
147 setIcon: function(path) | |
148 { | |
149 this._set("image", safari.extension.baseURI + path.replace("$size", "16"))
; | |
150 }, | |
151 setBadge: function(badge) | |
152 { | |
153 if (!badge) | |
154 this._set("badge", 0); | |
155 else if ("number" in badge) | |
156 this._set("badge", badge.number); | |
157 } | |
158 }; | |
159 | |
160 safari.application.addEventListener("activate", function(event) | |
161 { | |
162 // this event is also dispatched on windows that got focused. But we | |
163 // are only interested in tabs, which became active in their window. | |
164 if (!(event.target instanceof SafariBrowserTab)) | |
165 return; | |
166 | |
167 let visiblePage = event.target._visiblePage; | |
168 if (visiblePage) | |
169 ext.pages.onActivated._dispatch(visiblePage); | |
170 | |
171 // update the toolbar item for the page visible in the tab that just | |
172 // became active. If we can't find that page (e.g. when a page was | |
173 // opened in a new tab, and our content script didn't run yet), the | |
174 // toolbar item of the window, is reset to its intial configuration. | |
175 updateToolbarItemForPage(visiblePage, event.target.browserWindow); | |
176 }, true); | |
177 | |
178 | |
179 /* Pages */ | |
180 | |
181 var pages = Object.create(null); | |
182 var pageCounter = 0; | |
183 | |
184 var Page = function(id, tab, url) | |
185 { | |
186 this.id = id; | |
187 this._tab = tab; | |
188 this._frames = [{url: new URL(url), parent: null}]; | |
189 | |
190 if (tab.page) | |
191 this._messageProxy = new ext._MessageProxy(tab.page); | |
192 else | |
193 // while the new tab page is shown on Safari 7, the 'page' property | |
194 // of the tab is undefined, and we can't send messages to that page | |
195 this._messageProxy = { | |
196 handleRequest: function() {}, | |
197 handleResponse: function() {}, | |
198 sendMessage: function() {} | |
199 }; | |
200 | |
201 this.browserAction = new BrowserAction(this); | |
202 this.contextMenus = new ContextMenus(this); | |
203 }; | |
204 Page.prototype = { | |
205 get url() | |
206 { | |
207 return this._frames[0].url; | |
208 }, | |
209 sendMessage: function(message, responseCallback) | |
210 { | |
211 var documentIds = []; | |
212 for (var documentId in this._tab._documentLookup) | |
213 if (this._tab._documentLookup[documentId].pageId == this.id) | |
214 documentIds.push(documentId); | |
215 | |
216 this._messageProxy.sendMessage(message, responseCallback, | |
217 {targetDocuments: documentIds}); | |
218 } | |
219 }; | |
220 | |
221 ext.getPage = function(id) | |
222 { | |
223 return pages[id]; | |
224 }; | |
225 | |
226 var isPageActive = function(page) | |
227 { | |
228 var tab = page._tab; | |
229 var win = tab.browserWindow; | |
230 return win && tab == win.activeTab && page == tab._visiblePage; | |
231 }; | |
232 | |
233 var forgetPage = function(id) | |
234 { | |
235 ext.pages.onRemoved._dispatch(id); | |
236 | |
237 ext._removeFromAllPageMaps(id); | |
238 | |
239 var tab = pages[id]._tab; | |
240 | |
241 for (var documentId in tab._documentLookup) | |
242 { | |
243 if (tab._documentLookup[documentId].pageId == id) | |
244 delete tab._documentLookup[documentId]; | |
245 } | |
246 | |
247 delete tab._pages[id]; | |
248 delete pages[id]; | |
249 }; | |
250 | |
251 var replacePage = function(page) | |
252 { | |
253 var tab = page._tab; | |
254 tab._visiblePage = page; | |
255 | |
256 for (var id in tab._pages) | |
257 { | |
258 if (id != page.id) | |
259 forgetPage(id); | |
260 } | |
261 | |
262 if (isPageActive(page)) | |
263 updateToolbarItemForPage(page, tab.browserWindow); | |
264 }; | |
265 | |
266 var addPage = function(tab, url, prerendered) | |
267 { | |
268 var pageId = ++pageCounter; | |
269 | |
270 if (!('_pages' in tab)) | |
271 tab._pages = Object.create(null); | |
272 | |
273 if (!('_documentLookup' in tab)) | |
274 tab._documentLookup = Object.create(null); | |
275 | |
276 var page = new Page(pageId, tab, url); | |
277 pages[pageId] = tab._pages[pageId] = page; | |
278 | |
279 // When a new page is shown, forget the previous page associated | |
280 // with its tab, and reset the toolbar item if necessary. | |
281 // Note that it wouldn't be sufficient to do that when the old | |
282 // page is unloading, because Safari dispatches window.onunload | |
283 // only when reloading the page or following links, but not when | |
284 // you enter a new URL in the address bar. | |
285 if (!prerendered) | |
286 replacePage(page); | |
287 | |
288 return pageId; | |
289 }; | |
290 | |
291 ext.pages = { | |
292 open: function(url, callback) | |
293 { | |
294 var tab = safari.application.activeBrowserWindow.openTab(); | |
295 tab.url = url; | |
296 | |
297 if (callback) | |
298 { | |
299 var onNavigate = function(event) | |
300 { | |
301 if (event.target == tab) | |
302 { | |
303 safari.application.removeEventListener(onNavigate); | |
304 callback(tab._visiblePage); | |
305 } | |
306 }; | |
307 | |
308 safari.application.addEventListener("navigate", onNavigate); | |
309 } | |
310 }, | |
311 query: function(info, callback) | |
312 { | |
313 var matchedPages = []; | |
314 | |
315 for (var id in pages) | |
316 { | |
317 var page = pages[id]; | |
318 var win = page._tab.browserWindow; | |
319 | |
320 if ("active" in info && info.active != isPageActive(page)) | |
321 continue; | |
322 if ("lastFocusedWindow" in info && info.lastFocusedWindow != (win == saf
ari.application.activeBrowserWindow)) | |
323 continue; | |
324 | |
325 matchedPages.push(page); | |
326 }; | |
327 | |
328 callback(matchedPages); | |
329 }, | |
330 onLoading: new ext._EventTarget(), | |
331 onActivated: new ext._EventTarget(), | |
332 onRemoved: new ext._EventTarget() | |
333 }; | |
334 | |
335 safari.application.addEventListener("close", function(event) | |
336 { | |
337 // this event is dispatched on closing windows and tabs. However when a | |
338 // window is closed, it is first dispatched on each tab in the window and | |
339 // then on the window itself. But we are only interested in closed tabs. | |
340 if (!(event.target instanceof SafariBrowserTab)) | |
341 return; | |
342 | |
343 // when a tab is closed, forget the previous page associated with that | |
344 // tab. Note that it wouldn't be sufficient do that when the old page | |
345 // is unloading, because Safari dispatches window.onunload only when | |
346 // reloading the page or following links, but not when closing the tab. | |
347 for (var id in event.target._pages) | |
348 forgetPage(id); | |
349 }, true); | |
350 | |
351 // We generally rely on content scripts to report new pages, | |
352 // since Safari's extension API doesn't consider pre-rendered | |
353 // pages. However, when the extension initializes we have to | |
354 // use Safari's extension API to detect existing tabs. | |
355 safari.application.browserWindows.forEach(function(win) | |
356 { | |
357 for (var i = 0; i < win.tabs.length; i++) | |
358 { | |
359 var tab = win.tabs[i]; | |
360 var url = tab.url; | |
361 | |
362 // For the new tab page the url property is undefined. | |
363 if (url) | |
364 { | |
365 var pageId = addPage(tab, url, false); | |
366 tab.page.dispatchMessage("requestDocumentId", {pageId: pageId}); | |
367 } | |
368 } | |
369 }); | |
370 | |
371 | |
372 /* Web requests */ | |
373 | |
374 ext.webRequest = { | |
375 onBeforeRequest: new ext._EventTarget(), | |
376 handlerBehaviorChanged: function() | |
377 { | |
378 }, | |
379 getIndistinguishableTypes: function() | |
380 { | |
381 return []; | |
382 } | |
383 }; | |
384 | |
385 /* Message processing */ | |
386 | |
387 var dispatchedLegacyAPISupportMessage = false; | |
388 safari.application.addEventListener("message", function(event) | |
389 { | |
390 var tab = event.target; | |
391 var message = event.message; | |
392 var sender; | |
393 if ("documentId" in message && "_documentLookup" in tab) | |
394 { | |
395 sender = tab._documentLookup[message.documentId]; | |
396 if (sender) | |
397 { | |
398 sender.page = pages[sender.pageId]; | |
399 sender.frame = sender.page._frames[sender.frameId]; | |
400 } | |
401 } | |
402 | |
403 switch (event.name) | |
404 { | |
405 case "canLoad": | |
406 switch (message.category) | |
407 { | |
408 case "webRequest": | |
409 var results = ext.webRequest.onBeforeRequest._dispatch( | |
410 new URL(message.url, sender.frame.url), | |
411 message.type, sender.page, sender.frame | |
412 ); | |
413 | |
414 event.message = (results.indexOf(false) == -1); | |
415 break; | |
416 case "request": | |
417 var response = null; | |
418 var sendResponse = function(message) { response = message; }; | |
419 | |
420 ext.onMessage._dispatch(message.payload, sender, sendResponse); | |
421 | |
422 event.message = response; | |
423 break; | |
424 } | |
425 break; | |
426 case "request": | |
427 sender.page._messageProxy.handleRequest(message, sender); | |
428 break; | |
429 case "response": | |
430 // All documents within a page have the same pageId and that's all we | |
431 // care about here. | |
432 var pageId = tab._documentLookup[message.targetDocuments[0]].pageId; | |
433 pages[pageId]._messageProxy.handleResponse(message); | |
434 break; | |
435 case "replaced": | |
436 // when a prerendered page is shown, forget the previous page | |
437 // associated with its tab, and reset the toolbar item if necessary. | |
438 // Note that it wouldn't be sufficient to do that when the old | |
439 // page is unloading, because Safari dispatches window.onunload | |
440 // only when reloading the page or following links, but not when | |
441 // the current page is replaced with a prerendered page. | |
442 replacePage(sender.page); | |
443 break; | |
444 case "loading": | |
445 var pageId; | |
446 var frameId; | |
447 var documentId = message.documentId; | |
448 | |
449 if (message.isTopLevel) | |
450 { | |
451 pageId = addPage(tab, message.url, message.isPrerendered); | |
452 frameId = 0; | |
453 | |
454 ext.pages.onLoading._dispatch(pages[pageId]); | |
455 } | |
456 else | |
457 { | |
458 var page; | |
459 var parentFrame; | |
460 | |
461 var lastPageId; | |
462 var lastPage; | |
463 var lastPageTopLevelFrame; | |
464 | |
465 // find the parent frame and its page for this sub frame, | |
466 // by matching its referrer with the URL of frames previously | |
467 // loaded in the same tab. If there is more than one match, | |
468 // the most recent loaded page and frame is preferred. | |
469 for (var curPageId in tab._pages) | |
470 { | |
471 var curPage = pages[curPageId]; | |
472 | |
473 for (var i = 0; i < curPage._frames.length; i++) | |
474 { | |
475 var curFrame = curPage._frames[i]; | |
476 | |
477 if (curFrame.url.href == message.referrer) | |
478 { | |
479 pageId = curPageId; | |
480 page = curPage; | |
481 parentFrame = curFrame; | |
482 } | |
483 | |
484 if (i == 0) | |
485 { | |
486 lastPageId = curPageId; | |
487 lastPage = curPage; | |
488 lastPageTopLevelFrame = curFrame; | |
489 } | |
490 } | |
491 } | |
492 | |
493 // if we can't find the parent frame and its page, fall back to | |
494 // the page most recently loaded in the tab and its top level frame | |
495 if (!page) | |
496 { | |
497 pageId = lastPageId; | |
498 page = lastPage; | |
499 parentFrame = lastPageTopLevelFrame; | |
500 } | |
501 | |
502 frameId = page._frames.length; | |
503 page._frames.push({url: new URL(message.url), parent: parentFrame}); | |
504 } | |
505 | |
506 tab._documentLookup[documentId] = {pageId: pageId, frameId: frameId}; | |
507 | |
508 if (!dispatchedLegacyAPISupportMessage) | |
509 { | |
510 ext.onMessage._dispatch({ | |
511 type: "safari.legacyAPISupported", | |
512 legacyAPISupported: message.legacyAPISupported | |
513 }); | |
514 dispatchedLegacyAPISupportMessage = true; | |
515 } | |
516 break; | |
517 case "documentId": | |
518 tab._documentLookup[message.documentId] = { | |
519 pageId: message.pageId, frameId: 0 | |
520 }; | |
521 break; | |
522 } | |
523 }); | |
524 | |
525 | |
526 /* Storage */ | |
527 | |
528 ext.storage = { | |
529 get: function(keys, callback) | |
530 { | |
531 var items = {}; | |
532 var settings = safari.extension.settings; | |
533 | |
534 for (var i = 0; i < keys.length; i++) | |
535 { | |
536 var key = keys[i]; | |
537 if (key in settings) | |
538 items[key] = settings[key]; | |
539 } | |
540 | |
541 setTimeout(callback, 0, items); | |
542 }, | |
543 set: function(key, value, callback) | |
544 { | |
545 safari.extension.settings[key] = value; | |
546 | |
547 if (callback) | |
548 setTimeout(callback, 0); | |
549 }, | |
550 remove: function(key, callback) | |
551 { | |
552 delete safari.extension.settings[key]; | |
553 | |
554 if (callback) | |
555 setTimeout(callback, 0); | |
556 }, | |
557 onChanged: new ext._EventTarget() | |
558 }; | |
559 | |
560 safari.extension.settings.addEventListener("change", function(event) | |
561 { | |
562 var changes = {}; | |
563 var change = changes[event.key] = {}; | |
564 | |
565 if (event.oldValue != null) | |
566 change.oldValue = event.oldValue; | |
567 if (event.newValue != null) | |
568 change.newValue = event.newValue; | |
569 | |
570 ext.storage.onChanged._dispatch(changes); | |
571 }); | |
572 | |
573 | |
574 /* Options */ | |
575 | |
576 ext.showOptions = function(callback) | |
577 { | |
578 var optionsUrl = safari.extension.baseURI + "options.html"; | |
579 | |
580 for (var id in pages) | |
581 { | |
582 var page = pages[id]; | |
583 var tab = page._tab; | |
584 | |
585 if (page.url.href == optionsUrl && tab.browserWindow == safari.application
.activeBrowserWindow) | |
586 { | |
587 tab.activate(); | |
588 if (callback) | |
589 callback(page); | |
590 return; | |
591 } | |
592 } | |
593 | |
594 ext.pages.open(optionsUrl, callback); | |
595 }; | |
596 | |
597 /* Windows */ | |
598 ext.windows = { | |
599 // Safari doesn't provide as rich a windows API as Chrome does, so instead | |
600 // of chrome.windows.create we have to fall back to just opening a new tab. | |
601 create: function(createData, callback) | |
602 { | |
603 ext.pages.open(createData.url, callback); | |
604 } | |
605 }; | |
606 })(); | |
OLD | NEW |