1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/xpcom",
  5     "firebug/lib/object",
  6     "firebug/lib/locale",
  7     "firebug/lib/domplate",
  8     "firebug/lib/dom",
  9     "firebug/lib/options",
 10     "firebug/lib/persist",
 11     "firebug/lib/string",
 12     "firebug/lib/http",
 13     "firebug/lib/css",
 14     "firebug/lib/events",
 15     "firebug/cookies/baseObserver",
 16     "firebug/chrome/tabWatcher",
 17     "firebug/cookies/cookieReps",
 18     "firebug/cookies/cookieUtils",
 19     "firebug/cookies/cookie",
 20     "firebug/cookies/breakpoints",
 21     "firebug/cookies/cookieEvents",
 22     "firebug/lib/array",
 23 ],
 24 function(Xpcom, Obj, Locale, Domplate, Dom, Options, Persist, Str, Http, Css, Events,
 25     BaseObserver, TabWatcher, CookieReps, CookieUtils, Cookie, Breakpoints, CookieEvents, Arr) {
 26 
 27 // ********************************************************************************************* //
 28 // Constants
 29 
 30 const Cc = Components.classes;
 31 const Ci = Components.interfaces;
 32 
 33 const filterByPath = "cookies.filterByPath";
 34 
 35 const panelName = "cookies";
 36 
 37 const idnService = Xpcom.CCSV("@mozilla.org/network/idn-service;1", "nsIIDNService");
 38 
 39 // ********************************************************************************************* //
 40 // Cookie observer
 41 
 42 /**
 43  * @class This class represents an observer (nsIObserver) for cookie-changed
 44  * and cookie-rejected events. These events are dispatche by Firefox
 45  * see https://developer.mozilla.org/En/Observer_Notifications.
 46  */
 47 var CookieObserver = Obj.extend(BaseObserver,
 48 /** @lends CookieObserver */
 49 {
 50     // nsIObserver
 51     observe: function(aSubject, aTopic, aData)
 52     {
 53         try
 54         {
 55             if (!Firebug.CookieModule.isAlwaysEnabled())
 56                 return;
 57 
 58             // See: https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsICookieService
 59             // For all possible values.
 60             if (aTopic == "cookie-changed")
 61             {
 62                 var cookies = []
 63                 if (aData == "batch-deleted")
 64                 {
 65                     // In this case the subject is nsIArray.
 66                     var enumerator = aSubject.QueryInterface(Ci.nsIArray).enumerate();
 67                     while (enumerator.hasMoreElements())
 68                         cookies.push(enumerator.getNext().QueryInterface(Ci.nsICookie2));
 69 
 70                     // The event will be further distributed as standard "delete" event.
 71                     aData = "deleted";
 72                 }
 73                 else
 74                 {
 75                     aSubject = aSubject ? aSubject.QueryInterface(Ci.nsICookie2) : null;
 76                     cookies.push(aSubject);
 77                 }
 78 
 79                 for (var i=0; i<cookies.length; i++)
 80                     this.iterateContexts(this.onCookieChanged, cookies[i], aData);
 81             }
 82             else if (aTopic == "cookie-rejected")
 83             {
 84                 aSubject = aSubject.QueryInterface(Ci.nsIURI);
 85                 this.iterateContexts(this.onCookieRejected, aSubject, aData);
 86             }
 87         }
 88         catch (err)
 89         {
 90             if (FBTrace.DBG_ERRORS)
 91             {
 92                 FBTrace.sysout("cookies.CookieObserver.observe; ERROR " +
 93                     aTopic + ", " + aData, err);
 94                 FBTrace.sysout("cookies.CookieObserver.observe; subject ", aSubject);
 95             }
 96         }
 97     },
 98 
 99     iterateContexts: function(fn)
100     {
101         var oThis = this;
102         var args = Arr.cloneArray(arguments);
103         TabWatcher.iterateContexts(function(context)
104         {
105             args[0] = context;
106             fn.apply(oThis, args);
107         });
108     },
109 
110     /**
111      * @param {String} activeUri This object represents currently active host. Notice that there
112      *      can be more active hosts (activeHosts map) on one page in case 
113      *      of embedded iframes or/and previous redirects.
114      *      Properties:
115      *      host: www.example.com
116      *      path: /subdir/
117      *
118      * @param {String} host: Represents the host of a cookie for which
119      *      we are checking if it should be displayed for the active URI.
120      * 
121      * @param {String} path: Represents the path of a cookie for which
122      *      we are checking if it should be displayed for the active URI.
123      * 
124      * @returns {Boolean} If the method returns true the host/path belongs
125      *      to the activeUri.
126      */
127     isHostFromURI: function(activeUri, host, cookiePath)
128     {
129         var pathFilter = Options.get(filterByPath);
130 
131         // Compute the default path of the cookie according to the algorithm
132         // defined in RFC 6265 section 5.1.4
133         //
134         // Steps 2 and 3 (output "/" in case the cookie path is empty, its first
135         // character is "/" or there is no more than one "/")
136         if (cookiePath.length == 0 || cookiePath.charAt(0) != "/" ||
137             cookiePath.lastIndexOf("/") == 0)
138         {
139             cookiePath = "/";
140         }
141         else
142         {
143             // Step 4 (remove slash at the end of the active path according to)
144             cookiePath = cookiePath.substr(0, cookiePath.lastIndexOf("/"));
145         }
146 
147         // If the path filter is on, only cookies that match given path
148         // according to RFC 6265 section 5.1.4 will be displayed.
149         var requestPath = activeUri.path;
150         if (pathFilter && (cookiePath != requestPath && !(Str.hasPrefix(requestPath, cookiePath) &&
151             (Str.endsWith(cookiePath, "/") || requestPath.substr(cookiePath.length, 1) == "/"))))
152         {
153             return false;
154         }
155 
156         // The cookie must belong to given URI from this context,
157         // otherwise it won't be displayed in this tab.
158         var uri = CookieUtils.makeStrippedHost(activeUri.host);
159         if (uri == host)
160             return true;
161 
162         if (uri.length < host.length)
163             return false;
164 
165         var h = "." + host;
166         var u = "." + uri;
167         if (u.substr(u.length - h.length) == h)
168             return true;
169 
170         return false;
171     },
172 
173     isHostFromContext: function(context, host, path)
174     {
175         var location;
176         try
177         {
178             host = idnService.convertACEtoUTF8(host);
179         }
180         catch(exc)
181         {
182             if (FBTrace.DBG_ERRORS || FBTrace.DBG_COOKIES)
183                 FBTrace.sysout("Host could not be converted to UTF-8", exc);
184         }
185 
186         // Invalid in Chromebug.
187         try
188         {
189             location = context.window.location;
190             if (!location || !location.protocol)
191                 return;
192         }
193         catch (err)
194         {
195             return false;
196         }
197 
198         if (location.protocol.indexOf("http") != 0)
199             return false;
200 
201         var rawHost = CookieUtils.makeStrippedHost(host);
202 
203         // Test the current main URI first.
204         // The location isn't nsIURI, so make a fake object (aka nsIURI). 
205         var fakeUri = {host: location.host, path: location.pathname};
206         if (this.isHostFromURI(fakeUri, rawHost, path))
207             return true;
208 
209         // xxxHonza
210         // If the context.cookies is not initialized, it's bad. It means that
211         // neither temporary context no real context has been initialized
212         // One reason is that Sript model issues panel.show in onModuleActivate
213         // which consequently requests a file (double load prblem), which
214         // consequently rises this cookie event.
215         if (!context.cookies)
216             return false;
217 
218         // Now test if the cookie doesn't belong to some of the
219         // activeHosts (redirects, frames).    
220         var activeHosts = context.cookies.activeHosts;
221         for (var activeHost in activeHosts)
222         {
223             if (this.isHostFromURI(activeHosts[activeHost], rawHost, path))
224                 return true;
225         }
226 
227         return false;
228     },
229 
230     isCookieFromContext: function(context, cookie)
231     {
232         return this.isHostFromContext(context, cookie.host, cookie.path);
233     },
234 
235     onCookieChanged: function(context, cookie, action)
236     {
237         // If the action == "cleared" the cookie is *not* set. This action is triggered
238         // when all cookies are removed (cookieManager.removeAll)
239         // In such a case let's displaye the event in all contexts.
240         if (cookie && !this.isCookieFromContext(context, cookie))
241             return;
242 
243         if (FBTrace.DBG_COOKIES)
244             FBTrace.sysout("cookies.onCookieChanged: '" + (cookie ? cookie.name : "null") +
245                 "', " + action);
246 
247         if (action != "cleared")
248         {
249             // If log into the Console tab is on, create "deleted", "added" and "changed" events.
250             if (logEvents())
251                 this.logEvent(new CookieEvents.CookieChangedEvent(context, CookieUtils.makeCookieObject(cookie),
252                     action), context, "cookie");
253 
254             // Break on cookie if "Break On" is activated or if a cookie breakpoint exist.
255             Breakpoints.breakOnCookie(context, cookie, action);
256         }
257 
258         switch(action)
259         {
260           case "deleted":
261             this.onRemoveCookie(context, cookie);
262             break;
263           case "added":
264             this.onAddCookie(context, cookie);
265             break;
266           case "changed":
267             this.onUpdateCookie(context, cookie);
268             break;
269           case "cleared":
270             this.onClear(context);
271             return;
272           case "reload":
273             context.invalidatePanels(panelName);
274             return;
275         }
276     },
277 
278     onClear: function(context)
279     {
280         var panel = context.getPanel(panelName);
281         panel.clear();
282 
283         if (logEvents())
284             this.logEvent(new CookieEvents.CookieClearedEvent(), context, "cookiesCleared");
285     },
286 
287     onCookieRejected: function(context, uri)
288     {
289         var path = uri.path.substr(0, (uri.path.lastIndexOf("/") || 1));
290         if (!this.isHostFromContext(context, uri.host, path))
291             return;
292 
293         if (FBTrace.DBG_COOKIES)
294             FBTrace.sysout("cookies.onCookieRejected: " + uri.spec);
295 
296         // Mark host and all its cookies as rejected.
297         // xxxHonza there was an exception "context.cookies is undefined".
298         var activeHost = context.cookies.activeHosts[uri.host];
299         if (activeHost)
300             activeHost.rejected = true;
301 
302         var receivedCookies = activeHost ? activeHost.receivedCookies : null;
303         for (var i=0; receivedCookies && i<receivedCookies.length; i++)
304             receivedCookies[i].cookie.rejected = true;
305 
306         // Refresh the panel asynchronously.
307         context.invalidatePanels(panelName);
308 
309         // Bail out if events are not logged into the Console.
310         if (!logEvents())
311             return;
312 
313         // The "cookies-rejected" event is sent even if no cookies
314         // from the blocked site have been actually received.
315         // So, the receivedCookies array can be null.
316         // Don't display anything in the console in that case,
317         // there could be a lot of "Cookie Rejected" events.
318         // There would be actually one for each embedded request.
319         if (!receivedCookies)
320             return;
321 
322         // Create group log for list of rejected cookies.
323         var groupRow = Firebug.Console.openGroup(
324             [new CookieEvents.CookieRejectedEvent(context, uri)], 
325             context, "cookiesRejected", null, true, null, true);
326 
327         // The console can be disabled (since FB 1.2).
328         if (!groupRow)
329             return;
330 
331         // It's closed by default.
332         Css.removeClass(groupRow, "opened");
333         Firebug.Console.closeGroup(context, true);
334 
335         // Create embedded table.
336         CookieReps.CookieTable.render(receivedCookies, groupRow.lastChild);
337     },
338 
339     onAddCookie: function(context, cookie)
340     {
341         var panel = context.getPanel(panelName, true);
342         var repCookie = panel ? panel.findRepObject(cookie) : null;
343         if (repCookie)
344         {
345             this.onUpdateCookie(context, cookie);
346             return;
347         }
348 
349         if (!panel || !panel.table)
350             return;
351 
352         var repCookie = panel ? panel.findRepObject(cookie) : null;
353 
354         cookie = new Cookie(CookieUtils.makeCookieObject(cookie));
355 
356         var tbody = panel.table.lastChild;
357         var parent = tbody.lastChild ? tbody.lastChild : tbody;
358         var row = CookieReps.CookieRow.cookieTag.insertRows({cookies: [cookie]}, parent)[0];
359 
360         cookie.row = row;
361         row.repObject = cookie;
362 
363         //xxxHonza the new cookie should respect current sorting.
364     },
365 
366     onUpdateCookie: function(context, cookie)
367     {
368         var panel = context.getPanel(panelName, true);
369 
370         // The table doesn't have to be initialized yet.
371         if (!panel || !panel.table)
372             return;
373 
374         var repCookie = panel ? panel.findRepObject(cookie) : null;
375         if (!repCookie)
376         {
377             this.onAddCookie(context, cookie);
378             return;
379         }
380 
381         repCookie.cookie = CookieUtils.makeCookieObject(cookie);
382         repCookie.rawHost = CookieUtils.makeStrippedHost(cookie.host);
383 
384         // These are helpers so, the XML and JSON cookies don't have to be parsed
385         // again and again. But we need to reset them if the value is changed.
386         repCookie.json = null;
387         repCookie.xml = null;
388 
389         if (FBTrace.DBG_COOKIES)
390             FBTrace.sysout("cookies.onUpdateCookie: " + cookie.name, repCookie);
391 
392         var row = repCookie.row;
393         var rowTemplate = CookieReps.CookieRow;
394 
395         if (Css.hasClass(row, "opened"))
396         {
397             var cookieInfoBody = Dom.getElementByClass(row.nextSibling, "cookieInfoBody");
398 
399             // Invalidate content of all tabs.
400             cookieInfoBody.valuePresented = false;
401             cookieInfoBody.rawValuePresented = false;
402             cookieInfoBody.xmlPresented = false;
403             cookieInfoBody.jsonPresented = false;
404 
405             // Update tabs visibility and content of the selected tab.
406             rowTemplate.updateTabs(cookieInfoBody, repCookie, context);
407             rowTemplate.updateInfo(cookieInfoBody, repCookie, context);
408         }
409 
410         rowTemplate.updateRow(repCookie, context);
411     },
412 
413     onRemoveCookie: function(context, cookie)
414     {
415         var panel = context.getPanel(panelName, true);
416         var repCookie = panel ? panel.findRepObject(cookie) : null;
417         if (!repCookie)
418             return;
419 
420         // Remove cookie from UI.
421         var row = repCookie.row;
422         var parent = repCookie.row.parentNode;
423 
424         if (Css.hasClass(repCookie.row, "opened"))
425             parent.removeChild(row.nextSibling);
426 
427         if (!parent)
428             return;
429 
430         parent.removeChild(repCookie.row);
431     },
432 
433     logEvent: function(eventObject, context, className)
434     {
435         // xxxHonza: if the cookie is changed befor initContext, the log in
436         // console is lost.
437         Firebug.Console.log(eventObject, context, className, null, true);
438     }
439 });
440 
441 // ********************************************************************************************* //
442 // Helpers
443 
444 function logEvents()
445 {
446     return Options.get("cookies.logEvents");
447 }
448 
449 // ********************************************************************************************* //
450 
451 return CookieObserver;
452 
453 // ********************************************************************************************* //
454 });
455 
456