1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "arch/compilationunit",
  6     "firebug/lib/events",
  7     "firebug/lib/url",
  8     "firebug/chrome/window",
  9     "firebug/lib/css",
 10     "firebug/chrome/plugin",
 11 ],
 12 function(Obj, CompilationUnit, Events, Url, Win, Css) {
 13 
 14 // ********************************************************************************************* //
 15 // Constants
 16 
 17 const throttleTimeWindow = 200;
 18 const throttleMessageLimit = 30;
 19 const throttleInterval = 30;
 20 const throttleFlushCount = 20;
 21 const refreshDelay = 300;
 22 
 23 // ********************************************************************************************* //
 24 
 25 Firebug.TabContext = function(win, browser, chrome, persistedState)
 26 {
 27     this.window = win;
 28     this.browser = browser;
 29     this.persistedState = persistedState;
 30 
 31     this.name = Url.normalizeURL(this.getWindowLocation().toString());
 32 
 33     this.windows = [];
 34     this.panelMap = {};
 35     this.sidePanelNames = {};
 36 
 37     this.compilationUnits = {};
 38     this.sourceFileByTag = {}; // mozilla only
 39 
 40     // New nsITraceableChannel interface (introduced in FF3.0.4) makes possible
 41     // to re-implement source-cache so that it solves the double-load problem.
 42     // Anyway, keep the previous cache implementation for backward compatibility
 43     // (with Firefox 3.0.3 and lower)
 44     if (Components.interfaces.nsITraceableChannel)
 45         this.sourceCache = new Firebug.TabCache(this);
 46     else
 47         this.sourceCache = new Firebug.SourceCache(this);
 48 
 49     this.global = win;  // used by chromebug
 50 
 51     // -- Back end support --
 52     this.sourceFileMap = {};  // backend
 53 };
 54 
 55 Firebug.TabContext.prototype =
 56 {
 57     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 58     // Browser Tools Interface BrowserContext
 59 
 60     getCompilationUnit: function(url)
 61     {
 62         return this.compilationUnits[url];
 63     },
 64 
 65     getAllCompilationUnits: function()
 66     {
 67         return Firebug.SourceFile.mapAsArray(this.compilationUnits);
 68     },
 69 
 70     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 71 
 72     getWindowLocation: function()
 73     {
 74         return Win.safeGetWindowLocation(this.window);
 75     },
 76 
 77     getTitle: function()
 78     {
 79         if (this.window && this.window.document)
 80             return this.window.document.title;
 81         else
 82             return "";
 83     },
 84 
 85     getName: function()
 86     {
 87         if (!this.name || this.name === "about:blank")
 88         {
 89             var url = this.getWindowLocation().toString();
 90             if (Url.isDataURL(url))
 91             {
 92                 var props = Url.splitDataURL(url);
 93                 if (props.fileName)
 94                     this.name = "data url from "+props.fileName;
 95             }
 96             else
 97             {
 98                 this.name = Url.normalizeURL(url);
 99                 if (this.name === "about:blank" && this.window.frameElement)
100                     this.name += " in "+Css.getElementCSSSelector(this.window.frameElement);
101             }
102         }
103         return this.name;
104     },
105 
106     getGlobalScope: function()
107     {
108         return this.window;
109     },
110 
111     addSourceFile: function(sourceFile)
112     {
113         if (!this.sourceFileMap)
114         {
115             FBTrace.sysout("tabContext.addSourceFile; ERROR no source map!");
116             return;
117         }
118 
119         this.sourceFileMap[sourceFile.href] = sourceFile;
120         sourceFile.context = this;
121 
122         this.addTags(sourceFile);
123 
124         var kind = CompilationUnit.SCRIPT_TAG;
125         if (sourceFile.compilation_unit_type == "event")
126             kind = CompilationUnit.BROWSER_GENERATED;
127 
128         if (sourceFile.compilation_unit_type == "eval")
129             kind = CompilationUnit.EVAL;
130 
131         var url = sourceFile.href;
132         if (FBTrace.DBG_COMPILATION_UNITS)
133             FBTrace.sysout("onCompilationUnit " + url, [this, url, kind] );
134 
135         Firebug.connection.dispatch("onCompilationUnit", [this, url, kind]);
136 
137         // HACKs
138         var compilationUnit = this.getCompilationUnit(url);
139         if (!compilationUnit)
140         {
141             if (FBTrace.DBG_COMPILATION_UNITS || FBTrace.DBG_ERRORS)
142                 FBTrace.sysout("tabContext.addSourceFile; ERROR Unknown URL: " + url,
143                     this.compilationUnits);
144             return;
145         }
146 
147         compilationUnit.sourceFile = sourceFile;
148 
149         compilationUnit.getSourceLines(-1, -1, function onLines(compilationUnit,
150             firstLineNumber, lastLineNumber, lines)
151         {
152             Firebug.connection.dispatch("onSourceLines", arguments);
153 
154             if (FBTrace.DBG_COMPILATION_UNITS)
155                 FBTrace.sysout("onSourceLines "+compilationUnit.getURL() + " " + lines.length +
156                     " lines", compilationUnit);
157         });
158     },
159 
160     removeSourceFile: function(sourceFile)
161     {
162         if (FBTrace.DBG_SOURCEFILES)
163             FBTrace.sysout("tabContext.removeSourceFile " + sourceFile.href + " in context " +
164                 sourceFile.context.getName());
165 
166         delete this.sourceFileMap[sourceFile.href];
167         delete sourceFile.context;
168 
169         // ?? Firebug.onSourceFileDestroyed(this, sourceFile);
170     },
171 
172     addTags: function(sourceFile)
173     {
174         if (sourceFile.outerScript)
175             this.sourceFileByTag[sourceFile.outerScript.tag] = sourceFile;
176 
177         for (var innerTag in sourceFile.innerScripts)
178             this.sourceFileByTag[innerTag] = sourceFile;
179     },
180 
181     getSourceFileByTag: function(tag)
182     {
183         return this.sourceFileByTag[tag];
184     },
185 
186     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
187 
188     // backward compat
189     get chrome()
190     {
191         return Firebug.chrome;
192     },
193 
194     destroy: function(state)
195     {
196         // All existing timeouts need to be cleared
197         if (this.timeouts)
198         {
199             for (var timeout in this.timeouts)
200                 clearTimeout(timeout);
201         }
202 
203         // Also all waiting intervals must be cleared.
204         if (this.intervals)
205         {
206             for (var timeout in this.intervals)
207                 clearInterval(timeout);
208         }
209 
210         if (this.throttleTimeout)
211             clearTimeout(this.throttleTimeout);
212 
213         // All existing DOM listeners need to be cleared. Note that context is destroyed
214         // when the top level window is unloaded. However, some listeners can be registered
215         // to iframes (documents), which can be already unloaded at this point.
216         // Removing listeners from such 'unloaded' documents (or window) can throw
217         // "TypeError: can't access dead object"
218         // We should avoid these exceptions (even if they are not representing memory leaks)
219         this.unregisterAllListeners();
220 
221         state.panelState = {};
222 
223         // Inherit panelStates that have not been restored yet
224         if (this.persistedState)
225         {
226             for (var panelName in this.persistedState.panelState)
227                 state.panelState[panelName] = this.persistedState.panelState[panelName];
228         }
229 
230         // Destroy all panels in this context.
231         for (var panelName in this.panelMap)
232         {
233             var panelType = Firebug.getPanelType(panelName);
234             this.destroyPanel(panelType, state);
235         }
236 
237         if (FBTrace.DBG_INITIALIZE)
238             FBTrace.sysout("tabContext.destroy " + this.getName() + " set state ", state);
239     },
240 
241     getPanel: function(panelName, noCreate)
242     {
243         // Get "global" panelType, registered using Firebug.registerPanel
244         var panelType = Firebug.getPanelType(panelName);
245 
246         // The panelType can be "local", available only within the context.
247         if (!panelType && this.panelTypeMap && this.panelTypeMap.hasOwnProperty(panelName))
248             panelType = this.panelTypeMap[panelName];
249 
250         if (!panelType)
251             return null;
252 
253         if (!panelType.prototype)
254         {
255             FBTrace.sysout("tabContext.getPanel no prototype "+panelType, panelType);
256             return;
257         }
258 
259         // Create instance of the panelType only if it's enabled.
260         var enabled = panelType.prototype.isEnabled ? panelType.prototype.isEnabled() : true;
261         if (enabled)
262             return this.getPanelByType(panelType, noCreate);
263 
264         return null;
265     },
266 
267     getPanelByType: function(panelType, noCreate)
268     {
269         if (!panelType || !this.panelMap)
270             return null;
271 
272         var panelName = panelType.prototype.name;
273         if ( this.panelMap.hasOwnProperty(panelName) )
274             return this.panelMap[panelName];
275         else if (!noCreate)
276             return this.createPanel(panelType);
277     },
278 
279     eachPanelInContext: function(callback)
280     {
281         for (var panelName in this.panelMap)
282         {
283             if (this.panelMap.hasOwnProperty(panelName))
284             {
285                 var panel = this.panelMap[panelName];
286                 var rc = callback(panel);
287                 if (rc)
288                     return rc;
289             }
290         }
291     },
292 
293     createPanel: function(panelType)
294     {
295         // Instantiate a panel object. This is why panels are defined by prototype inheritance
296         var panel = new panelType();
297         this.panelMap[panel.name] = panel;
298 
299         if (FBTrace.DBG_PANELS)
300             FBTrace.sysout("tabContext.createPanel; Panel created: " + panel.name, panel);
301 
302         Events.dispatch(Firebug.modules, "onCreatePanel", [this, panel, panelType]);
303 
304         // Initialize panel and associate with a document.
305         if (panel.parentPanel)
306         {
307             // then this new panel is a side panel
308             panel.mainPanel = this.panelMap[panel.parentPanel];
309             if (panel.mainPanel)
310             {
311                 // then our panel map is consistent
312                 // wire the side panel to get UI events from the main panel
313                 panel.mainPanel.addListener(panel);
314             }
315             else
316             {
317                 // then our panel map is broken, maybe by an extension failure.
318                 if (FBTrace.DBG_ERRORS)
319                     FBTrace.sysout("tabContext.createPanel panel.mainPanel missing " +
320                         panel.name + " from " + panel.parentPanel.name);
321             }
322         }
323 
324         var doc = this.chrome.getPanelDocument(panelType);
325         panel.initialize(this, doc);
326 
327         return panel;
328     },
329 
330     destroyPanel: function(panelType, state)
331     {
332         var panelName = panelType.prototype.name;
333         var panel = this.panelMap[panelName];
334         if (!panel)
335             return;
336 
337         // Create an object to persist state, re-using old one if it was never restored
338         var panelState = panelName in state.panelState ? state.panelState[panelName] : {};
339         state.panelState[panelName] = panelState;
340 
341         try
342         {
343             // Destroy the panel and allow it to persist extra info to the state object
344             var dontRemove = panel.destroy(panelState);
345             delete this.panelMap[panelName];
346 
347             if (dontRemove)
348                 return;
349         }
350         catch (exc)
351         {
352             if (FBTrace.DBG_ERRORS)
353                 FBTrace.sysout("tabContext.destroy FAILS (" + panelName + ") " + exc, exc);
354 
355             // the destroy failed, don't keep the bad state
356             delete state.panelState[panelName];
357         }
358 
359         // Remove the panel node from the DOM and so delete its content.
360         var panelNode = panel.panelNode;
361         if (panelNode && panelNode.parentNode)
362             panelNode.parentNode.removeChild(panelNode);
363     },
364 
365     removePanel: function(panelType, state)
366     {
367         var panelName = panelType.prototype.name;
368         if (!this.panelMap.hasOwnProperty(panelName))
369             return null;
370 
371         state.panelState = {};
372 
373         this.destroyPanel(panelType, state);
374     },
375 
376     // allows a panel from one context to be used in other contexts.
377     setPanel: function(panelName, panel)
378     {
379         if (panel)
380             this.panelMap[panelName] = panel;
381         else
382             delete this.panelMap[panelName];
383     },
384 
385     invalidatePanels: function()
386     {
387         if (!this.invalidPanels)
388             this.invalidPanels = {};
389 
390         for (var i = 0; i < arguments.length; ++i)
391         {
392             var panelName = arguments[i];
393             var panel = this.getPanel(panelName, true);
394             if (panel && !panel.noRefresh)
395                 this.invalidPanels[panelName] = 1;
396         }
397 
398         if (this.refreshTimeout)
399         {
400             this.clearTimeout(this.refreshTimeout);
401             delete this.refreshTimeout;
402         }
403 
404         this.refreshTimeout = this.setTimeout(Obj.bindFixed(function()
405         {
406             var invalids = [];
407 
408             for (var panelName in this.invalidPanels)
409             {
410                 var panel = this.getPanel(panelName, true);
411                 if (panel)
412                 {
413                     if (panel.visible && !panel.editing)
414                         panel.refresh();
415                     else
416                         panel.needsRefresh = true;
417 
418                     // If the panel is being edited, we'll keep trying to
419                     // refresh it until editing is done
420                     if (panel.editing)
421                         invalids.push(panelName);
422                 }
423             }
424 
425             delete this.invalidPanels;
426             delete this.refreshTimeout;
427 
428             // Keep looping until every tab is valid
429             if (invalids.length)
430                 this.invalidatePanels.apply(this, invalids);
431 
432         }, this), refreshDelay);
433     },
434 
435     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
436     // Timeouts and Intervals
437 
438     setTimeout: function(fn, delay)
439     {
440         if (setTimeout == this.setTimeout)
441             throw new Error("setTimeout recursion");
442 
443         // we're using a sandboxed setTimeout function
444         var timeout = setTimeout(fn, delay);
445 
446         if (!this.timeouts)
447             this.timeouts = {};
448 
449         this.timeouts[timeout] = 1;
450 
451         return timeout;
452     },
453 
454     clearTimeout: function(timeout)
455     {
456         // we're using a sandboxed clearTimeout function
457         clearTimeout(timeout);
458 
459         if (this.timeouts)
460             delete this.timeouts[timeout];
461     },
462 
463     setInterval: function(fn, delay)
464     {
465         // we're using a sandboxed setInterval function
466         var timeout = setInterval(fn, delay);
467 
468         if (!this.intervals)
469             this.intervals = {};
470 
471         this.intervals[timeout] = 1;
472 
473         return timeout;
474     },
475 
476     clearInterval: function(timeout)
477     {
478         // we're using a sandboxed clearInterval function
479         clearInterval(timeout);
480 
481         if (this.intervals)
482             delete this.intervals[timeout];
483     },
484 
485     delay: function(message, object)
486     {
487         this.throttle(message, object, null, true);
488     },
489 
490     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
491 
492     // queue the call |object.message(arg)| or just delay it if forceDelay
493     throttle: function(message, object, args, forceDelay)
494     {
495         if (!this.throttleInit)
496         {
497             this.throttleBuildup = 0;
498             this.throttleQueue = [];
499             this.throttleTimeout = 0;
500             this.lastMessageTime = 0;
501             this.throttleInit = true;
502         }
503 
504         if (!forceDelay)
505         {
506             if (!Firebug.throttleMessages)
507             {
508                 message.apply(object, args);
509                 return false;
510             }
511 
512             // Count how many messages have been logged during the throttle period
513             var logTime = new Date().getTime();
514             if (logTime - this.lastMessageTime < throttleTimeWindow)
515                 ++this.throttleBuildup;
516             else
517                 this.throttleBuildup = 0;
518 
519             this.lastMessageTime = logTime;
520 
521             // If the throttle limit has been passed, enqueue the message to be
522             // logged later on a timer, otherwise just execute it now
523             if (!this.throttleQueue.length && this.throttleBuildup <= throttleMessageLimit)
524             {
525                 message.apply(object, args);
526                 return false;
527             }
528         }
529 
530         this.throttleQueue.push(message, object, args);
531 
532         if (this.throttleTimeout)
533             this.clearTimeout(this.throttleTimeout);
534 
535         var self = this;
536         this.throttleTimeout =
537             this.setTimeout(function() { self.flushThrottleQueue(); }, throttleInterval);
538 
539         return true;
540     },
541 
542     flushThrottleQueue: function()
543     {
544         var queue = this.throttleQueue;
545 
546         if (!queue[0])
547             FBTrace.sysout("tabContext.flushThrottleQueue no queue[0]", queue);
548 
549         var max = throttleFlushCount * 3;
550         if (max > queue.length)
551             max = queue.length;
552 
553         for (var i = 0; i < max; i += 3)
554             queue[i].apply(queue[i+1], queue[i+2]);
555 
556         queue.splice(0, throttleFlushCount*3);
557 
558         if (queue.length)
559         {
560             var self = this;
561             this.throttleTimeout =
562                 this.setTimeout(function f() { self.flushThrottleQueue(); }, throttleInterval);
563         }
564         else
565         {
566             this.throttleTimeout = 0;
567         }
568     },
569 
570     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
571     // Event Listeners
572 
573     addEventListener: function(parent, eventId, listener, capturing)
574     {
575         if (!this.listeners)
576             this.listeners = [];
577 
578         for (var i=0; i<this.listeners.length; i++)
579         {
580             var l = this.listeners[i];
581             if (l.parent == parent && l.eventId == eventId && l.listener == listener &&
582                 l.capturing == capturing)
583             {
584                 // Listener already registered!
585                 return;
586             }
587         }
588 
589         parent.addEventListener(eventId, listener, capturing);
590 
591         this.listeners.push({
592             parent: parent,
593             eventId: eventId,
594             listener: listener,
595             capturing: capturing,
596         });
597     },
598 
599     removeEventListener: function(parent, eventId, listener, capturing)
600     {
601         parent.removeEventListener(eventId, listener, capturing);
602 
603         if (!this.listeners)
604             this.listeners = [];
605 
606         for (var i=0; i<this.listeners.length; i++)
607         {
608             var l = this.listeners[i];
609             if (l.parent == parent && l.eventId == eventId && l.listener == listener &&
610                 l.capturing == capturing)
611             {
612                 this.listeners.splice(i, 1);
613                 break;
614             }
615         }
616     },
617 
618     /**
619      * Executed by the framework when the context is about to be destroyed.
620      */
621     unregisterAllListeners: function()
622     {
623         if (!this.listeners)
624             return;
625 
626         for (var i=0; i<this.listeners.length; i++)
627         {
628             var l = this.listeners[i];
629 
630             try
631             {
632                 l.parent.removeEventListener(l.eventId, l.listener, l.capturing);
633             }
634             catch (e)
635             {
636                 if (FBTrace.DBG_ERRORS)
637                 {
638                     FBTrace.sysout("tabContext.unregisterAllListeners; (" + l.eventId +
639                         ") " + e, e);
640                 }
641             }
642         }
643 
644         this.listeners = null;
645     }
646 };
647 
648 // ********************************************************************************************* //
649 // Registration
650 
651 return Firebug.TabContext;
652 
653 // ********************************************************************************************* //
654 });
655