Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
michael@0 | 6 | |
michael@0 | 7 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 8 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 9 | Cu.import("resource://gre/modules/Troubleshoot.jsm"); |
michael@0 | 10 | Cu.import("resource://gre/modules/ResetProfile.jsm"); |
michael@0 | 11 | |
michael@0 | 12 | XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
michael@0 | 13 | "resource://gre/modules/PluralForm.jsm"); |
michael@0 | 14 | |
michael@0 | 15 | window.addEventListener("load", function onload(event) { |
michael@0 | 16 | try { |
michael@0 | 17 | window.removeEventListener("load", onload, false); |
michael@0 | 18 | Troubleshoot.snapshot(function (snapshot) { |
michael@0 | 19 | for (let prop in snapshotFormatters) |
michael@0 | 20 | snapshotFormatters[prop](snapshot[prop]); |
michael@0 | 21 | }); |
michael@0 | 22 | populateResetBox(); |
michael@0 | 23 | setupEventListeners(); |
michael@0 | 24 | } catch (e) { |
michael@0 | 25 | Cu.reportError("stack of load error for about:support: " + e + ": " + e.stack); |
michael@0 | 26 | } |
michael@0 | 27 | }, false); |
michael@0 | 28 | |
michael@0 | 29 | // Each property in this object corresponds to a property in Troubleshoot.jsm's |
michael@0 | 30 | // snapshot data. Each function is passed its property's corresponding data, |
michael@0 | 31 | // and it's the function's job to update the page with it. |
michael@0 | 32 | let snapshotFormatters = { |
michael@0 | 33 | |
michael@0 | 34 | application: function application(data) { |
michael@0 | 35 | $("application-box").textContent = data.name; |
michael@0 | 36 | $("useragent-box").textContent = data.userAgent; |
michael@0 | 37 | $("supportLink").href = data.supportURL; |
michael@0 | 38 | let version = data.version; |
michael@0 | 39 | if (data.vendor) |
michael@0 | 40 | version += " (" + data.vendor + ")"; |
michael@0 | 41 | $("version-box").textContent = version; |
michael@0 | 42 | }, |
michael@0 | 43 | |
michael@0 | 44 | #ifdef MOZ_CRASHREPORTER |
michael@0 | 45 | crashes: function crashes(data) { |
michael@0 | 46 | let strings = stringBundle(); |
michael@0 | 47 | let daysRange = Troubleshoot.kMaxCrashAge / (24 * 60 * 60 * 1000); |
michael@0 | 48 | $("crashes-title").textContent = |
michael@0 | 49 | PluralForm.get(daysRange, strings.GetStringFromName("crashesTitle")) |
michael@0 | 50 | .replace("#1", daysRange); |
michael@0 | 51 | let reportURL; |
michael@0 | 52 | try { |
michael@0 | 53 | reportURL = Services.prefs.getCharPref("breakpad.reportURL"); |
michael@0 | 54 | // Ignore any non http/https urls |
michael@0 | 55 | if (!/^https?:/i.test(reportURL)) |
michael@0 | 56 | reportURL = null; |
michael@0 | 57 | } |
michael@0 | 58 | catch (e) { } |
michael@0 | 59 | if (!reportURL) { |
michael@0 | 60 | $("crashes-noConfig").style.display = "block"; |
michael@0 | 61 | $("crashes-noConfig").classList.remove("no-copy"); |
michael@0 | 62 | return; |
michael@0 | 63 | } |
michael@0 | 64 | else { |
michael@0 | 65 | $("crashes-allReports").style.display = "block"; |
michael@0 | 66 | $("crashes-allReports").classList.remove("no-copy"); |
michael@0 | 67 | } |
michael@0 | 68 | |
michael@0 | 69 | if (data.pending > 0) { |
michael@0 | 70 | $("crashes-allReportsWithPending").textContent = |
michael@0 | 71 | PluralForm.get(data.pending, strings.GetStringFromName("pendingReports")) |
michael@0 | 72 | .replace("#1", data.pending); |
michael@0 | 73 | } |
michael@0 | 74 | |
michael@0 | 75 | let dateNow = new Date(); |
michael@0 | 76 | $.append($("crashes-tbody"), data.submitted.map(function (crash) { |
michael@0 | 77 | let date = new Date(crash.date); |
michael@0 | 78 | let timePassed = dateNow - date; |
michael@0 | 79 | let formattedDate; |
michael@0 | 80 | if (timePassed >= 24 * 60 * 60 * 1000) |
michael@0 | 81 | { |
michael@0 | 82 | let daysPassed = Math.round(timePassed / (24 * 60 * 60 * 1000)); |
michael@0 | 83 | let daysPassedString = strings.GetStringFromName("crashesTimeDays"); |
michael@0 | 84 | formattedDate = PluralForm.get(daysPassed, daysPassedString) |
michael@0 | 85 | .replace("#1", daysPassed); |
michael@0 | 86 | } |
michael@0 | 87 | else if (timePassed >= 60 * 60 * 1000) |
michael@0 | 88 | { |
michael@0 | 89 | let hoursPassed = Math.round(timePassed / (60 * 60 * 1000)); |
michael@0 | 90 | let hoursPassedString = strings.GetStringFromName("crashesTimeHours"); |
michael@0 | 91 | formattedDate = PluralForm.get(hoursPassed, hoursPassedString) |
michael@0 | 92 | .replace("#1", hoursPassed); |
michael@0 | 93 | } |
michael@0 | 94 | else |
michael@0 | 95 | { |
michael@0 | 96 | let minutesPassed = Math.max(Math.round(timePassed / (60 * 1000)), 1); |
michael@0 | 97 | let minutesPassedString = strings.GetStringFromName("crashesTimeMinutes"); |
michael@0 | 98 | formattedDate = PluralForm.get(minutesPassed, minutesPassedString) |
michael@0 | 99 | .replace("#1", minutesPassed); |
michael@0 | 100 | } |
michael@0 | 101 | return $.new("tr", [ |
michael@0 | 102 | $.new("td", [ |
michael@0 | 103 | $.new("a", crash.id, null, {href : reportURL + crash.id}) |
michael@0 | 104 | ]), |
michael@0 | 105 | $.new("td", formattedDate) |
michael@0 | 106 | ]); |
michael@0 | 107 | })); |
michael@0 | 108 | }, |
michael@0 | 109 | #endif |
michael@0 | 110 | |
michael@0 | 111 | extensions: function extensions(data) { |
michael@0 | 112 | $.append($("extensions-tbody"), data.map(function (extension) { |
michael@0 | 113 | return $.new("tr", [ |
michael@0 | 114 | $.new("td", extension.name), |
michael@0 | 115 | $.new("td", extension.version), |
michael@0 | 116 | $.new("td", extension.isActive), |
michael@0 | 117 | $.new("td", extension.id), |
michael@0 | 118 | ]); |
michael@0 | 119 | })); |
michael@0 | 120 | }, |
michael@0 | 121 | |
michael@0 | 122 | experiments: function experiments(data) { |
michael@0 | 123 | $.append($("experiments-tbody"), data.map(function (experiment) { |
michael@0 | 124 | return $.new("tr", [ |
michael@0 | 125 | $.new("td", experiment.name), |
michael@0 | 126 | $.new("td", experiment.id), |
michael@0 | 127 | $.new("td", experiment.description), |
michael@0 | 128 | $.new("td", experiment.active), |
michael@0 | 129 | $.new("td", experiment.endDate), |
michael@0 | 130 | $.new("td", [ |
michael@0 | 131 | $.new("a", experiment.detailURL, null, {href : experiment.detailURL,}) |
michael@0 | 132 | ]), |
michael@0 | 133 | ]); |
michael@0 | 134 | })); |
michael@0 | 135 | }, |
michael@0 | 136 | |
michael@0 | 137 | modifiedPreferences: function modifiedPreferences(data) { |
michael@0 | 138 | $.append($("prefs-tbody"), sortedArrayFromObject(data).map( |
michael@0 | 139 | function ([name, value]) { |
michael@0 | 140 | return $.new("tr", [ |
michael@0 | 141 | $.new("td", name, "pref-name"), |
michael@0 | 142 | // Very long preference values can cause users problems when they |
michael@0 | 143 | // copy and paste them into some text editors. Long values generally |
michael@0 | 144 | // aren't useful anyway, so truncate them to a reasonable length. |
michael@0 | 145 | $.new("td", String(value).substr(0, 120), "pref-value"), |
michael@0 | 146 | ]); |
michael@0 | 147 | } |
michael@0 | 148 | )); |
michael@0 | 149 | }, |
michael@0 | 150 | |
michael@0 | 151 | graphics: function graphics(data) { |
michael@0 | 152 | // graphics-info-properties tbody |
michael@0 | 153 | if ("info" in data) { |
michael@0 | 154 | let trs = sortedArrayFromObject(data.info).map(function ([prop, val]) { |
michael@0 | 155 | return $.new("tr", [ |
michael@0 | 156 | $.new("th", prop, "column"), |
michael@0 | 157 | $.new("td", String(val)), |
michael@0 | 158 | ]); |
michael@0 | 159 | }); |
michael@0 | 160 | $.append($("graphics-info-properties"), trs); |
michael@0 | 161 | delete data.info; |
michael@0 | 162 | } |
michael@0 | 163 | |
michael@0 | 164 | // graphics-failures-tbody tbody |
michael@0 | 165 | if ("failures" in data) { |
michael@0 | 166 | $.append($("graphics-failures-tbody"), data.failures.map(function (val) { |
michael@0 | 167 | return $.new("tr", [$.new("td", val)]); |
michael@0 | 168 | })); |
michael@0 | 169 | delete data.failures; |
michael@0 | 170 | } |
michael@0 | 171 | |
michael@0 | 172 | // graphics-tbody tbody |
michael@0 | 173 | |
michael@0 | 174 | function localizedMsg(msgArray) { |
michael@0 | 175 | let nameOrMsg = msgArray.shift(); |
michael@0 | 176 | if (msgArray.length) { |
michael@0 | 177 | // formatStringFromName logs an NS_ASSERTION failure otherwise that says |
michael@0 | 178 | // "use GetStringFromName". Lame. |
michael@0 | 179 | try { |
michael@0 | 180 | return strings.formatStringFromName(nameOrMsg, msgArray, |
michael@0 | 181 | msgArray.length); |
michael@0 | 182 | } |
michael@0 | 183 | catch (err) { |
michael@0 | 184 | // Throws if nameOrMsg is not a name in the bundle. This shouldn't |
michael@0 | 185 | // actually happen though, since msgArray.length > 1 => nameOrMsg is a |
michael@0 | 186 | // name in the bundle, not a message, and the remaining msgArray |
michael@0 | 187 | // elements are parameters. |
michael@0 | 188 | return nameOrMsg; |
michael@0 | 189 | } |
michael@0 | 190 | } |
michael@0 | 191 | try { |
michael@0 | 192 | return strings.GetStringFromName(nameOrMsg); |
michael@0 | 193 | } |
michael@0 | 194 | catch (err) { |
michael@0 | 195 | // Throws if nameOrMsg is not a name in the bundle. |
michael@0 | 196 | } |
michael@0 | 197 | return nameOrMsg; |
michael@0 | 198 | } |
michael@0 | 199 | |
michael@0 | 200 | let out = Object.create(data); |
michael@0 | 201 | let strings = stringBundle(); |
michael@0 | 202 | |
michael@0 | 203 | out.acceleratedWindows = |
michael@0 | 204 | data.numAcceleratedWindows + "/" + data.numTotalWindows; |
michael@0 | 205 | if (data.windowLayerManagerType) |
michael@0 | 206 | out.acceleratedWindows += " " + data.windowLayerManagerType; |
michael@0 | 207 | if (data.windowLayerManagerRemote) |
michael@0 | 208 | out.acceleratedWindows += " (OMTC)"; |
michael@0 | 209 | if (data.numAcceleratedWindowsMessage) |
michael@0 | 210 | out.acceleratedWindows += |
michael@0 | 211 | " " + localizedMsg(data.numAcceleratedWindowsMessage); |
michael@0 | 212 | delete data.numAcceleratedWindows; |
michael@0 | 213 | delete data.numTotalWindows; |
michael@0 | 214 | delete data.windowLayerManagerType; |
michael@0 | 215 | delete data.numAcceleratedWindowsMessage; |
michael@0 | 216 | |
michael@0 | 217 | if ("direct2DEnabledMessage" in data) { |
michael@0 | 218 | out.direct2DEnabled = localizedMsg(data.direct2DEnabledMessage); |
michael@0 | 219 | delete data.direct2DEnabledMessage; |
michael@0 | 220 | delete data.direct2DEnabled; |
michael@0 | 221 | } |
michael@0 | 222 | |
michael@0 | 223 | if ("directWriteEnabled" in data) { |
michael@0 | 224 | out.directWriteEnabled = data.directWriteEnabled; |
michael@0 | 225 | if ("directWriteVersion" in data) |
michael@0 | 226 | out.directWriteEnabled += " (" + data.directWriteVersion + ")"; |
michael@0 | 227 | delete data.directWriteEnabled; |
michael@0 | 228 | delete data.directWriteVersion; |
michael@0 | 229 | } |
michael@0 | 230 | |
michael@0 | 231 | if ("webglRendererMessage" in data) { |
michael@0 | 232 | out.webglRenderer = localizedMsg(data.webglRendererMessage); |
michael@0 | 233 | delete data.webglRendererMessage; |
michael@0 | 234 | delete data.webglRenderer; |
michael@0 | 235 | } |
michael@0 | 236 | |
michael@0 | 237 | let localizedOut = {}; |
michael@0 | 238 | for (let prop in out) { |
michael@0 | 239 | let val = out[prop]; |
michael@0 | 240 | if (typeof(val) == "string" && !val) |
michael@0 | 241 | // Ignore properties that are empty strings. |
michael@0 | 242 | continue; |
michael@0 | 243 | try { |
michael@0 | 244 | var localizedName = strings.GetStringFromName(prop); |
michael@0 | 245 | } |
michael@0 | 246 | catch (err) { |
michael@0 | 247 | // This shouldn't happen, but if there's a reported graphics property |
michael@0 | 248 | // that isn't in the string bundle, don't let it break the page. |
michael@0 | 249 | localizedName = prop; |
michael@0 | 250 | } |
michael@0 | 251 | localizedOut[localizedName] = val; |
michael@0 | 252 | } |
michael@0 | 253 | let trs = sortedArrayFromObject(localizedOut).map(function ([prop, val]) { |
michael@0 | 254 | return $.new("tr", [ |
michael@0 | 255 | $.new("th", prop, "column"), |
michael@0 | 256 | $.new("td", val), |
michael@0 | 257 | ]); |
michael@0 | 258 | }); |
michael@0 | 259 | $.append($("graphics-tbody"), trs); |
michael@0 | 260 | }, |
michael@0 | 261 | |
michael@0 | 262 | javaScript: function javaScript(data) { |
michael@0 | 263 | $("javascript-incremental-gc").textContent = data.incrementalGCEnabled; |
michael@0 | 264 | }, |
michael@0 | 265 | |
michael@0 | 266 | accessibility: function accessibility(data) { |
michael@0 | 267 | $("a11y-activated").textContent = data.isActive; |
michael@0 | 268 | $("a11y-force-disabled").textContent = data.forceDisabled || 0; |
michael@0 | 269 | }, |
michael@0 | 270 | |
michael@0 | 271 | libraryVersions: function libraryVersions(data) { |
michael@0 | 272 | let strings = stringBundle(); |
michael@0 | 273 | let trs = [ |
michael@0 | 274 | $.new("tr", [ |
michael@0 | 275 | $.new("th", ""), |
michael@0 | 276 | $.new("th", strings.GetStringFromName("minLibVersions")), |
michael@0 | 277 | $.new("th", strings.GetStringFromName("loadedLibVersions")), |
michael@0 | 278 | ]) |
michael@0 | 279 | ]; |
michael@0 | 280 | sortedArrayFromObject(data).forEach( |
michael@0 | 281 | function ([name, val]) { |
michael@0 | 282 | trs.push($.new("tr", [ |
michael@0 | 283 | $.new("td", name), |
michael@0 | 284 | $.new("td", val.minVersion), |
michael@0 | 285 | $.new("td", val.version), |
michael@0 | 286 | ])); |
michael@0 | 287 | } |
michael@0 | 288 | ); |
michael@0 | 289 | $.append($("libversions-tbody"), trs); |
michael@0 | 290 | }, |
michael@0 | 291 | |
michael@0 | 292 | userJS: function userJS(data) { |
michael@0 | 293 | if (!data.exists) |
michael@0 | 294 | return; |
michael@0 | 295 | let userJSFile = Services.dirsvc.get("PrefD", Ci.nsIFile); |
michael@0 | 296 | userJSFile.append("user.js"); |
michael@0 | 297 | $("prefs-user-js-link").href = Services.io.newFileURI(userJSFile).spec; |
michael@0 | 298 | $("prefs-user-js-section").style.display = ""; |
michael@0 | 299 | // Clear the no-copy class |
michael@0 | 300 | $("prefs-user-js-section").className = ""; |
michael@0 | 301 | }, |
michael@0 | 302 | }; |
michael@0 | 303 | |
michael@0 | 304 | let $ = document.getElementById.bind(document); |
michael@0 | 305 | |
michael@0 | 306 | $.new = function $_new(tag, textContentOrChildren, className, attributes) { |
michael@0 | 307 | let elt = document.createElement(tag); |
michael@0 | 308 | if (className) |
michael@0 | 309 | elt.className = className; |
michael@0 | 310 | if (attributes) { |
michael@0 | 311 | for (let attrName in attributes) |
michael@0 | 312 | elt.setAttribute(attrName, attributes[attrName]); |
michael@0 | 313 | } |
michael@0 | 314 | if (Array.isArray(textContentOrChildren)) |
michael@0 | 315 | this.append(elt, textContentOrChildren); |
michael@0 | 316 | else |
michael@0 | 317 | elt.textContent = String(textContentOrChildren); |
michael@0 | 318 | return elt; |
michael@0 | 319 | }; |
michael@0 | 320 | |
michael@0 | 321 | $.append = function $_append(parent, children) { |
michael@0 | 322 | children.forEach(function (c) parent.appendChild(c)); |
michael@0 | 323 | }; |
michael@0 | 324 | |
michael@0 | 325 | function stringBundle() { |
michael@0 | 326 | return Services.strings.createBundle( |
michael@0 | 327 | "chrome://global/locale/aboutSupport.properties"); |
michael@0 | 328 | } |
michael@0 | 329 | |
michael@0 | 330 | function sortedArrayFromObject(obj) { |
michael@0 | 331 | let tuples = []; |
michael@0 | 332 | for (let prop in obj) |
michael@0 | 333 | tuples.push([prop, obj[prop]]); |
michael@0 | 334 | tuples.sort(function ([prop1, v1], [prop2, v2]) prop1.localeCompare(prop2)); |
michael@0 | 335 | return tuples; |
michael@0 | 336 | } |
michael@0 | 337 | |
michael@0 | 338 | function copyRawDataToClipboard(button) { |
michael@0 | 339 | if (button) |
michael@0 | 340 | button.disabled = true; |
michael@0 | 341 | try { |
michael@0 | 342 | Troubleshoot.snapshot(function (snapshot) { |
michael@0 | 343 | if (button) |
michael@0 | 344 | button.disabled = false; |
michael@0 | 345 | let str = Cc["@mozilla.org/supports-string;1"]. |
michael@0 | 346 | createInstance(Ci.nsISupportsString); |
michael@0 | 347 | str.data = JSON.stringify(snapshot, undefined, 2); |
michael@0 | 348 | let transferable = Cc["@mozilla.org/widget/transferable;1"]. |
michael@0 | 349 | createInstance(Ci.nsITransferable); |
michael@0 | 350 | transferable.init(getLoadContext()); |
michael@0 | 351 | transferable.addDataFlavor("text/unicode"); |
michael@0 | 352 | transferable.setTransferData("text/unicode", str, str.data.length * 2); |
michael@0 | 353 | Cc["@mozilla.org/widget/clipboard;1"]. |
michael@0 | 354 | getService(Ci.nsIClipboard). |
michael@0 | 355 | setData(transferable, null, Ci.nsIClipboard.kGlobalClipboard); |
michael@0 | 356 | #ifdef ANDROID |
michael@0 | 357 | // Present a toast notification. |
michael@0 | 358 | let message = { |
michael@0 | 359 | type: "Toast:Show", |
michael@0 | 360 | message: stringBundle().GetStringFromName("rawDataCopied"), |
michael@0 | 361 | duration: "short" |
michael@0 | 362 | }; |
michael@0 | 363 | Services.androidBridge.handleGeckoMessage(message); |
michael@0 | 364 | #endif |
michael@0 | 365 | }); |
michael@0 | 366 | } |
michael@0 | 367 | catch (err) { |
michael@0 | 368 | if (button) |
michael@0 | 369 | button.disabled = false; |
michael@0 | 370 | throw err; |
michael@0 | 371 | } |
michael@0 | 372 | } |
michael@0 | 373 | |
michael@0 | 374 | function getLoadContext() { |
michael@0 | 375 | return window.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 376 | .getInterface(Ci.nsIWebNavigation) |
michael@0 | 377 | .QueryInterface(Ci.nsILoadContext); |
michael@0 | 378 | } |
michael@0 | 379 | |
michael@0 | 380 | function copyContentsToClipboard() { |
michael@0 | 381 | // Get the HTML and text representations for the important part of the page. |
michael@0 | 382 | let contentsDiv = $("contents"); |
michael@0 | 383 | let dataHtml = contentsDiv.innerHTML; |
michael@0 | 384 | let dataText = createTextForElement(contentsDiv); |
michael@0 | 385 | |
michael@0 | 386 | // We can't use plain strings, we have to use nsSupportsString. |
michael@0 | 387 | let supportsStringClass = Cc["@mozilla.org/supports-string;1"]; |
michael@0 | 388 | let ssHtml = supportsStringClass.createInstance(Ci.nsISupportsString); |
michael@0 | 389 | let ssText = supportsStringClass.createInstance(Ci.nsISupportsString); |
michael@0 | 390 | |
michael@0 | 391 | let transferable = Cc["@mozilla.org/widget/transferable;1"] |
michael@0 | 392 | .createInstance(Ci.nsITransferable); |
michael@0 | 393 | transferable.init(getLoadContext()); |
michael@0 | 394 | |
michael@0 | 395 | // Add the HTML flavor. |
michael@0 | 396 | transferable.addDataFlavor("text/html"); |
michael@0 | 397 | ssHtml.data = dataHtml; |
michael@0 | 398 | transferable.setTransferData("text/html", ssHtml, dataHtml.length * 2); |
michael@0 | 399 | |
michael@0 | 400 | // Add the plain text flavor. |
michael@0 | 401 | transferable.addDataFlavor("text/unicode"); |
michael@0 | 402 | ssText.data = dataText; |
michael@0 | 403 | transferable.setTransferData("text/unicode", ssText, dataText.length * 2); |
michael@0 | 404 | |
michael@0 | 405 | // Store the data into the clipboard. |
michael@0 | 406 | let clipboard = Cc["@mozilla.org/widget/clipboard;1"] |
michael@0 | 407 | .getService(Ci.nsIClipboard); |
michael@0 | 408 | clipboard.setData(transferable, null, clipboard.kGlobalClipboard); |
michael@0 | 409 | |
michael@0 | 410 | #ifdef ANDROID |
michael@0 | 411 | // Present a toast notification. |
michael@0 | 412 | let message = { |
michael@0 | 413 | type: "Toast:Show", |
michael@0 | 414 | message: stringBundle().GetStringFromName("textCopied"), |
michael@0 | 415 | duration: "short" |
michael@0 | 416 | }; |
michael@0 | 417 | Services.androidBridge.handleGeckoMessage(message); |
michael@0 | 418 | #endif |
michael@0 | 419 | } |
michael@0 | 420 | |
michael@0 | 421 | // Return the plain text representation of an element. Do a little bit |
michael@0 | 422 | // of pretty-printing to make it human-readable. |
michael@0 | 423 | function createTextForElement(elem) { |
michael@0 | 424 | let serializer = new Serializer(); |
michael@0 | 425 | let text = serializer.serialize(elem); |
michael@0 | 426 | |
michael@0 | 427 | // Actual CR/LF pairs are needed for some Windows text editors. |
michael@0 | 428 | #ifdef XP_WIN |
michael@0 | 429 | text = text.replace(/\n/g, "\r\n"); |
michael@0 | 430 | #endif |
michael@0 | 431 | |
michael@0 | 432 | return text; |
michael@0 | 433 | } |
michael@0 | 434 | |
michael@0 | 435 | function Serializer() { |
michael@0 | 436 | } |
michael@0 | 437 | |
michael@0 | 438 | Serializer.prototype = { |
michael@0 | 439 | |
michael@0 | 440 | serialize: function (rootElem) { |
michael@0 | 441 | this._lines = []; |
michael@0 | 442 | this._startNewLine(); |
michael@0 | 443 | this._serializeElement(rootElem); |
michael@0 | 444 | this._startNewLine(); |
michael@0 | 445 | return this._lines.join("\n").trim() + "\n"; |
michael@0 | 446 | }, |
michael@0 | 447 | |
michael@0 | 448 | // The current line is always the line that writing will start at next. When |
michael@0 | 449 | // an element is serialized, the current line is updated to be the line at |
michael@0 | 450 | // which the next element should be written. |
michael@0 | 451 | get _currentLine() { |
michael@0 | 452 | return this._lines.length ? this._lines[this._lines.length - 1] : null; |
michael@0 | 453 | }, |
michael@0 | 454 | |
michael@0 | 455 | set _currentLine(val) { |
michael@0 | 456 | return this._lines[this._lines.length - 1] = val; |
michael@0 | 457 | }, |
michael@0 | 458 | |
michael@0 | 459 | _serializeElement: function (elem) { |
michael@0 | 460 | if (this._ignoreElement(elem)) |
michael@0 | 461 | return; |
michael@0 | 462 | |
michael@0 | 463 | // table |
michael@0 | 464 | if (elem.localName == "table") { |
michael@0 | 465 | this._serializeTable(elem); |
michael@0 | 466 | return; |
michael@0 | 467 | } |
michael@0 | 468 | |
michael@0 | 469 | // all other elements |
michael@0 | 470 | |
michael@0 | 471 | let hasText = false; |
michael@0 | 472 | for (let child of elem.childNodes) { |
michael@0 | 473 | if (child.nodeType == Node.TEXT_NODE) { |
michael@0 | 474 | let text = this._nodeText(child); |
michael@0 | 475 | this._appendText(text); |
michael@0 | 476 | hasText = hasText || !!text.trim(); |
michael@0 | 477 | } |
michael@0 | 478 | else if (child.nodeType == Node.ELEMENT_NODE) |
michael@0 | 479 | this._serializeElement(child); |
michael@0 | 480 | } |
michael@0 | 481 | |
michael@0 | 482 | // For headings, draw a "line" underneath them so they stand out. |
michael@0 | 483 | if (/^h[0-9]+$/.test(elem.localName)) { |
michael@0 | 484 | let headerText = (this._currentLine || "").trim(); |
michael@0 | 485 | if (headerText) { |
michael@0 | 486 | this._startNewLine(); |
michael@0 | 487 | this._appendText("-".repeat(headerText.length)); |
michael@0 | 488 | } |
michael@0 | 489 | } |
michael@0 | 490 | |
michael@0 | 491 | // Add a blank line underneath block elements but only if they contain text. |
michael@0 | 492 | if (hasText) { |
michael@0 | 493 | let display = window.getComputedStyle(elem).getPropertyValue("display"); |
michael@0 | 494 | if (display == "block") { |
michael@0 | 495 | this._startNewLine(); |
michael@0 | 496 | this._startNewLine(); |
michael@0 | 497 | } |
michael@0 | 498 | } |
michael@0 | 499 | }, |
michael@0 | 500 | |
michael@0 | 501 | _startNewLine: function (lines) { |
michael@0 | 502 | let currLine = this._currentLine; |
michael@0 | 503 | if (currLine) { |
michael@0 | 504 | // The current line is not empty. Trim it. |
michael@0 | 505 | this._currentLine = currLine.trim(); |
michael@0 | 506 | if (!this._currentLine) |
michael@0 | 507 | // The current line became empty. Discard it. |
michael@0 | 508 | this._lines.pop(); |
michael@0 | 509 | } |
michael@0 | 510 | this._lines.push(""); |
michael@0 | 511 | }, |
michael@0 | 512 | |
michael@0 | 513 | _appendText: function (text, lines) { |
michael@0 | 514 | this._currentLine += text; |
michael@0 | 515 | }, |
michael@0 | 516 | |
michael@0 | 517 | _serializeTable: function (table) { |
michael@0 | 518 | // Collect the table's column headings if in fact there are any. First |
michael@0 | 519 | // check thead. If there's no thead, check the first tr. |
michael@0 | 520 | let colHeadings = {}; |
michael@0 | 521 | let tableHeadingElem = table.querySelector("thead"); |
michael@0 | 522 | if (!tableHeadingElem) |
michael@0 | 523 | tableHeadingElem = table.querySelector("tr"); |
michael@0 | 524 | if (tableHeadingElem) { |
michael@0 | 525 | let tableHeadingCols = tableHeadingElem.querySelectorAll("th,td"); |
michael@0 | 526 | // If there's a contiguous run of th's in the children starting from the |
michael@0 | 527 | // rightmost child, then consider them to be column headings. |
michael@0 | 528 | for (let i = tableHeadingCols.length - 1; i >= 0; i--) { |
michael@0 | 529 | if (tableHeadingCols[i].localName != "th") |
michael@0 | 530 | break; |
michael@0 | 531 | colHeadings[i] = this._nodeText(tableHeadingCols[i]).trim(); |
michael@0 | 532 | } |
michael@0 | 533 | } |
michael@0 | 534 | let hasColHeadings = Object.keys(colHeadings).length > 0; |
michael@0 | 535 | if (!hasColHeadings) |
michael@0 | 536 | tableHeadingElem = null; |
michael@0 | 537 | |
michael@0 | 538 | let trs = table.querySelectorAll("table > tr, tbody > tr"); |
michael@0 | 539 | let startRow = |
michael@0 | 540 | tableHeadingElem && tableHeadingElem.localName == "tr" ? 1 : 0; |
michael@0 | 541 | |
michael@0 | 542 | if (startRow >= trs.length) |
michael@0 | 543 | // The table's empty. |
michael@0 | 544 | return; |
michael@0 | 545 | |
michael@0 | 546 | if (hasColHeadings && !this._ignoreElement(tableHeadingElem)) { |
michael@0 | 547 | // Use column headings. Print each tr as a multi-line chunk like: |
michael@0 | 548 | // Heading 1: Column 1 value |
michael@0 | 549 | // Heading 2: Column 2 value |
michael@0 | 550 | for (let i = startRow; i < trs.length; i++) { |
michael@0 | 551 | if (this._ignoreElement(trs[i])) |
michael@0 | 552 | continue; |
michael@0 | 553 | let children = trs[i].querySelectorAll("td"); |
michael@0 | 554 | for (let j = 0; j < children.length; j++) { |
michael@0 | 555 | let text = ""; |
michael@0 | 556 | if (colHeadings[j]) |
michael@0 | 557 | text += colHeadings[j] + ": "; |
michael@0 | 558 | text += this._nodeText(children[j]).trim(); |
michael@0 | 559 | this._appendText(text); |
michael@0 | 560 | this._startNewLine(); |
michael@0 | 561 | } |
michael@0 | 562 | this._startNewLine(); |
michael@0 | 563 | } |
michael@0 | 564 | return; |
michael@0 | 565 | } |
michael@0 | 566 | |
michael@0 | 567 | // Don't use column headings. Assume the table has only two columns and |
michael@0 | 568 | // print each tr in a single line like: |
michael@0 | 569 | // Column 1 value: Column 2 value |
michael@0 | 570 | for (let i = startRow; i < trs.length; i++) { |
michael@0 | 571 | if (this._ignoreElement(trs[i])) |
michael@0 | 572 | continue; |
michael@0 | 573 | let children = trs[i].querySelectorAll("th,td"); |
michael@0 | 574 | let rowHeading = this._nodeText(children[0]).trim(); |
michael@0 | 575 | this._appendText(rowHeading + ": " + this._nodeText(children[1]).trim()); |
michael@0 | 576 | this._startNewLine(); |
michael@0 | 577 | } |
michael@0 | 578 | this._startNewLine(); |
michael@0 | 579 | }, |
michael@0 | 580 | |
michael@0 | 581 | _ignoreElement: function (elem) { |
michael@0 | 582 | return elem.classList.contains("no-copy"); |
michael@0 | 583 | }, |
michael@0 | 584 | |
michael@0 | 585 | _nodeText: function (node) { |
michael@0 | 586 | return node.textContent.replace(/\s+/g, " "); |
michael@0 | 587 | }, |
michael@0 | 588 | }; |
michael@0 | 589 | |
michael@0 | 590 | function openProfileDirectory() { |
michael@0 | 591 | // Get the profile directory. |
michael@0 | 592 | let currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile); |
michael@0 | 593 | let profileDir = currProfD.path; |
michael@0 | 594 | |
michael@0 | 595 | // Show the profile directory. |
michael@0 | 596 | let nsLocalFile = Components.Constructor("@mozilla.org/file/local;1", |
michael@0 | 597 | "nsILocalFile", "initWithPath"); |
michael@0 | 598 | new nsLocalFile(profileDir).reveal(); |
michael@0 | 599 | } |
michael@0 | 600 | |
michael@0 | 601 | /** |
michael@0 | 602 | * Profile reset is only supported for the default profile if the appropriate migrator exists. |
michael@0 | 603 | */ |
michael@0 | 604 | function populateResetBox() { |
michael@0 | 605 | if (ResetProfile.resetSupported()) |
michael@0 | 606 | $("reset-box").style.visibility = "visible"; |
michael@0 | 607 | } |
michael@0 | 608 | |
michael@0 | 609 | /** |
michael@0 | 610 | * Set up event listeners for buttons. |
michael@0 | 611 | */ |
michael@0 | 612 | function setupEventListeners(){ |
michael@0 | 613 | $("show-update-history-button").addEventListener("click", function (event) { |
michael@0 | 614 | var prompter = Cc["@mozilla.org/updates/update-prompt;1"].createInstance(Ci.nsIUpdatePrompt); |
michael@0 | 615 | prompter.showUpdateHistory(window); |
michael@0 | 616 | }); |
michael@0 | 617 | $("reset-box-button").addEventListener("click", function (event){ |
michael@0 | 618 | ResetProfile.openConfirmationDialog(window); |
michael@0 | 619 | }); |
michael@0 | 620 | $("copy-raw-data-to-clipboard").addEventListener("click", function (event){ |
michael@0 | 621 | copyRawDataToClipboard(this); |
michael@0 | 622 | }); |
michael@0 | 623 | $("copy-to-clipboard").addEventListener("click", function (event){ |
michael@0 | 624 | copyContentsToClipboard(); |
michael@0 | 625 | }); |
michael@0 | 626 | $("profile-dir-button").addEventListener("click", function (event){ |
michael@0 | 627 | openProfileDirectory(); |
michael@0 | 628 | }); |
michael@0 | 629 | } |