Bug 1531915 [wpt PR 15578] - Fix flakiness of external/wpt/css/css-position/z-index...
[gecko.git] / toolkit / content / aboutSupport.js
blobc03d93bb407040bd1c5e92c56eb5bc7fed6540da
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
8 const {Troubleshoot} = ChromeUtils.import("resource://gre/modules/Troubleshoot.jsm");
9 const {ResetProfile} = ChromeUtils.import("resource://gre/modules/ResetProfile.jsm");
10 const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
12 ChromeUtils.defineModuleGetter(this, "PluralForm",
13                                "resource://gre/modules/PluralForm.jsm");
14 ChromeUtils.defineModuleGetter(this, "PlacesDBUtils",
15                                "resource://gre/modules/PlacesDBUtils.jsm");
17 window.addEventListener("load", function onload(event) {
18   try {
19     window.removeEventListener("load", onload);
20     Troubleshoot.snapshot(async function(snapshot) {
21       for (let prop in snapshotFormatters)
22         await snapshotFormatters[prop](snapshot[prop]);
23     });
24     populateActionBox();
25     setupEventListeners();
26   } catch (e) {
27     Cu.reportError("stack of load error for about:support: " + e + ": " + e.stack);
28   }
29 });
31 // Fluent uses lisp-case IDs so this converts
32 // the SentenceCase info IDs to lisp-case.
33 const FLUENT_IDENT_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
34 function toFluentID(str) {
35   if (!FLUENT_IDENT_REGEX.test(str)) {
36     return null;
37   }
38   return str.toString().replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
41 // Each property in this object corresponds to a property in Troubleshoot.jsm's
42 // snapshot data.  Each function is passed its property's corresponding data,
43 // and it's the function's job to update the page with it.
44 var snapshotFormatters = {
46   async application(data) {
47     $("application-box").textContent = data.name;
48     $("useragent-box").textContent = data.userAgent;
49     $("os-box").textContent = data.osVersion;
50     $("supportLink").href = data.supportURL;
51     let version = AppConstants.MOZ_APP_VERSION_DISPLAY;
52     if (data.vendor)
53       version += " (" + data.vendor + ")";
54     $("version-box").textContent = version;
55     $("buildid-box").textContent = data.buildID;
56     if (data.updateChannel)
57       $("updatechannel-box").textContent = data.updateChannel;
58     $("profile-dir-box").textContent = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
60     try {
61       let launcherStatusTextId = "launcher-process-status-unknown";
62       switch (data.launcherProcessState) {
63         case 0:
64         case 1:
65         case 2:
66           launcherStatusTextId = "launcher-process-status-" + data.launcherProcessState;
67           break;
68       }
70       document.l10n.setAttributes($("launcher-process-box"), launcherStatusTextId);
71     } catch (e) {}
73     let statusTextId = "multi-process-status-unknown";
75     // Whitelist of known values with string descriptions:
76     switch (data.autoStartStatus) {
77       case 0:
78       case 1:
79       case 2:
80       case 4:
81       case 6:
82       case 7:
83       case 8:
84         statusTextId = "multi-process-status-" + data.autoStartStatus;
85         break;
86     }
88     document.l10n.setAttributes($("multiprocess-box-process-count"),
89                                 "multi-process-windows",
90                                 {
91                                   remoteWindows: data.numRemoteWindows,
92                                   totalWindows: data.numTotalWindows,
93                                 });
94     document.l10n.setAttributes($("multiprocess-box-status"), statusTextId);
96     if (data.remoteAutoStart) {
97       $("contentprocesses-box").textContent = data.currentContentProcesses +
98                                               "/" +
99                                               data.maxContentProcesses;
100     } else {
101       $("contentprocesses-row").hidden = true;
102     }
104     if (Services.policies) {
105       let policiesStrId = "";
106       let aboutPolicies = "about:policies";
107       switch (data.policiesStatus) {
108         case Services.policies.INACTIVE:
109           policiesStrId = "policies-inactive";
110           break;
112         case Services.policies.ACTIVE:
113           policiesStrId = "policies-active";
114           aboutPolicies += "#active";
115           break;
117         default:
118           policiesStrId = "policies-error";
119           aboutPolicies += "#errors";
120           break;
121       }
123       if (data.policiesStatus != Services.policies.INACTIVE) {
124         let activePolicies = $.new("a", null, null, {
125           href: aboutPolicies,
126         });
127         document.l10n.setAttributes(activePolicies, policiesStrId);
128         $("policies-status").appendChild(activePolicies);
129       } else {
130         document.l10n.setAttributes($("policies-status"), policiesStrId);
131       }
132     } else {
133       $("policies-status-row").hidden = true;
134     }
136     let keyLocationServiceGoogleFound = data.keyLocationServiceGoogleFound ? "found" : "missing";
137     document.l10n.setAttributes($("key-location-service-google-box"), keyLocationServiceGoogleFound);
139     let keySafebrowsingGoogleFound = data.keySafebrowsingGoogleFound ? "found" : "missing";
140     document.l10n.setAttributes($("key-safebrowsing-google-box"), keySafebrowsingGoogleFound);
142     let keyMozillaFound = data.keyMozillaFound ? "found" : "missing";
143     document.l10n.setAttributes($("key-mozilla-box"), keyMozillaFound);
145     $("safemode-box").textContent = data.safeMode;
146   },
148   crashes(data) {
149     if (!AppConstants.MOZ_CRASHREPORTER)
150       return;
152     let daysRange = Troubleshoot.kMaxCrashAge / (24 * 60 * 60 * 1000);
153     document.l10n.setAttributes($("crashes-title"), "report-crash-for-days", { days: daysRange });
154     let reportURL;
155     try {
156       reportURL = Services.prefs.getCharPref("breakpad.reportURL");
157       // Ignore any non http/https urls
158       if (!/^https?:/i.test(reportURL))
159         reportURL = null;
160     } catch (e) { }
161     if (!reportURL) {
162       $("crashes-noConfig").style.display = "block";
163       $("crashes-noConfig").classList.remove("no-copy");
164       return;
165     }
166     $("crashes-allReports").style.display = "block";
167     $("crashes-allReports").classList.remove("no-copy");
169     if (data.pending > 0) {
170       document.l10n.setAttributes($("crashes-allReportsWithPending"), "pending-reports", { reports: data.pending });
171     }
173     let dateNow = new Date();
174     $.append($("crashes-tbody"), data.submitted.map(function(crash) {
175       let date = new Date(crash.date);
176       let timePassed = dateNow - date;
177       let formattedDateStrId;
178       let formattedDateStrArgs;
179       if (timePassed >= 24 * 60 * 60 * 1000) {
180         let daysPassed = Math.round(timePassed / (24 * 60 * 60 * 1000));
181         formattedDateStrId = "crashes-time-days";
182         formattedDateStrArgs = { days: daysPassed };
183       } else if (timePassed >= 60 * 60 * 1000) {
184         let hoursPassed = Math.round(timePassed / (60 * 60 * 1000));
185         formattedDateStrId = "crashes-time-hours";
186         formattedDateStrArgs = { hours: hoursPassed };
187       } else {
188         let minutesPassed = Math.max(Math.round(timePassed / (60 * 1000)), 1);
189         formattedDateStrId = "crashes-time-minutes";
190         formattedDateStrArgs = { minutes: minutesPassed };
191       }
192       return $.new("tr", [
193         $.new("td", [
194           $.new("a", crash.id, null, {href: reportURL + crash.id}),
195         ]),
196         $.new("td", null, null, {"data-l10n-id": formattedDateStrId, "data-l10n-args": formattedDateStrArgs}),
197       ]);
198     }));
199   },
201   extensions(data) {
202     $.append($("extensions-tbody"), data.map(function(extension) {
203       return $.new("tr", [
204         $.new("td", extension.name),
205         $.new("td", extension.version),
206         $.new("td", extension.isActive),
207         $.new("td", extension.id),
208       ]);
209     }));
210   },
212   securitySoftware(data) {
213     if (!AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
214       $("security-software-title").hidden = true;
215       $("security-software-table").hidden = true;
216       return;
217     }
219     $("security-software-antivirus").textContent = data.registeredAntiVirus;
220     $("security-software-antispyware").textContent = data.registeredAntiSpyware;
221     $("security-software-firewall").textContent = data.registeredFirewall;
222   },
224   features(data) {
225     $.append($("features-tbody"), data.map(function(feature) {
226       return $.new("tr", [
227         $.new("td", feature.name),
228         $.new("td", feature.version),
229         $.new("td", feature.id),
230       ]);
231     }));
232   },
234   modifiedPreferences(data) {
235     $.append($("prefs-tbody"), sortedArrayFromObject(data).map(
236       function([name, value]) {
237         return $.new("tr", [
238           $.new("td", name, "pref-name"),
239           // Very long preference values can cause users problems when they
240           // copy and paste them into some text editors.  Long values generally
241           // aren't useful anyway, so truncate them to a reasonable length.
242           $.new("td", String(value).substr(0, 120), "pref-value"),
243         ]);
244       }
245     ));
246   },
248   lockedPreferences(data) {
249     $.append($("locked-prefs-tbody"), sortedArrayFromObject(data).map(
250       function([name, value]) {
251         return $.new("tr", [
252           $.new("td", name, "pref-name"),
253           $.new("td", String(value).substr(0, 120), "pref-value"),
254         ]);
255       }
256     ));
257   },
259   async graphics(data) {
260     function localizedMsg(msg) {
261       if (typeof msg == "object" && msg.key) {
262         return document.l10n.formatValue(msg.key, msg.args);
263       }
264       let msgId = toFluentID(msg);
265       if (msgId) {
266         return document.l10n.formatValue(msgId);
267       }
268       return "";
269     }
271     // Read APZ info out of data.info, stripping it out in the process.
272     let apzInfo = [];
273     let formatApzInfo = function(info) {
274       let out = [];
275       for (let type of ["Wheel", "Touch", "Drag", "Keyboard", "Autoscroll"]) {
276         let key = "Apz" + type + "Input";
278         if (!(key in info))
279           continue;
281         delete info[key];
283         out.push(toFluentID(type.toLowerCase() + "Enabled"));
284       }
286       return out;
287     };
289     // Create a <tr> element with key and value columns.
290     //
291     // @key      Text in the key column. Localized automatically, unless starts with "#".
292     // @value    Fluent ID for text in the value column, or array of children.
293     function buildRow(key, value) {
294       let title = key[0] == "#" ? key.substr(1) : key;
295       let keyStrId = toFluentID(key);
296       let valueStrId = Array.isArray(value) ? null : toFluentID(value);
297       let td = $.new("td", value);
298       td.style["white-space"] = "pre-wrap";
299       if (valueStrId) {
300         document.l10n.setAttributes(td, valueStrId);
301       }
303       let th = $.new("th", title, "column");
304       if (!key.startsWith("#")) {
305         document.l10n.setAttributes(th, keyStrId);
306       }
307       return $.new("tr", [
308         th,
309         td,
310       ]);
311     }
313     // @where    The name in "graphics-<name>-tbody", of the element to append to.
314     // @trs      Array of row elements.
315     function addRows(where, trs) {
316       $.append($("graphics-" + where + "-tbody"), trs);
317     }
319     // Build and append a row.
320     //
321     // @where    The name in "graphics-<name>-tbody", of the element to append to.
322     function addRow(where, key, value) {
323       addRows(where, [buildRow(key, value)]);
324     }
325     if ("info" in data) {
326       apzInfo = formatApzInfo(data.info);
328       let trs = sortedArrayFromObject(data.info).map(function([prop, val]) {
329         return $.new("tr", [
330           $.new("th", prop, "column"),
331           $.new("td", String(val)),
332         ]);
333       });
334       addRows("diagnostics", trs);
336       delete data.info;
337     }
339     let windowUtils = window.windowUtils;
340     let gpuProcessPid = windowUtils.gpuProcessPid;
342     if (gpuProcessPid != -1) {
343       let gpuProcessKillButton = null;
344       if (AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_DEV_EDITION) {
345         gpuProcessKillButton = $.new("button");
347         gpuProcessKillButton.addEventListener("click", function() {
348           windowUtils.terminateGPUProcess();
349         });
351         document.l10n.setAttributes(gpuProcessKillButton, "gpu-process-kill-button");
352       }
354       addRow("diagnostics", "gpu-process-pid", [new Text(gpuProcessPid)]);
355       if (gpuProcessKillButton) {
356         addRow("diagnostics", "gpu-process", [gpuProcessKillButton]);
357       }
358     }
360     if ((AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_DEV_EDITION) && AppConstants.platform != "macosx") {
361       let gpuDeviceResetButton = $.new("button");
363       gpuDeviceResetButton.addEventListener("click", function() {
364         windowUtils.triggerDeviceReset();
365       });
367       document.l10n.setAttributes(gpuDeviceResetButton, "gpu-device-reset-button");
368       addRow("diagnostics", "gpu-device-reset", [gpuDeviceResetButton]);
369     }
371     // graphics-failures-tbody tbody
372     if ("failures" in data) {
373       // If indices is there, it should be the same length as failures,
374       // (see Troubleshoot.jsm) but we check anyway:
375       if ("indices" in data && data.failures.length == data.indices.length) {
376         let combined = [];
377         for (let i = 0; i < data.failures.length; i++) {
378           let assembled = assembleFromGraphicsFailure(i, data);
379           combined.push(assembled);
380         }
381         combined.sort(function(a, b) {
382             if (a.index < b.index) return -1;
383             if (a.index > b.index) return 1;
384             return 0;
385         });
386         $.append($("graphics-failures-tbody"),
387                  combined.map(function(val) {
388                    return $.new("tr", [$.new("th", val.header, "column"),
389                                        $.new("td", val.message)]);
390                  }));
391         delete data.indices;
392       } else {
393         $.append($("graphics-failures-tbody"),
394           [$.new("tr", [$.new("th", "LogFailure", "column"),
395                         $.new("td", data.failures.map(function(val) {
396                           return $.new("p", val);
397                        }))])]);
398       }
399       delete data.failures;
400     } else {
401       $("graphics-failures-tbody").style.display = "none";
402     }
404     // Add a new row to the table, and take the key (or keys) out of data.
405     //
406     // @where        Table section to add to.
407     // @key          Data key to use.
408     // @colKey       The localization key to use, if different from key.
409     async function addRowFromKey(where, key, colKey) {
410       if (!(key in data))
411         return;
412       colKey = colKey || key;
414       let value;
415       let messageKey = key + "Message";
416       if (messageKey in data) {
417         value = await localizedMsg(data[messageKey]);
418         delete data[messageKey];
419       } else {
420         value = data[key];
421       }
422       delete data[key];
424       if (value) {
425         addRow(where, colKey, [new Text(value)]);
426       }
427     }
429     // graphics-features-tbody
430     let compositor = "";
431     if (data.windowLayerManagerRemote) {
432       compositor = data.windowLayerManagerType;
433       if (data.windowUsingAdvancedLayers) {
434         compositor += " (Advanced Layers)";
435       }
436     } else {
437       let noOMTCString = await document.l10n.formatValue("main-thread-no-omtc");
438       compositor = "BasicLayers (" + noOMTCString + ")";
439     }
440     addRow("features", "compositing", [new Text(compositor)]);
441     delete data.windowLayerManagerRemote;
442     delete data.windowLayerManagerType;
443     delete data.numTotalWindows;
444     delete data.numAcceleratedWindows;
445     delete data.numAcceleratedWindowsMessage;
446     delete data.windowUsingAdvancedLayers;
448     addRow("features", "asyncPanZoom",
449            apzInfo.length
450            ? [new Text((await document.l10n.formatValues(apzInfo.map(id => { return {id}; }))).join("; "))]
451            : "apz-none");
452     let featureKeys = [
453       "webgl1WSIInfo",
454       "webgl1Renderer",
455       "webgl1Version",
456       "webgl1DriverExtensions",
457       "webgl1Extensions",
458       "webgl2WSIInfo",
459       "webgl2Renderer",
460       "webgl2Version",
461       "webgl2DriverExtensions",
462       "webgl2Extensions",
463       ["supportsHardwareH264", "hardware-h264"],
464       ["direct2DEnabled", "#Direct2D"],
465       "usesTiling",
466       "contentUsesTiling",
467       "offMainThreadPaintEnabled",
468       "offMainThreadPaintWorkerCount",
469       "targetFrameRate",
470     ];
471     for (let feature of featureKeys) {
472       if (Array.isArray(feature)) {
473         await addRowFromKey("features", feature[0], feature[1]);
474         continue;
475       }
476       await addRowFromKey("features", feature);
477     }
479     if ("directWriteEnabled" in data) {
480       let message = data.directWriteEnabled;
481       if ("directWriteVersion" in data)
482         message += " (" + data.directWriteVersion + ")";
483       await addRow("features", "#DirectWrite", [new Text(message)]);
484       delete data.directWriteEnabled;
485       delete data.directWriteVersion;
486     }
488     // Adapter tbodies.
489     let adapterKeys = [
490       ["adapterDescription", "gpu-description"],
491       ["adapterVendorID", "gpu-vendor-id"],
492       ["adapterDeviceID", "gpu-device-id"],
493       ["driverVersion", "gpu-driver-version"],
494       ["driverDate", "gpu-driver-date"],
495       ["adapterDrivers", "gpu-drivers"],
496       ["adapterSubsysID", "gpu-subsys-id"],
497       ["adapterRAM", "gpu-ram"],
498     ];
500     function showGpu(id, suffix) {
501       function get(prop) {
502         return data[prop + suffix];
503       }
505       let trs = [];
506       for (let [prop, key] of adapterKeys) {
507         let value = get(prop);
508         if (value === undefined || value === "")
509           continue;
510         trs.push(buildRow(key, value));
511       }
513       if (trs.length == 0) {
514         $("graphics-" + id + "-tbody").style.display = "none";
515         return;
516       }
518       let active = "yes";
519       if ("isGPU2Active" in data && ((suffix == "2") != data.isGPU2Active)) {
520         active = "no";
521       }
523       addRow(id, "gpu-active", active);
524       addRows(id, trs);
525     }
526     showGpu("gpu-1", "");
527     showGpu("gpu-2", "2");
529     // Remove adapter keys.
530     for (let [prop /* key */] of adapterKeys) {
531       delete data[prop];
532       delete data[prop + "2"];
533     }
534     delete data.isGPU2Active;
536     let featureLog = data.featureLog;
537     delete data.featureLog;
539     let features = [];
540     for (let feature of featureLog.features) {
541       // Only add interesting decisions - ones that were not automatic based on
542       // all.js/gfxPrefs defaults.
543       if (feature.log.length > 1 || feature.log[0].status != "available") {
544         features.push(feature);
545       }
546     }
548     if (features.length) {
549       for (let feature of features) {
550         let trs = [];
551         for (let entry of feature.log) {
552           if (entry.type == "default" && entry.status == "available")
553             continue;
555           let contents;
556           if (entry.message.length > 0 && entry.message[0] == "#") {
557             // This is a failure ID. See nsIGfxInfo.idl.
558             let m = /#BLOCKLIST_FEATURE_FAILURE_BUG_(\d+)/.exec(entry.message);
559             if (m) {
560               let bugSpan = $.new("span");
561               document.l10n.setAttributes(bugSpan, "blocklisted-bug");
563               let bugHref = $.new("a");
564               bugHref.href = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + m[1];
565               document.l10n.setAttributes(bugHref, "bug-link", { bugNumber: m[1]});
567               contents = [bugSpan, bugHref];
568             } else {
569               let unknownFailure = $.new("span");
570               document.l10n.setAttributes(unknownFailure, "unknown-failure", { failureCode: entry.message.substr(1) });
571               contents = [unknownFailure];
572             }
573           } else {
574             contents = entry.status + " by " + entry.type + ": " + entry.message;
575           }
577           trs.push($.new("tr", [
578             $.new("td", contents),
579           ]));
580         }
581         addRow("decisions", "#" + feature.name, [$.new("table", trs)]);
582       }
583     } else {
584       $("graphics-decisions-tbody").style.display = "none";
585     }
587     if (featureLog.fallbacks.length) {
588       for (let fallback of featureLog.fallbacks) {
589         addRow("workarounds", fallback.name, [new Text(fallback.message)]);
590       }
591     } else {
592       $("graphics-workarounds-tbody").style.display = "none";
593     }
595     let crashGuards = data.crashGuards;
596     delete data.crashGuards;
598     if (crashGuards.length) {
599       for (let guard of crashGuards) {
600         let resetButton = $.new("button");
601         let onClickReset = function() {
602           Services.prefs.setIntPref(guard.prefName, 0);
603           resetButton.removeEventListener("click", onClickReset);
604           resetButton.disabled = true;
605         };
607         document.l10n.setAttributes(resetButton, "reset-on-next-restart");
608         resetButton.addEventListener("click", onClickReset);
610         addRow("crashguards", guard.type + "CrashGuard", [resetButton]);
611       }
612     } else {
613       $("graphics-crashguards-tbody").style.display = "none";
614     }
616     // Now that we're done, grab any remaining keys in data and drop them into
617     // the diagnostics section.
618     for (let key in data) {
619       let value = data[key];
620       addRow("diagnostics", key, [new Text(value)]);
621     }
622   },
624   media(data) {
625     function insertBasicInfo(key, value) {
626       function createRow(key, value) {
627         let th = $.new("th", null, "column");
628         document.l10n.setAttributes(th, key);
629         let td = $.new("td", value);
630         td.style["white-space"] = "pre-wrap";
631         td.colSpan = 8;
632         return $.new("tr", [th, td]);
633       }
634       $.append($("media-info-tbody"), [createRow(key, value)]);
635     }
637     function createDeviceInfoRow(device) {
638       let deviceInfo = Ci.nsIAudioDeviceInfo;
640       let states = {};
641       states[deviceInfo.STATE_DISABLED] = "Disabled";
642       states[deviceInfo.STATE_UNPLUGGED] = "Unplugged";
643       states[deviceInfo.STATE_ENABLED] = "Enabled";
645       let preferreds = {};
646       preferreds[deviceInfo.PREF_NONE] = "None";
647       preferreds[deviceInfo.PREF_MULTIMEDIA] = "Multimedia";
648       preferreds[deviceInfo.PREF_VOICE] = "Voice";
649       preferreds[deviceInfo.PREF_NOTIFICATION] = "Notification";
650       preferreds[deviceInfo.PREF_ALL] = "All";
652       let formats = {};
653       formats[deviceInfo.FMT_S16LE] = "S16LE";
654       formats[deviceInfo.FMT_S16BE] = "S16BE";
655       formats[deviceInfo.FMT_F32LE] = "F32LE";
656       formats[deviceInfo.FMT_F32BE] = "F32BE";
658       function toPreferredString(preferred) {
659         if (preferred == deviceInfo.PREF_NONE) {
660           return preferreds[deviceInfo.PREF_NONE];
661         } else if (preferred & deviceInfo.PREF_ALL) {
662           return preferreds[deviceInfo.PREF_ALL];
663         }
664         let str = "";
665         for (let pref of [deviceInfo.PREF_MULTIMEDIA,
666                           deviceInfo.PREF_VOICE,
667                           deviceInfo.PREF_NOTIFICATION]) {
668           if (preferred & pref) {
669             str += " " + preferreds[pref];
670           }
671         }
672         return str;
673       }
675       function toFromatString(dev) {
676         let str = "default: " + formats[dev.defaultFormat] + ", support:";
677         for (let fmt of [deviceInfo.FMT_S16LE,
678                          deviceInfo.FMT_S16BE,
679                          deviceInfo.FMT_F32LE,
680                          deviceInfo.FMT_F32BE]) {
681           if (dev.supportedFormat & fmt) {
682             str += " " + formats[fmt];
683           }
684         }
685         return str;
686       }
688       function toRateString(dev) {
689         return "default: " + dev.defaultRate +
690                ", support: " + dev.minRate + " - " + dev.maxRate;
691       }
693       function toLatencyString(dev) {
694         return dev.minLatency + " - " + dev.maxLatency;
695       }
697       return $.new("tr", [$.new("td", device.name),
698                           $.new("td", device.groupId),
699                           $.new("td", device.vendor),
700                           $.new("td", states[device.state]),
701                           $.new("td", toPreferredString(device.preferred)),
702                           $.new("td", toFromatString(device)),
703                           $.new("td", device.maxChannels),
704                           $.new("td", toRateString(device)),
705                           $.new("td", toLatencyString(device))]);
706     }
708     function insertDeviceInfo(side, devices) {
709       let rows = [];
710       for (let dev of devices) {
711         rows.push(createDeviceInfoRow(dev));
712       }
713       $.append($("media-" + side + "-devices-tbody"), rows);
714     }
716     // Basic information
717     insertBasicInfo("audio-backend", data.currentAudioBackend);
718     insertBasicInfo("max-audio-channels", data.currentMaxAudioChannels);
719     insertBasicInfo("sample-rate", data.currentPreferredSampleRate);
721     // Output devices information
722     insertDeviceInfo("output", data.audioOutputDevices);
724     // Input devices information
725     insertDeviceInfo("input", data.audioInputDevices);
726   },
728   javaScript(data) {
729     $("javascript-incremental-gc").textContent = data.incrementalGCEnabled;
730   },
732   accessibility(data) {
733     $("a11y-activated").textContent = data.isActive;
734     $("a11y-force-disabled").textContent = data.forceDisabled || 0;
736     let a11yHandlerUsed = $("a11y-handler-used");
737     if (a11yHandlerUsed) {
738       a11yHandlerUsed.textContent = data.handlerUsed;
739     }
741     let a11yInstantiator = $("a11y-instantiator");
742     if (a11yInstantiator) {
743       a11yInstantiator.textContent = data.instantiator;
744     }
745   },
747   libraryVersions(data) {
748     let trs = [
749       $.new("tr", [
750         $.new("th", ""),
751         $.new("th", null, null, {"data-l10n-id": "min-lib-versions"}),
752         $.new("th", null, null, {"data-l10n-id": "loaded-lib-versions"}),
753       ]),
754     ];
755     sortedArrayFromObject(data).forEach(
756       function([name, val]) {
757         trs.push($.new("tr", [
758           $.new("td", name),
759           $.new("td", val.minVersion),
760           $.new("td", val.version),
761         ]));
762       }
763     );
764     $.append($("libversions-tbody"), trs);
765   },
767   userJS(data) {
768     if (!data.exists)
769       return;
770     let userJSFile = Services.dirsvc.get("PrefD", Ci.nsIFile);
771     userJSFile.append("user.js");
772     $("prefs-user-js-link").href = Services.io.newFileURI(userJSFile).spec;
773     $("prefs-user-js-section").style.display = "";
774     // Clear the no-copy class
775     $("prefs-user-js-section").className = "";
776   },
778   sandbox(data) {
779     if (!AppConstants.MOZ_SANDBOX)
780       return;
782     let tbody = $("sandbox-tbody");
783     for (let key in data) {
784       // Simplify the display a little in the common case.
785       if (key === "hasPrivilegedUserNamespaces" &&
786           data[key] === data.hasUserNamespaces) {
787         continue;
788       }
789       if (key === "syscallLog") {
790         // Not in this table.
791         continue;
792       }
793       let keyStrId = toFluentID(key);
794       let th = $.new("th", null, "column");
795       document.l10n.setAttributes(th, keyStrId);
796       tbody.appendChild($.new("tr", [
797         th,
798         $.new("td", data[key]),
799       ]));
800     }
802     if ("syscallLog" in data) {
803       let syscallBody = $("sandbox-syscalls-tbody");
804       let argsHead = $("sandbox-syscalls-argshead");
805       for (let syscall of data.syscallLog) {
806         if (argsHead.colSpan < syscall.args.length) {
807           argsHead.colSpan = syscall.args.length;
808         }
809         let procTypeStrId = toFluentID(syscall.procType);
810         let cells = [
811           $.new("td", syscall.index, "integer"),
812           $.new("td", syscall.msecAgo / 1000),
813           $.new("td", syscall.pid, "integer"),
814           $.new("td", syscall.tid, "integer"),
815           $.new("td", null, null, {"data-l10n-id": "sandbox-proc-type-" + procTypeStrId}),
816           $.new("td", syscall.syscall, "integer"),
817         ];
818         for (let arg of syscall.args) {
819           cells.push($.new("td", arg, "integer"));
820         }
821         syscallBody.appendChild($.new("tr", cells));
822       }
823     }
824   },
826   intl(data) {
827     $("intl-locale-requested").textContent =
828       JSON.stringify(data.localeService.requested);
829     $("intl-locale-available").textContent =
830       JSON.stringify(data.localeService.available);
831     $("intl-locale-supported").textContent =
832       JSON.stringify(data.localeService.supported);
833     $("intl-locale-regionalprefs").textContent =
834       JSON.stringify(data.localeService.regionalPrefs);
835     $("intl-locale-default").textContent =
836       JSON.stringify(data.localeService.defaultLocale);
838     $("intl-osprefs-systemlocales").textContent =
839       JSON.stringify(data.osPrefs.systemLocales);
840     $("intl-osprefs-regionalprefs").textContent =
841       JSON.stringify(data.osPrefs.regionalPrefsLocales);
842   },
845 var $ = document.getElementById.bind(document);
847 $.new = function $_new(tag, textContentOrChildren, className, attributes) {
848   let elt = document.createElement(tag);
849   if (className) {
850     elt.className = className;
851   }
852   if (attributes) {
853     if (attributes["data-l10n-id"]) {
854       let args = attributes.hasOwnProperty("data-l10n-args") ?
855         attributes["data-l10n-args"] :
856         undefined;
857       document.l10n.setAttributes(elt,
858                                   attributes["data-l10n-id"],
859                                   args);
860       delete attributes["data-l10n-id"];
861       if (args) {
862         delete attributes["data-l10n-args"];
863       }
864     }
866     for (let attrName in attributes) {
867       elt.setAttribute(attrName, attributes[attrName]);
868     }
869   }
870   if (Array.isArray(textContentOrChildren)) {
871     this.append(elt, textContentOrChildren);
872   } else if (!attributes || !attributes["data-l10n-id"]) {
873     elt.textContent = String(textContentOrChildren);
874   }
875   return elt;
878 $.append = function $_append(parent, children) {
879   children.forEach(c => parent.appendChild(c));
882 function assembleFromGraphicsFailure(i, data) {
883   // Only cover the cases we have today; for example, we do not have
884   // log failures that assert and we assume the log level is 1/error.
885   let message = data.failures[i];
886   let index = data.indices[i];
887   let what = "";
888   if (message.search(/\[GFX1-\]: \(LF\)/) == 0) {
889     // Non-asserting log failure - the message is substring(14)
890     what = "LogFailure";
891     message = message.substring(14);
892   } else if (message.search(/\[GFX1-\]: /) == 0) {
893     // Non-asserting - the message is substring(9)
894     what = "Error";
895     message = message.substring(9);
896   } else if (message.search(/\[GFX1\]: /) == 0) {
897     // Asserting - the message is substring(8)
898     what = "Assert";
899     message = message.substring(8);
900   }
901   let assembled = {"index": index,
902                    "header": ("(#" + index + ") " + what),
903                    "message": message};
904   return assembled;
907 function sortedArrayFromObject(obj) {
908   let tuples = [];
909   for (let prop in obj)
910     tuples.push([prop, obj[prop]]);
911   tuples.sort(([prop1, v1], [prop2, v2]) => prop1.localeCompare(prop2));
912   return tuples;
915 function copyRawDataToClipboard(button) {
916   if (button)
917     button.disabled = true;
918   try {
919     Troubleshoot.snapshot(async function(snapshot) {
920       if (button)
921         button.disabled = false;
922       let str = Cc["@mozilla.org/supports-string;1"].
923                 createInstance(Ci.nsISupportsString);
924       str.data = JSON.stringify(snapshot, undefined, 2);
925       let transferable = Cc["@mozilla.org/widget/transferable;1"].
926                          createInstance(Ci.nsITransferable);
927       transferable.init(getLoadContext());
928       transferable.addDataFlavor("text/unicode");
929       transferable.setTransferData("text/unicode", str, str.data.length * 2);
930       Services.clipboard.setData(transferable, null, Ci.nsIClipboard.kGlobalClipboard);
931       if (AppConstants.platform == "android") {
932         // Present a snackbar notification.
933         var {Snackbars} = ChromeUtils.import("resource://gre/modules/Snackbars.jsm");
934         let rawDataCopiedString = await document.l10n.formatValue("raw-data-copied");
935         Snackbars.show(rawDataCopiedString, Snackbars.LENGTH_SHORT);
936       }
937     });
938   } catch (err) {
939     if (button)
940       button.disabled = false;
941     throw err;
942   }
945 function getLoadContext() {
946   return window.docShell.QueryInterface(Ci.nsILoadContext);
949 async function copyContentsToClipboard() {
950   // Get the HTML and text representations for the important part of the page.
951   let contentsDiv = $("contents");
952   let dataHtml = contentsDiv.innerHTML;
953   let dataText = createTextForElement(contentsDiv);
955   // We can't use plain strings, we have to use nsSupportsString.
956   let supportsStringClass = Cc["@mozilla.org/supports-string;1"];
957   let ssHtml = supportsStringClass.createInstance(Ci.nsISupportsString);
958   let ssText = supportsStringClass.createInstance(Ci.nsISupportsString);
960   let transferable = Cc["@mozilla.org/widget/transferable;1"]
961                        .createInstance(Ci.nsITransferable);
962   transferable.init(getLoadContext());
964   // Add the HTML flavor.
965   transferable.addDataFlavor("text/html");
966   ssHtml.data = dataHtml;
967   transferable.setTransferData("text/html", ssHtml, dataHtml.length * 2);
969   // Add the plain text flavor.
970   transferable.addDataFlavor("text/unicode");
971   ssText.data = dataText;
972   transferable.setTransferData("text/unicode", ssText, dataText.length * 2);
974   // Store the data into the clipboard.
975   Services.clipboard.setData(transferable, null, Services.clipboard.kGlobalClipboard);
977   if (AppConstants.platform == "android") {
978     // Present a snackbar notification.
979     var {Snackbars} = ChromeUtils.import("resource://gre/modules/Snackbars.jsm");
980     let textCopiedString = await document.l10n.formatValue("text-copied");
981     Snackbars.show(textCopiedString, Snackbars.LENGTH_SHORT);
982   }
985 // Return the plain text representation of an element.  Do a little bit
986 // of pretty-printing to make it human-readable.
987 function createTextForElement(elem) {
988   let serializer = new Serializer();
989   let text = serializer.serialize(elem);
991   // Actual CR/LF pairs are needed for some Windows text editors.
992   if (AppConstants.platform == "win") {
993     text = text.replace(/\n/g, "\r\n");
994   }
996   return text;
999 function Serializer() {
1002 Serializer.prototype = {
1004   serialize(rootElem) {
1005     this._lines = [];
1006     this._startNewLine();
1007     this._serializeElement(rootElem);
1008     this._startNewLine();
1009     return this._lines.join("\n").trim() + "\n";
1010   },
1012   // The current line is always the line that writing will start at next.  When
1013   // an element is serialized, the current line is updated to be the line at
1014   // which the next element should be written.
1015   get _currentLine() {
1016     return this._lines.length ? this._lines[this._lines.length - 1] : null;
1017   },
1019   set _currentLine(val) {
1020     return this._lines[this._lines.length - 1] = val;
1021   },
1023   _serializeElement(elem) {
1024     if (this._ignoreElement(elem))
1025       return;
1027     // table
1028     if (elem.localName == "table") {
1029       this._serializeTable(elem);
1030       return;
1031     }
1033     // all other elements
1035     let hasText = false;
1036     for (let child of elem.childNodes) {
1037       if (child.nodeType == Node.TEXT_NODE) {
1038         let text = this._nodeText(child);
1039         this._appendText(text);
1040         hasText = hasText || !!text.trim();
1041       } else if (child.nodeType == Node.ELEMENT_NODE) {
1042         this._serializeElement(child);
1043       }
1044     }
1046     // For headings, draw a "line" underneath them so they stand out.
1047     if (/^h[0-9]+$/.test(elem.localName)) {
1048       let headerText = (this._currentLine || "").trim();
1049       if (headerText) {
1050         this._startNewLine();
1051         this._appendText("-".repeat(headerText.length));
1052       }
1053     }
1055     // Add a blank line underneath block elements but only if they contain text.
1056     if (hasText) {
1057       let display = window.getComputedStyle(elem).getPropertyValue("display");
1058       if (display == "block") {
1059         this._startNewLine();
1060         this._startNewLine();
1061       }
1062     }
1063   },
1065   _startNewLine(lines) {
1066     let currLine = this._currentLine;
1067     if (currLine) {
1068       // The current line is not empty.  Trim it.
1069       this._currentLine = currLine.trim();
1070       if (!this._currentLine)
1071         // The current line became empty.  Discard it.
1072         this._lines.pop();
1073     }
1074     this._lines.push("");
1075   },
1077   _appendText(text, lines) {
1078     this._currentLine += text;
1079   },
1081   _isHiddenSubHeading(th) {
1082     return th.parentNode.parentNode.style.display == "none";
1083   },
1085   _serializeTable(table) {
1086     // Collect the table's column headings if in fact there are any.  First
1087     // check thead.  If there's no thead, check the first tr.
1088     let colHeadings = {};
1089     let tableHeadingElem = table.querySelector("thead");
1090     if (!tableHeadingElem)
1091       tableHeadingElem = table.querySelector("tr");
1092     if (tableHeadingElem) {
1093       let tableHeadingCols = tableHeadingElem.querySelectorAll("th,td");
1094       // If there's a contiguous run of th's in the children starting from the
1095       // rightmost child, then consider them to be column headings.
1096       for (let i = tableHeadingCols.length - 1; i >= 0; i--) {
1097         let col = tableHeadingCols[i];
1098         if (col.localName != "th" || col.classList.contains("title-column"))
1099           break;
1100         colHeadings[i] = this._nodeText(col).trim();
1101       }
1102     }
1103     let hasColHeadings = Object.keys(colHeadings).length > 0;
1104     if (!hasColHeadings)
1105       tableHeadingElem = null;
1107     let trs = table.querySelectorAll("table > tr, tbody > tr");
1108     let startRow =
1109       tableHeadingElem && tableHeadingElem.localName == "tr" ? 1 : 0;
1111     if (startRow >= trs.length)
1112       // The table's empty.
1113       return;
1115     if (hasColHeadings && !this._ignoreElement(tableHeadingElem)) {
1116       // Use column headings.  Print each tr as a multi-line chunk like:
1117       //   Heading 1: Column 1 value
1118       //   Heading 2: Column 2 value
1119       for (let i = startRow; i < trs.length; i++) {
1120         if (this._ignoreElement(trs[i]))
1121           continue;
1122         let children = trs[i].querySelectorAll("td");
1123         for (let j = 0; j < children.length; j++) {
1124           let text = "";
1125           if (colHeadings[j])
1126             text += colHeadings[j] + ": ";
1127           text += this._nodeText(children[j]).trim();
1128           this._appendText(text);
1129           this._startNewLine();
1130         }
1131         this._startNewLine();
1132       }
1133       return;
1134     }
1136     // Don't use column headings.  Assume the table has only two columns and
1137     // print each tr in a single line like:
1138     //   Column 1 value: Column 2 value
1139     for (let i = startRow; i < trs.length; i++) {
1140       if (this._ignoreElement(trs[i]))
1141         continue;
1142       let children = trs[i].querySelectorAll("th,td");
1143       let rowHeading = this._nodeText(children[0]).trim();
1144       if (children[0].classList.contains("title-column")) {
1145         if (!this._isHiddenSubHeading(children[0]))
1146           this._appendText(rowHeading);
1147       } else if (children.length == 1) {
1148         // This is a single-cell row.
1149         this._appendText(rowHeading);
1150       } else {
1151         let childTables = trs[i].querySelectorAll("table");
1152         if (childTables.length) {
1153           // If we have child tables, don't use nodeText - its trs are already
1154           // queued up from querySelectorAll earlier.
1155           this._appendText(rowHeading + ": ");
1156         } else {
1157           this._appendText(rowHeading + ": " + this._nodeText(children[1]).trim());
1158         }
1159       }
1160       this._startNewLine();
1161     }
1162     this._startNewLine();
1163   },
1165   _ignoreElement(elem) {
1166     return elem.classList.contains("no-copy");
1167   },
1169   _nodeText(node) {
1170     return node.textContent.replace(/\s+/g, " ");
1171   },
1174 function openProfileDirectory() {
1175   // Get the profile directory.
1176   let currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile);
1177   let profileDir = currProfD.path;
1179   // Show the profile directory.
1180   let nsLocalFile = Components.Constructor("@mozilla.org/file/local;1",
1181                                            "nsIFile", "initWithPath");
1182   new nsLocalFile(profileDir).reveal();
1186  * Profile reset is only supported for the default profile if the appropriate migrator exists.
1187  */
1188 function populateActionBox() {
1189   if (ResetProfile.resetSupported()) {
1190     $("reset-box").style.display = "block";
1191     $("action-box").style.display = "block";
1192   }
1193   if (!Services.appinfo.inSafeMode && AppConstants.platform !== "android") {
1194     $("safe-mode-box").style.display = "block";
1195     $("action-box").style.display = "block";
1197     if (Services.policies && !Services.policies.isAllowed("safeMode")) {
1198       $("restart-in-safe-mode-button").setAttribute("disabled", "true");
1199     }
1200   }
1203 // Prompt user to restart the browser in safe mode
1204 function safeModeRestart() {
1205   let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
1206                      .createInstance(Ci.nsISupportsPRBool);
1207   Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
1209   if (!cancelQuit.data) {
1210     Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
1211   }
1214  * Set up event listeners for buttons.
1215  */
1216 function setupEventListeners() {
1217   let button = $("show-update-history-button");
1218   if (button) {
1219     button.addEventListener("click", function(event) {
1220       var prompter = Cc["@mozilla.org/updates/update-prompt;1"].createInstance(Ci.nsIUpdatePrompt);
1221       prompter.showUpdateHistory(window);
1222     });
1223   }
1224   button = $("reset-box-button");
1225   if (button) {
1226     button.addEventListener("click", function(event) {
1227       ResetProfile.openConfirmationDialog(window);
1228     });
1229   }
1230   button = $("restart-in-safe-mode-button");
1231   if (button) {
1232     button.addEventListener("click", function(event) {
1233       if (Services.obs.enumerateObservers("restart-in-safe-mode").hasMoreElements()) {
1234         Services.obs.notifyObservers(null, "restart-in-safe-mode");
1235       } else {
1236         safeModeRestart();
1237       }
1238     });
1239   }
1240   button = $("verify-place-integrity-button");
1241   if (button) {
1242     button.addEventListener("click", function(event) {
1243       PlacesDBUtils.checkAndFixDatabase().then((tasksStatusMap) => {
1244         let logs = [];
1245         for (let [key, value] of tasksStatusMap) {
1246           logs.push(`> Task: ${key}`);
1247           let prefix = value.succeeded ? "+ " : "- ";
1248           logs = logs.concat(value.logs.map(m => `${prefix}${m}`));
1249         }
1250         $("verify-place-result").style.display = "block";
1251         $("verify-place-result").classList.remove("no-copy");
1252         $("verify-place-result").textContent = logs.join("\n");
1253       });
1254     });
1255   }
1257   $("copy-raw-data-to-clipboard").addEventListener("click", function(event) {
1258     copyRawDataToClipboard(this);
1259   });
1260   $("copy-to-clipboard").addEventListener("click", function(event) {
1261     copyContentsToClipboard();
1262   });
1263   $("profile-dir-button").addEventListener("click", function(event) {
1264     openProfileDirectory();
1265   });