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

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

Issue 5464830253203456: Refactored the abstraction layer to address prerendered pages on Safari caused by leaky abstraction (Closed)
Patch Set: Addressed comments Created April 11, 2014, 2:47 p.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 | « popupBlocker.js ('k') | safari/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
1 /* 1 /*
2 * This file is part of Adblock Plus <http://adblockplus.org/>, 2 * This file is part of Adblock Plus <http://adblockplus.org/>,
3 * Copyright (C) 2006-2014 Eyeo GmbH 3 * Copyright (C) 2006-2014 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
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details. 12 * GNU General Public License for more details.
13 * 13 *
14 * You should have received a copy of the GNU General Public License 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/>. 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */ 16 */
17 17
18 (function() 18 (function()
19 { 19 {
20 /* Events */ 20 /* Pages */
21 21
22 var TabEventTarget = function() 22 var pages = {__proto__: null};
23 var pageCounter = 0;
24
25 var Page = function(id, tab, url, prerendered)
23 { 26 {
24 WrappedEventTarget.apply(this, arguments); 27 this._id = id;
25 }; 28 this._tab = tab;
26 TabEventTarget.prototype = { 29 this._frames = [{url: url, parent: null}];
27 __proto__: WrappedEventTarget.prototype, 30 this._prerendered = prerendered;
28 _wrapListener: function(listener) 31
29 { 32 if (tab.page)
30 return function(event) 33 this._messageProxy = new ext._MessageProxy(tab.page);
31 { 34 else
32 if (event.target instanceof SafariBrowserTab) 35 // while the new tab page is shown on Safari 7, the 'page' property
33 listener(new Tab(event.target)); 36 // of the tab is undefined, and we can't send messages to that page
37 this._messageProxy = {
38 handleRequest: function() {},
39 handleResponse: function() {},
40 sendMessage: function() {}
34 }; 41 };
35 }
36 };
37
38 var LoadingTabEventTarget = function(target)
39 {
40 WrappedEventTarget.call(this, target, "message", false);
41 };
42 LoadingTabEventTarget.prototype = {
43 __proto__: WrappedEventTarget.prototype,
44 _wrapListener: function(listener)
45 {
46 return function (event)
47 {
48 if (event.name == "loading")
49 listener(new Tab(event.target));
50 };
51 }
52 };
53
54 var BackgroundMessageEventTarget = function()
55 {
56 MessageEventTarget.call(this, safari.application);
57 };
58 BackgroundMessageEventTarget.prototype = {
59 __proto__: MessageEventTarget.prototype,
60 _getResponseDispatcher: function(event)
61 {
62 return event.target.page;
63 },
64 _getSenderDetails: function(event)
65 {
66 return {
67 tab: new Tab(event.target),
68 frame: new Frame(
69 event.message.documentUrl,
70 event.message.isTopLevel,
71 event.target
72 )
73 };
74 }
75 };
76
77
78 /* Tabs */
79
80 Tab = function(tab)
81 {
82 this._tab = tab;
83 42
84 this.browserAction = new BrowserAction(this); 43 this.browserAction = new BrowserAction(this);
85
86 this.onLoading = new LoadingTabEventTarget(tab);
87 this.onCompleted = new TabEventTarget(tab, "navigate", false);
88 this.onActivated = new TabEventTarget(tab, "activate", false);
89 this.onRemoved = new TabEventTarget(tab, "close", false);
90 }; 44 };
91 Tab.prototype = { 45 Page.prototype = {
92 get url() 46 get url()
93 { 47 {
94 return this._tab.url; 48 return this._frames[0].url;
95 },
96 close: function()
97 {
98 this._tab.close();
99 }, 49 },
100 activate: function() 50 activate: function()
101 { 51 {
102 this._tab.activate(); 52 this._tab.activate();
103 }, 53 },
104 sendMessage: function(message, responseCallback) 54 sendMessage: function(message, responseCallback)
105 { 55 {
106 _sendMessage( 56 this._messageProxy.sendMessage(message, responseCallback, {pageId: this._i d});
107 message, responseCallback,
108 this._tab.page, this._tab
109 );
110 } 57 }
111 }; 58 };
112 59
113 TabMap = function(deleteOnPageUnload) 60 var isPageActive = function(page)
114 { 61 {
115 this._data = []; 62 return page._tab == page._tab.browserWindow.activeTab && !page._prerendered;
116 this._deleteOnPageUnload = deleteOnPageUnload; 63 };
117 64
118 this.delete = this.delete.bind(this); 65 var forgetPage = function(id)
119 this._delete = this._delete.bind(this); 66 {
67 ext._removeFromAllPageMaps(id);
68 delete pages[id];
120 }; 69 };
121 TabMap.prototype = 70
71 var replacePage = function(page)
122 { 72 {
123 _indexOf: function(tab) 73 for (var id in pages)
124 { 74 {
125 for (var i = 0; i < this._data.length; i++) 75 if (id != page._id && pages[id]._tab == page._tab)
126 if (this._data[i].tab._tab == tab._tab) 76 forgetPage(id);
127 return i; 77 }
128 78
129 return -1; 79 if (isPageActive(page))
130 }, 80 updateToolbarItemForPage(page);
131 _delete: function(tab) 81 };
82
83 ext.pages = {
84 open: function(url, callback)
132 { 85 {
133 // delay so that other onClosed listeners can still look this tab up 86 var tab = safari.application.activeBrowserWindow.openTab();
134 setTimeout(this.delete.bind(this, tab), 0); 87 tab.url = url;
135 },
136 get: function(tab) {
137 var idx;
138 88
139 if (!tab || (idx = this._indexOf(tab)) == -1) 89 if (callback)
140 return null;
141
142 return this._data[idx].value;
143 },
144 set: function(tab, value)
145 {
146 var idx = this._indexOf(tab);
147
148 if (idx != -1)
149 this._data[idx].value = value;
150 else
151 { 90 {
152 this._data.push({value: value, tab: tab}); 91 var onLoading = function(page)
153 92 {
154 tab.onRemoved.addListener(this._delete); 93 if (page._tab == tab)
155 if (this._deleteOnPageUnload) 94 {
156 tab.onLoading.addListener(this.delete); 95 ext.pages.onLoading.removeListener(onLoading);
96 callback(page);
97 }
98 };
99 ext.pages.onLoading.addListener(onLoading);
157 } 100 }
158 }, 101 },
159 has: function(tab) 102 query: function(info, callback)
160 { 103 {
161 return this._indexOf(tab) != -1; 104 var matchedPages = [];
105
106 for (var id in pages)
107 {
108 var page = pages[id];
109 var win = page._tab.browserWindow;
110
111 if ("active" in info && info.active != isPageActive(page))
112 continue;
113 if ("lastFocusedWindow" in info && info.lastFocusedWindow != (win == saf ari.application.activeBrowserWindow))
114 continue;
115
116 matchedPages.push(page);
117 };
118
119 callback(matchedPages);
162 }, 120 },
163 clear: function() 121 onLoading: new ext._EventTarget()
164 {
165 while (this._data.length > 0)
166 this.delete(this._data[0].tab);
167 },
168 delete: function(tab)
169 {
170 var idx = this._indexOf(tab);
171
172 if (idx != -1)
173 {
174 tab = this._data[idx].tab;
175 this._data.splice(idx, 1);
176
177 tab.onRemoved.removeListener(this._delete);
178 tab.onLoading.removeListener(this.delete);
179 }
180 }
181 }; 122 };
182 123
183 ext.tabs = { 124 safari.application.addEventListener("close", function(event)
184 onLoading: new LoadingTabEventTarget(safari.application), 125 {
185 onCompleted: new TabEventTarget(safari.application, "navigate", true), 126 // this event is dispatched on closing windows and tabs. However when a
186 onActivated: new TabEventTarget(safari.application, "activate", true), 127 // window is closed, it is first dispatched on each tab in the window and
187 onRemoved: new TabEventTarget(safari.application, "close", true) 128 // then on the window itself. But we are only interested in closed tabs.
188 }; 129 if (!(event.target instanceof SafariBrowserTab))
130 return;
131
132 // when a tab is closed, forget the previous page associated with that
133 // tab. Note that it wouldn't be sufficient do that when the old page
134 // is unloading, because Safari dispatches window.onunload only when
135 // reloading the page or following links, but not when closing the tab.
136 for (var id in pages)
137 {
138 if (pages[id]._tab == event.target)
139 forgetPage(id);
140 }
141 }, true);
189 142
190 143
191 /* Browser actions */ 144 /* Browser actions */
192 145
193 var toolbarItemProperties = {}; 146 var toolbarItemProperties = {};
194 147
195 var getToolbarItemProperty = function(name)
196 {
197 var property = toolbarItemProperties[name];
198 if (!property)
199 {
200 property = {tabs: new TabMap()};
201 toolbarItemProperties[name] = property;
202 }
203 return property;
204 };
205
206 var getToolbarItemForWindow = function(win) 148 var getToolbarItemForWindow = function(win)
207 { 149 {
208 for (var i = 0; i < safari.extension.toolbarItems.length; i++) 150 for (var i = 0; i < safari.extension.toolbarItems.length; i++)
209 { 151 {
210 var toolbarItem = safari.extension.toolbarItems[i]; 152 var toolbarItem = safari.extension.toolbarItems[i];
211 153
212 if (toolbarItem.browserWindow == win) 154 if (toolbarItem.browserWindow == win)
213 return toolbarItem; 155 return toolbarItem;
214 } 156 }
215 157
216 return null; 158 return null;
217 }; 159 };
218 160
219 var BrowserAction = function(tab) 161 var updateToolbarItemForPage = function(page, win) {
162 var toolbarItem = getToolbarItemForWindow(win || page._tab.browserWindow);
163 if (!toolbarItem)
164 return;
165
166 for (var name in toolbarItemProperties)
167 {
168 var property = toolbarItemProperties[name];
169
170 if (page && property.pages.has(page))
171 toolbarItem[name] = property.pages.get(page);
172 else
173 toolbarItem[name] = property.global;
174 }
175 };
176
177 var BrowserAction = function(page)
220 { 178 {
221 this._tab = tab; 179 this._page = page;
222 }; 180 };
223 BrowserAction.prototype = { 181 BrowserAction.prototype = {
224 _set: function(name, value) 182 _set: function(name, value)
225 { 183 {
226 var currentWindow = this._tab._tab.browserWindow; 184 var toolbarItem = getToolbarItemForWindow(this._page._tab.browserWindow);
227 var toolbarItem = getToolbarItemForWindow(currentWindow); 185 if (!toolbarItem)
186 return;
228 187
229 if (toolbarItem) 188 var property = toolbarItemProperties[name];
230 { 189 if (!property)
231 var property = getToolbarItemProperty(name); 190 property = toolbarItemProperties[name] = {
232 property.tabs.set(this._tab, value); 191 pages: new ext.PageMap(),
192 global: toolbarItem[name]
193 };
233 194
234 if (!("global" in property)) 195 property.pages.set(this._page, value);
235 property.global = toolbarItem[name];
236 196
237 if (this._tab._tab == currentWindow.activeTab) 197 if (isPageActive(this._page))
238 toolbarItem[name] = value; 198 toolbarItem[name] = value;
239 }
240 }, 199 },
241 setIcon: function(path) 200 setIcon: function(path)
242 { 201 {
243 this._set("image", safari.extension.baseURI + path.replace("$size", "16")) ; 202 this._set("image", safari.extension.baseURI + path.replace("$size", "16")) ;
244 }, 203 },
245 setBadge: function(badge) 204 setBadge: function(badge)
246 { 205 {
247 if (!badge) 206 if (!badge)
248 this._set("badge", 0); 207 this._set("badge", 0);
249 else if ("number" in badge) 208 else if ("number" in badge)
250 this._set("badge", badge.number); 209 this._set("badge", badge.number);
251 } 210 }
252 }; 211 };
253 212
254 ext.tabs.onActivated.addListener(function(tab) 213 safari.application.addEventListener("activate", function(event)
255 { 214 {
256 var toolbarItem = getToolbarItemForWindow(tab._tab.browserWindow); 215 // this event is also dispatched on windows that got focused. But we
257 216 // are only interested in tabs, which became active in their window.
258 if (!toolbarItem) 217 if (!(event.target instanceof SafariBrowserTab))
259 return; 218 return;
260 219
261 for (var name in toolbarItemProperties) 220 // update the toolbar item for the page visible in the tab that just
221 // became active. If we can't find that page (e.g. when a page was
222 // opened in a new tab, and our content script didn't run yet), the
223 // toolbar item of the window, is reset to its intial configuration.
224 var activePage = null;
225 for (var id in pages)
262 { 226 {
263 var property = toolbarItemProperties[name]; 227 var page = pages[id];
228 if (page._tab == event.target && !page._prerendered)
229 {
230 activePage = page;
231 break;
232 }
233 }
264 234
265 if (property.tabs.has(tab)) 235 updateToolbarItemForPage(activePage, event.target.browserWindow);
266 toolbarItem[name] = property.tabs.get(tab); 236 }, true);
267 else 237
268 toolbarItem[name] = property.global; 238
239 /* Web requests */
240
241 ext.webRequest = {
242 onBeforeRequest: new ext._EventTarget(true),
243 handlerBehaviorChanged: function() {}
244 };
245
246
247 /* Context menus */
248
249 var contextMenuItems = [];
250 var isContextMenuHidden = true;
251
252 ext.contextMenus = {
253 addMenuItem: function(title, contexts, onclick)
254 {
255 contextMenuItems.push({
256 id: String(contextMenuItems.length),
257 title: title,
258 item: null,
259 contexts: contexts,
260 onclick: onclick
261 });
262 this.showMenuItems();
263 },
264 removeMenuItems: function()
265 {
266 contextMenuItems = [];
267 this.hideMenuItems();
268 },
269 showMenuItems: function()
270 {
271 isContextMenuHidden = false;
272 },
273 hideMenuItems: function()
274 {
275 isContextMenuHidden = true;
276 }
277 };
278
279 safari.application.addEventListener("contextmenu", function(event)
280 {
281 if (isContextMenuHidden)
282 return;
283
284 var context = event.userInfo.tagName;
285 if (context == "img")
286 context = "image";
287 if (!event.userInfo.srcUrl)
288 context = null;
289
290 for (var i = 0; i < contextMenuItems.length; i++)
291 {
292 // Supported contexts are: all, audio, image, video
293 var menuItem = contextMenuItems[i];
294 if (menuItem.contexts.indexOf("all") == -1 && menuItem.contexts.indexOf(co ntext) == -1)
295 continue;
296
297 event.contextMenu.appendContextMenuItem(menuItem.id, menuItem.title);
269 } 298 }
270 }); 299 });
271 300
272 ext.tabs.onLoading.addListener(function(tab) 301 safari.application.addEventListener("command", function(event)
273 { 302 {
274 var currentWindow = tab._tab.browserWindow; 303 for (var i = 0; i < contextMenuItems.length; i++)
275
276 var toolbarItem;
277 if (tab._tab == currentWindow.activeTab)
278 toolbarItem = getToolbarItemForWindow(currentWindow);
279 else
280 toolbarItem = null;
281
282 for (var name in toolbarItemProperties)
283 { 304 {
284 var property = toolbarItemProperties[name]; 305 if (contextMenuItems[i].id == event.command)
285 property.tabs.delete(tab); 306 {
286 307 contextMenuItems[i].onclick(event.userInfo.srcUrl, pages[event.userInfo. pageId]);
287 if (toolbarItem) 308 break;
288 toolbarItem[name] = property.global; 309 }
289 } 310 }
290 }); 311 });
291 312
292 313
293 /* Windows */ 314 /* Background page */
294 315
295 Window = function(win) 316 ext.backgroundPage = {
296 { 317 getWindow: function()
297 this._win = win;
298 }
299 Window.prototype = {
300 get visible()
301 { 318 {
302 return this._win.visible; 319 return window;
303 },
304 getAllTabs: function(callback)
305 {
306 callback(this._win.tabs.map(function(tab) { return new Tab(tab); }));
307 },
308 getActiveTab: function(callback)
309 {
310 callback(new Tab(this._win.activeTab));
311 },
312 openTab: function(url, callback)
313 {
314 var tab = this._win.openTab();
315 tab.url = url;
316
317 if (callback)
318 callback(new Tab(tab));
319 } 320 }
320 }; 321 };
321 322
322 323
323 /* Frames */ 324 /* Background page proxy (for access from content scripts) */
324 325
325 Frame = function(url, isTopLevel, tab) 326 var backgroundPageProxy = {
326 { 327 cache: new ext.PageMap(),
327 this.url = url;
328
329 // there is no way to discover frames with Safari's API.
330 // so if this isn't the top level frame, assume that the parent is.
331 // this is the best we can do for Safari. :(
332 if (!isTopLevel)
333 this.parent = new Frame(tab.url, true);
334 else
335 this.parent = null;
336 };
337
338
339 /* Background page proxy */
340
341 var proxy = {
342 tabs: [],
343 objects: [],
344 328
345 registerObject: function(obj, objects) 329 registerObject: function(obj, objects)
346 { 330 {
347 var objectId = objects.indexOf(obj); 331 var objectId = objects.indexOf(obj);
348 332
349 if (objectId == -1) 333 if (objectId == -1)
350 objectId = objects.push(obj) - 1; 334 objectId = objects.push(obj) - 1;
351 335
352 return objectId; 336 return objectId;
353 }, 337 },
(...skipping 28 matching lines...) Expand all
382 spec.items = this.serializeSequence(obj, objects, memo); 366 spec.items = this.serializeSequence(obj, objects, memo);
383 return spec; 367 return spec;
384 } 368 }
385 369
386 if (obj.constructor != Date && obj.constructor != RegExp) 370 if (obj.constructor != Date && obj.constructor != RegExp)
387 return {type: "object", objectId: this.registerObject(obj, objects)}; 371 return {type: "object", objectId: this.registerObject(obj, objects)};
388 } 372 }
389 373
390 return {type: "value", value: obj}; 374 return {type: "value", value: obj};
391 }, 375 },
392 createCallback: function(callbackId, tab) 376 createCallback: function(callbackId, pageId, frameId)
393 { 377 {
394 var proxy = this; 378 var proxy = this;
395 379
396 return function() 380 return function()
397 { 381 {
398 var idx = proxy.tabs.indexOf(tab); 382 var page = pages[pageId];
383 if (!page)
384 return;
399 385
400 if (idx != -1) { 386 var objects = proxy.cache.get(page);
401 var objects = proxy.objects[idx]; 387 if (!objects)
388 return;
402 389
403 tab.page.dispatchMessage("proxyCallback", 390 page._tab.page.dispatchMessage("proxyCallback",
404 { 391 {
405 callbackId: callbackId, 392 pageId: pageId,
406 contextId: proxy.registerObject(this, objects), 393 frameId: frameId,
407 args: proxy.serializeSequence(arguments, objects) 394 callbackId: callbackId,
408 }); 395 contextId: proxy.registerObject(this, objects),
409 } 396 args: proxy.serializeSequence(arguments, objects)
397 });
410 }; 398 };
411 }, 399 },
412 deserialize: function(spec, objects, tab, memo) 400 deserialize: function(spec, objects, pageId, memo)
413 { 401 {
414 switch (spec.type) 402 switch (spec.type)
415 { 403 {
416 case "value": 404 case "value":
417 return spec.value; 405 return spec.value;
418 case "hosted": 406 case "hosted":
419 return objects[spec.objectId]; 407 return objects[spec.objectId];
420 case "callback": 408 case "callback":
421 return this.createCallback(spec.callbackId, tab); 409 return this.createCallback(spec.callbackId, pageId, spec.frameId);
422 case "object": 410 case "object":
423 case "array": 411 case "array":
424 if (!memo) 412 if (!memo)
425 memo = {specs: [], objects: []}; 413 memo = {specs: [], objects: []};
426 414
427 var idx = memo.specs.indexOf(spec); 415 var idx = memo.specs.indexOf(spec);
428 if (idx != -1) 416 if (idx != -1)
429 return memo.objects[idx]; 417 return memo.objects[idx];
430 418
431 var obj; 419 var obj;
432 if (spec.type == "array") 420 if (spec.type == "array")
433 obj = []; 421 obj = [];
434 else 422 else
435 obj = {}; 423 obj = {};
436 424
437 memo.specs.push(spec); 425 memo.specs.push(spec);
438 memo.objects.push(obj); 426 memo.objects.push(obj);
439 427
440 if (spec.type == "array") 428 if (spec.type == "array")
441 for (var i = 0; i < spec.items.length; i++) 429 for (var i = 0; i < spec.items.length; i++)
442 obj.push(this.deserialize(spec.items[i], objects, tab, memo)); 430 obj.push(this.deserialize(spec.items[i], objects, pageId, memo));
443 else 431 else
444 for (var k in spec.properties) 432 for (var k in spec.properties)
445 obj[k] = this.deserialize(spec.properties[k], objects, tab, memo); 433 obj[k] = this.deserialize(spec.properties[k], objects, pageId, mem o);
446 434
447 return obj; 435 return obj;
448 } 436 }
449 }, 437 },
450 createObjectCache: function(tab) 438 getObjectCache: function(page)
451 { 439 {
452 var objects = [window]; 440 var objects = this.cache.get(page);
453 441 if (!objects)
454 this.tabs.push(tab);
455 this.objects.push(objects);
456
457 tab.addEventListener("close", function()
458 { 442 {
459 var idx = this.tabs.indexOf(tab); 443 objects = [window];
460 444 this.cache.set(page, objects);
461 if (idx != -1) 445 }
462 {
463 this.tabs.splice(idx, 1);
464 this.objects.splice(idx, 1);
465 }
466 }.bind(this));
467
468 return objects;
469 },
470 getObjectCache: function(tab)
471 {
472 var idx = this.tabs.indexOf(tab);
473 var objects;
474
475 if (idx != -1)
476 objects = this.objects[idx];
477 else
478 objects = this.objects[idx] = this.createObjectCache(tab);
479
480 return objects; 446 return objects;
481 }, 447 },
482 fail: function(error) 448 fail: function(error)
483 { 449 {
484 if (error instanceof Error) 450 if (error instanceof Error)
485 error = error.message; 451 error = error.message;
486 return {succeed: false, error: error}; 452 return {succeed: false, error: error};
487 }, 453 },
488 _handleMessage: function(message, tab) 454 handleMessage: function(message)
489 { 455 {
490 var objects = this.getObjectCache(tab); 456 var objects = this.getObjectCache(pages[message.pageId]);
491 457
492 switch (message.type) 458 switch (message.type)
493 { 459 {
494 case "getProperty": 460 case "getProperty":
495 var obj = objects[message.objectId]; 461 var obj = objects[message.objectId];
496 462
497 try 463 try
498 { 464 {
499 var value = obj[message.property]; 465 var value = obj[message.property];
500 } 466 }
501 catch (e) 467 catch (e)
502 { 468 {
503 return this.fail(e); 469 return this.fail(e);
504 } 470 }
505 471
506 return {succeed: true, result: this.serialize(value, objects)}; 472 return {succeed: true, result: this.serialize(value, objects)};
507 case "setProperty": 473 case "setProperty":
508 var obj = objects[message.objectId]; 474 var obj = objects[message.objectId];
509 var value = this.deserialize(message.value, objects, tab); 475 var value = this.deserialize(message.value, objects, message.pageId);
510 476
511 try 477 try
512 { 478 {
513 obj[message.property] = value; 479 obj[message.property] = value;
514 } 480 }
515 catch (e) 481 catch (e)
516 { 482 {
517 return this.fail(e); 483 return this.fail(e);
518 } 484 }
519 485
520 return {succeed: true}; 486 return {succeed: true};
521 case "callFunction": 487 case "callFunction":
522 var func = objects[message.functionId]; 488 var func = objects[message.functionId];
523 var context = objects[message.contextId]; 489 var context = objects[message.contextId];
524 490
525 var args = []; 491 var args = [];
526 for (var i = 0; i < message.args.length; i++) 492 for (var i = 0; i < message.args.length; i++)
527 args.push(this.deserialize(message.args[i], objects, tab)); 493 args.push(this.deserialize(message.args[i], objects, message.pageId) );
528 494
529 try 495 try
530 { 496 {
531 var result = func.apply(context, args); 497 var result = func.apply(context, args);
532 } 498 }
533 catch (e) 499 catch (e)
534 { 500 {
535 return this.fail(e); 501 return this.fail(e);
536 } 502 }
537 503
(...skipping 16 matching lines...) Expand all
554 objectInfo.prototypeOf = "Object"; 520 objectInfo.prototypeOf = "Object";
555 if (obj == Function.prototype) 521 if (obj == Function.prototype)
556 objectInfo.prototypeOf = "Function"; 522 objectInfo.prototypeOf = "Function";
557 523
558 return objectInfo; 524 return objectInfo;
559 } 525 }
560 } 526 }
561 }; 527 };
562 528
563 529
564 /* Web request blocking */ 530 /* Message processing */
565
566 ext.webRequest = {
567 onBeforeRequest: {
568 _listeners: [],
569
570 _handleMessage: function(message, rawTab)
571 {
572 var tab = new Tab(rawTab);
573 var frame = new Frame(message.documentUrl, message.isTopLevel, rawTab);
574
575 for (var i = 0; i < this._listeners.length; i++)
576 {
577 if (this._listeners[i](message.url, message.type, tab, frame) === fals e)
578 return false;
579 }
580
581 return true;
582 },
583 addListener: function(listener)
584 {
585 this._listeners.push(listener);
586 },
587 removeListener: function(listener)
588 {
589 var idx = this._listeners.indexOf(listener);
590 if (idx != -1)
591 this._listeners.splice(idx, 1);
592 }
593 },
594 handlerBehaviorChanged: function() {}
595 };
596
597
598 /* Synchronous messaging */
599 531
600 safari.application.addEventListener("message", function(event) 532 safari.application.addEventListener("message", function(event)
601 { 533 {
602 if (event.name == "canLoad") 534 switch (event.name)
603 { 535 {
604 var handler; 536 case "canLoad":
537 switch (event.message.category)
538 {
539 case "loading":
540 var pageId;
541 var frameId;
605 542
606 switch (event.message.type) 543 if (event.message.isTopLevel)
607 { 544 {
608 case "proxy": 545 pageId = ++pageCounter;
609 handler = proxy; 546 frameId = 0;
610 break;
611 case "webRequest":
612 handler = ext.webRequest.onBeforeRequest;
613 break;
614 }
615 547
616 event.message = handler._handleMessage(event.message.payload, event.target ); 548 var isPrerendered = event.message.isPrerendered;
549 var page = pages[pageId] = new Page(
550 pageId,
551 event.target,
552 event.message.url,
553 isPrerendered
554 );
555
556 // when a new page is shown, forget the previous page associated
557 // with its tab, and reset the toolbar item if necessary.
558 // Note that it wouldn't be sufficient to do that when the old
559 // page is unloading, because Safari dispatches window.onunload
560 // only when reloading the page or following links, but not when
561 // you enter a new URL in the address bar.
562 if (!isPrerendered)
563 replacePage(page);
564
565 ext.pages.onLoading._dispatch(page);
566 }
567 else
568 {
569 var page;
570 var parentFrame;
571
572 var lastPageId;
573 var lastPage;
574 var lastPageTopLevelFrame;
575
576 // find the parent frame and its page for this sub frame,
577 // by matching its referrer with the URL of frames previously
578 // loaded in the same tab. If there is more than one match,
579 // the most recent loaded page and frame is preferred.
580 for (var curPageId in pages)
581 {
582 var curPage = pages[curPageId];
583 if (curPage._tab != event.target)
584 continue;
585
586 for (var i = 0; i < curPage._frames.length; i++)
587 {
588 var curFrame = curPage._frames[i];
589
590 if (curFrame.url == event.message.referrer)
591 {
592 pageId = curPageId;
593 page = curPage;
594 parentFrame = curFrame;
595 }
596
597 if (i == 0)
598 {
599 lastPageId = curPageId;
600 lastPage = curPage;
601 lastPageTopLevelFrame = curFrame;
602 }
603 }
604 }
605
606 // if we can't find the parent frame and its page, fall back to
607 // the page most recently loaded in the tab and its top level fram e
608 if (!page)
609 {
610 pageId = lastPageId;
611 page = lastPage;
612 parentFrame = lastPageTopLevelFrame;
613 }
614
615 frameId = page._frames.length;
616 page._frames.push({
617 url: event.message.url,
618 parent: parentFrame
619 });
620 }
621
622 event.message = {pageId: pageId, frameId: frameId};
623 break;
624 case "webRequest":
625 var page = pages[event.message.pageId];
626
627 event.message = ext.webRequest.onBeforeRequest._dispatch(
628 event.message.url,
629 event.message.type,
630 page,
631 page._frames[event.message.frameId]
632 );
633 break;
634 case "proxy":
635 event.message = backgroundPageProxy.handleMessage(event.message);
636 break;
637 }
638 break;
639 case "request":
640 var page = pages[event.message.pageId];
641 var sender = {page: page, frame: page._frames[event.message.frameId]};
642 page._messageProxy.handleRequest(event.message, sender);
643 break;
644 case "response":
645 pages[event.message.pageId]._messageProxy.handleResponse(event.message);
646 break;
647 case "replaced":
648 var page = pages[event.message.pageId];
649 page._prerendered = false;
650
651 // when a prerendered page is shown, forget the previous page
652 // associated with its tab, and reset the toolbar item if necessary.
653 // Note that it wouldn't be sufficient to do that when the old
654 // page is unloading, because Safari dispatches window.onunload
655 // only when reloading the page or following links, but not when
656 // the current page is replaced with a prerendered page.
657 replacePage(page);
658 break;
617 } 659 }
618 }, true); 660 });
619 661
620 662
621 /* API */ 663 /* Storage */
622 664
623 ext.windows = {
624 getAll: function(callback)
625 {
626 callback(safari.application.browserWindows.map(function(win)
627 {
628 return new Window(win);
629 }));
630 },
631 getLastFocused: function(callback)
632 {
633 callback(new Window(safari.application.activeBrowserWindow));
634 }
635 };
636
637 ext.backgroundPage = {
638 getWindow: function()
639 {
640 return safari.extension.globalPage.contentWindow;
641 }
642 };
643
644 ext.onMessage = new BackgroundMessageEventTarget();
645 ext.storage = safari.extension.settings; 665 ext.storage = safari.extension.settings;
646
647 var contextMenuItems = [];
648 var isContextMenuHidden = true;
649 ext.contextMenus = {
650 addMenuItem: function(title, contexts, onclick)
651 {
652 contextMenuItems.push({
653 id: String(contextMenuItems.length),
654 title: title,
655 item: null,
656 contexts: contexts,
657 onclick: onclick
658 });
659 this.showMenuItems();
660 },
661 removeMenuItems: function()
662 {
663 contextMenuItems = [];
664 this.hideMenuItems();
665 },
666 showMenuItems: function()
667 {
668 isContextMenuHidden = false;
669 },
670 hideMenuItems: function()
671 {
672 isContextMenuHidden = true;
673 }
674 };
675
676 // Create context menu items
677 safari.application.addEventListener("contextmenu", function(event)
678 {
679 if (isContextMenuHidden)
680 return;
681
682 var context = event.userInfo.tagName;
683 if (context == "img")
684 context = "image";
685 if (!event.userInfo.srcUrl)
686 context = null;
687
688 for (var i = 0; i < contextMenuItems.length; i++)
689 {
690 // Supported contexts are: all, audio, image, video
691 var menuItem = contextMenuItems[i];
692 if (menuItem.contexts.indexOf("all") == -1 && menuItem.contexts.indexOf(co ntext) == -1)
693 continue;
694
695 event.contextMenu.appendContextMenuItem(menuItem.id, menuItem.title);
696 }
697 }, false);
698
699 // Handle context menu item clicks
700 safari.application.addEventListener("command", function(event)
701 {
702 for (var i = 0; i < contextMenuItems.length; i++)
703 {
704 if (contextMenuItems[i].id == event.command)
705 {
706 contextMenuItems[i].onclick(event.userInfo.srcUrl, new Tab(safari.applic ation.activeBrowserWindow.activeTab));
707 break;
708 }
709 }
710 }, false);
711 })(); 666 })();
OLDNEW
« no previous file with comments | « popupBlocker.js ('k') | safari/ext/common.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld