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