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

Powered by Google App Engine
This is Rietveld