1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/chrome/firefox",
  7     "firebug/lib/xpcom",
  8     "firebug/net/requestObserver",
  9     "firebug/lib/events",
 10     "firebug/lib/url",
 11     "firebug/lib/http",
 12     "firebug/chrome/window",
 13     "firebug/lib/string",
 14     "firebug/lib/array",
 15     "firebug/trace/debug",
 16     "firebug/trace/traceListener",
 17     "firebug/trace/traceModule",
 18     "firebug/chrome/tabContext",
 19 ],
 20 function(Obj, Firebug, Firefox, Xpcom, HttpRequestObserver, Events, Url, Http, Win,
 21     Str, Arr, Debug, TraceListener, TraceModule) {
 22 
 23 // ********************************************************************************************* //
 24 // Constants
 25 
 26 const Cc = Components.classes;
 27 const Ci = Components.interfaces;
 28 const nsIWebNavigation = Ci.nsIWebNavigation;
 29 const nsIWebProgressListener = Ci.nsIWebProgressListener;
 30 const nsIWebProgress = Ci.nsIWebProgress;
 31 const nsISupportsWeakReference = Ci.nsISupportsWeakReference;
 32 const nsISupports = Ci.nsISupports;
 33 const nsIURI = Ci.nsIURI;
 34 
 35 const NOTIFY_STATE_DOCUMENT = nsIWebProgress.NOTIFY_STATE_DOCUMENT;
 36 
 37 const STATE_IS_WINDOW = nsIWebProgressListener.STATE_IS_WINDOW;
 38 const STATE_IS_DOCUMENT = nsIWebProgressListener.STATE_IS_DOCUMENT;
 39 const STATE_IS_REQUEST = nsIWebProgressListener.STATE_IS_REQUEST;
 40 
 41 const STATE_START = nsIWebProgressListener.STATE_START;
 42 const STATE_STOP = nsIWebProgressListener.STATE_STOP;
 43 const STATE_TRANSFERRING = nsIWebProgressListener.STATE_TRANSFERRING;
 44 
 45 const STOP_ALL = nsIWebNavigation.STOP_ALL;
 46 
 47 const dummyURI = "about:layout-dummy-request";
 48 const aboutBlank = "about:blank";
 49 
 50 // ********************************************************************************************* //
 51 // Globals
 52 
 53 var contexts = [];
 54 
 55 var showContextTimeout = 200;
 56 
 57 // ********************************************************************************************* //
 58 
 59 /**
 60  * @object TabWatcher object is responsible for monitoring page load/unload events
 61  * and triggering proper Firebug UI refresh by firing events. This object is also
 62  * responsible for creation of a context object that contains meta-data about currently
 63  * debugged page.
 64  */
 65 Firebug.TabWatcher = Obj.extend(new Firebug.Listener(),
 66 /** @lends Firebug.TabWatcher */
 67 {
 68     // Store contexts where they can be accessed externally
 69     contexts: contexts,
 70 
 71     initialize: function()
 72     {
 73         this.traceListener = new TraceListener("->", "DBG_WINDOWS", true);
 74         TraceModule.addListener(this.traceListener);
 75 
 76         HttpRequestObserver.addObserver(TabWatcherHttpObserver, "firebug-http-event", false);
 77     },
 78 
 79     initializeUI: function()
 80     {
 81         var tabBrowser = Firefox.getElementById("content");
 82 
 83         if (FBTrace.DBG_INITIALIZE)
 84             FBTrace.sysout("-> tabWatcher initializeUI "+tabBrowser);
 85 
 86         if (tabBrowser)
 87             tabBrowser.addProgressListener(TabProgressListener);
 88     },
 89 
 90     destroy: function()
 91     {
 92         if (FBTrace.DBG_WINDOWS)
 93             FBTrace.sysout("-> tabWatcher destroy");
 94 
 95         this.shuttingDown = true;
 96 
 97         HttpRequestObserver.removeObserver(TabWatcherHttpObserver, "firebug-http-event");
 98 
 99         var tabBrowser = Firefox.getElementById("content");
100         if (tabBrowser)
101         {
102             try
103             {
104                 // Exception thrown: tabBrowser.removeProgressListener is not a function
105                 // when Firebug is in detached state and the origin browser window is closed.
106                 tabBrowser.removeProgressListener(TabProgressListener);
107             }
108             catch (e)
109             {
110                 FBTrace.sysout("tabWatcher.destroy; EXCEPTION " + e, e);
111             }
112 
113             var browsers = Firefox.getBrowsers();
114             for (var i = 0; i < browsers.length; ++i)
115             {
116                 var browser = browsers[i];
117                 this.unwatchTopWindow(browser.contentWindow);
118                 unregisterFrameListener(browser);
119             }
120         }
121 
122         TraceModule.removeListener(this.traceListener);
123 
124         var listeners = TabWatcherUnloader.listeners;
125         if (listeners.length > 0)
126         {
127             if (FBTrace.DBG_ERRORS)
128             {
129                 FBTrace.sysout("-> tabWatcher.destroy; ERROR unregistered listeners! (" +
130                     listeners.length + ")", listeners);
131             }
132 
133             TabWatcherUnloader.unregisterAll();
134         }
135     },
136 
137     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
138 
139     /**
140      * Called when tabBrowser browsers get a new location OR when we get a explicit user op
141      * to open firebug.
142      * Attaches to a top-level window. Creates context unless we just re-activated on an
143      * existing context.
144      */
145     watchTopWindow: function(win, uri, userCommands)
146     {
147         if (FBTrace.DBG_WINDOWS)
148             FBTrace.sysout("-> tabWatcher.watchTopWindow for: " +
149                 (uri instanceof nsIURI?uri.spec:uri) + ", tab: " +
150                 Win.getWindowProxyIdForWindow(win));
151 
152         if (!win)
153         {
154             if (FBTrace.DBG_ERRORS)
155                 FBTrace.sysout("-> tabWatcher.watchTopWindow should not have a null window!");
156             return false;
157         }
158 
159         // Do not use Firefox.getCurrentBrowser(); since the current tab can be already
160         // different from what is passed into this function (see issue 4681)
161         // Can be also null, if the window is just closing.
162         var selectedBrowser = Win.getBrowserByWindow(win);
163 
164         var context = this.getContextByWindow(win);
165         if (context) // then we've looked at this window before in this FF session...
166         {
167             if (FBTrace.DBG_ACTIVATION)
168                 FBTrace.sysout("-> tabWatcher.watchTopWindow context exists "+context.getName());
169 
170             if (!this.shouldShowContext(context))
171             {
172                 // ...but now it is not wanted.
173                 if (context.browser)
174                     delete context.browser.showFirebug;
175                 this.unwatchContext(win, context);
176 
177                 return;  // did not create a context
178             }
179 
180             // Special case for about:blank (see issue 5120)
181             // HTML panel's edit mode can cause onStateChange changes and context
182             // recreation.
183             if (context.loaded && context == Firebug.currentContext &&
184                 context.getName() == "about:blank")
185             {
186                 FBTrace.sysout("tabWatcher.watchTopWindow; page already watched");
187                 return;
188             }
189         }
190         else // then we've not looked this window in this session
191         {
192             // decide whether this window will be debugged or not
193             var url = (uri instanceof nsIURI) ? uri.spec : uri;
194             if (!this.shouldCreateContext(selectedBrowser, url, userCommands))
195             {
196                 if (FBTrace.DBG_ACTIVATION)
197                     FBTrace.sysout("-> tabWatcher will not create context ");
198 
199                 delete selectedBrowser.showFirebug;
200                 this.watchContext(win, null);
201 
202                 return false;  // we did not create a context
203             }
204 
205             var browser = this.getBrowserByWindow(win);
206 
207             context = this.createContext(win, browser, Firebug.getContextType());
208         }
209 
210         if (win instanceof Ci.nsIDOMWindow && win.parent == win && context)
211         {
212             // xxxHonza: This place can be called multiple times for one window, so
213             // make sure event listeners are not registered twice.
214             // There should be a better way to find out whether the listeneres are actually
215             // registered for the window.
216             context.removeEventListener(win, "pageshow", onLoadWindowContent,
217                 onLoadWindowContent.capturing);
218             context.removeEventListener(win, "DOMContentLoaded", onLoadWindowContent,
219                 onLoadWindowContent.capturing);
220 
221             // Re-register again since it could have been done too soon before.
222             context.addEventListener(win, "pageshow", onLoadWindowContent,
223                 onLoadWindowContent.capturing);
224             context.addEventListener(win, "DOMContentLoaded", onLoadWindowContent,
225                 onLoadWindowContent.capturing);
226 
227             if (FBTrace.DBG_WINDOWS)
228                 FBTrace.sysout("-> tabWatcher.watchTopWindow addEventListener for pageshow, " +
229                     "DomContentLoaded " + Win.safeGetWindowLocation(win));
230         }
231 
232         // Dispatch watchWindow for the outer most DOM window
233         this.watchWindow(win, context);
234 
235         // This is one of two places that loaded is set. The other is in watchLoadedTopWindow
236         if (context && !context.loaded)
237         {
238             context.loaded = !context.browser.webProgress.isLoadingDocument;
239 
240             // If the loaded flag is set, the proper event should be dispatched.
241             if (context.loaded)
242                 Events.dispatch(this.fbListeners, "loadedContext", [context]);
243 
244             if (FBTrace.DBG_WINDOWS)
245                 FBTrace.sysout("-> tabWatcher context " +
246                     (context.loaded ? '*** LOADED ***' : 'isLoadingDocument') +
247                     " in watchTopWindow, id: "+context.uid+", uri: "+
248                     (uri instanceof nsIURI ? uri.spec : uri));
249         }
250 
251         if (context && !context.loaded && !context.showContextTimeout)
252         {
253             this.rushShowContextTimeout(win, context, 20);
254         }
255         else
256         {
257             if (FBTrace.DBG_WINDOWS)
258                 FBTrace.sysout("-> watchTopWindow context.loaded:" + context.loaded + " for " +
259                     context.getName());
260 
261             this.rushShowContext(win, context);
262         }
263 
264         return context;  // we did create or find a context
265     },
266 
267     rushShowContextTimeout: function(win, context, tryAgain)
268     {
269         if (FBTrace.DBG_WINDOWS)
270             FBTrace.sysout("-> rushShowContextTimeout: tryAgain: " + tryAgain);
271 
272         // still loading, we want to showContext one time but not too agressively
273         var handler = Obj.bindFixed(function delayShowContext()
274         {
275             if (FBTrace.DBG_WINDOWS)
276                 FBTrace.sysout("-> watchTopWindow delayShowContext id:" +
277                     context.showContextTimeout, context);
278 
279             if (context.browser && context.browser.webProgress.isLoadingDocument && --tryAgain > 0)
280             {
281                 this.rushShowContextTimeout(win, context, tryAgain);
282                 return;
283             }
284 
285             // Sometimes context.window is not defined, especially when running tests.
286             if (context.window)
287             {
288                 this.rushShowContext(win, context);  // calls showContext
289             }
290             else
291             {
292                 if (FBTrace.DBG_ERRORS)
293                     FBTrace.sysout("tabWatcher watchTopWindow no context.window " +
294                         (context.browser? context.browser.currentURI.spec :
295                         " and no context.browser"));
296             }
297         }, this);
298 
299         context.showContextTimeout = window.setTimeout(handler, showContextTimeout);
300     },
301 
302     rushShowContext: function(win, context)
303     {
304         // then the timeout even has not run, we'll not need it after all.
305         if (context.showContextTimeout)
306             clearTimeout(context.showContextTimeout);
307         delete context.showContextTimeout;
308 
309         // Call showContext only for currently active tab.
310         var currentURI = Firefox.getCurrentURI();
311         if (!currentURI || currentURI.spec != context.browser.currentURI.spec)
312         {
313             if (FBTrace.DBG_WINDOWS)
314             {
315                 FBTrace.sysout("-> rushShowContext: Do not show context as it's not " +
316                     "the active tab: " + context.browser.currentURI.spec);
317             }
318             return;
319         }
320 
321         this.watchContext(win, context);  // calls showContext
322     },
323 
324     // Listeners decide to show or not
325     shouldShowContext: function(context)
326     {
327         if (Events.dispatch2(this.fbListeners, "shouldShowContext", [context]))
328             return true;
329         else
330             return false;
331     },
332 
333     // Listeners given force-in and veto on URIs/Window.
334     shouldCreateContext: function(browser, url, userCommands)
335     {
336         // called when win has no context, answers the question: create one, true or false?
337 
338         if (!this.fbListeners)
339             return userCommands;
340 
341         // Do not Create if any Listener says true to shouldNotCreateContext
342         if (Events.dispatch2(this.fbListeners, "shouldNotCreateContext",
343             [browser, url, userCommands]))
344         {
345             if (FBTrace.DBG_ACTIVATION)
346                 FBTrace.sysout("-> shouldNotCreateContext vetos create context for: " + url);
347             return false;
348         }
349 
350         if (FBTrace.DBG_ACTIVATION)
351             FBTrace.sysout("-> shouldCreateContext FBLISTENERS", this.fbListeners);
352 
353         // Create if any listener says true to showCreateContext
354         if (Events.dispatch2(this.fbListeners, "shouldCreateContext",
355             [browser, url, userCommands]))
356         {
357              if (FBTrace.DBG_ACTIVATION)
358                  FBTrace.sysout("-> shouldCreateContext with user: "+userCommands+
359                     " one listener says yes to "+ url, this.fbListeners);
360             return true;
361         }
362 
363         if (FBTrace.DBG_ACTIVATION)
364             FBTrace.sysout("-> shouldCreateContext with user: "+userCommands +
365                 " no opinion for: "+ url);
366 
367         // create if user said so and no one else has an opinion.
368         return userCommands;
369     },
370 
371     createContext: function(win, browser, contextType)
372     {
373         // If the page is reloaded, store the persisted state from the previous
374         // page on the new context
375         var persistedState = browser.persistedState;
376         delete browser.persistedState;
377         var location = Win.safeGetWindowLocation(win).toString();
378         //if (!persistedState || persistedState.location != location)
379         //    persistedState = null;
380 
381         // xxxHonza, xxxJJB: web application detection. Based on domain check.
382         var prevDomain = persistedState ? Url.getDomain(persistedState.location) : null;
383         var domain = Url.getDomain(location);
384         // Remove this, see 3484
385         //if (!persistedState || prevDomain != domain)
386         //    persistedState = null;
387 
388         // The proper instance of Firebug.chrome object (different for detached Firebug and
389         // accessible as Firebug.chrome property) must be used for the context object.
390         // (the global context object Firebug.currentContext is also different for
391         // detached firebug).
392         var context = new contextType(win, browser, Firebug.chrome, persistedState);
393         contexts.push(context);
394 
395         context.uid =  Obj.getUniqueId();
396 
397         browser.showFirebug = true; // this is the only place we should set showFirebug.
398 
399         if (FBTrace.DBG_WINDOWS || FBTrace.DBG_ACTIVATION)
400         {
401             FBTrace.sysout("-> tabWatcher *** INIT *** context, id: " + context.uid +
402                 ", " + context.getName() + " browser " + browser.currentURI.spec +
403                 " Firebug.chrome.window: " + Firebug.chrome.window.location +
404                 " context.window: " + Win.safeGetWindowLocation(context.window));
405         }
406 
407         Events.dispatch(this.fbListeners, "initContext", [context, persistedState]);
408 
409         return context;
410     },
411 
412     /**
413      * Called once the document within a tab is completely loaded.
414      */
415     watchLoadedTopWindow: function(win)
416     {
417         var isSystem = Url.isSystemPage(win);
418 
419         var context = this.getContextByWindow(win);
420         if (context && !context.window)
421         {
422             if (FBTrace.DBG_WINDOWS)
423                 FBTrace.sysout("-> tabWatcher.watchLoadedTopWindow bailing !!!, context.window: " +
424                     context.window + ", isSystem: " + isSystem);
425 
426             this.unwatchTopWindow(win);
427             this.watchContext(win, null, isSystem);
428             return;
429         }
430 
431         if (FBTrace.DBG_WINDOWS)
432             FBTrace.sysout("-> watchLoadedTopWindow context: " +
433                 (context ? (context.uid + ", loaded=" + context.loaded) : "undefined")+
434                 ", " + Win.safeGetWindowLocation(win));
435 
436         if (context && !context.loaded)
437         {
438             context.loaded = true;
439 
440             if (FBTrace.DBG_WINDOWS)
441                 FBTrace.sysout("-> Context *** LOADED *** in watchLoadedTopWindow, id: " +
442                     context.uid + ", uri: " + Win.safeGetWindowLocation(win));
443 
444             Events.dispatch(this.fbListeners, "loadedContext", [context]);
445 
446             // DOMContentLoaded arrived. Whether or not we did showContext at 400ms, do it now.
447             this.rushShowContext(win, context);
448         }
449     },
450 
451     /**
452      * Attaches to a window that may be either top-level or a frame within the page.
453      */
454     watchWindow: function(win, context, skipCompletedDocuments)
455     {
456         if (!context)
457             context = this.getContextByWindow(Win.getRootWindow(win));
458 
459         var location = Win.safeGetWindowLocation(win);
460 
461         // For every window we watch, prepare for unwatch. It's OK if this is called
462         // more times (see 2695).
463         if (context)
464             TabWatcherUnloader.registerWindow(win);
465 
466         try
467         {
468             // If the documents is already completed do not register the window
469             // it should be registered already at this point
470             // This condition avoids situation when "about:document-onload-blocker"
471             // and STATE_START is fired for a window, which is consequently never
472             // firing "unload" and so, stays registered within context.windows
473             // See issue 5582 (comment #4)
474             if (skipCompletedDocuments && win.document.readyState == "complete")
475                 return;
476         }
477         catch (err)
478         {
479         }
480 
481         // Unfortunately, dummy requests that trigger the call to watchWindow
482         // are called several times, so we have to avoid dispatching watchWindow
483         // more than once
484         if (context && context.windows.indexOf(win) == -1)
485         {
486             context.windows.push(win);
487 
488             if (FBTrace.DBG_WINDOWS)
489             {
490                 FBTrace.sysout("-> tabWatcher.watchWindow; " + Win.safeGetWindowLocation(win) +
491                     " [" + Win.getWindowId(win).toString() + "] " + context.windows.length +
492                     " - " + win.document.readyState);
493             }
494 
495             Events.dispatch(this.fbListeners, "watchWindow", [context, win]);
496 
497             if (FBTrace.DBG_WINDOWS)
498             {
499                 FBTrace.sysout("-> watchWindow for: " + location + ", context: " + context.uid);
500 
501                 if (context)
502                 {
503                     for (var i = 0; i < context.windows.length; i++)
504                         FBTrace.sysout("context: " + context.uid + ", window in context: " +
505                             context.windows[i].location.href);
506                 }
507             }
508         }
509     },
510 
511     /**
512      * Detaches from a top-level window. Destroys context
513      * Called when windows are closed, or user closes firebug
514      */
515     unwatchTopWindow: function(win)
516     {
517         var context = this.getContextByWindow(win);
518         if (FBTrace.DBG_WINDOWS)
519         {
520             FBTrace.sysout("-> tabWatcher.unwatchTopWindow for: " +
521                 (context ? context.getWindowLocation() : "NULL Context") +
522                 ", context: " + context);
523         }
524 
525         this.unwatchContext(win, context);
526 
527         // Make sure all listeners ('unload' and 'pagehide') are removed.
528         Win.iterateWindows(win, function(win)
529         {
530             TabWatcherUnloader.unregisterWindow(win);
531         });
532 
533         // we might later allow extensions to reject unwatch
534         return true;
535     },
536 
537     /**
538      * Detaches from a window, top-level or frame (interior)
539      */
540     unwatchWindow: function(win)
541     {
542         var context = this.getContextByWindow(win);
543 
544         if (!context)
545         {
546             if (FBTrace.DBG_ERRORS)
547             {
548                 FBTrace.sysout("unwatchWindow: ERROR no context for win " +
549                     Win.safeGetWindowLocation(win));
550             }
551             return;
552         }
553 
554         var index = context.windows.indexOf(win);
555         if (FBTrace.DBG_WINDOWS)
556         {
557             FBTrace.sysout("-> tabWatcher.unwatchWindow; " + Win.safeGetWindowLocation(win) +
558                 " [" + Win.getWindowId(win).toString() + "] " + context.windows.length +
559                 " - " + win.document.readyState);
560         }
561 
562         if (index != -1)
563         {
564             context.windows.splice(index, 1);
565             Events.dispatch(this.fbListeners, "unwatchWindow", [context, win]);
566         }
567     },
568 
569     /**
570      * Attaches to the window inside a browser because of user-activation
571      * returns false if no context was created by the attach attempt, eg extension rejected page
572      */
573     watchBrowser: function(browser)
574     {
575         if (FBTrace.DBG_WINDOWS)
576         {
577             var uri = Http.safeGetURI(browser);
578             FBTrace.sysout("-> tabWatcher.watchBrowser for: " +
579                 (uri instanceof nsIURI?uri.spec:uri));
580         }
581 
582         registerFrameListener(browser);
583 
584         var shouldDispatch = this.watchTopWindow(browser.contentWindow,
585             Http.safeGetURI(browser), true);
586         if (shouldDispatch)
587         {
588             Events.dispatch(this.fbListeners, "watchBrowser", [browser]);
589             return true;
590         }
591 
592         return false;
593     },
594 
595     /**
596      * User closes Firebug
597      */
598     unwatchBrowser: function(browser, userCommands)
599     {
600         if (FBTrace.DBG_WINDOWS)
601         {
602             var uri = Http.safeGetURI(browser);
603             FBTrace.sysout("-> tabWatcher.unwatchBrowser for: " +
604                 (uri instanceof nsIURI ? uri.spec : uri) + " user commands: " + userCommands +
605                 (browser ? "" : "NULL BROWSER"));
606         }
607 
608         if (!browser)
609             return;
610 
611         delete browser.showFirebug;
612 
613         unregisterFrameListener(browser);
614 
615         var shouldDispatch = this.unwatchTopWindow(browser.contentWindow);
616 
617         if (shouldDispatch)
618         {
619             Events.dispatch(this.fbListeners, "unwatchBrowser", [browser, userCommands]);
620             return true;
621         }
622         return false;
623     },
624 
625     // called when tabs change in firefox
626     watchContext: function(win, context, isSystem)
627     {
628         if (this.shuttingDown)
629             return;
630 
631         var browser = context ? context.browser : this.getBrowserByWindow(win);
632         if (browser)
633             browser.isSystemPage = isSystem;
634 
635         if (FBTrace.DBG_WINDOWS)
636             FBTrace.sysout("-> tabWatcher context *** SHOW *** (watchContext), id: " +
637                 (context?context.uid:"null")+", uri: "+win.location.href);
638 
639         // context is null if we don't want to debug this browser
640         Events.dispatch(this.fbListeners, "showContext", [browser, context]);
641     },
642 
643     unwatchContext: function(win, context)
644     {
645         if (!context)
646         {
647             var browser = this.getBrowserByWindow(win);
648             if (browser)
649             {
650                 browser.persistedState = {};
651                 delete browser.showFirebug;
652 
653                 // context is null if we don't want to debug this browser
654                 Events.dispatch(this.fbListeners, "showContext", [browser, null]);
655             }
656 
657             Events.dispatch(this.fbListeners, "destroyContext",
658                 [null, (browser?browser.persistedState:null), browser]);
659             return;
660         }
661 
662         var persistedState = {location: context.getWindowLocation()};
663         context.browser.persistedState = persistedState;  // store our state on FF browser elt
664 
665         Win.iterateWindows(context.window, function(win)
666         {
667             Events.dispatch(Firebug.TabWatcher.fbListeners, "unwatchWindow", [context, win]);
668         });
669 
670         Events.dispatch(this.fbListeners, "destroyContext", [context, persistedState, context.browser]);
671 
672         if (FBTrace.DBG_WINDOWS || FBTrace.DBG_ACTIVATION)
673             FBTrace.sysout("-> tabWatcher.unwatchContext *** DESTROY *** context " + context.uid +
674                 " for: " + (context.window && !context.window.closed?context.window.location :
675                 "no window or closed ") + " aborted: " + context.aborted);
676 
677         context.destroy(persistedState);
678 
679         // Remove context from the list of contexts.
680         Arr.remove(contexts, context);
681 
682         for (var name in context)
683             delete context[name];
684 
685         // unwatchContext can be called on an unload event after another tab is selected
686         var currentBrowser = Firefox.getCurrentBrowser();
687         if (!currentBrowser.showFirebug)
688         {
689             // context is null if we don't want to debug this browser
690             Events.dispatch(this.fbListeners, "showContext", [browser, null]);
691         }
692     },
693 
694     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
695 
696     getContextByWindow: function(winIn)
697     {
698         if (!winIn)
699             return;
700 
701         var rootWindow = Win.getRootWindow(winIn);
702 
703         if (FBTrace.DBG_ROOT_WINDOW) // too much output to use INITIALIZE
704             FBTrace.sysout("winIn: "+Win.safeGetWindowLocation(winIn).substr(0,50)+
705                 " rootWindow: "+Win.safeGetWindowLocation(rootWindow));
706 
707         if (rootWindow)
708         {
709             for (var i = 0; i < contexts.length; ++i)
710             {
711                 var context = contexts[i];
712                 if (context.window == rootWindow)
713                     return context;
714             }
715         }
716     },
717 
718     getContextBySandbox: function(sandbox)
719     {
720         for (var i = 0; i < contexts.length; ++i)
721         {
722             var context = contexts[i];
723             if (context.sandboxes)
724             {
725                 for (var iframe = 0; iframe < context.sandboxes.length; iframe++)
726                 {
727                     if (context.sandboxes[iframe] == sandbox)
728                         return context;
729                 }
730             }
731         }
732         return null;
733     },
734 
735     getContextByGlobal: function(global)
736     {
737         return this.getContextByWindow(global) || this.getContextBySandbox(global);
738     },
739 
740     // deprecated, use Win.getBrowserByWindow
741     getBrowserByWindow: function(win)
742     {
743         if (this.shuttingDown)
744             return null;
745 
746         var browsers = Firefox.getBrowsers();
747         for (var i = 0; i < browsers.length; ++i)
748         {
749             var browser = browsers[i];
750             if (browser.contentWindow == win)
751             {
752                 registerFrameListener(browser); // Yikes side effect!
753                 return browser;
754             }
755         }
756 
757         return null;
758     },
759 
760     iterateContexts: function(fn)
761     {
762         for (var i = 0; i < contexts.length; ++i)
763         {
764             var rc = fn(contexts[i]);
765             if (rc)
766                 return rc;
767         }
768     },
769 
770     // Called by script panel, not sure where this belongs.
771     reloadPageFromMemory: function(context)
772     {
773         if (!context)
774             context = Firebug.currentContext;
775 
776         if (context.browser)
777             context.browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE)
778         else
779             context.window.location.reload();
780     },
781 });
782 
783 // ********************************************************************************************* //
784 
785 var TabWatcherUnloader =
786 {
787     listeners: [],
788 
789     registerWindow: function(win)
790     {
791         var root = (win.parent == win);
792         var eventName = (root && (win.location.href !== "about:blank")) ? "pagehide" : "unload";
793         var listener = Obj.bind(root ? this.onPageHide : this.onUnload, this);
794         Events.addEventListener(win, eventName, listener, false);
795 
796         if (FBTrace.DBG_WINDOWS)
797             FBTrace.sysout("-> tabWatcher.registerWindow; addEventListener for " + eventName+
798                 " on " + Win.safeGetWindowLocation(win));
799 
800         this.listeners.push({
801             window: win,
802             listener: listener,
803             eventName: eventName
804         });
805     },
806 
807     unregisterWindow: function(win)
808     {
809         var newListeners = [];
810         for (var i=0; i<this.listeners.length; i++)
811         {
812             var listener = this.listeners[i];
813             if (listener.window != win)
814             {
815                 newListeners.push(listener);
816             }
817             else
818             {
819                 Events.removeEventListener(win, listener.eventName, listener.listener, false);
820 
821                 if (FBTrace.DBG_WINDOWS)
822                     FBTrace.sysout("-> tabWatcher.unregisterWindow; removeEventListener for " +
823                         listener.eventName + " on " + Win.safeGetWindowLocation(win));
824             }
825         }
826         this.listeners = newListeners;
827     },
828 
829     unregisterAll: function()
830     {
831         for (var i=0; i<this.listeners.length; i++)
832         {
833             var listener = this.listeners[i];
834             Events.removeEventListener(listener.win, listener.eventName, listener.listener, false);
835         }
836 
837         this.listeners = [];
838     },
839 
840     onPageHide: function(event)
841     {
842         var win = event.currentTarget;
843         this.unregisterWindow(win);
844 
845         if (FBTrace.DBG_WINDOWS)
846             FBTrace.sysout("-> tabWatcher.Unloader; PAGE HIDE (" +
847                 this.listeners.length + ") " + win.location, event);
848 
849         onPageHideTopWindow(event);
850     },
851 
852     onUnload: function(event)
853     {
854         var win = event.currentTarget;
855         this.unregisterWindow(win);
856 
857         if (FBTrace.DBG_WINDOWS)
858             FBTrace.sysout("-> tabWatcher.Unloader; PAGE UNLOAD (" +
859                 this.listeners.length + ") " + win.location, event);
860 
861         onUnloadWindow(event);
862     },
863 };
864 
865 Firebug.TabWatcherUnloader = TabWatcherUnloader;
866 
867 // ********************************************************************************************* //
868 
869 var TabProgressListener = Obj.extend(Http.BaseProgressListener,
870 {
871     onLocationChange: function(progress, request, uri)
872     {
873         // Only watch windows that are their own parent - e.g. not frames
874         if (progress.DOMWindow.parent == progress.DOMWindow)
875         {
876             var srcWindow = Http.getWindowForRequest(request);
877             var browser = srcWindow ? Firebug.TabWatcher.getBrowserByWindow(srcWindow) : null;
878 
879             if (FBTrace.DBG_WINDOWS || FBTrace.DBG_ACTIVATION)
880             {
881                 var requestFromFirebuggedWindow = browser && browser.showFirebug;
882                 FBTrace.sysout("-> TabProgressListener.onLocationChange "+
883                     progress.DOMWindow.location+" to: "+
884                     (uri?uri.spec:"null location")+
885                     (requestFromFirebuggedWindow?" from firebugged window":" no firebug"));
886             }
887 
888             // See issue 4040
889             // the onStateChange will deal with this troublesome case
890             //if (uri && uri.spec === "about:blank")
891             //    return;
892 
893             // document.open() was called, the document was cleared.
894             if (uri && uri.scheme === "wyciwyg")
895                 evictTopWindow(progress.DOMWindow, uri);
896 
897             if (uri)
898                 Firebug.TabWatcher.watchTopWindow(progress.DOMWindow, uri);
899             else // the location change to a non-uri means we need to hide
900                 Firebug.TabWatcher.watchContext(progress.DOMWindow, null, true);
901         }
902     },
903 
904     onStateChange: function(progress, request, flag, status)
905     {
906         if (FBTrace.DBG_WINDOWS)
907         {
908             var win = progress.DOMWindow;
909             FBTrace.sysout("-> TabProgressListener.onStateChanged for: " +
910                 Http.safeGetRequestName(request) + ", win: " + win.location.href +
911                 ", content URL: " + (win.document ? win.document.URL : "no content URL") +
912                 " " + Http.getStateDescription(flag));
913         }
914     }
915 });
916 
917 // ********************************************************************************************* //
918 // Obsolete
919 
920 var FrameProgressListener = Obj.extend(Http.BaseProgressListener,
921 {
922     onStateChange: function(progress, request, flag, status)
923     {
924         if (FBTrace.DBG_WINDOWS)
925         {
926             var win = progress.DOMWindow;
927             FBTrace.sysout("-> FrameProgressListener.onStateChanged for: " +
928                 Http.safeGetRequestName(request) + ", win: " + win.location.href +
929                 ", content URL: " + (win.document ? win.document.URL : "no content URL") +
930                 " " + Http.getStateDescription(flag) + ", " + status);
931         }
932 
933         if (flag & STATE_IS_REQUEST && flag & STATE_START)
934         {
935             // We need to get the hook in as soon as the new DOMWindow is created, but before
936             // it starts executing any scripts in the page.  After lengthy analysis, it seems
937             // that the start of these "dummy" requests is the only state that works.
938 
939             var safeName = Http.safeGetRequestName(request);
940             if (safeName && ((safeName == dummyURI) || safeName == "about:document-onload-blocker"))
941             {
942                 var win = progress.DOMWindow;
943                 // Another weird edge case here - when opening a new tab with about:blank,
944                 // "unload" is dispatched to the document, but onLocationChange is not called
945                 // again, so we have to call watchTopWindow here
946 
947                 if (win.parent == win && (win.location.href == "about:blank"))
948                 {
949                     Firebug.TabWatcher.watchTopWindow(win, win.location.href);
950                     return;
951                 }
952                 else
953                 {
954                     Firebug.TabWatcher.watchWindow(win, null, true);
955                 }
956             }
957         }
958 
959         // Later I discovered that XHTML documents don't dispatch the dummy requests, so this
960         // is our best shot here at hooking them.
961         if (flag & STATE_IS_DOCUMENT && flag & STATE_TRANSFERRING)
962         {
963             Firebug.TabWatcher.watchWindow(progress.DOMWindow);
964             return;
965         }
966 
967     }
968 });
969 
970 // Obsolete
971 // Registers frame listener for specified tab browser.
972 function registerFrameListener(browser)
973 {
974     if (browser.frameListener)
975         return;
976 
977     browser.frameListener = FrameProgressListener;  // just a mark saying we've registered. TODO remove!
978     browser.addProgressListener(FrameProgressListener);
979 
980     if (FBTrace.DBG_WINDOWS)
981     {
982         var win = browser.contentWindow;
983         FBTrace.sysout("-> tabWatcher register FrameProgressListener for: "+
984             Win.safeGetWindowLocation(win)+", tab: "+Win.getWindowProxyIdForWindow(win));
985     }
986 }
987 
988 function unregisterFrameListener(browser)
989 {
990     if (browser.frameListener)
991     {
992         delete browser.frameListener;
993         browser.removeProgressListener(FrameProgressListener);
994     }
995 
996     if (FBTrace.DBG_WINDOWS)
997     {
998         var win = browser.contentWindow;
999         FBTrace.sysout("-> tabWatcher unregister FrameProgressListener for: "+
1000             Win.safeGetWindowLocation(win)+", tab: "+Win.getWindowProxyIdForWindow(win));
1001     }
1002 }
1003 
1004 // ********************************************************************************************* //
1005 
1006 function getRefererHeader(request)
1007 {
1008     var http = Xpcom.QI(request, Ci.nsIHttpChannel);
1009     var referer = null;
1010     http.visitRequestHeaders({
1011         visitHeader: function(name, value)
1012         {
1013             if (name == 'referer')
1014                 referer = value;
1015         }
1016     });
1017     return referer;
1018 }
1019 
1020 // ********************************************************************************************* //
1021 
1022 var TabWatcherHttpObserver = Obj.extend(Object,
1023 {
1024     dispatchName: "TabWatcherHttpObserver",
1025 
1026     // nsIObserver
1027     observe: function(aSubject, aTopic, aData)
1028     {
1029         try
1030         {
1031             if (aTopic == "http-on-modify-request")
1032             {
1033                 aSubject = aSubject.QueryInterface(Ci.nsIHttpChannel);
1034                 this.onModifyRequest(aSubject);
1035             }
1036         }
1037         catch (err)
1038         {
1039             Debug.ERROR(err);
1040         }
1041     },
1042 
1043     onModifyRequest: function(request)
1044     {
1045         var win = Http.getWindowForRequest(request);
1046         if (win)
1047             var tabId = Win.getWindowProxyIdForWindow(win);
1048 
1049         // Tab watcher is only interested in tab related requests.
1050         if (!tabId)
1051             return;
1052 
1053         // Ignore redirects
1054         if (request.URI.spec != request.originalURI.spec)
1055             return;
1056 
1057         // A document request for the specified tab is here. It can be a top window
1058         // request (win == win.parent) or embedded iframe request.
1059         if (request.loadFlags & Ci.nsIHttpChannel.LOAD_DOCUMENT_URI)
1060         {
1061             if ((FBTrace.DBG_ACTIVATION || FBTrace.DBG_WINDOWS) && win == win.parent)
1062             {
1063                 FBTrace.sysout("-> tabWatcher Firebug.TabWatcherHttpObserver *** START *** " +
1064                     "document request for: " + request.URI.spec + " window for request is "+
1065                     Win.safeGetWindowLocation(win));
1066             }
1067 
1068             if (win == win.parent)
1069             {
1070                 // Make sure the frame listener is registered for top level window, so
1071                 // we can get all onStateChange events and init context for all opened tabs.
1072                 var browser = Firebug.TabWatcher.getBrowserByWindow(win);
1073 
1074                 if (!browser)
1075                     return;
1076 
1077                 delete browser.FirebugLink;
1078 
1079                 // then this page is opened in new tab or window
1080                 if (Win.safeGetWindowLocation(win).toString() == "about:blank")
1081                 {
1082                     var referer = getRefererHeader(request);
1083                     if (referer)
1084                     {
1085                         try
1086                         {
1087                             var srcURI = Url.makeURI(referer);
1088                             browser.FirebugLink = {src: srcURI, dst: request.URI};
1089                         }
1090                         catch(e)
1091                         {
1092                             if (FBTrace.DBG_ERRORS)
1093                                 FBTrace.sysout("tabWatcher.onModifyRequest failed to make URI from "+
1094                                     referer+" because "+exc, exc);
1095                         }
1096                     }
1097                 }
1098                 else
1099                 {
1100                     // Here we know the source of the request is 'win'. For viral activation
1101                     // and web app tracking
1102                     browser.FirebugLink = {src: browser.currentURI, dst: request.URI};
1103                 }
1104                 if (FBTrace.DBG_ACTIVATION && browser.FirebugLink)
1105                     FBTrace.sysout("tabWatcher.onModifyRequest created FirebugLink from "+
1106                         browser.FirebugLink.src.spec + " to "+browser.FirebugLink.dst.spec);
1107             }
1108         }
1109     },
1110 
1111     QueryInterface : function (aIID)
1112     {
1113         if (aIID.equals(Ci.nsIObserver) ||
1114             aIID.equals(Ci.nsISupportsWeakReference) ||
1115             aIID.equals(Ci.nsISupports))
1116         {
1117             return this;
1118         }
1119 
1120         throw Components.results.NS_NOINTERFACE;
1121     }
1122 });
1123 
1124 // ********************************************************************************************* //
1125 // Local Helpers
1126 
1127 function onPageHideTopWindow(event)
1128 {
1129     var win = event.currentTarget;  // we set the handler on a window
1130     var doc = event.target; // the pagehide is sent to the document.
1131     if (doc.defaultView != win)
1132         return; // ignore page hides on interior windows
1133 
1134     if (FBTrace.DBG_WINDOWS)
1135         FBTrace.sysout("-> tabWatcher pagehide event.currentTarget "+
1136             Win.safeGetWindowLocation(win), event);
1137 
1138     // http://developer.mozilla.org/en/docs/Using_Firefox_1.5_caching#pagehide_event
1139     // then the page is cached and there cannot be an unload handler
1140     if (event.persisted || Win.safeGetWindowLocation(win) === aboutBlank)
1141     {
1142         //  see Bug 484710 -  add pageIgnore event for pages that are ejected from the bfcache
1143         if (FBTrace.DBG_WINDOWS)
1144             FBTrace.sysout("-> tabWatcher onPageHideTopWindow for: " +
1145                 Win.safeGetWindowLocation(win));
1146 
1147         Firebug.TabWatcher.unwatchTopWindow(win);
1148     }
1149     else
1150     {
1151         // Page is not cached, there may be an unload
1152         Events.addEventListener(win, "unload", onUnloadTopWindow, true);
1153 
1154         if (FBTrace.DBG_WINDOWS)
1155             FBTrace.sysout("-> tabWatcher onPageHideTopWindow set unload handler " +
1156                 Win.safeGetWindowLocation(win));
1157     }
1158 }
1159 
1160 function evictTopWindow(win, uri)
1161 {
1162     if (FBTrace.DBG_WINDOWS)
1163         FBTrace.sysout("-> tabWatcher evictTopWindow win "+Win.safeGetWindowLocation(win) +
1164             " uri "+uri.spec);
1165 
1166     Firebug.TabWatcher.unwatchTopWindow(win);
1167 }
1168 
1169 function onUnloadTopWindow(event)
1170 {
1171     var win = event.currentTarget;
1172     Events.removeEventListener(win, "unload", onUnloadTopWindow, true);
1173 
1174     if (FBTrace.DBG_WINDOWS)
1175         FBTrace.sysout("-> tabWatcher onUnloadTopWindow for: " + Win.safeGetWindowLocation(win) +
1176             " typeof: " + typeof(win));
1177 
1178     Firebug.TabWatcher.unwatchTopWindow(win);
1179 }
1180 
1181 function onLoadWindowContent(event)
1182 {
1183     if (FBTrace.DBG_WINDOWS)
1184         FBTrace.sysout("-> tabWatcher.onLoadWindowContent event.type: " + event.type);
1185 
1186     var win = event.currentTarget;
1187     try
1188     {
1189         Events.removeEventListener(win, "pageshow", onLoadWindowContent,
1190             onLoadWindowContent.capturing);
1191 
1192         if (FBTrace.DBG_WINDOWS)
1193             FBTrace.sysout("-> tabWatcher.onLoadWindowContent pageshow removeEventListener " +
1194                 Win.safeGetWindowLocation(win));
1195     }
1196     catch (exc)
1197     {
1198         if (FBTrace.DBG_ERRORS)
1199             FBTrace.sysout("-> tabWatcher.onLoadWindowContent removeEventListener pageshow fails",
1200                 exc);
1201     }
1202 
1203     try
1204     {
1205         Events.removeEventListener(win, "DOMContentLoaded", onLoadWindowContent,
1206             onLoadWindowContent.capturing);
1207 
1208         if (FBTrace.DBG_WINDOWS)
1209             FBTrace.sysout("-> tabWatcher.onLoadWindowContent DOMContentLoaded " +
1210                 "removeEventListener " + Win.safeGetWindowLocation(win));
1211     }
1212     catch (exc)
1213     {
1214         if (FBTrace.DBG_ERRORS)
1215             FBTrace.sysout("-> tabWatcher.onLoadWindowContent removeEventListener " +
1216                 "DOMContentLoaded fails", exc);
1217     }
1218 
1219     // Signal that we got the onLoadWindowContent event. This prevents the
1220     // FrameProgressListener from sending it.
1221     var context = Firebug.TabWatcher.getContextByWindow(win);
1222     if (context)
1223         context.onLoadWindowContent = true;
1224 
1225     try
1226     {
1227         if (FBTrace.DBG_WINDOWS)
1228             FBTrace.sysout("-> tabWatcher.onLoadWindowContent:" +
1229                 Win.safeGetWindowLocation(win), win);
1230 
1231         Firebug.TabWatcher.watchLoadedTopWindow(win);
1232     }
1233     catch(exc)
1234     {
1235         if (FBTrace.DBG_ERRORS)
1236             FBTrace.sysout("-> tabWatchter onLoadWindowContent FAILS: "+exc, exc);
1237     }
1238 }
1239 
1240 onLoadWindowContent.capturing = false;
1241 
1242 function onUnloadWindow(event)
1243 {
1244     var win = event.currentTarget;
1245     var eventType = "unload";
1246 
1247     if (FBTrace.DBG_WINDOWS)
1248         FBTrace.sysout("-> tabWatcher.onUnloadWindow for: "+Win.safeGetWindowLocation(win) +
1249             " removeEventListener: "+ eventType);
1250 
1251     Firebug.TabWatcher.unwatchWindow(win);
1252 }
1253 
1254 // ********************************************************************************************* //
1255 
1256 window.__defineGetter__("TabWatcher", function deprecatedTabWatcher()
1257 {
1258     if (FBTrace.DBG_ERRORS)
1259         FBTrace.sysout("deprecated TabWatcher global accessed");
1260 
1261     return Firebug.TabWatcher;
1262 });
1263 
1264 // ********************************************************************************************* //
1265 // Registration
1266 
1267 return Firebug.TabWatcher;
1268 
1269 // ********************************************************************************************* //
1270 });
1271