+ let allSecond = document.querySelectorAll("#" + id + " > div");
+ for (let elt of allSecond) {
+ if (elt.getAttribute("id") == childId) {
+ elt.style.display = "block";
+ } else {
+ elt.style.display = "none";
+ }
+ }
+ }
+}
+
+// Migrate sync data from the default profile to the dev-edition profile.
+// Returns a promise of a true value if migration succeeded, or false if it
+// failed.
+function migrateToDevEdition(urlParams) {
+ let defaultProfilePath;
+ try {
+ defaultProfilePath = window.getDefaultProfilePath();
+ } catch (e) {} // no default profile.
+ let migrateSyncCreds = false;
+ if (defaultProfilePath) {
+ try {
+ migrateSyncCreds = Services.prefs.getBoolPref("identity.fxaccounts.migrateToDevEdition");
+ } catch (e) {}
+ }
+
+ if (!migrateSyncCreds) {
+ return Promise.resolve(false);
+ }
+
+ Cu.import("resource://gre/modules/osfile.jsm");
+ let fxAccountsStorage = OS.Path.join(defaultProfilePath, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
+ return OS.File.read(fxAccountsStorage, { encoding: "utf-8" }).then(text => {
+ let accountData = JSON.parse(text).accountData;
+ updateDisplayedEmail(accountData);
+ return fxAccounts.setSignedInUser(accountData);
+ }).then(() => {
+ return fxAccounts.promiseAccountsForceSigninURI().then(url => {
+ show("remote");
+ wrapper.init(url, urlParams);
+ });
+ }).then(null, error => {
+ log("Failed to migrate FX Account: " + error);
+ show("stage", "intro");
+ // load the remote frame in the background
+ fxAccounts.promiseAccountsSignUpURI().then(uri => {
+ wrapper.init(uri, urlParams)
+ }).catch(e => {
+ console.log("Failed to load signup page", e);
+ setErrorPage("configError");
+ });
+ }).then(() => {
+ // Reset the pref after migration.
+ Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false);
+ return true;
+ }).then(null, err => {
+ Cu.reportError("Failed to reset the migrateToDevEdition pref: " + err);
+ return false;
+ });
+}
+
+// Helper function that returns the path of the default profile on disk. Will be
+// overridden in tests.
+function getDefaultProfilePath() {
+ let defaultProfile = Cc["@mozilla.org/toolkit/profile-service;1"]
+ .getService(Ci.nsIToolkitProfileService)
+ .defaultProfile;
+ return defaultProfile.rootDir.path;
+}
+
+document.addEventListener("DOMContentLoaded", function onload() {
+ document.removeEventListener("DOMContentLoaded", onload, true);
+ init();
+ var buttonGetStarted = document.getElementById("buttonGetStarted");
+ buttonGetStarted.addEventListener("click", getStarted);
+
+ var buttonRetry = document.getElementById("buttonRetry");
+ buttonRetry.addEventListener("click", retry);
+
+ var oldsync = document.getElementById("oldsync");
+ oldsync.addEventListener("click", handleOldSync);
+
+ var buttonOpenPrefs = document.getElementById("buttonOpenPrefs")
+ buttonOpenPrefs.addEventListener("click", openPrefs);
+}, true);
+
+function initObservers() {
+ function observe(subject, topic, data) {
+ log("about:accounts observed " + topic);
+ if (topic == fxAccountsCommon.ONLOGOUT_NOTIFICATION) {
+ // All about:account windows get changed to action=signin on logout.
+ window.location = "about:accounts?action=signin";
+ return;
+ }
+
+ // must be onverified - we want to open preferences.
+ openPrefs();
+ }
+
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.addObserver(observe, topic, false);
+ }
+ window.addEventListener("unload", function(event) {
+ log("about:accounts unloading")
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.removeObserver(observe, topic);
+ }
+ });
+}
+initObservers();
diff --git a/application/basilisk/base/content/aboutaccounts/aboutaccounts.xhtml b/application/basilisk/base/content/aboutaccounts/aboutaccounts.xhtml
new file mode 100644
index 000000000..e188aa496
--- /dev/null
+++ b/application/basilisk/base/content/aboutaccounts/aboutaccounts.xhtml
@@ -0,0 +1,111 @@
+
+
+
+ %htmlDTD;
+
+ %brandDTD;
+
+ %globalDTD;
+
+ %aboutAccountsDTD;
+
+ %syncBrandDTD;
+]>
+
+
+
+
&syncBrand.fullName.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &aboutAccountsConfig.syncPreferences.label;
+
+
+
+
+
+
+ &aboutAccounts.welcome;
+
+
+
+
+
+ &aboutAccountsConfig.description;
+
+
+ &aboutAccountsConfig.startButton.label;
+
+
+
+ &aboutAccountsConfig.useOldSync.label;
+
+
+
+
+
+
+ &aboutAccounts.noConnection.title;
+
+
+
+
+
+ &aboutAccounts.noConnection.description;
+
+
+ &aboutAccounts.noConnection.retry;
+
+
+
+
+
+
+ &aboutAccounts.badConfig.title;
+
+
+
+
+
+ &aboutAccounts.badConfig.description;
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/basilisk/base/content/aboutaccounts/images/fox.png b/application/basilisk/base/content/aboutaccounts/images/fox.png
new file mode 100644
index 000000000..83af78d6c
Binary files /dev/null and b/application/basilisk/base/content/aboutaccounts/images/fox.png differ
diff --git a/application/basilisk/base/content/aboutaccounts/main.css b/application/basilisk/base/content/aboutaccounts/main.css
new file mode 100644
index 000000000..c85292408
--- /dev/null
+++ b/application/basilisk/base/content/aboutaccounts/main.css
@@ -0,0 +1,166 @@
+*,
+*:before,
+*:after {
+ box-sizing: border-box;
+}
+
+html {
+ background-color: #F2F2F2;
+ height: 100%;
+}
+
+body {
+ color: #424f59;
+ font: message-box;
+ font-size: 14px;
+ height: 100%;
+}
+
+a {
+ color: #0095dd;
+ cursor: pointer; /* Use the correct cursor for anchors without an href */
+}
+
+a:active {
+ outline: none;
+}
+
+a:focus {
+ outline: 1px dotted #0095dd;
+}
+
+
+a.no-underline {
+ text-decoration: none;
+}
+
+#stage {
+ background:#fff;
+ border-radius: 5px;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.25);
+ margin: 0 auto;
+ min-height: 300px;
+ padding: 60px 40px 40px 40px;
+ position: relative;
+ text-align: center;
+ top: 80px;
+ width: 420px;
+}
+
+header h1
+{
+ font-size: 24px;
+ font-weight: 200;
+ line-height: 1em;
+}
+
+#intro header h1 {
+ margin: 0 0 32px 0;
+}
+
+#manage header h1 {
+ margin: 0 0 12px 0;
+}
+
+#manage header #email {
+ margin-bottom: 23px;
+ color: rgb(138, 155, 168);
+ font-size: 19px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.description {
+ font-size: 18px;
+}
+
+.button-row {
+ margin-top: 45px;
+ margin-bottom:20px;
+}
+
+.button-row button,
+.button-row a.button {
+ background: #0095dd;
+ border: none;
+ border-radius: 5px;
+ color: #FFFFFF;
+ cursor: pointer;
+ font-size: 24px;
+ padding: 15px 0;
+ transition-duration: 150ms;
+ transition-property: background-color;
+ width: 100%;
+}
+
+.button-row a.button {
+ display: inline-block;
+ text-decoration: none;
+}
+
+.button-row a.button:active,
+.button-row a.button:hover,
+.button-row a.button:focus,
+.button-row button:active,
+.button-row button:hover,
+.button-row button:focus {
+ background: #08c;
+}
+
+
+.graphic-sync-intro {
+ background-image: url(chrome://browser/skin/fxa/sync-illustration.png);
+ background-repeat: no-repeat;
+ background-size: contain;
+ height: 231px;
+ width: 231px;
+ margin: 0 auto;
+ overflow: hidden;
+ text-indent: 100%;
+ white-space: nowrap;
+}
+
+.description,
+.button-row {
+ margin-top: 30px;
+}
+
+.links {
+ margin: 20px 0;
+}
+
+@media only screen and (max-width: 500px) {
+ html {
+ background: #fff;
+ }
+
+ #stage {
+ box-shadow: none;
+ margin: 30px auto 0 auto;
+ min-height: none;
+ min-width: 320px;
+ padding: 0 10px;
+ width: 100%;
+ }
+
+ .button-row {
+ margin-top: 20px;
+ }
+
+ .button-row button,
+ .button-row a.button {
+ padding: 10px 0;
+ }
+
+}
+
+/* Retina */
+@media
+only screen and (min-device-pixel-ratio: 2),
+only screen and ( min-resolution: 192dpi),
+only screen and ( min-resolution: 2dppx) {
+ .graphic-sync-intro {
+ background-image: url(chrome://browser/skin/fxa/sync-illustration@2x.png);
+ }
+}
diff --git a/application/basilisk/base/content/aboutaccounts/normalize.css b/application/basilisk/base/content/aboutaccounts/normalize.css
new file mode 100644
index 000000000..c02ab25de
--- /dev/null
+++ b/application/basilisk/base/content/aboutaccounts/normalize.css
@@ -0,0 +1,402 @@
+/*! normalize.css v2.1.3 | MIT License | git.io/normalize */
+
+/* ==========================================================================
+ HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined in IE 8/9.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * Correct `inline-block` display not defined in IE 8/9.
+ */
+
+audio,
+canvas,
+video {
+ display: inline-block;
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9.
+ * Hide the `template` element in IE, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* ==========================================================================
+ Base
+ ========================================================================== */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background: transparent;
+}
+
+/**
+ * Address `outline` inconsistency between Chrome and other browsers.
+ */
+
+a:focus {
+ outline: thin dotted;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* ==========================================================================
+ Typography
+ ========================================================================== */
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari 5, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9, Safari 5, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari 5 and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Correct font family set oddly in Safari 5 and Chrome.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, serif;
+ font-size: 1em;
+}
+
+/**
+ * Improve readability of pre-formatted text in all browsers.
+ */
+
+pre {
+ white-space: pre-wrap;
+}
+
+/**
+ * Set consistent quote types.
+ */
+
+q {
+ quotes: "\201C" "\201D" "\2018" "\2019";
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* ==========================================================================
+ Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow displayed oddly in IE 9.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* ==========================================================================
+ Figures
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari 5.
+ */
+
+figure {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Forms
+ ========================================================================== */
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * 1. Correct font family not being inherited in all browsers.
+ * 2. Correct font size not being inherited in all browsers.
+ * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome.
+ */
+
+button,
+input,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+button,
+input {
+ line-height: normal;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+.
+ * Correct `select` style inheritance in Firefox 4+ and Opera.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome.
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ box-sizing: content-box; /* 2 */
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari 5 and Chrome
+ * on OS X.
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * 1. Remove default vertical scrollbar in IE 8/9.
+ * 2. Improve readability and alignment in all browsers.
+ */
+
+textarea {
+ overflow: auto; /* 1 */
+ vertical-align: top; /* 2 */
+}
+
+/* ==========================================================================
+ Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/application/basilisk/base/content/abouthealthreport/abouthealth.css b/application/basilisk/base/content/abouthealthreport/abouthealth.css
new file mode 100644
index 000000000..3dd40fc24
--- /dev/null
+++ b/application/basilisk/base/content/abouthealthreport/abouthealth.css
@@ -0,0 +1,15 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+html, body {
+ height: 100%;
+}
+
+#remote-report {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ display: flex;
+}
diff --git a/application/basilisk/base/content/abouthealthreport/abouthealth.js b/application/basilisk/base/content/abouthealthreport/abouthealth.js
new file mode 100644
index 000000000..e04497eae
--- /dev/null
+++ b/application/basilisk/base/content/abouthealthreport/abouthealth.js
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const prefs = new Preferences("datareporting.healthreport.");
+
+const PREF_UNIFIED = "toolkit.telemetry.unified";
+const PREF_REPORTING_URL = "datareporting.healthreport.about.reportUrl";
+
+var healthReportWrapper = {
+ init() {
+ let iframe = document.getElementById("remote-report");
+ iframe.addEventListener("load", healthReportWrapper.initRemotePage);
+ iframe.src = this._getReportURI().spec;
+ prefs.observe("uploadEnabled", this.updatePrefState, healthReportWrapper);
+ },
+
+ uninit() {
+ prefs.ignore("uploadEnabled", this.updatePrefState, healthReportWrapper);
+ },
+
+ _getReportURI() {
+ let url = Services.urlFormatter.formatURLPref(PREF_REPORTING_URL);
+ return Services.io.newURI(url);
+ },
+
+ setDataSubmission(enabled) {
+ MozSelfSupport.healthReportDataSubmissionEnabled = enabled;
+ this.updatePrefState();
+ },
+
+ updatePrefState() {
+ try {
+ let prefsObj = {
+ enabled: MozSelfSupport.healthReportDataSubmissionEnabled,
+ };
+ healthReportWrapper.injectData("prefs", prefsObj);
+ } catch (ex) {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PREFS_FAILED);
+ }
+ },
+
+ sendTelemetryPingList() {
+ console.log("AboutHealthReport: Collecting Telemetry ping list.");
+ MozSelfSupport.getTelemetryPingList().then((list) => {
+ console.log("AboutHealthReport: Sending Telemetry ping list.");
+ this.injectData("telemetry-ping-list", list);
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Collecting ping list failed: " + ex);
+ });
+ },
+
+ sendTelemetryPingData(pingId) {
+ console.log("AboutHealthReport: Collecting Telemetry ping data.");
+ MozSelfSupport.getTelemetryPing(pingId).then((ping) => {
+ console.log("AboutHealthReport: Sending Telemetry ping data.");
+ this.injectData("telemetry-ping-data", {
+ id: pingId,
+ pingData: ping,
+ });
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Loading ping data failed: " + ex);
+ this.injectData("telemetry-ping-data", {
+ id: pingId,
+ error: "error-generic",
+ });
+ });
+ },
+
+ sendCurrentEnvironment() {
+ console.log("AboutHealthReport: Sending Telemetry environment data.");
+ MozSelfSupport.getCurrentTelemetryEnvironment().then((environment) => {
+ this.injectData("telemetry-current-environment-data", environment);
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Collecting current environment data failed: " + ex);
+ });
+ },
+
+ sendCurrentPingData() {
+ console.log("AboutHealthReport: Sending current Telemetry ping data.");
+ MozSelfSupport.getCurrentTelemetrySubsessionPing().then((ping) => {
+ this.injectData("telemetry-current-ping-data", ping);
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Collecting current ping data failed: " + ex);
+ });
+ },
+
+ injectData(type, content) {
+ let report = this._getReportURI();
+
+ // file URIs can't be used for targetOrigin, so we use "*" for this special case
+ // in all other cases, pass in the URL to the report so we properly restrict the message dispatch
+ let reportUrl = report.scheme == "file" ? "*" : report.spec;
+
+ let data = {
+ type,
+ content
+ }
+
+ let iframe = document.getElementById("remote-report");
+ iframe.contentWindow.postMessage(data, reportUrl);
+ },
+
+ handleRemoteCommand(evt) {
+ // Do an origin check to harden against the frame content being loaded from unexpected locations.
+ let allowedPrincipal = Services.scriptSecurityManager.getCodebasePrincipal(this._getReportURI());
+ let targetPrincipal = evt.target.nodePrincipal;
+ if (!allowedPrincipal.equals(targetPrincipal)) {
+ Cu.reportError(`Origin check failed for message "${evt.detail.command}": ` +
+ `target origin is "${targetPrincipal.origin}", expected "${allowedPrincipal.origin}"`);
+ return;
+ }
+
+ switch (evt.detail.command) {
+ case "DisableDataSubmission":
+ this.setDataSubmission(false);
+ break;
+ case "EnableDataSubmission":
+ this.setDataSubmission(true);
+ break;
+ case "RequestCurrentPrefs":
+ this.updatePrefState();
+ break;
+ case "RequestTelemetryPingList":
+ this.sendTelemetryPingList();
+ break;
+ case "RequestTelemetryPingData":
+ this.sendTelemetryPingData(evt.detail.id);
+ break;
+ case "RequestCurrentEnvironment":
+ this.sendCurrentEnvironment();
+ break;
+ case "RequestCurrentPingData":
+ this.sendCurrentPingData();
+ break;
+ default:
+ Cu.reportError("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
+ break;
+ }
+ },
+
+ initRemotePage() {
+ let iframe = document.getElementById("remote-report").contentDocument;
+ iframe.addEventListener("RemoteHealthReportCommand",
+ function onCommand(e) { healthReportWrapper.handleRemoteCommand(e); });
+ healthReportWrapper.updatePrefState();
+ },
+
+ // error handling
+ ERROR_INIT_FAILED: 1,
+ ERROR_PAYLOAD_FAILED: 2,
+ ERROR_PREFS_FAILED: 3,
+
+ reportFailure(error) {
+ let details = {
+ errorType: error,
+ }
+ healthReportWrapper.injectData("error", details);
+ },
+
+ handleInitFailure() {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_INIT_FAILED);
+ },
+
+ handlePayloadFailure() {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PAYLOAD_FAILED);
+ },
+}
+
+window.addEventListener("load", function() { healthReportWrapper.init(); });
+window.addEventListener("unload", function() { healthReportWrapper.uninit(); });
diff --git a/application/basilisk/base/content/abouthealthreport/abouthealth.xhtml b/application/basilisk/base/content/abouthealthreport/abouthealth.xhtml
new file mode 100644
index 000000000..464635788
--- /dev/null
+++ b/application/basilisk/base/content/abouthealthreport/abouthealth.xhtml
@@ -0,0 +1,31 @@
+
+
+
+ %htmlDTD;
+
+ %brandDTD;
+
+ %securityPrefsDTD;
+
+ %aboutHealthReportDTD;
+]>
+
+
+
+
&abouthealth.pagetitle;
+
+
+
+
+
+
+
+
+
diff --git a/application/basilisk/base/content/abouthome/aboutHome.css b/application/basilisk/base/content/abouthome/aboutHome.css
new file mode 100644
index 000000000..c40235c9e
--- /dev/null
+++ b/application/basilisk/base/content/abouthome/aboutHome.css
@@ -0,0 +1,371 @@
+%if 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+%endif
+
+html {
+ font: message-box;
+ font-size: 100%;
+ background-color: hsl(0,0%,95%);
+ color: #000;
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ width: 100%;
+ height: 100%;
+}
+
+input,
+button {
+ font-size: inherit;
+ font-family: inherit;
+}
+
+a {
+ color: -moz-nativehyperlinktext;
+ text-decoration: none;
+}
+
+.spacer {
+ -moz-box-flex: 1;
+}
+
+#topSection {
+ text-align: center;
+}
+
+#brandLogo {
+ height: 192px;
+ width: 192px;
+ margin: 22px auto 31px;
+ background-image: url("chrome://branding/content/about-logo.png");
+ background-size: 192px auto;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+#searchIconAndTextContainer {
+ width: 470px;
+}
+
+#searchIconAndTextContainer {
+ display: -moz-box;
+ height: 36px;
+ position: relative;
+}
+
+#searchIcon {
+ border: 1px transparent;
+ padding: 0;
+ margin: 0;
+ width: 36px;
+ height: 36px;
+ background: url("chrome://browser/skin/search-indicator-magnifying-glass.svg") center center no-repeat;
+ position: absolute;
+}
+
+#searchText {
+ margin-left: 0;
+ -moz-box-flex: 1;
+ padding-top: 6px;
+ padding-bottom: 6px;
+ padding-inline-start: 34px;
+ padding-inline-end: 8px;
+ background: hsla(0,0%,100%,.9) padding-box;
+ border: 1px solid;
+ border-radius: 2px 0 0 2px;
+ border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+ box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
+ 0 0 2px hsla(210,65%,9%,.1) inset,
+ 0 1px 0 hsla(0,0%,100%,.2);
+ color: inherit;
+ unicode-bidi: plaintext;
+}
+
+#searchText:dir(rtl) {
+ border-radius: 0 2px 2px 0;
+}
+
+#searchText[aria-expanded="true"] {
+ border-radius: 2px 0 0 0;
+}
+
+#searchText[aria-expanded="true"]:dir(rtl) {
+ border-radius: 0 2px 0 0;
+}
+
+#searchText[keepfocus],
+#searchText:focus,
+#searchText[autofocus] {
+ border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6);
+}
+
+#searchSubmit {
+ margin-inline-start: -1px;
+ color: transparent;
+ background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go") center center no-repeat, linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
+ padding: 0;
+ border: 1px solid;
+ border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+ border-radius: 0 2px 2px 0;
+ border-inline-start: 1px solid transparent;
+ box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset,
+ 0 1px 0 hsla(0,0%,100%,.2);
+ cursor: pointer;
+ transition-property: background-color, border-color, box-shadow;
+ transition-duration: 150ms;
+ width: 50px;
+}
+
+#searchSubmit:dir(rtl) {
+ border-radius: 2px 0 0 2px;
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl"), linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1));
+}
+
+#searchText:focus + #searchSubmit,
+#searchText[keepfocus] + #searchSubmit,
+#searchText + #searchSubmit:hover,
+#searchText[autofocus] + #searchSubmit {
+ border-color: #59b5fc #45a3e7 #3294d5;
+}
+
+#searchText:focus + #searchSubmit,
+#searchText[keepfocus] + #searchSubmit,
+#searchText[autofocus] + #searchSubmit {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#4cb1ff, #1793e5);
+ box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
+ 0 0 0 1px hsla(0,0%,100%,.1) inset,
+ 0 1px 0 hsla(210,54%,20%,.03);
+}
+
+#searchText:focus + #searchSubmit:dir(rtl),
+#searchText[keepfocus] + #searchSubmit:dir(rtl),
+#searchText[autofocus] + #searchSubmit:dir(rtl) {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl-inverted"), linear-gradient(#4cb1ff, #1793e5);
+}
+
+#searchText + #searchSubmit:hover {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#66bdff, #0d9eff);
+ box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
+ 0 0 0 1px hsla(0,0%,100%,.1) inset,
+ 0 1px 0 hsla(210,54%,20%,.03),
+ 0 0 4px hsla(206,100%,20%,.2);
+}
+
+#searchText + #searchSubmit:dir(rtl):hover {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl-inverted"), linear-gradient(#66bdff, #0d9eff);
+}
+
+#searchText + #searchSubmit:hover:active {
+ box-shadow: 0 1px 1px hsla(211,79%,6%,.1) inset,
+ 0 0 1px hsla(211,79%,6%,.2) inset;
+ transition-duration: 0ms;
+}
+
+#launcher {
+ display: -moz-box;
+ -moz-box-align: center;
+ -moz-box-pack: center;
+ width: 100%;
+ background-color: hsla(0,0%,0%,.03);
+ border-top: 1px solid hsla(0,0%,0%,.03);
+ box-shadow: 0 1px 2px hsla(0,0%,0%,.02) inset,
+ 0 -1px 0 hsla(0,0%,100%,.25);
+}
+
+#launcher:not([session]),
+body[narrow] #launcher[session] {
+ display: block; /* display separator and restore button on separate lines */
+ text-align: center;
+ white-space: nowrap; /* prevent navigational buttons from wrapping */
+}
+
+.launchButton {
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ margin: 16px 1px;
+ padding: 14px 6px;
+ min-width: 88px;
+ max-width: 176px;
+ max-height: 85px;
+ vertical-align: top;
+ white-space: normal;
+ background: transparent padding-box;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ color: #525c66;
+ font-size: 75%;
+ cursor: pointer;
+ transition-property: background-color, border-color, box-shadow;
+ transition-duration: 150ms;
+}
+
+body[narrow] #launcher[session] > .launchButton {
+ margin: 4px 1px;
+}
+
+.launchButton:hover {
+ background-color: hsla(211,79%,6%,.03);
+ border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+}
+
+.launchButton:hover:active {
+ background-image: linear-gradient(hsla(211,79%,6%,.02), hsla(211,79%,6%,.05));
+ border-color: hsla(210,54%,20%,.2) hsla(210,54%,20%,.23) hsla(210,54%,20%,.25);
+ box-shadow: 0 1px 1px hsla(211,79%,6%,.05) inset,
+ 0 0 1px hsla(211,79%,6%,.1) inset;
+ transition-duration: 0ms;
+}
+
+.launchButton[hidden],
+#launcher:not([session]) > #restorePreviousSessionSeparator,
+#launcher:not([session]) > #restorePreviousSession {
+ display: none;
+}
+
+#restorePreviousSessionSeparator {
+ width: 3px;
+ height: 116px;
+ margin: 0 10px;
+ background-image: linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)),
+ linear-gradient(hsla(211,79%,6%,0), hsla(211,79%,6%,.2), hsla(211,79%,6%,0)),
+ linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0));
+ background-position: left top, center, right bottom;
+ background-size: 1px auto;
+ background-repeat: no-repeat;
+}
+
+body[narrow] #restorePreviousSessionSeparator {
+ margin: 0 auto;
+ width: 512px;
+ height: 3px;
+ background-image: linear-gradient(to right, hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)),
+ linear-gradient(to right, hsla(211,79%,6%,0), hsla(211,79%,6%,.2), hsla(211,79%,6%,0)),
+ linear-gradient(to right, hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0));
+ background-size: auto 1px;
+}
+
+#restorePreviousSession {
+ max-width: none;
+ font-size: 90%;
+}
+
+body[narrow] #restorePreviousSession {
+ font-size: 80%;
+}
+
+.launchButton::before {
+ display: block;
+ width: 32px;
+ height: 32px;
+ margin: 0 auto 6px;
+ line-height: 0; /* remove extra vertical space due to non-zero font-size */
+}
+
+#downloads::before {
+ content: url("chrome://browser/content/abouthome/downloads.png");
+}
+
+#bookmarks::before {
+ content: url("chrome://browser/content/abouthome/bookmarks.png");
+}
+
+#history::before {
+ content: url("chrome://browser/content/abouthome/history.png");
+}
+
+#addons::before {
+ content: url("chrome://browser/content/abouthome/addons.png");
+}
+
+#sync::before {
+ content: url("chrome://browser/content/abouthome/sync.png");
+}
+
+#settings::before {
+ content: url("chrome://browser/content/abouthome/settings.png");
+}
+
+#restorePreviousSession::before {
+ content: url("chrome://browser/content/abouthome/restore-large.png");
+ height: 48px;
+ width: 48px;
+ display: inline-block; /* display on same line as text label */
+ vertical-align: middle;
+ margin-bottom: 0;
+ margin-inline-end: 8px;
+}
+
+#restorePreviousSession:dir(rtl)::before {
+ transform: scaleX(-1);
+}
+
+body[narrow] #restorePreviousSession::before {
+ content: url("chrome://browser/content/abouthome/restore.png");
+ height: 32px;
+ width: 32px;
+}
+
+/* [HiDPI]
+ * At resolutions above 1dppx, prefer downscaling the 2x Retina graphics
+ * rather than upscaling the original-size ones (bug 818940).
+ */
+@media not all and (max-resolution: 1dppx) {
+ #brandLogo {
+ background-image: url("chrome://branding/content/about-logo@2x.png");
+ }
+
+ .launchButton::before,
+ #aboutMozilla::before {
+ transform: scale(.5);
+ transform-origin: 0 0;
+ }
+
+ .launchButton:dir(rtl)::before,
+ #aboutMozilla:dir(rtl)::before {
+ transform: scale(.5) translateX(32px);
+ }
+
+ #downloads::before {
+ content: url("chrome://browser/content/abouthome/downloads@2x.png");
+ }
+
+ #bookmarks::before {
+ content: url("chrome://browser/content/abouthome/bookmarks@2x.png");
+ }
+
+ #history::before {
+ content: url("chrome://browser/content/abouthome/history@2x.png");
+ }
+
+ #addons::before {
+ content: url("chrome://browser/content/abouthome/addons@2x.png");
+ }
+
+ #sync::before {
+ content: url("chrome://browser/content/abouthome/sync@2x.png");
+ }
+
+ #settings::before {
+ content: url("chrome://browser/content/abouthome/settings@2x.png");
+ }
+
+ #restorePreviousSession::before {
+ content: url("chrome://browser/content/abouthome/restore-large@2x.png");
+ }
+
+ body[narrow] #restorePreviousSession::before {
+ content: url("chrome://browser/content/abouthome/restore@2x.png");
+ }
+
+ #restorePreviousSession:dir(rtl)::before {
+ transform: scale(-0.5, 0.5) translateX(24px);
+ transform-origin: top center;
+ }
+}
+
diff --git a/application/basilisk/base/content/abouthome/aboutHome.js b/application/basilisk/base/content/abouthome/aboutHome.js
new file mode 100644
index 000000000..f7fdaee81
--- /dev/null
+++ b/application/basilisk/base/content/abouthome/aboutHome.js
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../contentSearchUI.js */
+
+var searchText;
+
+// This global tracks if the page has been set up before, to prevent double inits
+var gInitialized = false;
+var gObserver = new MutationObserver(function(mutations) {
+ for (let mutation of mutations) {
+ // The addition of the restore session button changes our width:
+ if (mutation.attributeName == "session") {
+ fitToWidth();
+ }
+ }
+});
+
+window.addEventListener("pageshow", function() {
+ // Delay search engine setup, cause browser.js::BrowserOnAboutPageLoad runs
+ // later and may use asynchronous getters.
+ window.gObserver.observe(document.documentElement, { attributes: true });
+ window.gObserver.observe(document.getElementById("launcher"), { attributes: true });
+ fitToWidth();
+ setupSearch();
+ window.addEventListener("resize", fitToWidth);
+
+ // Ask chrome to update snippets.
+ var event = new CustomEvent("AboutHomeLoad", {bubbles:true});
+ document.dispatchEvent(event);
+});
+
+window.addEventListener("pagehide", function() {
+ window.gObserver.disconnect();
+ window.removeEventListener("resize", fitToWidth);
+});
+
+window.addEventListener("keypress", ev => {
+ if (ev.defaultPrevented) {
+ return;
+ }
+
+ // don't focus the search-box on keypress if something other than the
+ // body or document element has focus - don't want to steal input from other elements
+ // Make an exception for
and elements (and input[type=button|submit])
+ // which don't usefully take keypresses anyway.
+ // (except space, which is handled below)
+ if (document.activeElement && document.activeElement != document.body &&
+ document.activeElement != document.documentElement &&
+ !["a", "button"].includes(document.activeElement.localName) &&
+ !document.activeElement.matches("input:-moz-any([type=button],[type=submit])")) {
+ return;
+ }
+
+ let modifiers = ev.ctrlKey + ev.altKey + ev.metaKey;
+ // ignore Ctrl/Cmd/Alt, but not Shift
+ // also ignore Tab, Insert, PageUp, etc., and Space
+ if (modifiers != 0 || ev.charCode == 0 || ev.charCode == 32)
+ return;
+
+ searchText.focus();
+ // need to send the first keypress outside the search-box manually to it
+ searchText.value += ev.key;
+});
+
+function onSearchSubmit(aEvent) {
+ gContentSearchController.search(aEvent);
+}
+
+
+var gContentSearchController;
+
+function setupSearch() {
+ // Set submit button label for when CSS background are disabled (e.g.
+ // high contrast mode).
+ document.getElementById("searchSubmit").value =
+ document.body.getAttribute("dir") == "ltr" ? "\u25B6" : "\u25C0";
+
+ // The "autofocus" attribute doesn't focus the form element
+ // immediately when the element is first drawn, so the
+ // attribute is also used for styling when the page first loads.
+ searchText = document.getElementById("searchText");
+ searchText.addEventListener("blur", function searchText_onBlur() {
+ searchText.removeEventListener("blur", searchText_onBlur);
+ searchText.removeAttribute("autofocus");
+ });
+
+ if (!gContentSearchController) {
+ gContentSearchController =
+ new ContentSearchUIController(searchText, searchText.parentNode,
+ "abouthome", "homepage");
+ }
+}
+
+/**
+ * Inform the test harness that we're done loading the page.
+ */
+function loadCompleted() {
+ var event = new CustomEvent("AboutHomeLoadSnippetsCompleted", {bubbles:true});
+ document.dispatchEvent(event);
+}
+
+function fitToWidth() {
+ if (document.documentElement.scrollWidth > window.innerWidth) {
+ document.body.setAttribute("narrow", "true");
+ } else if (document.body.hasAttribute("narrow")) {
+ document.body.removeAttribute("narrow");
+ fitToWidth();
+ }
+}
diff --git a/application/basilisk/base/content/abouthome/aboutHome.xhtml b/application/basilisk/base/content/abouthome/aboutHome.xhtml
new file mode 100644
index 000000000..d98b2d1b1
--- /dev/null
+++ b/application/basilisk/base/content/abouthome/aboutHome.xhtml
@@ -0,0 +1,68 @@
+
+
+
+
+
+ %htmlDTD;
+
+ %globalDTD;
+
+ %aboutHomeDTD;
+
+ %browserDTD;
+]>
+
+
+
+ &abouthome.pageTitle;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
&abouthome.downloadsButton.label;
+
&abouthome.bookmarksButton.label;
+
&abouthome.historyButton.label;
+
&abouthome.addonsButton.label;
+
&abouthome.syncButton.label;
+#ifdef XP_WIN
+
&abouthome.preferencesButtonWin.label;
+#else
+
&abouthome.preferencesButtonUnix.label;
+#endif
+
+
&historyRestoreLastSession.label;
+
+
+
diff --git a/application/basilisk/base/content/abouthome/addons.png b/application/basilisk/base/content/abouthome/addons.png
new file mode 100644
index 000000000..41519ce49
Binary files /dev/null and b/application/basilisk/base/content/abouthome/addons.png differ
diff --git a/application/basilisk/base/content/abouthome/addons@2x.png b/application/basilisk/base/content/abouthome/addons@2x.png
new file mode 100644
index 000000000..d4d04ee8c
Binary files /dev/null and b/application/basilisk/base/content/abouthome/addons@2x.png differ
diff --git a/application/basilisk/base/content/abouthome/bookmarks.png b/application/basilisk/base/content/abouthome/bookmarks.png
new file mode 100644
index 000000000..5c7e194a6
Binary files /dev/null and b/application/basilisk/base/content/abouthome/bookmarks.png differ
diff --git a/application/basilisk/base/content/abouthome/bookmarks@2x.png b/application/basilisk/base/content/abouthome/bookmarks@2x.png
new file mode 100644
index 000000000..7ede00744
Binary files /dev/null and b/application/basilisk/base/content/abouthome/bookmarks@2x.png differ
diff --git a/application/basilisk/base/content/abouthome/downloads.png b/application/basilisk/base/content/abouthome/downloads.png
new file mode 100644
index 000000000..3d4d10e7a
Binary files /dev/null and b/application/basilisk/base/content/abouthome/downloads.png differ
diff --git a/application/basilisk/base/content/abouthome/downloads@2x.png b/application/basilisk/base/content/abouthome/downloads@2x.png
new file mode 100644
index 000000000..d384a22c6
Binary files /dev/null and b/application/basilisk/base/content/abouthome/downloads@2x.png differ
diff --git a/application/basilisk/base/content/abouthome/history.png b/application/basilisk/base/content/abouthome/history.png
new file mode 100644
index 000000000..ae742b1aa
Binary files /dev/null and b/application/basilisk/base/content/abouthome/history.png differ
diff --git a/application/basilisk/base/content/abouthome/history@2x.png b/application/basilisk/base/content/abouthome/history@2x.png
new file mode 100644
index 000000000..696902e7c
Binary files /dev/null and b/application/basilisk/base/content/abouthome/history@2x.png differ
diff --git a/application/basilisk/base/content/abouthome/restore-large.png b/application/basilisk/base/content/abouthome/restore-large.png
new file mode 100644
index 000000000..ef593e6e1
Binary files /dev/null and b/application/basilisk/base/content/abouthome/restore-large.png differ
diff --git a/application/basilisk/base/content/abouthome/restore-large@2x.png b/application/basilisk/base/content/abouthome/restore-large@2x.png
new file mode 100644
index 000000000..d5c71d0b0
Binary files /dev/null and b/application/basilisk/base/content/abouthome/restore-large@2x.png differ
diff --git a/application/basilisk/base/content/abouthome/restore.png b/application/basilisk/base/content/abouthome/restore.png
new file mode 100644
index 000000000..5c3d6f437
Binary files /dev/null and b/application/basilisk/base/content/abouthome/restore.png differ
diff --git a/application/basilisk/base/content/abouthome/restore@2x.png b/application/basilisk/base/content/abouthome/restore@2x.png
new file mode 100644
index 000000000..5acb63052
Binary files /dev/null and b/application/basilisk/base/content/abouthome/restore@2x.png differ
diff --git a/application/basilisk/base/content/abouthome/settings.png b/application/basilisk/base/content/abouthome/settings.png
new file mode 100644
index 000000000..4b0c30990
Binary files /dev/null and b/application/basilisk/base/content/abouthome/settings.png differ
diff --git a/application/basilisk/base/content/abouthome/settings@2x.png b/application/basilisk/base/content/abouthome/settings@2x.png
new file mode 100644
index 000000000..c77cb9a92
Binary files /dev/null and b/application/basilisk/base/content/abouthome/settings@2x.png differ
diff --git a/application/basilisk/base/content/abouthome/sync.png b/application/basilisk/base/content/abouthome/sync.png
new file mode 100644
index 000000000..11e40cc93
Binary files /dev/null and b/application/basilisk/base/content/abouthome/sync.png differ
diff --git a/application/basilisk/base/content/abouthome/sync@2x.png b/application/basilisk/base/content/abouthome/sync@2x.png
new file mode 100644
index 000000000..6354f5bf9
Binary files /dev/null and b/application/basilisk/base/content/abouthome/sync@2x.png differ
diff --git a/application/basilisk/base/content/baseMenuOverlay.xul b/application/basilisk/base/content/baseMenuOverlay.xul
new file mode 100644
index 000000000..fb763fb44
--- /dev/null
+++ b/application/basilisk/base/content/baseMenuOverlay.xul
@@ -0,0 +1,118 @@
+
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+%brandDTD;
+
+%baseMenuOverlayDTD;
+]>
+
diff --git a/application/basilisk/base/content/blockedSite.xhtml b/application/basilisk/base/content/blockedSite.xhtml
new file mode 100644
index 000000000..df33bfacb
--- /dev/null
+++ b/application/basilisk/base/content/blockedSite.xhtml
@@ -0,0 +1,191 @@
+
+
+
+ %htmlDTD;
+
+ %globalDTD;
+
+ %brandDTD;
+
+ %blockedSiteDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
&safeb.blocked.phishingPage.title2;
+ &safeb.blocked.malwarePage.title;
+ &safeb.blocked.unwantedPage.title;
+
+
+
+
+
+
+
&safeb.blocked.phishingPage.shortDesc2;
+
&safeb.blocked.malwarePage.shortDesc;
+
&safeb.blocked.unwantedPage.shortDesc;
+
+
+
+
+
&safeb.blocked.phishingPage.longDesc2;
+
&safeb.blocked.malwarePage.longDesc;
+
&safeb.blocked.unwantedPage.longDesc;
+
+
+
+
+
+
+ &safeb.palm.decline.label;
+
+
+
+
+
+
diff --git a/application/basilisk/base/content/browser-addons.js b/application/basilisk/base/content/browser-addons.js
new file mode 100644
index 000000000..378437b2b
--- /dev/null
+++ b/application/basilisk/base/content/browser-addons.js
@@ -0,0 +1,869 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Removes a doorhanger notification if all of the installs it was notifying
+// about have ended in some way.
+function removeNotificationOnEnd(notification, installs) {
+ let count = installs.length;
+
+ function maybeRemove(install) {
+ install.removeListener(this);
+
+ if (--count == 0) {
+ // Check that the notification is still showing
+ let current = PopupNotifications.getNotification(notification.id, notification.browser);
+ if (current === notification)
+ notification.remove();
+ }
+ }
+
+ for (let install of installs) {
+ install.addListener({
+ onDownloadCancelled: maybeRemove,
+ onDownloadFailed: maybeRemove,
+ onInstallFailed: maybeRemove,
+ onInstallEnded: maybeRemove
+ });
+ }
+}
+
+const gXPInstallObserver = {
+ _findChildShell(aDocShell, aSoughtShell) {
+ if (aDocShell == aSoughtShell)
+ return aDocShell;
+
+ var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem);
+ for (var i = 0; i < node.childCount; ++i) {
+ var docShell = node.getChildAt(i);
+ docShell = this._findChildShell(docShell, aSoughtShell);
+ if (docShell == aSoughtShell)
+ return docShell;
+ }
+ return null;
+ },
+
+ _getBrowser(aDocShell) {
+ for (let browser of gBrowser.browsers) {
+ if (this._findChildShell(browser.docShell, aDocShell))
+ return browser;
+ }
+ return null;
+ },
+
+ pendingInstalls: new WeakMap(),
+
+ showInstallConfirmation(browser, installInfo, height = undefined) {
+ // If the confirmation notification is already open cache the installInfo
+ // and the new confirmation will be shown later
+ if (PopupNotifications.getNotification("addon-install-confirmation", browser)) {
+ let pending = this.pendingInstalls.get(browser);
+ if (pending) {
+ pending.push(installInfo);
+ } else {
+ this.pendingInstalls.set(browser, [installInfo]);
+ }
+ return;
+ }
+
+ let showNextConfirmation = () => {
+ // Make sure the browser is still alive.
+ if (gBrowser.browsers.indexOf(browser) == -1)
+ return;
+
+ let pending = this.pendingInstalls.get(browser);
+ if (pending && pending.length)
+ this.showInstallConfirmation(browser, pending.shift());
+ }
+
+ // If all installs have already been cancelled in some way then just show
+ // the next confirmation
+ if (installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)) {
+ showNextConfirmation();
+ return;
+ }
+
+ const anchorID = "addons-notification-icon";
+
+ // Make notifications persistent
+ var options = {
+ displayURI: installInfo.originatingURI,
+ persistent: true,
+ };
+
+ let acceptInstallation = () => {
+ for (let install of installInfo.installs)
+ install.install();
+ installInfo = null;
+
+ Services.telemetry
+ .getHistogramById("SECURITY_UI")
+ .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH);
+ };
+
+ let cancelInstallation = () => {
+ if (installInfo) {
+ for (let install of installInfo.installs) {
+ // The notification may have been closed because the add-ons got
+ // cancelled elsewhere, only try to cancel those that are still
+ // pending install.
+ if (install.state != AddonManager.STATE_CANCELLED)
+ install.cancel();
+ }
+ }
+
+ showNextConfirmation();
+ };
+
+ let unsigned = installInfo.installs.filter(i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING);
+ let someUnsigned = unsigned.length > 0 && unsigned.length < installInfo.installs.length;
+
+ options.eventCallback = (aEvent) => {
+ switch (aEvent) {
+ case "removed":
+ cancelInstallation();
+ break;
+ case "shown":
+ let addonList = document.getElementById("addon-install-confirmation-content");
+ while (addonList.firstChild)
+ addonList.firstChild.remove();
+
+ for (let install of installInfo.installs) {
+ let container = document.createElement("hbox");
+
+ let name = document.createElement("label");
+ name.setAttribute("value", install.addon.name);
+ name.setAttribute("class", "addon-install-confirmation-name");
+ container.appendChild(name);
+
+ if (someUnsigned && install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+ let unsignedLabel = document.createElement("label");
+ unsignedLabel.setAttribute("value",
+ gNavigatorBundle.getString("addonInstall.unsigned"));
+ unsignedLabel.setAttribute("class",
+ "addon-install-confirmation-unsigned");
+ container.appendChild(unsignedLabel);
+ }
+
+ addonList.appendChild(container);
+ }
+ break;
+ }
+ };
+
+ options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+
+ let messageString;
+ let notification = document.getElementById("addon-install-confirmation-notification");
+ if (unsigned.length == installInfo.installs.length) {
+ // None of the add-ons are verified
+ messageString = gNavigatorBundle.getString("addonConfirmInstallUnsigned.message");
+ notification.setAttribute("warning", "true");
+ options.learnMoreURL += "unsigned-addons";
+ } else if (unsigned.length == 0) {
+ // All add-ons are verified or don't need to be verified
+ messageString = gNavigatorBundle.getString("addonConfirmInstall.message");
+ notification.removeAttribute("warning");
+ options.learnMoreURL += "find-and-install-add-ons";
+ } else {
+ // Some of the add-ons are unverified, the list of names will indicate
+ // which
+ messageString = gNavigatorBundle.getString("addonConfirmInstallSomeUnsigned.message");
+ notification.setAttribute("warning", "true");
+ options.learnMoreURL += "unsigned-addons";
+ }
+
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+
+ messageString = PluralForm.get(installInfo.installs.length, messageString);
+ messageString = messageString.replace("#1", brandShortName);
+ messageString = messageString.replace("#2", installInfo.installs.length);
+
+ let action = {
+ label: gNavigatorBundle.getString("addonInstall.acceptButton.label"),
+ accessKey: gNavigatorBundle.getString("addonInstall.acceptButton.accesskey"),
+ callback: acceptInstallation,
+ };
+
+ let secondaryAction = {
+ label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
+ accessKey: gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"),
+ callback: () => {},
+ };
+
+ if (height) {
+ notification.style.minHeight = height + "px";
+ }
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ if (tab) {
+ gBrowser.selectedTab = tab;
+ }
+
+ let popup = PopupNotifications.show(browser, "addon-install-confirmation",
+ messageString, anchorID, action,
+ [secondaryAction], options);
+
+ removeNotificationOnEnd(popup, installInfo.installs);
+
+ Services.telemetry
+ .getHistogramById("SECURITY_UI")
+ .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
+ },
+
+ observe(aSubject, aTopic, aData) {
+ var brandBundle = document.getElementById("bundle_brand");
+ var installInfo = aSubject.wrappedJSObject;
+ var browser = installInfo.browser;
+
+ // Make sure the browser is still alive.
+ if (!browser || gBrowser.browsers.indexOf(browser) == -1)
+ return;
+
+ const anchorID = "addons-notification-icon";
+ var messageString, action;
+ var brandShortName = brandBundle.getString("brandShortName");
+
+ var notificationID = aTopic;
+ // Make notifications persistent
+ var options = {
+ displayURI: installInfo.originatingURI,
+ persistent: true,
+ hideClose: true,
+ timeout: Date.now() + 30000,
+ };
+
+ switch (aTopic) {
+ case "addon-install-disabled": {
+ notificationID = "xpinstall-disabled";
+ let secondaryActions = null;
+
+ if (gPrefService.prefIsLocked("xpinstall.enabled")) {
+ messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked");
+ buttons = [];
+ } else {
+ messageString = gNavigatorBundle.getString("xpinstallDisabledMessage");
+
+ action = {
+ label: gNavigatorBundle.getString("xpinstallDisabledButton"),
+ accessKey: gNavigatorBundle.getString("xpinstallDisabledButton.accesskey"),
+ callback: function editPrefs() {
+ gPrefService.setBoolPref("xpinstall.enabled", true);
+ }
+ };
+
+ secondaryActions = [{
+ label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
+ accessKey: gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"),
+ callback: () => {},
+ }];
+ }
+
+ PopupNotifications.show(browser, notificationID, messageString, anchorID,
+ action, secondaryActions, options);
+ break; }
+ case "addon-install-origin-blocked": {
+ messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage",
+ [brandShortName]);
+
+ options.removeOnDismissal = true;
+ options.persistent = false;
+
+ let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI");
+ secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
+ let popup = PopupNotifications.show(browser, notificationID,
+ messageString, anchorID,
+ null, null, options);
+ removeNotificationOnEnd(popup, installInfo.installs);
+ break; }
+ case "addon-install-blocked": {
+ messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage",
+ [brandShortName]);
+
+ let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI");
+ action = {
+ label: gNavigatorBundle.getString("xpinstallPromptAllowButton"),
+ accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"),
+ callback() {
+ secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH);
+ installInfo.install();
+ }
+ };
+ let secondaryAction = {
+ label: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow"),
+ accessKey: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow.accesskey"),
+ callback: () => {},
+ };
+
+ secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
+ let popup = PopupNotifications.show(browser, notificationID,
+ messageString, anchorID,
+ action, [secondaryAction], options);
+ removeNotificationOnEnd(popup, installInfo.installs);
+ break; }
+ case "addon-install-started": {
+ let needsDownload = function needsDownload(aInstall) {
+ return aInstall.state != AddonManager.STATE_DOWNLOADED;
+ }
+ // If all installs have already been downloaded then there is no need to
+ // show the download progress
+ if (!installInfo.installs.some(needsDownload))
+ return;
+ notificationID = "addon-progress";
+ messageString = gNavigatorBundle.getString("addonDownloadingAndVerifying");
+ messageString = PluralForm.get(installInfo.installs.length, messageString);
+ messageString = messageString.replace("#1", installInfo.installs.length);
+ options.installs = installInfo.installs;
+ options.contentWindow = browser.contentWindow;
+ options.sourceURI = browser.currentURI;
+ options.eventCallback = function(aEvent) {
+ switch (aEvent) {
+ case "shown":
+ let notificationElement = [...this.owner.panel.childNodes]
+ .find(n => n.notification == this);
+ if (notificationElement) {
+ if (Preferences.get("xpinstall.customConfirmationUI", false)) {
+ notificationElement.setAttribute("mainactiondisabled", "true");
+ } else {
+ notificationElement.button.hidden = true;
+ }
+ }
+ break;
+ case "removed":
+ options.contentWindow = null;
+ options.sourceURI = null;
+ break;
+ }
+ };
+ action = {
+ label: gNavigatorBundle.getString("addonInstall.acceptButton.label"),
+ accessKey: gNavigatorBundle.getString("addonInstall.acceptButton.accesskey"),
+ callback: () => {},
+ };
+ let secondaryAction = {
+ label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
+ accessKey: gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"),
+ callback: () => {
+ for (let install of installInfo.installs) {
+ if (install.state != AddonManager.STATE_CANCELLED) {
+ install.cancel();
+ }
+ }
+ },
+ };
+ let notification = PopupNotifications.show(browser, notificationID, messageString,
+ anchorID, action,
+ [secondaryAction], options);
+ notification._startTime = Date.now();
+
+ break; }
+ case "addon-install-failed": {
+ options.removeOnDismissal = true;
+ options.persistent = false;
+
+ // TODO This isn't terribly ideal for the multiple failure case
+ for (let install of installInfo.installs) {
+ let host;
+ try {
+ host = options.displayURI.host;
+ } catch (e) {
+ // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
+ }
+
+ if (!host)
+ host = (install.sourceURI instanceof Ci.nsIStandardURL) &&
+ install.sourceURI.host;
+
+ let error = (host || install.error == 0) ? "addonInstallError" : "addonLocalInstallError";
+ let args;
+ if (install.error < 0) {
+ error += install.error;
+ args = [brandShortName, install.name];
+ } else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ error += "Blocklisted";
+ args = [install.name];
+ } else {
+ error += "Incompatible";
+ args = [brandShortName, Services.appinfo.version, install.name];
+ }
+
+ // Add Learn More link when refusing to install an unsigned add-on
+ if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
+ options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
+ }
+
+ messageString = gNavigatorBundle.getFormattedString(error, args);
+
+ PopupNotifications.show(browser, notificationID, messageString, anchorID,
+ action, null, options);
+
+ // Can't have multiple notifications with the same ID, so stop here.
+ break;
+ }
+ this._removeProgressNotification(browser);
+ break; }
+ case "addon-install-confirmation": {
+ let showNotification = () => {
+ let height = undefined;
+
+ if (PopupNotifications.isPanelOpen) {
+ let rect = document.getElementById("addon-progress-notification").getBoundingClientRect();
+ height = rect.height;
+ }
+
+ this._removeProgressNotification(browser);
+ this.showInstallConfirmation(browser, installInfo, height);
+ };
+
+ let progressNotification = PopupNotifications.getNotification("addon-progress", browser);
+ if (progressNotification) {
+ let downloadDuration = Date.now() - progressNotification._startTime;
+ let securityDelay = Services.prefs.getIntPref("security.dialog_enable_delay") - downloadDuration;
+ if (securityDelay > 0) {
+ setTimeout(() => {
+ // The download may have been cancelled during the security delay
+ if (PopupNotifications.getNotification("addon-progress", browser))
+ showNotification();
+ }, securityDelay);
+ break;
+ }
+ }
+ showNotification();
+ break; }
+ case "addon-install-complete": {
+ let needsRestart = installInfo.installs.some(function(i) {
+ return i.addon.pendingOperations != AddonManager.PENDING_NONE;
+ });
+
+ let secondaryActions = null;
+
+ if (needsRestart) {
+ notificationID = "addon-install-restart";
+ messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart");
+ action = {
+ label: gNavigatorBundle.getString("addonInstallRestartButton"),
+ accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"),
+ callback() {
+ BrowserUtils.restartApplication();
+ }
+ };
+ secondaryActions = [{
+ label: gNavigatorBundle.getString("addonInstallRestartIgnoreButton"),
+ accessKey: gNavigatorBundle.getString("addonInstallRestartIgnoreButton.accesskey"),
+ callback: () => {},
+ }];
+ } else {
+ messageString = gNavigatorBundle.getString("addonsInstalled");
+ action = null;
+ }
+
+ messageString = PluralForm.get(installInfo.installs.length, messageString);
+ messageString = messageString.replace("#1", installInfo.installs[0].name);
+ messageString = messageString.replace("#2", installInfo.installs.length);
+ messageString = messageString.replace("#3", brandShortName);
+
+ // Remove notification on dismissal, since it's possible to cancel the
+ // install through the addons manager UI, making the "restart" prompt
+ // irrelevant.
+ options.removeOnDismissal = true;
+ options.persistent = false;
+
+ PopupNotifications.show(browser, notificationID, messageString, anchorID,
+ action, secondaryActions, options);
+ break; }
+ }
+ },
+ _removeProgressNotification(aBrowser) {
+ let notification = PopupNotifications.getNotification("addon-progress", aBrowser);
+ if (notification)
+ notification.remove();
+ }
+};
+
+const gExtensionsNotifications = {
+ initialized: false,
+ init() {
+ this.updateAlerts();
+ this.boundUpdate = this.updateAlerts.bind(this);
+ ExtensionsUI.on("change", this.boundUpdate);
+ this.initialized = true;
+ },
+
+ uninit() {
+ // uninit() can race ahead of init() in some cases, if that happens,
+ // we have no handler to remove.
+ if (!this.initialized) {
+ return;
+ }
+ ExtensionsUI.off("change", this.boundUpdate);
+ },
+
+ updateAlerts() {
+ let sideloaded = ExtensionsUI.sideloaded;
+ let updates = ExtensionsUI.updates;
+ if (sideloaded.size + updates.size == 0) {
+ gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_ADDONS);
+ } else {
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_ADDONS,
+ "addon-alert");
+ }
+
+ let container = document.getElementById("PanelUI-footer-addons");
+
+ while (container.firstChild) {
+ container.firstChild.remove();
+ }
+
+ const DEFAULT_EXTENSION_ICON =
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ let items = 0;
+ for (let update of updates) {
+ if (++items > 4) {
+ break;
+ }
+
+ let button = document.createElement("toolbarbutton");
+ let text = gNavigatorBundle.getFormattedString("webextPerms.updateMenuItem", [update.addon.name]);
+ button.setAttribute("label", text);
+
+ let icon = update.addon.iconURL || DEFAULT_EXTENSION_ICON;
+ button.setAttribute("image", icon);
+
+ button.addEventListener("click", evt => {
+ ExtensionsUI.showUpdate(gBrowser, update);
+ });
+
+ container.appendChild(button);
+ }
+
+ let appName;
+ for (let addon of sideloaded) {
+ if (++items > 4) {
+ break;
+ }
+ if (!appName) {
+ let brandBundle = document.getElementById("bundle_brand");
+ appName = brandBundle.getString("brandShortName");
+ }
+
+ let button = document.createElement("toolbarbutton");
+ let text = gNavigatorBundle.getFormattedString("webextPerms.sideloadMenuItem", [addon.name, appName]);
+ button.setAttribute("label", text);
+
+ let icon = addon.iconURL || DEFAULT_EXTENSION_ICON;
+ button.setAttribute("image", icon);
+
+ button.addEventListener("click", evt => {
+ ExtensionsUI.showSideloaded(gBrowser, addon);
+ });
+
+ container.appendChild(button);
+ }
+ },
+};
+
+var LightWeightThemeWebInstaller = {
+ init() {
+ let mm = window.messageManager;
+ mm.addMessageListener("LightWeightThemeWebInstaller:Install", this);
+ mm.addMessageListener("LightWeightThemeWebInstaller:Preview", this);
+ mm.addMessageListener("LightWeightThemeWebInstaller:ResetPreview", this);
+ },
+
+ receiveMessage(message) {
+ // ignore requests from background tabs
+ if (message.target != gBrowser.selectedBrowser) {
+ return;
+ }
+
+ let data = message.data;
+
+ switch (message.name) {
+ case "LightWeightThemeWebInstaller:Install": {
+ this._installRequest(data.themeData, data.baseURI);
+ break;
+ }
+ case "LightWeightThemeWebInstaller:Preview": {
+ this._preview(data.themeData, data.baseURI);
+ break;
+ }
+ case "LightWeightThemeWebInstaller:ResetPreview": {
+ this._resetPreview(data && data.baseURI);
+ break;
+ }
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "TabSelect": {
+ this._resetPreview();
+ break;
+ }
+ }
+ },
+
+ get _manager() {
+ let temp = {};
+ Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
+ delete this._manager;
+ return this._manager = temp.LightweightThemeManager;
+ },
+
+ _installRequest(dataString, baseURI) {
+ let data = this._manager.parseTheme(dataString, baseURI);
+
+ if (!data) {
+ return;
+ }
+
+ let uri = makeURI(baseURI);
+
+ // A notification bar with the option to undo is normally shown after a
+ // theme is installed. But the discovery pane served from the url(s)
+ // below has its own toggle switch for quick undos, so don't show the
+ // notification in that case.
+ let notify = uri.prePath != "https://discovery.addons.mozilla.org";
+ if (notify) {
+ try {
+ if (Services.prefs.getBoolPref("extensions.webapi.testing")
+ && (uri.prePath == "https://discovery.addons.allizom.org"
+ || uri.prePath == "https://discovery.addons-dev.allizom.org")) {
+ notify = false;
+ }
+ } catch (e) {
+ // getBoolPref() throws if the testing pref isn't set. ignore it.
+ }
+ }
+
+ if (this._isAllowed(baseURI)) {
+ this._install(data, notify);
+ return;
+ }
+
+ let allowButtonText =
+ gNavigatorBundle.getString("lwthemeInstallRequest.allowButton");
+ let allowButtonAccesskey =
+ gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey");
+ let message =
+ gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message",
+ [uri.host]);
+ let buttons = [{
+ label: allowButtonText,
+ accessKey: allowButtonAccesskey,
+ callback() {
+ LightWeightThemeWebInstaller._install(data, notify);
+ }
+ }];
+
+ this._removePreviousNotifications();
+
+ let notificationBox = gBrowser.getNotificationBox();
+ let notificationBar =
+ notificationBox.appendNotification(message, "lwtheme-install-request", "",
+ notificationBox.PRIORITY_INFO_MEDIUM,
+ buttons);
+ notificationBar.persistence = 1;
+ },
+
+ _install(newLWTheme, notify) {
+ let previousLWTheme = this._manager.currentTheme;
+
+ let listener = {
+ onEnabling(aAddon, aRequiresRestart) {
+ if (!aRequiresRestart) {
+ return;
+ }
+
+ let messageString = gNavigatorBundle.getFormattedString("lwthemeNeedsRestart.message",
+ [aAddon.name], 1);
+
+ let action = {
+ label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"),
+ accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"),
+ callback() {
+ BrowserUtils.restartApplication();
+ }
+ };
+
+ let options = {
+ persistent: true
+ };
+
+ PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change",
+ messageString, "addons-notification-icon",
+ action, null, options);
+ },
+
+ onEnabled(aAddon) {
+ if (notify) {
+ LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme);
+ }
+ }
+ };
+
+ AddonManager.addAddonListener(listener);
+ this._manager.currentTheme = newLWTheme;
+ AddonManager.removeAddonListener(listener);
+ },
+
+ _postInstallNotification(newTheme, previousTheme) {
+ function text(id) {
+ return gNavigatorBundle.getString("lwthemePostInstallNotification." + id);
+ }
+
+ let buttons = [{
+ label: text("undoButton"),
+ accessKey: text("undoButton.accesskey"),
+ callback() {
+ LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id);
+ LightWeightThemeWebInstaller._manager.currentTheme = previousTheme;
+ }
+ }, {
+ label: text("manageButton"),
+ accessKey: text("manageButton.accesskey"),
+ callback() {
+ BrowserOpenAddonsMgr("addons://list/theme");
+ }
+ }];
+
+ this._removePreviousNotifications();
+
+ let notificationBox = gBrowser.getNotificationBox();
+ let notificationBar =
+ notificationBox.appendNotification(text("message"),
+ "lwtheme-install-notification", "",
+ notificationBox.PRIORITY_INFO_MEDIUM,
+ buttons);
+ notificationBar.persistence = 1;
+ notificationBar.timeout = Date.now() + 20000; // 20 seconds
+ },
+
+ _removePreviousNotifications() {
+ let box = gBrowser.getNotificationBox();
+
+ ["lwtheme-install-request",
+ "lwtheme-install-notification"].forEach(function(value) {
+ let notification = box.getNotificationWithValue(value);
+ if (notification)
+ box.removeNotification(notification);
+ });
+ },
+
+ _preview(dataString, baseURI) {
+ if (!this._isAllowed(baseURI))
+ return;
+
+ let data = this._manager.parseTheme(dataString, baseURI);
+ if (!data)
+ return;
+
+ this._resetPreview();
+ gBrowser.tabContainer.addEventListener("TabSelect", this);
+ this._manager.previewTheme(data);
+ },
+
+ _resetPreview(baseURI) {
+ if (baseURI && !this._isAllowed(baseURI))
+ return;
+ gBrowser.tabContainer.removeEventListener("TabSelect", this);
+ this._manager.resetPreview();
+ },
+
+ _isAllowed(srcURIString) {
+ let uri;
+ try {
+ uri = makeURI(srcURIString);
+ } catch (e) {
+ // makeURI fails if srcURIString is a nonsense URI
+ return false;
+ }
+
+ if (!uri.schemeIs("https")) {
+ return false;
+ }
+
+ let pm = Services.perms;
+ return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
+ }
+};
+
+/*
+ * Listen for Lightweight Theme styling changes and update the browser's theme accordingly.
+ */
+var LightweightThemeListener = {
+ _modifiedStyles: [],
+
+ init() {
+ XPCOMUtils.defineLazyGetter(this, "styleSheet", function() {
+ for (let i = document.styleSheets.length - 1; i >= 0; i--) {
+ let sheet = document.styleSheets[i];
+ if (sheet.href == "chrome://browser/skin/browser-lightweightTheme.css")
+ return sheet;
+ }
+ return undefined;
+ });
+
+ Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
+ Services.obs.addObserver(this, "lightweight-theme-optimized", false);
+ if (document.documentElement.hasAttribute("lwtheme"))
+ this.updateStyleSheet(document.documentElement.style.backgroundImage);
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "lightweight-theme-styling-update");
+ Services.obs.removeObserver(this, "lightweight-theme-optimized");
+ },
+
+ /**
+ * Append the headerImage to the background-image property of all rulesets in
+ * browser-lightweightTheme.css.
+ *
+ * @param headerImage - a string containing a CSS image for the lightweight theme header.
+ */
+ updateStyleSheet(headerImage) {
+ if (!this.styleSheet)
+ return;
+ this.substituteRules(this.styleSheet.cssRules, headerImage);
+ },
+
+ substituteRules(ruleList, headerImage, existingStyleRulesModified = 0) {
+ let styleRulesModified = 0;
+ for (let i = 0; i < ruleList.length; i++) {
+ let rule = ruleList[i];
+ if (rule instanceof Ci.nsIDOMCSSGroupingRule) {
+ // Add the number of modified sub-rules to the modified count
+ styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified);
+ } else if (rule instanceof Ci.nsIDOMCSSStyleRule) {
+ if (!rule.style.backgroundImage)
+ continue;
+ let modifiedIndex = existingStyleRulesModified + styleRulesModified;
+ if (!this._modifiedStyles[modifiedIndex])
+ this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage };
+
+ rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage;
+ styleRulesModified++;
+ } else {
+ Cu.reportError("Unsupported rule encountered");
+ }
+ }
+ return styleRulesModified;
+ },
+
+ // nsIObserver
+ observe(aSubject, aTopic, aData) {
+ if ((aTopic != "lightweight-theme-styling-update" && aTopic != "lightweight-theme-optimized") ||
+ !this.styleSheet)
+ return;
+
+ if (aTopic == "lightweight-theme-optimized" && aSubject != window)
+ return;
+
+ let themeData = JSON.parse(aData);
+ if (!themeData)
+ return;
+ this.updateStyleSheet("url(" + themeData.headerURL + ")");
+ },
+};
diff --git a/application/basilisk/base/content/browser-captivePortal.js b/application/basilisk/base/content/browser-captivePortal.js
new file mode 100644
index 000000000..c2e45c4ed
--- /dev/null
+++ b/application/basilisk/base/content/browser-captivePortal.js
@@ -0,0 +1,257 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+XPCOMUtils.defineLazyServiceGetter(this, "cps",
+ "@mozilla.org/network/captive-portal-service;1",
+ "nsICaptivePortalService");
+
+var CaptivePortalWatcher = {
+ /**
+ * This constant is chosen to be large enough for a portal recheck to complete,
+ * and small enough that the delay in opening a tab isn't too noticeable.
+ * Please see comments for _delayedCaptivePortalDetected for more details.
+ */
+ PORTAL_RECHECK_DELAY_MS: Preferences.get("captivedetect.portalRecheckDelayMS", 500),
+
+ // This is the value used to identify the captive portal notification.
+ PORTAL_NOTIFICATION_VALUE: "captive-portal-detected",
+
+ // This holds a weak reference to the captive portal tab so that we
+ // don't leak it if the user closes it.
+ _captivePortalTab: null,
+
+ /**
+ * If a portal is detected when we don't have focus, we first wait for focus
+ * and then add the tab if, after a recheck, the portal is still active. This
+ * is set to true while we wait so that in the unlikely event that we receive
+ * another notification while waiting, we don't do things twice.
+ */
+ _delayedCaptivePortalDetectedInProgress: false,
+
+ // In the situation above, this is set to true while we wait for the recheck.
+ // This flag exists so that tests can appropriately simulate a recheck.
+ _waitingForRecheck: false,
+
+ get _captivePortalNotification() {
+ let nb = document.getElementById("high-priority-global-notificationbox");
+ return nb.getNotificationWithValue(this.PORTAL_NOTIFICATION_VALUE);
+ },
+
+ get canonicalURL() {
+ return Services.prefs.getCharPref("captivedetect.canonicalURL");
+ },
+
+ get _browserBundle() {
+ delete this._browserBundle;
+ return this._browserBundle =
+ Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ },
+
+ init() {
+ Services.obs.addObserver(this, "captive-portal-login", false);
+ Services.obs.addObserver(this, "captive-portal-login-abort", false);
+ Services.obs.addObserver(this, "captive-portal-login-success", false);
+
+ if (cps.state == cps.LOCKED_PORTAL) {
+ // A captive portal has already been detected.
+ this._captivePortalDetected();
+
+ // Automatically open a captive portal tab if there's no other browser window.
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ if (windows.getNext() == window && !windows.hasMoreElements()) {
+ this.ensureCaptivePortalTab();
+ }
+ }
+
+ cps.recheckCaptivePortal();
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "captive-portal-login");
+ Services.obs.removeObserver(this, "captive-portal-login-abort");
+ Services.obs.removeObserver(this, "captive-portal-login-success");
+
+
+ if (this._delayedCaptivePortalDetectedInProgress) {
+ Services.obs.removeObserver(this, "xul-window-visible");
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "captive-portal-login":
+ this._captivePortalDetected();
+ break;
+ case "captive-portal-login-abort":
+ case "captive-portal-login-success":
+ this._captivePortalGone();
+ break;
+ case "xul-window-visible":
+ this._delayedCaptivePortalDetected();
+ break;
+ }
+ },
+
+ _captivePortalDetected() {
+ if (this._delayedCaptivePortalDetectedInProgress) {
+ return;
+ }
+
+ let win = RecentWindow.getMostRecentBrowserWindow();
+ // If no browser window has focus, open and show the tab when we regain focus.
+ // This is so that if a different application was focused, when the user
+ // (re-)focuses a browser window, we open the tab immediately in that window
+ // so they can log in before continuing to browse.
+ if (win != Services.ww.activeWindow) {
+ this._delayedCaptivePortalDetectedInProgress = true;
+ Services.obs.addObserver(this, "xul-window-visible", false);
+ }
+
+ this._showNotification();
+ },
+
+ /**
+ * Called after we regain focus if we detect a portal while a browser window
+ * doesn't have focus. Triggers a portal recheck to reaffirm state, and adds
+ * the tab if needed after a short delay to allow the recheck to complete.
+ */
+ _delayedCaptivePortalDetected() {
+ if (!this._delayedCaptivePortalDetectedInProgress) {
+ return;
+ }
+
+ let win = RecentWindow.getMostRecentBrowserWindow();
+ if (win != Services.ww.activeWindow) {
+ // The window that got focused was not a browser window.
+ return;
+ }
+ Services.obs.removeObserver(this, "xul-window-visible");
+ this._delayedCaptivePortalDetectedInProgress = false;
+
+ if (win != window) {
+ // Some other browser window got focus, we don't have to do anything.
+ return;
+ }
+ // Trigger a portal recheck. The user may have logged into the portal via
+ // another client, or changed networks.
+ cps.recheckCaptivePortal();
+ this._waitingForRecheck = true;
+ let requestTime = Date.now();
+
+ let self = this;
+ Services.obs.addObserver(function observer() {
+ let time = Date.now() - requestTime;
+ Services.obs.removeObserver(observer, "captive-portal-check-complete");
+ self._waitingForRecheck = false;
+ if (cps.state != cps.LOCKED_PORTAL) {
+ // We're free of the portal!
+ return;
+ }
+
+ if (time <= self.PORTAL_RECHECK_DELAY_MS) {
+ // The amount of time elapsed since we requested a recheck (i.e. since
+ // the browser window was focused) was small enough that we can add and
+ // focus a tab with the login page with no noticeable delay.
+ self.ensureCaptivePortalTab();
+ }
+ }, "captive-portal-check-complete", false);
+ },
+
+ _captivePortalGone() {
+ if (this._delayedCaptivePortalDetectedInProgress) {
+ Services.obs.removeObserver(this, "xul-window-visible");
+ this._delayedCaptivePortalDetectedInProgress = false;
+ }
+
+ this._removeNotification();
+ },
+
+ handleEvent(aEvent) {
+ if (aEvent.type != "TabSelect" || !this._captivePortalTab || !this._captivePortalNotification) {
+ return;
+ }
+
+ let tab = this._captivePortalTab.get();
+ let n = this._captivePortalNotification;
+ if (!tab || !n) {
+ return;
+ }
+
+ let doc = tab.ownerDocument;
+ let button = n.querySelector("button.notification-button");
+ if (doc.defaultView.gBrowser.selectedTab == tab) {
+ button.style.visibility = "hidden";
+ } else {
+ button.style.visibility = "visible";
+ }
+ },
+
+ _showNotification() {
+ let buttons = [
+ {
+ label: this._browserBundle.GetStringFromName("captivePortal.showLoginPage"),
+ callback: () => {
+ this.ensureCaptivePortalTab();
+
+ // Returning true prevents the notification from closing.
+ return true;
+ },
+ isDefault: true,
+ },
+ ];
+
+ let message = this._browserBundle.GetStringFromName("captivePortal.infoMessage2");
+
+ let closeHandler = (aEventName) => {
+ if (aEventName != "removed") {
+ return;
+ }
+ gBrowser.tabContainer.removeEventListener("TabSelect", this);
+ };
+
+ let nb = document.getElementById("high-priority-global-notificationbox");
+ nb.appendNotification(message, this.PORTAL_NOTIFICATION_VALUE, "",
+ nb.PRIORITY_INFO_MEDIUM, buttons, closeHandler);
+
+ gBrowser.tabContainer.addEventListener("TabSelect", this);
+ },
+
+ _removeNotification() {
+ let n = this._captivePortalNotification;
+ if (!n || !n.parentNode) {
+ return;
+ }
+ n.close();
+ },
+
+ ensureCaptivePortalTab() {
+ let tab;
+ if (this._captivePortalTab) {
+ tab = this._captivePortalTab.get();
+ }
+
+ // If the tab is gone or going, we need to open a new one.
+ if (!tab || tab.closing || !tab.parentNode) {
+ tab = gBrowser.addTab(this.canonicalURL, { ownerTab: gBrowser.selectedTab });
+ this._captivePortalTab = Cu.getWeakReference(tab);
+ }
+
+ gBrowser.selectedTab = tab;
+
+ let canonicalURI = makeURI(this.canonicalURL);
+
+ // When we are no longer captive, close the tab if it's at the canonical URL.
+ let tabCloser = () => {
+ Services.obs.removeObserver(tabCloser, "captive-portal-login-abort");
+ Services.obs.removeObserver(tabCloser, "captive-portal-login-success");
+ if (!tab || tab.closing || !tab.parentNode || !tab.linkedBrowser ||
+ !tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI)) {
+ return;
+ }
+ gBrowser.removeTab(tab);
+ }
+ Services.obs.addObserver(tabCloser, "captive-portal-login-abort", false);
+ Services.obs.addObserver(tabCloser, "captive-portal-login-success", false);
+ },
+};
diff --git a/application/basilisk/base/content/browser-charsetmenu.inc b/application/basilisk/base/content/browser-charsetmenu.inc
new file mode 100644
index 000000000..806b1cf03
--- /dev/null
+++ b/application/basilisk/base/content/browser-charsetmenu.inc
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
diff --git a/application/basilisk/base/content/browser-compacttheme.js b/application/basilisk/base/content/browser-compacttheme.js
new file mode 100644
index 000000000..87095df51
--- /dev/null
+++ b/application/basilisk/base/content/browser-compacttheme.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Listeners for the compact theme. This adds an extra stylesheet
+ * to browser.xul if a pref is set and no other themes are applied.
+ */
+var CompactTheme = {
+ styleSheetLocation: "chrome://browser/skin/compacttheme.css",
+ styleSheet: null,
+ initialized: false,
+
+ get isStyleSheetEnabled() {
+ return this.styleSheet && !this.styleSheet.sheet.disabled;
+ },
+
+ get isThemeCurrentlyApplied() {
+ let theme = LightweightThemeManager.currentTheme;
+ return theme && (
+ theme.id == "firefox-compact-dark@mozilla.org" ||
+ theme.id == "firefox-compact-light@mozilla.org");
+ },
+
+ init() {
+ this.initialized = true;
+ Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
+
+ if (this.isThemeCurrentlyApplied) {
+ this._toggleStyleSheet(true);
+ }
+ },
+
+ createStyleSheet() {
+ let styleSheetAttr = `href="${this.styleSheetLocation}" type="text/css"`;
+ this.styleSheet = document.createProcessingInstruction(
+ "xml-stylesheet", styleSheetAttr);
+ this.styleSheet.addEventListener("load", this);
+ document.insertBefore(this.styleSheet, document.documentElement);
+ this.styleSheet.sheet.disabled = true;
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "lightweight-theme-styling-update") {
+ let newTheme = JSON.parse(data);
+ if (newTheme && (
+ newTheme.id == "firefox-compact-light@mozilla.org" ||
+ newTheme.id == "firefox-compact-dark@mozilla.org")) {
+ // We are using the theme ID on this object instead of always referencing
+ // LightweightThemeManager.currentTheme in case this is a preview
+ this._toggleStyleSheet(true);
+ } else {
+ this._toggleStyleSheet(false);
+ }
+
+ }
+ },
+
+ handleEvent(e) {
+ if (e.type === "load") {
+ this.styleSheet.removeEventListener("load", this);
+ this.refreshBrowserDisplay();
+ }
+ },
+
+ refreshBrowserDisplay() {
+ // Don't touch things on the browser if gBrowserInit.onLoad hasn't
+ // yet fired.
+ if (this.initialized) {
+ gBrowser.tabContainer._positionPinnedTabs();
+ }
+ },
+
+ _toggleStyleSheet(enabled) {
+ let wasEnabled = this.isStyleSheetEnabled;
+ if (enabled) {
+ // The stylesheet may not have been created yet if it wasn't
+ // needed on initial load. Make it now.
+ if (!this.styleSheet) {
+ this.createStyleSheet();
+ }
+ this.styleSheet.sheet.disabled = false;
+ this.refreshBrowserDisplay();
+ } else if (!enabled && wasEnabled) {
+ this.styleSheet.sheet.disabled = true;
+ this.refreshBrowserDisplay();
+ }
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "lightweight-theme-styling-update");
+ if (this.styleSheet) {
+ this.styleSheet.removeEventListener("load", this);
+ }
+ this.styleSheet = null;
+ }
+};
+
+#ifdef INSTALL_COMPACT_THEMES
+// If the compact theme is going to be applied in gBrowserInit.onLoad,
+// then preload it now. This prevents a flash of unstyled content where the
+// normal theme is applied while the compact theme stylesheet is loading.
+if (this != Services.appShell.hiddenDOMWindow && CompactTheme.isThemeCurrentlyApplied) {
+ CompactTheme.createStyleSheet();
+}
+#endif
diff --git a/application/basilisk/base/content/browser-context.inc b/application/basilisk/base/content/browser-context.inc
new file mode 100644
index 000000000..fbad5e584
--- /dev/null
+++ b/application/basilisk/base/content/browser-context.inc
@@ -0,0 +1,452 @@
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# NB: IF YOU ADD ITEMS TO THIS FILE, PLEASE UPDATE THE WHITELIST IN
+# BrowserUITelemetry.jsm. SEE BUG 991757 FOR DETAILS.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# label and data-usercontextid are dynamically set.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifdef CONTEXT_COPY_IMAGE_CONTENTS
+
+#endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/basilisk/base/content/browser-ctrlTab.js b/application/basilisk/base/content/browser-ctrlTab.js
new file mode 100644
index 000000000..fba07be20
--- /dev/null
+++ b/application/basilisk/base/content/browser-ctrlTab.js
@@ -0,0 +1,586 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tab previews utility, produces thumbnails
+ */
+var tabPreviews = {
+ init: function tabPreviews_init() {
+ if (this._selectedTab)
+ return;
+ this._selectedTab = gBrowser.selectedTab;
+
+ gBrowser.tabContainer.addEventListener("TabSelect", this);
+ gBrowser.tabContainer.addEventListener("SSTabRestored", this);
+
+ let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
+ .getService(Ci.nsIScreenManager);
+ let left = {}, top = {}, width = {}, height = {};
+ screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height);
+ this.aspectRatio = height.value / width.value;
+ },
+
+ get: function tabPreviews_get(aTab) {
+ let uri = aTab.linkedBrowser.currentURI.spec;
+
+ if (aTab.__thumbnail_lastURI &&
+ aTab.__thumbnail_lastURI != uri) {
+ aTab.__thumbnail = null;
+ aTab.__thumbnail_lastURI = null;
+ }
+
+ if (aTab.__thumbnail)
+ return aTab.__thumbnail;
+
+ if (aTab.getAttribute("pending") == "true") {
+ let img = new Image;
+ img.src = PageThumbs.getThumbnailURL(uri);
+ return img;
+ }
+
+ return this.capture(aTab, !aTab.hasAttribute("busy"));
+ },
+
+ capture: function tabPreviews_capture(aTab, aShouldCache) {
+ let browser = aTab.linkedBrowser;
+ let uri = browser.currentURI.spec;
+ let canvas = PageThumbs.createCanvas(window);
+ PageThumbs.shouldStoreThumbnail(browser, (aDoStore) => {
+ if (aDoStore && aShouldCache) {
+ PageThumbs.captureAndStore(browser, function() {
+ let img = new Image;
+ img.src = PageThumbs.getThumbnailURL(uri);
+ aTab.__thumbnail = img;
+ aTab.__thumbnail_lastURI = uri;
+ canvas.getContext("2d").drawImage(img, 0, 0);
+ });
+ } else {
+ PageThumbs.captureToCanvas(browser, canvas, () => {
+ if (aShouldCache) {
+ aTab.__thumbnail = canvas;
+ aTab.__thumbnail_lastURI = uri;
+ }
+ });
+ }
+ });
+ return canvas;
+ },
+
+ handleEvent: function tabPreviews_handleEvent(event) {
+ switch (event.type) {
+ case "TabSelect":
+ if (this._selectedTab &&
+ this._selectedTab.parentNode &&
+ !this._pendingUpdate) {
+ // Generate a thumbnail for the tab that was selected.
+ // The timeout keeps the UI snappy and prevents us from generating thumbnails
+ // for tabs that will be closed. During that timeout, don't generate other
+ // thumbnails in case multiple TabSelect events occur fast in succession.
+ this._pendingUpdate = true;
+ setTimeout(function(self, aTab) {
+ self._pendingUpdate = false;
+ if (aTab.parentNode &&
+ !aTab.hasAttribute("busy") &&
+ !aTab.hasAttribute("pending"))
+ self.capture(aTab, true);
+ }, 2000, this, this._selectedTab);
+ }
+ this._selectedTab = event.target;
+ break;
+ case "SSTabRestored":
+ this.capture(event.target, true);
+ break;
+ }
+ }
+};
+
+var tabPreviewPanelHelper = {
+ opening(host) {
+ host.panel.hidden = false;
+
+ var handler = this._generateHandler(host);
+ host.panel.addEventListener("popupshown", handler);
+ host.panel.addEventListener("popuphiding", handler);
+
+ host._prevFocus = document.commandDispatcher.focusedElement;
+ },
+ _generateHandler(host) {
+ var self = this;
+ return function(event) {
+ if (event.target == host.panel) {
+ host.panel.removeEventListener(event.type, arguments.callee);
+ self["_" + event.type](host);
+ }
+ };
+ },
+ _popupshown(host) {
+ if ("setupGUI" in host)
+ host.setupGUI();
+ },
+ _popuphiding(host) {
+ if ("suspendGUI" in host)
+ host.suspendGUI();
+
+ if (host._prevFocus) {
+ Services.focus.setFocus(host._prevFocus, Ci.nsIFocusManager.FLAG_NOSCROLL);
+ host._prevFocus = null;
+ } else
+ gBrowser.selectedBrowser.focus();
+
+ if (host.tabToSelect) {
+ gBrowser.selectedTab = host.tabToSelect;
+ host.tabToSelect = null;
+ }
+ }
+};
+
+/**
+ * Ctrl-Tab panel
+ */
+var ctrlTab = {
+ get panel() {
+ delete this.panel;
+ return this.panel = document.getElementById("ctrlTab-panel");
+ },
+ get showAllButton() {
+ delete this.showAllButton;
+ return this.showAllButton = document.getElementById("ctrlTab-showAll");
+ },
+ get previews() {
+ delete this.previews;
+ return this.previews = this.panel.getElementsByClassName("ctrlTab-preview");
+ },
+ get maxTabPreviews() {
+ delete this.maxTabPreviews;
+ return this.maxTabPreviews = this.previews.length - 1;
+ },
+ get canvasWidth() {
+ delete this.canvasWidth;
+ return this.canvasWidth = Math.ceil(screen.availWidth * .85 / this.maxTabPreviews);
+ },
+ get canvasHeight() {
+ delete this.canvasHeight;
+ return this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio);
+ },
+ get keys() {
+ var keys = {};
+ ["close", "find", "selectAll"].forEach(function(key) {
+ keys[key] = document.getElementById("key_" + key)
+ .getAttribute("key")
+ .toLocaleLowerCase().charCodeAt(0);
+ });
+ delete this.keys;
+ return this.keys = keys;
+ },
+ _selectedIndex: 0,
+ get selected() {
+ return this._selectedIndex < 0 ?
+ document.activeElement :
+ this.previews.item(this._selectedIndex);
+ },
+ get isOpen() {
+ return this.panel.state == "open" || this.panel.state == "showing" || this._timer;
+ },
+ get tabCount() {
+ return this.tabList.length;
+ },
+ get tabPreviewCount() {
+ return Math.min(this.maxTabPreviews, this.tabCount);
+ },
+
+ get tabList() {
+ return this._recentlyUsedTabs;
+ },
+
+ init: function ctrlTab_init() {
+ if (!this._recentlyUsedTabs) {
+ tabPreviews.init();
+
+ this._initRecentlyUsedTabs();
+ this._init(true);
+ }
+ },
+
+ uninit: function ctrlTab_uninit() {
+ this._recentlyUsedTabs = null;
+ this._init(false);
+ },
+
+ prefName: "browser.ctrlTab.previews",
+ readPref: function ctrlTab_readPref() {
+ var enable =
+ gPrefService.getBoolPref(this.prefName) &&
+ (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") ||
+ !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders"));
+
+ if (enable)
+ this.init();
+ else
+ this.uninit();
+ },
+ observe(aSubject, aTopic, aPrefName) {
+ this.readPref();
+ },
+
+ updatePreviews: function ctrlTab_updatePreviews() {
+ for (let i = 0; i < this.previews.length; i++)
+ this.updatePreview(this.previews[i], this.tabList[i]);
+
+ var showAllLabel = gNavigatorBundle.getString("ctrlTab.listAllTabs.label");
+ this.showAllButton.label =
+ PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount);
+ this.showAllButton.hidden = !allTabs.canOpen;
+ },
+
+ updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
+ if (aPreview == this.showAllButton)
+ return;
+
+ aPreview._tab = aTab;
+
+ if (aPreview.firstChild)
+ aPreview.removeChild(aPreview.firstChild);
+ if (aTab) {
+ let canvasWidth = this.canvasWidth;
+ let canvasHeight = this.canvasHeight;
+ aPreview.appendChild(tabPreviews.get(aTab));
+ aPreview.setAttribute("label", aTab.label);
+ aPreview.setAttribute("tooltiptext", aTab.label);
+ aPreview.setAttribute("canvaswidth", canvasWidth);
+ aPreview.setAttribute("canvasstyle",
+ "max-width:" + canvasWidth + "px;" +
+ "min-width:" + canvasWidth + "px;" +
+ "max-height:" + canvasHeight + "px;" +
+ "min-height:" + canvasHeight + "px;");
+ if (aTab.image)
+ aPreview.setAttribute("image", aTab.image);
+ else
+ aPreview.removeAttribute("image");
+ aPreview.hidden = false;
+ } else {
+ aPreview.hidden = true;
+ aPreview.removeAttribute("label");
+ aPreview.removeAttribute("tooltiptext");
+ aPreview.removeAttribute("image");
+ }
+ },
+
+ advanceFocus: function ctrlTab_advanceFocus(aForward) {
+ let selectedIndex = Array.indexOf(this.previews, this.selected);
+ do {
+ selectedIndex += aForward ? 1 : -1;
+ if (selectedIndex < 0)
+ selectedIndex = this.previews.length - 1;
+ else if (selectedIndex >= this.previews.length)
+ selectedIndex = 0;
+ } while (this.previews[selectedIndex].hidden);
+
+ if (this._selectedIndex == -1) {
+ // Focus is already in the panel.
+ this.previews[selectedIndex].focus();
+ } else {
+ this._selectedIndex = selectedIndex;
+ }
+
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ this._openPanel();
+ }
+ },
+
+ _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) {
+ if (this._trackMouseOver)
+ aPreview.focus();
+ },
+
+ pick: function ctrlTab_pick(aPreview) {
+ if (!this.tabCount)
+ return;
+
+ var select = (aPreview || this.selected);
+
+ if (select == this.showAllButton)
+ this.showAllTabs();
+ else
+ this.close(select._tab);
+ },
+
+ showAllTabs: function ctrlTab_showAllTabs(aPreview) {
+ this.close();
+ document.getElementById("Browser:ShowAllTabs").doCommand();
+ },
+
+ remove: function ctrlTab_remove(aPreview) {
+ if (aPreview._tab)
+ gBrowser.removeTab(aPreview._tab);
+ },
+
+ attachTab: function ctrlTab_attachTab(aTab, aPos) {
+ if (aTab.closing)
+ return;
+
+ if (aPos == 0)
+ this._recentlyUsedTabs.unshift(aTab);
+ else if (aPos)
+ this._recentlyUsedTabs.splice(aPos, 0, aTab);
+ else
+ this._recentlyUsedTabs.push(aTab);
+ },
+
+ detachTab: function ctrlTab_detachTab(aTab) {
+ var i = this._recentlyUsedTabs.indexOf(aTab);
+ if (i >= 0)
+ this._recentlyUsedTabs.splice(i, 1);
+ },
+
+ open: function ctrlTab_open() {
+ if (this.isOpen)
+ return;
+
+ document.addEventListener("keyup", this, true);
+
+ this.updatePreviews();
+ this._selectedIndex = 1;
+
+ // Add a slight delay before showing the UI, so that a quick
+ // "ctrl-tab" keypress just flips back to the MRU tab.
+ this._timer = setTimeout(function(self) {
+ self._timer = null;
+ self._openPanel();
+ }, 200, this);
+ },
+
+ _openPanel: function ctrlTab_openPanel() {
+ tabPreviewPanelHelper.opening(this);
+
+ this.panel.width = Math.min(screen.availWidth * .99,
+ this.canvasWidth * 1.25 * this.tabPreviewCount);
+ var estimateHeight = this.canvasHeight * 1.25 + 75;
+ this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2,
+ screen.availTop + (screen.availHeight - estimateHeight) / 2,
+ false);
+ },
+
+ close: function ctrlTab_close(aTabToSelect) {
+ if (!this.isOpen)
+ return;
+
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ this.suspendGUI();
+ if (aTabToSelect)
+ gBrowser.selectedTab = aTabToSelect;
+ return;
+ }
+
+ this.tabToSelect = aTabToSelect;
+ this.panel.hidePopup();
+ },
+
+ setupGUI: function ctrlTab_setupGUI() {
+ this.selected.focus();
+ this._selectedIndex = -1;
+
+ // Track mouse movement after a brief delay so that the item that happens
+ // to be under the mouse pointer initially won't be selected unintentionally.
+ this._trackMouseOver = false;
+ setTimeout(function(self) {
+ if (self.isOpen)
+ self._trackMouseOver = true;
+ }, 0, this);
+ },
+
+ suspendGUI: function ctrlTab_suspendGUI() {
+ document.removeEventListener("keyup", this, true);
+
+ for (let preview of this.previews) {
+ this.updatePreview(preview, null);
+ }
+ },
+
+ onKeyPress: function ctrlTab_onKeyPress(event) {
+ var isOpen = this.isOpen;
+
+ if (isOpen) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ switch (event.keyCode) {
+ case event.DOM_VK_TAB:
+ if (event.ctrlKey && !event.altKey && !event.metaKey) {
+ if (isOpen) {
+ this.advanceFocus(!event.shiftKey);
+ } else if (!event.shiftKey) {
+ event.preventDefault();
+ event.stopPropagation();
+ let tabs = gBrowser.visibleTabs;
+ if (tabs.length > 2) {
+ this.open();
+ } else if (tabs.length == 2) {
+ let index = tabs[0].selected ? 1 : 0;
+ gBrowser.selectedTab = tabs[index];
+ }
+ }
+ }
+ break;
+ default:
+ if (isOpen && event.ctrlKey) {
+ if (event.keyCode == event.DOM_VK_DELETE) {
+ this.remove(this.selected);
+ break;
+ }
+ switch (event.charCode) {
+ case this.keys.close:
+ this.remove(this.selected);
+ break;
+ case this.keys.find:
+ case this.keys.selectAll:
+ this.showAllTabs();
+ break;
+ }
+ }
+ }
+ },
+
+ removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
+ if (this.tabCount == 2) {
+ this.close();
+ return;
+ }
+
+ this.updatePreviews();
+
+ if (this.selected.hidden)
+ this.advanceFocus(false);
+ if (this.selected == this.showAllButton)
+ this.advanceFocus(false);
+
+ // If the current tab is removed, another tab can steal our focus.
+ if (aTab.selected && this.panel.state == "open") {
+ setTimeout(function(selected) {
+ selected.focus();
+ }, 0, this.selected);
+ }
+ },
+
+ handleEvent: function ctrlTab_handleEvent(event) {
+ switch (event.type) {
+ case "SSWindowRestored":
+ this._initRecentlyUsedTabs();
+ break;
+ case "TabAttrModified":
+ // tab attribute modified (e.g. label, busy, image, selected)
+ for (let i = this.previews.length - 1; i >= 0; i--) {
+ if (this.previews[i]._tab && this.previews[i]._tab == event.target) {
+ this.updatePreview(this.previews[i], event.target);
+ break;
+ }
+ }
+ break;
+ case "TabSelect":
+ this.detachTab(event.target);
+ this.attachTab(event.target, 0);
+ break;
+ case "TabOpen":
+ this.attachTab(event.target, 1);
+ break;
+ case "TabClose":
+ this.detachTab(event.target);
+ if (this.isOpen)
+ this.removeClosingTabFromUI(event.target);
+ break;
+ case "keypress":
+ this.onKeyPress(event);
+ break;
+ case "keyup":
+ if (event.keyCode == event.DOM_VK_CONTROL)
+ this.pick();
+ break;
+ case "popupshowing":
+ if (event.target.id == "menu_viewPopup")
+ document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen;
+ break;
+ }
+ },
+
+ filterForThumbnailExpiration(aCallback) {
+ // Save a few more thumbnails than we actually display, so that when tabs
+ // are closed, the previews we add instead still get thumbnails.
+ const extraThumbnails = 3;
+ const thumbnailCount = Math.min(this.tabPreviewCount + extraThumbnails,
+ this.tabCount);
+
+ let urls = [];
+ for (let i = 0; i < thumbnailCount; i++)
+ urls.push(this.tabList[i].linkedBrowser.currentURI.spec);
+
+ aCallback(urls);
+ },
+
+ _initRecentlyUsedTabs() {
+ this._recentlyUsedTabs =
+ Array.filter(gBrowser.tabs, tab => !tab.closing)
+ .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed);
+ },
+
+ _init: function ctrlTab__init(enable) {
+ var toggleEventListener = enable ? "addEventListener" : "removeEventListener";
+
+ window[toggleEventListener]("SSWindowRestored", this, false);
+
+ var tabContainer = gBrowser.tabContainer;
+ tabContainer[toggleEventListener]("TabOpen", this, false);
+ tabContainer[toggleEventListener]("TabAttrModified", this, false);
+ tabContainer[toggleEventListener]("TabSelect", this, false);
+ tabContainer[toggleEventListener]("TabClose", this, false);
+
+ document[toggleEventListener]("keypress", this, false);
+ gBrowser.mTabBox.handleCtrlTab = !enable;
+
+ if (enable)
+ PageThumbs.addExpirationFilter(this);
+ else
+ PageThumbs.removeExpirationFilter(this);
+
+ // If we're not running, hide the "Show All Tabs" menu item,
+ // as Shift+Ctrl+Tab will be handled by the tab bar.
+ document.getElementById("menu_showAllTabs").hidden = !enable;
+ document.getElementById("menu_viewPopup")[toggleEventListener]("popupshowing", this);
+
+ // Also disable the to ensure Shift+Ctrl+Tab never triggers
+ // Show All Tabs.
+ var key_showAllTabs = document.getElementById("key_showAllTabs");
+ if (enable)
+ key_showAllTabs.removeAttribute("disabled");
+ else
+ key_showAllTabs.setAttribute("disabled", "true");
+ }
+};
+
+
+/**
+ * All Tabs menu
+ */
+var allTabs = {
+ get toolbarButton() {
+ return document.getElementById("alltabs-button");
+ },
+
+ get canOpen() {
+ return isElementVisible(this.toolbarButton);
+ },
+
+ open: function allTabs_open() {
+ if (this.canOpen) {
+ // Without setTimeout, the menupopup won't stay open when invoking
+ // "View > Show All Tabs" and the menu bar auto-hides.
+ setTimeout(() => {
+ this.toolbarButton.open = true;
+ }, 0);
+ }
+ }
+};
diff --git a/application/basilisk/base/content/browser-customization.js b/application/basilisk/base/content/browser-customization.js
new file mode 100644
index 000000000..e0bc2b7f9
--- /dev/null
+++ b/application/basilisk/base/content/browser-customization.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Customization handler prepares this browser window for entering and exiting
+ * customization mode by handling customizationstarting and customizationending
+ * events.
+ */
+var CustomizationHandler = {
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "customizationstarting":
+ this._customizationStarting();
+ break;
+ case "customizationchange":
+ this._customizationChange();
+ break;
+ case "customizationending":
+ this._customizationEnding(aEvent.detail);
+ break;
+ }
+ },
+
+ isCustomizing() {
+ return document.documentElement.hasAttribute("customizing");
+ },
+
+ _customizationStarting() {
+ // Disable the toolbar context menu items
+ let menubar = document.getElementById("main-menubar");
+ for (let childNode of menubar.childNodes)
+ childNode.setAttribute("disabled", true);
+
+ let cmd = document.getElementById("cmd_CustomizeToolbars");
+ cmd.setAttribute("disabled", "true");
+
+ UpdateUrlbarSearchSplitterState();
+
+ CombinedStopReload.uninit();
+ PlacesToolbarHelper.customizeStart();
+ DownloadsButton.customizeStart();
+
+ // The additional padding on the sides of the browser
+ // can cause the customize tab to get clipped.
+ let tabContainer = gBrowser.tabContainer;
+ if (tabContainer.getAttribute("overflow") == "true") {
+ let tabstrip = tabContainer.mTabstrip;
+ tabstrip.ensureElementIsVisible(gBrowser.selectedTab, true);
+ }
+ },
+
+ _customizationChange() {
+ PlacesToolbarHelper.customizeChange();
+ },
+
+ _customizationEnding(aDetails) {
+ // Update global UI elements that may have been added or removed
+ if (aDetails.changed) {
+ gURLBar = document.getElementById("urlbar");
+
+ gHomeButton.updateTooltip();
+ XULBrowserWindow.init();
+
+#ifndef XP_MACOSX
+ updateEditUIVisibility();
+#endif
+
+ // Hacky: update the PopupNotifications' object's reference to the iconBox,
+ // if it already exists, since it may have changed if the URL bar was
+ // added/removed.
+ if (!window.__lookupGetter__("PopupNotifications")) {
+ PopupNotifications.iconBox =
+ document.getElementById("notification-popup-box");
+ }
+
+ }
+
+ PlacesToolbarHelper.customizeDone();
+ DownloadsButton.customizeDone();
+
+ // The url bar splitter state is dependent on whether stop/reload
+ // and the location bar are combined, so we need this ordering
+ CombinedStopReload.init();
+ UpdateUrlbarSearchSplitterState();
+
+ // Update the urlbar
+ URLBarSetURI();
+ XULBrowserWindow.asyncUpdateUI();
+
+ // Re-enable parts of the UI we disabled during the dialog
+ let menubar = document.getElementById("main-menubar");
+ for (let childNode of menubar.childNodes)
+ childNode.setAttribute("disabled", false);
+ let cmd = document.getElementById("cmd_CustomizeToolbars");
+ cmd.removeAttribute("disabled");
+
+ gBrowser.selectedBrowser.focus();
+ }
+}
diff --git a/application/basilisk/base/content/browser-data-submission-info-bar.js b/application/basilisk/base/content/browser-data-submission-info-bar.js
new file mode 100644
index 000000000..11e0df344
--- /dev/null
+++ b/application/basilisk/base/content/browser-data-submission-info-bar.js
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "DataNotificationInfoBar::";
+
+/**
+ * Represents an info bar that shows a data submission notification.
+ */
+var gDataNotificationInfoBar = {
+ _OBSERVERS: [
+ "datareporting:notify-data-policy:request",
+ "datareporting:notify-data-policy:close",
+ ],
+
+ _DATA_REPORTING_NOTIFICATION: "data-reporting",
+
+ get _notificationBox() {
+ delete this._notificationBox;
+ return this._notificationBox = document.getElementById("global-notificationbox");
+ },
+
+ get _log() {
+ let Log = Cu.import("resource://gre/modules/Log.jsm", {}).Log;
+ delete this._log;
+ return this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+ },
+
+ init() {
+ window.addEventListener("unload", () => {
+ for (let o of this._OBSERVERS) {
+ Services.obs.removeObserver(this, o);
+ }
+ });
+
+ for (let o of this._OBSERVERS) {
+ Services.obs.addObserver(this, o, true);
+ }
+ },
+
+ _getDataReportingNotification(name = this._DATA_REPORTING_NOTIFICATION) {
+ return this._notificationBox.getNotificationWithValue(name);
+ },
+
+ _displayDataPolicyInfoBar(request) {
+ if (this._getDataReportingNotification()) {
+ return;
+ }
+
+ let brandBundle = document.getElementById("bundle_brand");
+ let appName = brandBundle.getString("brandShortName");
+ let vendorName = brandBundle.getString("vendorShortName");
+
+ let message = gNavigatorBundle.getFormattedString(
+ "dataReportingNotification.message",
+ [appName, vendorName]);
+
+ this._actionTaken = false;
+
+ let buttons = [{
+ label: gNavigatorBundle.getString("dataReportingNotification.button.label"),
+ accessKey: gNavigatorBundle.getString("dataReportingNotification.button.accessKey"),
+ popup: null,
+ callback: () => {
+ this._actionTaken = true;
+ window.openAdvancedPreferences("dataChoicesTab");
+ },
+ }];
+
+ this._log.info("Creating data reporting policy notification.");
+ this._notificationBox.appendNotification(
+ message,
+ this._DATA_REPORTING_NOTIFICATION,
+ null,
+ this._notificationBox.PRIORITY_INFO_HIGH,
+ buttons,
+ event => {
+ if (event == "removed") {
+ Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close", null);
+ }
+ }
+ );
+ // It is important to defer calling onUserNotifyComplete() until we're
+ // actually sure the notification was displayed. If we ever called
+ // onUserNotifyComplete() without showing anything to the user, that
+ // would be very good for user choice. It may also have legal impact.
+ request.onUserNotifyComplete();
+ },
+
+ _clearPolicyNotification() {
+ let notification = this._getDataReportingNotification();
+ if (notification) {
+ this._log.debug("Closing notification.");
+ notification.close();
+ }
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "datareporting:notify-data-policy:request":
+ let request = subject.wrappedJSObject.object;
+ try {
+ this._displayDataPolicyInfoBar(request);
+ } catch (ex) {
+ request.onUserNotifyFailed(ex);
+ }
+ break;
+
+ case "datareporting:notify-data-policy:close":
+ // If this observer fires, it means something else took care of
+ // responding. Therefore, we don't need to do anything. So, we
+ // act like we took action and clear state.
+ this._actionTaken = true;
+ this._clearPolicyNotification();
+ break;
+
+ default:
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ ]),
+};
diff --git a/application/basilisk/base/content/browser-doctype.inc b/application/basilisk/base/content/browser-doctype.inc
new file mode 100644
index 000000000..10015d898
--- /dev/null
+++ b/application/basilisk/base/content/browser-doctype.inc
@@ -0,0 +1,23 @@
+
+%brandDTD;
+
+%browserDTD;
+
+%baseMenuDTD;
+
+%charsetDTD;
+
+%textcontextDTD;
+
+ %customizeToolbarDTD;
+
+%placesDTD;
+
+%safebrowsingDTD;
+
+%aboutHomeDTD;
+
+%syncBrandDTD;
+]>
+
diff --git a/application/basilisk/base/content/browser-feeds.js b/application/basilisk/base/content/browser-feeds.js
new file mode 100644
index 000000000..5b6453325
--- /dev/null
+++ b/application/basilisk/base/content/browser-feeds.js
@@ -0,0 +1,640 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#filter substitution
+
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+
+const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
+const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed";
+const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed";
+
+const PREF_SHOW_FIRST_RUN_UI = "browser.feeds.showFirstRunUI";
+
+const PREF_SELECTED_APP = "browser.feeds.handlers.application";
+const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice";
+const PREF_SELECTED_ACTION = "browser.feeds.handler";
+const PREF_SELECTED_READER = "browser.feeds.handler.default";
+
+const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application";
+const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice";
+const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler";
+const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default";
+
+const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application";
+const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice";
+const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler";
+const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default";
+
+const PREF_UPDATE_DELAY = 2000;
+
+const SETTABLE_PREFS = new Set([
+ PREF_VIDEO_SELECTED_ACTION,
+ PREF_AUDIO_SELECTED_ACTION,
+ PREF_SELECTED_ACTION,
+ PREF_VIDEO_SELECTED_READER,
+ PREF_AUDIO_SELECTED_READER,
+ PREF_SELECTED_READER,
+ PREF_VIDEO_SELECTED_WEB,
+ PREF_AUDIO_SELECTED_WEB,
+ PREF_SELECTED_WEB
+]);
+
+const EXECUTABLE_PREFS = new Set([
+ PREF_SELECTED_APP,
+ PREF_VIDEO_SELECTED_APP,
+ PREF_AUDIO_SELECTED_APP
+]);
+
+const VALID_ACTIONS = new Set(["ask", "reader", "bookmarks"]);
+const VALID_READERS = new Set(["web", "client", "default", "bookmarks"]);
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "SHOULD_LOG",
+ "feeds.log", false);
+
+function LOG(str) {
+ if (SHOULD_LOG)
+ dump("*** Feeds: " + str + "\n");
+}
+
+function getPrefActionForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_ACTION;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_ACTION;
+
+ default:
+ return PREF_SELECTED_ACTION;
+ }
+}
+
+function getPrefReaderForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_READER;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_READER;
+
+ default:
+ return PREF_SELECTED_READER;
+ }
+}
+
+function getPrefWebForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_WEB;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_WEB;
+
+ default:
+ return PREF_SELECTED_WEB;
+ }
+}
+
+function getPrefAppForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_APP;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_APP;
+
+ default:
+ return PREF_SELECTED_APP;
+ }
+}
+
+/**
+ * Maps a feed type to a maybe-feed mimetype.
+ */
+function getMimeTypeForFeedType(aFeedType) {
+ switch (aFeedType) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return TYPE_MAYBE_VIDEO_FEED;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return TYPE_MAYBE_AUDIO_FEED;
+
+ default:
+ return TYPE_MAYBE_FEED;
+ }
+}
+
+/**
+ * The Feed Handler object manages discovery of RSS/ATOM feeds in web pages
+ * and shows UI when they are discovered.
+ */
+var FeedHandler = {
+ _prefChangeCallback: null,
+
+ /** Called when the user clicks on the Subscribe to This Page... menu item,
+ * or when the user clicks the feed button when the page contains multiple
+ * feeds.
+ * Builds a menu of unique feeds associated with the page, and if there
+ * is only one, shows the feed inline in the browser window.
+ * @param container
+ * The feed list container (menupopup or subview) to be populated.
+ * @param isSubview
+ * Whether we're creating a subview (true) or menu (false/undefined)
+ * @return true if the menu/subview should be shown, false if there was only
+ * one feed and the feed should be shown inline in the browser
+ * window (do not show the menupopup/subview).
+ */
+ buildFeedList(container, isSubview) {
+ let feeds = gBrowser.selectedBrowser.feeds;
+ if (!isSubview && feeds == null) {
+ // XXX hack -- menu opening depends on setting of an "open"
+ // attribute, and the menu refuses to open if that attribute is
+ // set (because it thinks it's already open). onpopupshowing gets
+ // called after the attribute is unset, and it doesn't get unset
+ // if we return false. so we unset it here; otherwise, the menu
+ // refuses to work past this point.
+ container.parentNode.removeAttribute("open");
+ return false;
+ }
+
+ for (let i = container.childNodes.length - 1; i >= 0; --i) {
+ let node = container.childNodes[i];
+ if (isSubview && node.localName == "label")
+ continue;
+ container.removeChild(node);
+ }
+
+ if (!feeds || feeds.length <= 1)
+ return false;
+
+ // Build the menu showing the available feed choices for viewing.
+ let itemNodeType = isSubview ? "toolbarbutton" : "menuitem";
+ for (let feedInfo of feeds) {
+ let item = document.createElement(itemNodeType);
+ let baseTitle = feedInfo.title || feedInfo.href;
+ item.setAttribute("label", baseTitle);
+ item.setAttribute("feed", feedInfo.href);
+ item.setAttribute("tooltiptext", feedInfo.href);
+ item.setAttribute("crop", "center");
+ let className = "feed-" + itemNodeType;
+ if (isSubview) {
+ className += " subviewbutton";
+ }
+ item.setAttribute("class", className);
+ container.appendChild(item);
+ }
+ return true;
+ },
+
+ /**
+ * Subscribe to a given feed. Called when
+ * 1. Page has a single feed and user clicks feed icon in location bar
+ * 2. Page has a single feed and user selects Subscribe menu item
+ * 3. Page has multiple feeds and user selects from feed icon popup (or subview)
+ * 4. Page has multiple feeds and user selects from Subscribe submenu
+ * @param href
+ * The feed to subscribe to. May be null, in which case the
+ * event target's feed attribute is examined.
+ * @param event
+ * The event this method is handling. Used to decide where
+ * to open the preview UI. (Optional, unless href is null)
+ */
+ subscribeToFeed(href, event) {
+ // Just load the feed in the content area to either subscribe or show the
+ // preview UI
+ if (!href)
+ href = event.target.getAttribute("feed");
+ urlSecurityCheck(href, gBrowser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+ let feedURI = makeURI(href, document.characterSet);
+ // Use the feed scheme so X-Moz-Is-Feed will be set
+ // The value doesn't matter
+ if (/^https?$/.test(feedURI.scheme))
+ href = "feed:" + href;
+ this.loadFeed(href, event);
+ },
+
+ loadFeed(href, event) {
+ let feeds = gBrowser.selectedBrowser.feeds;
+ try {
+ openUILink(href, event, { ignoreAlt: true });
+ } finally {
+ // We might default to a livebookmarks modal dialog,
+ // so reset that if the user happens to click it again
+ gBrowser.selectedBrowser.feeds = feeds;
+ }
+ },
+
+ get _feedMenuitem() {
+ delete this._feedMenuitem;
+ return this._feedMenuitem = document.getElementById("singleFeedMenuitemState");
+ },
+
+ get _feedMenupopup() {
+ delete this._feedMenupopup;
+ return this._feedMenupopup = document.getElementById("multipleFeedsMenuState");
+ },
+
+ /**
+ * Update the browser UI to show whether or not feeds are available when
+ * a page is loaded or the user switches tabs to a page that has feeds.
+ */
+ updateFeeds() {
+ if (this._updateFeedTimeout)
+ clearTimeout(this._updateFeedTimeout);
+
+ let feeds = gBrowser.selectedBrowser.feeds;
+ let haveFeeds = feeds && feeds.length > 0;
+
+ let feedButton = document.getElementById("feed-button");
+ if (feedButton) {
+ if (haveFeeds) {
+ feedButton.removeAttribute("disabled");
+ } else {
+ feedButton.setAttribute("disabled", "true");
+ }
+ }
+
+ if (!haveFeeds) {
+ this._feedMenuitem.setAttribute("disabled", "true");
+ this._feedMenuitem.removeAttribute("hidden");
+ this._feedMenupopup.setAttribute("hidden", "true");
+ return;
+ }
+
+ if (feeds.length > 1) {
+ this._feedMenuitem.setAttribute("hidden", "true");
+ this._feedMenupopup.removeAttribute("hidden");
+ } else {
+ this._feedMenuitem.setAttribute("feed", feeds[0].href);
+ this._feedMenuitem.removeAttribute("disabled");
+ this._feedMenuitem.removeAttribute("hidden");
+ this._feedMenupopup.setAttribute("hidden", "true");
+ }
+ },
+
+ addFeed(link, browserForLink) {
+ if (!browserForLink.feeds)
+ browserForLink.feeds = [];
+
+ browserForLink.feeds.push({ href: link.href, title: link.title });
+
+ // If this addition was for the current browser, update the UI. For
+ // background browsers, we'll update on tab switch.
+ if (browserForLink == gBrowser.selectedBrowser) {
+ // Batch updates to avoid updating the UI for multiple onLinkAdded events
+ // fired within 100ms of each other.
+ if (this._updateFeedTimeout)
+ clearTimeout(this._updateFeedTimeout);
+ this._updateFeedTimeout = setTimeout(this.updateFeeds.bind(this), 100);
+ }
+ },
+
+ /**
+ * Get the human-readable display name of a file. This could be the
+ * application name.
+ * @param file
+ * A nsIFile to look up the name of
+ * @return The display name of the application represented by the file.
+ */
+ _getFileDisplayName(file) {
+#ifdef XP_WIN
+ if (file instanceof Ci.nsILocalFileWin) {
+ try {
+ return file.getVersionInfoField("FileDescription");
+ } catch (e) {}
+ }
+#elif XP_MACOSX
+ if (file instanceof Ci.nsILocalFileMac) {
+ try {
+ return file.bundleDisplayName;
+ } catch (e) {}
+ }
+#endif
+
+ return file.leafName;
+ },
+
+ _chooseClientApp(aTitle, aTypeName, aBrowser) {
+ const prefName = getPrefAppForType(aTypeName);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ fp.init(window, aTitle, Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterApps);
+
+ fp.open((aResult) => {
+ if (aResult == Ci.nsIFilePicker.returnOK) {
+ let selectedApp = fp.file;
+ if (selectedApp) {
+ // XXXben - we need to compare this with the running instance
+ // executable just don't know how to do that via script
+ // XXXmano TBD: can probably add this to nsIShellService
+
+ let appName = "";
+#ifdef XP_WIN
+ appName = "@MOZ_APP_NAME@.exe";
+#elif XP_MACOSX
+ appName = "@MOZ_MACBUNDLE_NAME@";
+#else
+ appName = "@MOZ_APP_NAME@-bin";
+#endif
+ if (fp.file.leafName != appName) {
+ Services.prefs.setComplexValue(prefName, Ci.nsILocalFile, selectedApp);
+ aBrowser.messageManager.sendAsyncMessage("FeedWriter:SetApplicationLauncherMenuItem",
+ { name: this._getFileDisplayName(selectedApp),
+ type: "SelectedAppMenuItem" });
+ }
+ }
+ }
+ });
+
+ },
+
+ executeClientApp(aSpec, aTitle, aSubtitle, aFeedHandler) {
+ // aFeedHandler is either "default", indicating the system default reader, or a pref-name containing
+ // an nsILocalFile pointing to the feed handler's executable.
+
+ let clientApp = null;
+ if (aFeedHandler == "default") {
+ clientApp = Cc["@mozilla.org/browser/shell-service;1"]
+ .getService(Ci.nsIShellService)
+ .defaultFeedReader;
+ } else {
+ clientApp = Services.prefs.getComplexValue(aFeedHandler, Ci.nsILocalFile);
+ }
+
+ // For the benefit of applications that might know how to deal with more
+ // URLs than just feeds, send feed: URLs in the following format:
+ //
+ // http urls: replace scheme with feed, e.g.
+ // http://foo.com/index.rdf -> feed://foo.com/index.rdf
+ // other urls: prepend feed: scheme, e.g.
+ // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf
+ let feedURI = NetUtil.newURI(aSpec);
+ if (feedURI.schemeIs("http")) {
+ feedURI.scheme = "feed";
+ aSpec = feedURI.spec;
+ } else {
+ aSpec = "feed:" + aSpec;
+ }
+
+ // Retrieving the shell service might fail on some systems, most
+ // notably systems where GNOME is not installed.
+ try {
+ let ss = Cc["@mozilla.org/browser/shell-service;1"]
+ .getService(Ci.nsIShellService);
+ ss.openApplicationWithURI(clientApp, aSpec);
+ } catch (e) {
+ // If we couldn't use the shell service, fallback to using a
+ // nsIProcess instance
+ let p = Cc["@mozilla.org/process/util;1"]
+ .createInstance(Ci.nsIProcess);
+ p.init(clientApp);
+ p.run(false, [aSpec], 1);
+ }
+ },
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ init() {
+ window.messageManager.addMessageListener("FeedWriter:ChooseClientApp", this);
+ window.messageManager.addMessageListener("FeedWriter:GetSubscriptionUI", this);
+ window.messageManager.addMessageListener("FeedWriter:SetFeedPrefsAndSubscribe", this);
+ window.messageManager.addMessageListener("FeedWriter:ShownFirstRun", this);
+
+ Services.ppmm.addMessageListener("FeedConverter:ExecuteClientApp", this);
+
+ const prefs = Services.prefs;
+ prefs.addObserver(PREF_SELECTED_ACTION, this, true);
+ prefs.addObserver(PREF_SELECTED_READER, this, true);
+ prefs.addObserver(PREF_SELECTED_WEB, this, true);
+ prefs.addObserver(PREF_VIDEO_SELECTED_ACTION, this, true);
+ prefs.addObserver(PREF_VIDEO_SELECTED_READER, this, true);
+ prefs.addObserver(PREF_VIDEO_SELECTED_WEB, this, true);
+ prefs.addObserver(PREF_AUDIO_SELECTED_ACTION, this, true);
+ prefs.addObserver(PREF_AUDIO_SELECTED_READER, this, true);
+ prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this, true);
+ },
+
+ uninit() {
+ Services.ppmm.removeMessageListener("FeedConverter:ExecuteClientApp", this);
+
+ this._prefChangeCallback = null;
+ },
+
+ // nsIObserver
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ LOG(`Pref changed ${data}`)
+ if (this._prefChangeCallback) {
+ this._prefChangeCallback.disarm();
+ }
+ // Multiple prefs are set at the same time, debounce to reduce noise
+ // This can happen in one feed and we want to message all feed pages
+ this._prefChangeCallback = new DeferredTask(() => {
+ this._prefChanged(data);
+ }, PREF_UPDATE_DELAY);
+ this._prefChangeCallback.arm();
+ }
+ },
+
+ _prefChanged(prefName) {
+ // Don't observe for PREF_*SELECTED_APP as user likely just picked one
+ // That is also handled by SetApplicationLauncherMenuItem call
+ // Rather than the others which happen on subscription
+ switch (prefName) {
+ case PREF_SELECTED_READER:
+ case PREF_SELECTED_WEB:
+ case PREF_VIDEO_SELECTED_READER:
+ case PREF_VIDEO_SELECTED_WEB:
+ case PREF_AUDIO_SELECTED_READER:
+ case PREF_AUDIO_SELECTED_WEB:
+ case PREF_SELECTED_ACTION:
+ case PREF_VIDEO_SELECTED_ACTION:
+ case PREF_AUDIO_SELECTED_ACTION:
+ const response = {
+ default: this._getReaderForType(Ci.nsIFeed.TYPE_FEED),
+ [Ci.nsIFeed.TYPE_AUDIO]: this._getReaderForType(Ci.nsIFeed.TYPE_AUDIO),
+ [Ci.nsIFeed.TYPE_VIDEO]: this._getReaderForType(Ci.nsIFeed.TYPE_VIDEO)
+ };
+ Services.mm.broadcastAsyncMessage("FeedWriter:PreferenceUpdated",
+ response);
+ break;
+ }
+ },
+
+ _initSubscriptionUIResponse(feedType) {
+ const wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
+ getService(Ci.nsIWebContentConverterService);
+ const handlersRaw = wccr.getContentHandlers(getMimeTypeForFeedType(feedType));
+ const handlers = [];
+ for (let handler of handlersRaw) {
+ LOG(`Handler found: ${handler}`);
+ handlers.push({
+ name: handler.name,
+ uri: handler.uri
+ });
+ }
+ let showFirstRunUI = true;
+ // eslint-disable-next-line mozilla/use-default-preference-values
+ try {
+ showFirstRunUI = Services.prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI);
+ } catch (ex) { }
+ const response = { handlers, showFirstRunUI };
+ let selectedClientApp;
+ const feedTypePref = getPrefAppForType(feedType);
+ try {
+ selectedClientApp = Services.prefs.getComplexValue(feedTypePref, Ci.nsILocalFile);
+ } catch (ex) {
+ // Just do nothing, then we won't bother populating
+ }
+
+ let defaultClientApp = null;
+ try {
+ // This can sometimes not exist
+ defaultClientApp = Cc["@mozilla.org/browser/shell-service;1"]
+ .getService(Ci.nsIShellService)
+ .defaultFeedReader;
+ } catch (ex) {
+ // Just do nothing, then we don't bother populating
+ }
+
+ if (selectedClientApp && selectedClientApp.exists()) {
+ if (defaultClientApp && selectedClientApp.path != defaultClientApp.path) {
+ // Only set the default menu item if it differs from the selected one
+ response.defaultMenuItem = this._getFileDisplayName(defaultClientApp);
+ }
+ response.selectedMenuItem = this._getFileDisplayName(selectedClientApp);
+ }
+ response.reader = this._getReaderForType(feedType);
+ return response;
+ },
+
+ _setPref(aPrefName, aPrefValue, aIsComplex = false) {
+ LOG(`FeedWriter._setPref ${aPrefName}`);
+ // Ensure we have a pref that is settable
+ if (aPrefName && SETTABLE_PREFS.has(aPrefName)) {
+ if (aIsComplex) {
+ const supportsString = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ supportsString.data = aPrefValue;
+ Services.prefs.setComplexValue(aPrefName, Ci.nsISupportsString, supportsString);
+ } else {
+ Services.prefs.setCharPref(aPrefName, aPrefValue);
+ }
+ } else {
+ LOG(`FeedWriter._setPref ${aPrefName} not allowed`);
+ }
+ },
+
+ _getReaderForType(feedType) {
+ let prefs = Services.prefs;
+ let handler = "bookmarks";
+ let url;
+ // eslint-disable-next-line mozilla/use-default-preference-values
+ try {
+ handler = prefs.getCharPref(getPrefReaderForType(feedType));
+ } catch (ex) { }
+
+ if (handler === "web") {
+ try {
+ url = prefs.getComplexValue(getPrefWebForType(feedType), Ci.nsISupportsString).data;
+ } catch (ex) {
+ LOG("FeedWriter._setSelectedHandler: invalid or no handler in prefs");
+ url = null;
+ }
+ }
+ const alwaysUse = this._getAlwaysUseState(feedType);
+ const action = prefs.getCharPref(getPrefActionForType(feedType));
+ return { handler, url, alwaysUse, action };
+ },
+
+ _getAlwaysUseState(feedType) {
+ try {
+ return Services.prefs.getCharPref(getPrefActionForType(feedType)) != "ask";
+ } catch (ex) { }
+ return false;
+ },
+
+ receiveMessage(msg) {
+ let handler;
+ switch (msg.name) {
+ case "FeedWriter:GetSubscriptionUI":
+ const response = this._initSubscriptionUIResponse(msg.data.feedType);
+ msg.target.messageManager
+ .sendAsyncMessage("FeedWriter:GetSubscriptionUIResponse",
+ response);
+ break;
+ case "FeedWriter:ChooseClientApp":
+ this._chooseClientApp(msg.data.title, msg.data.feedType, msg.target);
+ break;
+ case "FeedWriter:ShownFirstRun":
+ Services.prefs.setBoolPref(PREF_SHOW_FIRST_RUN_UI, false);
+ break;
+ case "FeedWriter:SetFeedPrefsAndSubscribe":
+ const settings = msg.data;
+ if (!settings.action || !VALID_ACTIONS.has(settings.action)) {
+ LOG(`Invalid action ${settings.action}`);
+ return;
+ }
+ if (!settings.reader || !VALID_READERS.has(settings.reader)) {
+ LOG(`Invalid reader ${settings.reader}`);
+ return;
+ }
+ const actionPref = getPrefActionForType(settings.feedType);
+ this._setPref(actionPref, settings.action);
+ const readerPref = getPrefReaderForType(settings.feedType);
+ this._setPref(readerPref, settings.reader);
+ handler = null;
+
+ switch (settings.reader) {
+ case "web":
+ // This is a web set URI by content using window.registerContentHandler()
+ // Lets make sure we know about it before setting it
+ const webPref = getPrefWebForType(settings.feedType);
+ let wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
+ getService(Ci.nsIWebContentConverterService);
+ // If the user provided an invalid web URL this function won't give us a reference
+ handler = wccr.getWebContentHandlerByURI(getMimeTypeForFeedType(settings.feedType), settings.uri);
+ if (handler) {
+ this._setPref(webPref, settings.uri, true);
+ if (settings.useAsDefault) {
+ wccr.setAutoHandler(getMimeTypeForFeedType(settings.feedType), handler);
+ }
+ msg.target.messageManager
+ .sendAsyncMessage("FeedWriter:SetFeedPrefsAndSubscribeResponse",
+ { redirect: handler.getHandlerURI(settings.feedLocation) });
+ } else {
+ LOG(`No handler found for web ${settings.feedType} ${settings.uri}`);
+ }
+ break;
+ default:
+ const feedService = Cc["@mozilla.org/browser/feeds/result-service;1"].
+ getService(Ci.nsIFeedResultService);
+
+ feedService.addToClientReader(settings.feedLocation,
+ settings.feedTitle,
+ settings.feedSubtitle,
+ settings.feedType,
+ settings.reader);
+ }
+ break;
+ case "FeedConverter:ExecuteClientApp":
+ // Always check feedHandler is from a set array of executable prefs
+ if (EXECUTABLE_PREFS.has(msg.data.feedHandler)) {
+ this.executeClientApp(msg.data.spec, msg.data.title,
+ msg.data.subtitle, msg.data.feedHandler);
+ } else {
+ LOG(`FeedConverter:ExecuteClientApp - Will not exec ${msg.data.feedHandler}`);
+ }
+ break;
+ }
+ },
+};
diff --git a/application/basilisk/base/content/browser-fullScreenAndPointerLock.js b/application/basilisk/base/content/browser-fullScreenAndPointerLock.js
new file mode 100644
index 000000000..9df360947
--- /dev/null
+++ b/application/basilisk/base/content/browser-fullScreenAndPointerLock.js
@@ -0,0 +1,666 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var PointerlockFsWarning = {
+
+ _element: null,
+ _origin: null,
+
+ /**
+ * Timeout object for managing timeout request. If it is started when
+ * the previous call hasn't finished, it would automatically cancelled
+ * the previous one.
+ */
+ Timeout: class {
+ constructor(func, delay) {
+ this._id = 0;
+ this._func = func;
+ this._delay = delay;
+ }
+ start() {
+ this.cancel();
+ this._id = setTimeout(() => this._handle(), this._delay);
+ }
+ cancel() {
+ if (this._id) {
+ clearTimeout(this._id);
+ this._id = 0;
+ }
+ }
+ _handle() {
+ this._id = 0;
+ this._func();
+ }
+ get delay() {
+ return this._delay;
+ }
+ },
+
+ showPointerLock(aOrigin) {
+ if (!document.fullscreen) {
+ let timeout = gPrefService.getIntPref("pointer-lock-api.warning.timeout");
+ this.show(aOrigin, "pointerlock-warning", timeout, 0);
+ }
+ },
+
+ showFullScreen(aOrigin) {
+ let timeout = gPrefService.getIntPref("full-screen-api.warning.timeout");
+ let delay = gPrefService.getIntPref("full-screen-api.warning.delay");
+ this.show(aOrigin, "fullscreen-warning", timeout, delay);
+ },
+
+ // Shows a warning that the site has entered fullscreen or
+ // pointer lock for a short duration.
+ show(aOrigin, elementId, timeout, delay) {
+
+ if (!this._element) {
+ this._element = document.getElementById(elementId);
+ // Setup event listeners
+ this._element.addEventListener("transitionend", this);
+ window.addEventListener("mousemove", this, true);
+ // The timeout to hide the warning box after a while.
+ this._timeoutHide = new this.Timeout(() => {
+ this._state = "hidden";
+ }, timeout);
+ // The timeout to show the warning box when the pointer is at the top
+ this._timeoutShow = new this.Timeout(() => {
+ this._state = "ontop";
+ this._timeoutHide.start();
+ }, delay);
+ }
+
+ // Set the strings on the warning UI.
+ if (aOrigin) {
+ this._origin = aOrigin;
+ }
+ let uri = BrowserUtils.makeURI(this._origin);
+ let host = null;
+ try {
+ host = uri.host;
+ } catch (e) { }
+ let textElem = this._element.querySelector(".pointerlockfswarning-domain-text");
+ if (!host) {
+ textElem.setAttribute("hidden", true);
+ } else {
+ textElem.removeAttribute("hidden");
+ let hostElem = this._element.querySelector(".pointerlockfswarning-domain");
+ // Document's principal's URI has a host. Display a warning including it.
+ let utils = {};
+ Cu.import("resource://gre/modules/DownloadUtils.jsm", utils);
+ hostElem.textContent = utils.DownloadUtils.getURIHost(uri.spec)[0];
+ }
+
+ this._element.dataset.identity =
+ gIdentityHandler.pointerlockFsWarningClassName;
+
+ // User should be allowed to explicitly disable
+ // the prompt if they really want.
+ if (this._timeoutHide.delay <= 0) {
+ return;
+ }
+
+ // Explicitly set the last state to hidden to avoid the warning
+ // box being hidden immediately because of mousemove.
+ this._state = "onscreen";
+ this._lastState = "hidden";
+ this._timeoutHide.start();
+ },
+
+ close() {
+ if (!this._element) {
+ return;
+ }
+ // Cancel any pending timeout
+ this._timeoutHide.cancel();
+ this._timeoutShow.cancel();
+ // Reset state of the warning box
+ this._state = "hidden";
+ this._element.setAttribute("hidden", true);
+ // Remove all event listeners
+ this._element.removeEventListener("transitionend", this);
+ window.removeEventListener("mousemove", this, true);
+ // Clear fields
+ this._element = null;
+ this._timeoutHide = null;
+ this._timeoutShow = null;
+
+ // Ensure focus switches away from the (now hidden) warning box.
+ // If the user clicked buttons in the warning box, it would have
+ // been focused, and any key events would be directed at the (now
+ // hidden) chrome document instead of the target document.
+ gBrowser.selectedBrowser.focus();
+ },
+
+ // State could be one of "onscreen", "ontop", "hiding", and
+ // "hidden". Setting the state to "onscreen" and "ontop" takes
+ // effect immediately, while setting it to "hidden" actually
+ // turns the state to "hiding" before the transition finishes.
+ _lastState: null,
+ _STATES: ["hidden", "ontop", "onscreen"],
+ get _state() {
+ for (let state of this._STATES) {
+ if (this._element.hasAttribute(state)) {
+ return state;
+ }
+ }
+ return "hiding";
+ },
+ set _state(newState) {
+ let currentState = this._state;
+ if (currentState == newState) {
+ return;
+ }
+ if (currentState != "hiding") {
+ this._lastState = currentState;
+ this._element.removeAttribute(currentState);
+ }
+ if (newState != "hidden") {
+ if (currentState != "hidden") {
+ this._element.setAttribute(newState, true);
+ } else {
+ // When the previous state is hidden, the display was none,
+ // thus no box was constructed. We need to wait for the new
+ // display value taking effect first, otherwise, there won't
+ // be any transition. Since requestAnimationFrame callback is
+ // generally triggered before any style flush and layout, we
+ // should wait for the second animation frame.
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ if (this._element) {
+ this._element.setAttribute(newState, true);
+ }
+ });
+ });
+ }
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "mousemove": {
+ let state = this._state;
+ if (state == "hidden") {
+ // If the warning box is currently hidden, show it after
+ // a short delay if the pointer is at the top.
+ if (event.clientY != 0) {
+ this._timeoutShow.cancel();
+ } else if (this._timeoutShow.delay >= 0) {
+ this._timeoutShow.start();
+ }
+ } else {
+ let elemRect = this._element.getBoundingClientRect();
+ if (state == "hiding" && this._lastState != "hidden") {
+ // If we are on the hiding transition, and the pointer
+ // moved near the box, restore to the previous state.
+ if (event.clientY <= elemRect.bottom + 50) {
+ this._state = this._lastState;
+ this._timeoutHide.start();
+ }
+ } else if (state == "ontop" || this._lastState != "hidden") {
+ // State being "ontop" or the previous state not being
+ // "hidden" indicates this current warning box is shown
+ // in response to user's action. Hide it immediately when
+ // the pointer leaves that area.
+ if (event.clientY > elemRect.bottom + 50) {
+ this._state = "hidden";
+ this._timeoutHide.cancel();
+ }
+ }
+ }
+ break;
+ }
+ case "transitionend": {
+ if (this._state == "hiding") {
+ this._element.setAttribute("hidden", true);
+ }
+ break;
+ }
+ }
+ }
+};
+
+var PointerLock = {
+
+ init() {
+ window.messageManager.addMessageListener("PointerLock:Entered", this);
+ window.messageManager.addMessageListener("PointerLock:Exited", this);
+ },
+
+ receiveMessage(aMessage) {
+ switch (aMessage.name) {
+ case "PointerLock:Entered": {
+ PointerlockFsWarning.showPointerLock(aMessage.data.originNoSuffix);
+ break;
+ }
+ case "PointerLock:Exited": {
+ PointerlockFsWarning.close();
+ break;
+ }
+ }
+ }
+};
+
+var FullScreen = {
+ _MESSAGES: [
+ "DOMFullscreen:Request",
+ "DOMFullscreen:NewOrigin",
+ "DOMFullscreen:Exit",
+ "DOMFullscreen:Painted",
+ ],
+
+ init() {
+ // called when we go into full screen, even if initiated by a web page script
+ window.addEventListener("fullscreen", this, true);
+ window.addEventListener("MozDOMFullscreen:Entered", this,
+ /* useCapture */ true,
+ /* wantsUntrusted */ false);
+ window.addEventListener("MozDOMFullscreen:Exited", this,
+ /* useCapture */ true,
+ /* wantsUntrusted */ false);
+ for (let type of this._MESSAGES) {
+ window.messageManager.addMessageListener(type, this);
+ }
+
+ if (window.fullScreen)
+ this.toggle();
+ },
+
+ uninit() {
+ for (let type of this._MESSAGES) {
+ window.messageManager.removeMessageListener(type, this);
+ }
+ this.cleanup();
+ },
+
+ toggle() {
+ var enterFS = window.fullScreen;
+
+ // Toggle the View:FullScreen command, which controls elements like the
+ // fullscreen menuitem, and menubars.
+ let fullscreenCommand = document.getElementById("View:FullScreen");
+ if (enterFS) {
+ fullscreenCommand.setAttribute("checked", enterFS);
+ } else {
+ fullscreenCommand.removeAttribute("checked");
+ }
+
+#ifdef XP_MACOSX
+ // Make sure the menu items are adjusted.
+ document.getElementById("enterFullScreenItem").hidden = enterFS;
+ document.getElementById("exitFullScreenItem").hidden = !enterFS;
+#endif
+
+ if (!this._fullScrToggler) {
+ this._fullScrToggler = document.getElementById("fullscr-toggler");
+ this._fullScrToggler.addEventListener("mouseover", this._expandCallback);
+ this._fullScrToggler.addEventListener("dragenter", this._expandCallback);
+ this._fullScrToggler.addEventListener("touchmove", this._expandCallback, {passive: true});
+ }
+
+ if (enterFS) {
+ gNavToolbox.setAttribute("inFullscreen", true);
+ document.documentElement.setAttribute("inFullscreen", true);
+ if (!document.fullscreenElement && this.useLionFullScreen)
+ document.documentElement.setAttribute("OSXLionFullscreen", true);
+ } else {
+ gNavToolbox.removeAttribute("inFullscreen");
+ document.documentElement.removeAttribute("inFullscreen");
+ document.documentElement.removeAttribute("OSXLionFullscreen");
+ }
+
+ if (!document.fullscreenElement)
+ this._updateToolbars(enterFS);
+
+ if (enterFS) {
+ document.addEventListener("keypress", this._keyToggleCallback);
+ document.addEventListener("popupshown", this._setPopupOpen);
+ document.addEventListener("popuphidden", this._setPopupOpen);
+ // In DOM fullscreen mode, we hide toolbars with CSS
+ if (!document.fullscreenElement)
+ this.hideNavToolbox(true);
+ } else {
+ this.showNavToolbox(false);
+ // This is needed if they use the context menu to quit fullscreen
+ this._isPopupOpen = false;
+ this.cleanup();
+ // In TabsInTitlebar._update(), we cancel the appearance update on
+ // resize event for exiting fullscreen, since that happens before we
+ // change the UI here in the "fullscreen" event. Hence we need to
+ // call it here to ensure the appearance is properly updated. See
+ // TabsInTitlebar._update() and bug 1173768.
+ TabsInTitlebar.updateAppearance(true);
+ }
+
+ if (enterFS && !document.fullscreenElement) {
+ Services.telemetry.getHistogramById("FX_BROWSER_FULLSCREEN_USED")
+ .add(1);
+ }
+ },
+
+ exitDomFullScreen() {
+ document.exitFullscreen();
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "fullscreen":
+ this.toggle();
+ break;
+ case "MozDOMFullscreen:Entered": {
+ // The event target is the element which requested the DOM
+ // fullscreen. If we were entering DOM fullscreen for a remote
+ // browser, the target would be `gBrowser` and the original
+ // target would be the browser which was the parameter of
+ // `remoteFrameFullscreenChanged` call. If the fullscreen
+ // request was initiated from an in-process browser, we need
+ // to get its corresponding browser here.
+ let browser;
+ if (event.target == gBrowser) {
+ browser = event.originalTarget;
+ } else {
+ let topWin = event.target.ownerGlobal.top;
+ browser = gBrowser.getBrowserForContentWindow(topWin);
+ }
+ TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
+ this.enterDomFullscreen(browser);
+ break;
+ }
+ case "MozDOMFullscreen:Exited":
+ TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
+ this.cleanupDomFullscreen();
+ break;
+ }
+ },
+
+ receiveMessage(aMessage) {
+ let browser = aMessage.target;
+ switch (aMessage.name) {
+ case "DOMFullscreen:Request": {
+ this._windowUtils.remoteFrameFullscreenChanged(browser);
+ break;
+ }
+ case "DOMFullscreen:NewOrigin": {
+ // Don't show the warning if we've already exited fullscreen.
+ if (document.fullscreen) {
+ PointerlockFsWarning.showFullScreen(aMessage.data.originNoSuffix);
+ }
+ break;
+ }
+ case "DOMFullscreen:Exit": {
+ this._windowUtils.remoteFrameFullscreenReverted();
+ break;
+ }
+ case "DOMFullscreen:Painted": {
+ Services.obs.notifyObservers(window, "fullscreen-painted", "");
+ TelemetryStopwatch.finish("FULLSCREEN_CHANGE_MS");
+ break;
+ }
+ }
+ },
+
+ enterDomFullscreen(aBrowser) {
+
+ if (!document.fullscreenElement) {
+ return;
+ }
+
+ // If we have a current pointerlock warning shown then hide it
+ // before transition.
+ PointerlockFsWarning.close();
+
+ // If it is a remote browser, send a message to ask the content
+ // to enter fullscreen state. We don't need to do so if it is an
+ // in-process browser, since all related document should have
+ // entered fullscreen state at this point.
+ // This should be done before the active tab check below to ensure
+ // that the content document handles the pending request. Doing so
+ // before the check is fine since we also check the activeness of
+ // the requesting document in content-side handling code.
+ if (this._isRemoteBrowser(aBrowser)) {
+ aBrowser.messageManager.sendAsyncMessage("DOMFullscreen:Entered");
+ }
+
+ // If we've received a fullscreen notification, we have to ensure that the
+ // element that's requesting fullscreen belongs to the browser that's currently
+ // active. If not, we exit fullscreen since the "full-screen document" isn't
+ // actually visible now.
+ if (!aBrowser || gBrowser.selectedBrowser != aBrowser ||
+ // The top-level window has lost focus since the request to enter
+ // full-screen was made. Cancel full-screen.
+ Services.focus.activeWindow != window) {
+ // This function is called synchronously in fullscreen change, so
+ // we have to avoid calling exitFullscreen synchronously here.
+ setTimeout(() => document.exitFullscreen(), 0);
+ return;
+ }
+
+ document.documentElement.setAttribute("inDOMFullscreen", true);
+
+ if (gFindBarInitialized) {
+ gFindBar.close(true);
+ }
+
+ // Exit DOM full-screen mode upon open, close, or change tab.
+ gBrowser.tabContainer.addEventListener("TabOpen", this.exitDomFullScreen);
+ gBrowser.tabContainer.addEventListener("TabClose", this.exitDomFullScreen);
+ gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen);
+
+ // Add listener to detect when the fullscreen window is re-focused.
+ // If a fullscreen window loses focus, we show a warning when the
+ // fullscreen window is refocused.
+ window.addEventListener("activate", this);
+ },
+
+ cleanup() {
+ if (!window.fullScreen) {
+ MousePosTracker.removeListener(this);
+ document.removeEventListener("keypress", this._keyToggleCallback);
+ document.removeEventListener("popupshown", this._setPopupOpen);
+ document.removeEventListener("popuphidden", this._setPopupOpen);
+ }
+ },
+
+ cleanupDomFullscreen() {
+ window.messageManager
+ .broadcastAsyncMessage("DOMFullscreen:CleanUp");
+
+ PointerlockFsWarning.close();
+ gBrowser.tabContainer.removeEventListener("TabOpen", this.exitDomFullScreen);
+ gBrowser.tabContainer.removeEventListener("TabClose", this.exitDomFullScreen);
+ gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen);
+ window.removeEventListener("activate", this);
+
+ document.documentElement.removeAttribute("inDOMFullscreen");
+ },
+
+ _isRemoteBrowser(aBrowser) {
+ return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true";
+ },
+
+ get _windowUtils() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ },
+
+ getMouseTargetRect() {
+ return this._mouseTargetRect;
+ },
+
+ // Event callbacks
+ _expandCallback() {
+ FullScreen.showNavToolbox();
+ },
+ onMouseEnter() {
+ FullScreen.hideNavToolbox();
+ },
+ _keyToggleCallback(aEvent) {
+ // if we can use the keyboard (eg Ctrl+L or Ctrl+E) to open the toolbars, we
+ // should provide a way to collapse them too.
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+ FullScreen.hideNavToolbox();
+ } else if (aEvent.keyCode == aEvent.DOM_VK_F6) {
+ // F6 is another shortcut to the address bar, but its not covered in OpenLocation()
+ FullScreen.showNavToolbox();
+ }
+ },
+
+ // Checks whether we are allowed to collapse the chrome
+ _isPopupOpen: false,
+ _isChromeCollapsed: false,
+ _safeToCollapse() {
+ if (!gPrefService.getBoolPref("browser.fullscreen.autohide"))
+ return false;
+
+ // a popup menu is open in chrome: don't collapse chrome
+ if (this._isPopupOpen)
+ return false;
+
+ // On OS X Lion we don't want to hide toolbars.
+ if (this.useLionFullScreen)
+ return false;
+
+ // a textbox in chrome is focused (location bar anyone?): don't collapse chrome
+ if (document.commandDispatcher.focusedElement &&
+ document.commandDispatcher.focusedElement.ownerDocument == document &&
+ document.commandDispatcher.focusedElement.localName == "input") {
+ return false;
+ }
+
+ return true;
+ },
+
+ _setPopupOpen(aEvent) {
+ // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed.
+ // Otherwise, they would not affect chrome and the user would expect the chrome to go away.
+ // e.g. we wouldn't want the autoscroll icon firing this event, so when the user
+ // toggles chrome when moving mouse to the top, it doesn't go away again.
+ if (aEvent.type == "popupshown" && !FullScreen._isChromeCollapsed &&
+ aEvent.target.localName != "tooltip" && aEvent.target.localName != "window")
+ FullScreen._isPopupOpen = true;
+ else if (aEvent.type == "popuphidden" && aEvent.target.localName != "tooltip" &&
+ aEvent.target.localName != "window") {
+ FullScreen._isPopupOpen = false;
+ // Try again to hide toolbar when we close the popup.
+ FullScreen.hideNavToolbox(true);
+ }
+ },
+
+ // Autohide helpers for the context menu item
+ getAutohide(aItem) {
+ aItem.setAttribute("checked", gPrefService.getBoolPref("browser.fullscreen.autohide"));
+ },
+ setAutohide() {
+ gPrefService.setBoolPref("browser.fullscreen.autohide", !gPrefService.getBoolPref("browser.fullscreen.autohide"));
+ // Try again to hide toolbar when we change the pref.
+ FullScreen.hideNavToolbox(true);
+ },
+
+ showNavToolbox(trackMouse = true) {
+ this._fullScrToggler.hidden = true;
+ gNavToolbox.removeAttribute("fullscreenShouldAnimate");
+ gNavToolbox.style.marginTop = "";
+
+ if (!this._isChromeCollapsed) {
+ return;
+ }
+
+ // Track whether mouse is near the toolbox
+ if (trackMouse && !this.useLionFullScreen) {
+ let rect = gBrowser.mPanelContainer.getBoundingClientRect();
+ this._mouseTargetRect = {
+ top: rect.top + 50,
+ bottom: rect.bottom,
+ left: rect.left,
+ right: rect.right
+ };
+ MousePosTracker.addListener(this);
+ }
+
+ this._isChromeCollapsed = false;
+ },
+
+ hideNavToolbox(aAnimate = false) {
+ if (this._isChromeCollapsed || !this._safeToCollapse())
+ return;
+
+ this._fullScrToggler.hidden = false;
+
+ if (aAnimate && gPrefService.getBoolPref("browser.fullscreen.animate")) {
+ gNavToolbox.setAttribute("fullscreenShouldAnimate", true);
+ // Hide the fullscreen toggler until the transition ends.
+ let listener = () => {
+ gNavToolbox.removeEventListener("transitionend", listener, true);
+ if (this._isChromeCollapsed)
+ this._fullScrToggler.hidden = false;
+ };
+ gNavToolbox.addEventListener("transitionend", listener, true);
+ this._fullScrToggler.hidden = true;
+ }
+
+ gNavToolbox.style.marginTop =
+ -gNavToolbox.getBoundingClientRect().height + "px";
+ this._isChromeCollapsed = true;
+ MousePosTracker.removeListener(this);
+ },
+
+ _updateToolbars(aEnterFS) {
+ for (let el of document.querySelectorAll("toolbar[fullscreentoolbar=true]")) {
+ if (aEnterFS) {
+ // Give the main nav bar and the tab bar the fullscreen context menu,
+ // otherwise remove context menu to prevent breakage
+ el.setAttribute("saved-context", el.getAttribute("context"));
+ if (el.id == "nav-bar" || el.id == "TabsToolbar")
+ el.setAttribute("context", "autohide-context");
+ else
+ el.removeAttribute("context");
+
+ // Set the inFullscreen attribute to allow specific styling
+ // in fullscreen mode
+ el.setAttribute("inFullscreen", true);
+ } else {
+ if (el.hasAttribute("saved-context")) {
+ el.setAttribute("context", el.getAttribute("saved-context"));
+ el.removeAttribute("saved-context");
+ }
+ el.removeAttribute("inFullscreen");
+ }
+ }
+
+ ToolbarIconColor.inferFromText();
+
+ // For Lion fullscreen, all fullscreen controls are hidden, don't
+ // bother to touch them. If we don't stop here, the following code
+ // could cause the native fullscreen button be shown unexpectedly.
+ // See bug 1165570.
+ if (this.useLionFullScreen) {
+ return;
+ }
+
+ var fullscreenctls = document.getElementById("window-controls");
+ var navbar = document.getElementById("nav-bar");
+ var ctlsOnTabbar = window.toolbar.visible;
+ if (fullscreenctls.parentNode == navbar && ctlsOnTabbar) {
+ fullscreenctls.removeAttribute("flex");
+ document.getElementById("TabsToolbar").appendChild(fullscreenctls);
+ } else if (fullscreenctls.parentNode.id == "TabsToolbar" && !ctlsOnTabbar) {
+ fullscreenctls.setAttribute("flex", "1");
+ navbar.appendChild(fullscreenctls);
+ }
+ fullscreenctls.hidden = !aEnterFS;
+ }
+};
+XPCOMUtils.defineLazyGetter(FullScreen, "useLionFullScreen", function() {
+ // We'll only use OS X Lion full screen if we're
+ // * on OS X
+ // * on Lion or higher (Darwin 11+)
+ // * have fullscreenbutton="true"
+#ifdef XP_MACOSX
+ let MacOSX11OrHigher = Services.vc.compare(Services.sysinfo.getProperty("version"), "11") >= 0;
+ return MacOSX11OrHigher && document.documentElement.getAttribute("fullscreenbutton") == "true";
+#else
+ return false;
+#endif
+
+});
diff --git a/application/basilisk/base/content/browser-fullZoom.js b/application/basilisk/base/content/browser-fullZoom.js
new file mode 100644
index 000000000..6f3f0271b
--- /dev/null
+++ b/application/basilisk/base/content/browser-fullZoom.js
@@ -0,0 +1,526 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Controls the "full zoom" setting and its site-specific preferences.
+ */
+var FullZoom = {
+ // Identifies the setting in the content prefs database.
+ name: "browser.content.full-zoom",
+
+ // browser.zoom.siteSpecific preference cache
+ _siteSpecificPref: undefined,
+
+ // browser.zoom.updateBackgroundTabs preference cache
+ updateBackgroundTabs: undefined,
+
+ // This maps the browser to monotonically increasing integer
+ // tokens. _browserTokenMap[browser] is increased each time the zoom is
+ // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses.
+ _browserTokenMap: new WeakMap(),
+
+ // Stores initial locations if we receive onLocationChange
+ // events before we're initialized.
+ _initialLocations: new WeakMap(),
+
+ get siteSpecific() {
+ return this._siteSpecificPref;
+ },
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
+ Ci.nsIObserver,
+ Ci.nsIContentPrefObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+
+ // Initialization & Destruction
+
+ init: function FullZoom_init() {
+ gBrowser.addEventListener("ZoomChangeUsingMouseWheel", this);
+
+ // Register ourselves with the service so we know when our pref changes.
+ this._cps2 = Cc["@mozilla.org/content-pref/service;1"].
+ getService(Ci.nsIContentPrefService2);
+ this._cps2.addObserverForName(this.name, this);
+
+ this._siteSpecificPref =
+ gPrefService.getBoolPref("browser.zoom.siteSpecific");
+ this.updateBackgroundTabs =
+ gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
+ // Listen for changes to the browser.zoom branch so we can enable/disable
+ // updating background tabs and per-site saving and restoring of zoom levels.
+ gPrefService.addObserver("browser.zoom.", this, true);
+
+ // If we received onLocationChange events for any of the current browsers
+ // before we were initialized we want to replay those upon initialization.
+ for (let browser of gBrowser.browsers) {
+ if (this._initialLocations.has(browser)) {
+ this.onLocationChange(...this._initialLocations.get(browser), browser);
+ }
+ }
+
+ // This should be nulled after initialization.
+ this._initialLocations = null;
+ },
+
+ destroy: function FullZoom_destroy() {
+ gPrefService.removeObserver("browser.zoom.", this);
+ this._cps2.removeObserverForName(this.name, this);
+ gBrowser.removeEventListener("ZoomChangeUsingMouseWheel", this);
+ },
+
+
+ // Event Handlers
+
+ // nsIDOMEventListener
+
+ handleEvent: function FullZoom_handleEvent(event) {
+ switch (event.type) {
+ case "ZoomChangeUsingMouseWheel":
+ let browser = this._getTargetedBrowser(event);
+ this._ignorePendingZoomAccesses(browser);
+ this._applyZoomToPref(browser);
+ break;
+ }
+ },
+
+ // nsIObserver
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "nsPref:changed":
+ switch (aData) {
+ case "browser.zoom.siteSpecific":
+ this._siteSpecificPref =
+ gPrefService.getBoolPref("browser.zoom.siteSpecific");
+ break;
+ case "browser.zoom.updateBackgroundTabs":
+ this.updateBackgroundTabs =
+ gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
+ break;
+ }
+ break;
+ }
+ },
+
+ // nsIContentPrefObserver
+
+ onContentPrefSet: function FullZoom_onContentPrefSet(aGroup, aName, aValue, aIsPrivate) {
+ this._onContentPrefChanged(aGroup, aValue, aIsPrivate);
+ },
+
+ onContentPrefRemoved: function FullZoom_onContentPrefRemoved(aGroup, aName, aIsPrivate) {
+ this._onContentPrefChanged(aGroup, undefined, aIsPrivate);
+ },
+
+ /**
+ * Appropriately updates the zoom level after a content preference has
+ * changed.
+ *
+ * @param aGroup The group of the changed preference.
+ * @param aValue The new value of the changed preference. Pass undefined to
+ * indicate the preference's removal.
+ */
+ _onContentPrefChanged: function FullZoom__onContentPrefChanged(aGroup, aValue, aIsPrivate) {
+ if (this._isNextContentPrefChangeInternal) {
+ // Ignore changes that FullZoom itself makes. This works because the
+ // content pref service calls callbacks before notifying observers, and it
+ // does both in the same turn of the event loop.
+ delete this._isNextContentPrefChangeInternal;
+ return;
+ }
+
+ let browser = gBrowser.selectedBrowser;
+ if (!browser.currentURI)
+ return;
+
+ let ctxt = this._loadContextFromBrowser(browser);
+ let domain = this._cps2.extractDomain(browser.currentURI.spec);
+ if (aGroup) {
+ if (aGroup == domain && ctxt.usePrivateBrowsing == aIsPrivate)
+ this._applyPrefToZoom(aValue, browser);
+ return;
+ }
+
+ this._globalValue = aValue === undefined ? aValue :
+ this._ensureValid(aValue);
+
+ // If the current page doesn't have a site-specific preference, then its
+ // zoom should be set to the new global preference now that the global
+ // preference has changed.
+ let hasPref = false;
+ let token = this._getBrowserToken(browser);
+ this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
+ handleResult() { hasPref = true; },
+ handleCompletion: function() {
+ if (!hasPref && token.isCurrent)
+ this._applyPrefToZoom(undefined, browser);
+ }.bind(this)
+ });
+ },
+
+ // location change observer
+
+ /**
+ * Called when the location of a tab changes.
+ * When that happens, we need to update the current zoom level if appropriate.
+ *
+ * @param aURI
+ * A URI object representing the new location.
+ * @param aIsTabSwitch
+ * Whether this location change has happened because of a tab switch.
+ * @param aBrowser
+ * (optional) browser object displaying the document
+ */
+ onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) {
+ let browser = aBrowser || gBrowser.selectedBrowser;
+
+ // If we haven't been initialized yet but receive an onLocationChange
+ // notification then let's store and replay it upon initialization.
+ if (this._initialLocations) {
+ this._initialLocations.set(browser, [aURI, aIsTabSwitch]);
+ return;
+ }
+
+ // Ignore all pending async zoom accesses in the browser. Pending accesses
+ // that started before the location change will be prevented from applying
+ // to the new location.
+ this._ignorePendingZoomAccesses(browser);
+
+ if (!aURI || (aIsTabSwitch && !this.siteSpecific)) {
+ this._notifyOnLocationChange(browser);
+ return;
+ }
+
+ // Avoid the cps roundtrip and apply the default/global pref.
+ if (aURI.spec == "about:blank") {
+ this._applyPrefToZoom(undefined, browser,
+ this._notifyOnLocationChange.bind(this, browser));
+ return;
+ }
+
+ // Media documents should always start at 1, and are not affected by prefs.
+ if (!aIsTabSwitch && browser.isSyntheticDocument) {
+ ZoomManager.setZoomForBrowser(browser, 1);
+ // _ignorePendingZoomAccesses already called above, so no need here.
+ this._notifyOnLocationChange(browser);
+ return;
+ }
+
+ // See if the zoom pref is cached.
+ let ctxt = this._loadContextFromBrowser(browser);
+ let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
+ if (pref) {
+ this._applyPrefToZoom(pref.value, browser,
+ this._notifyOnLocationChange.bind(this, browser));
+ return;
+ }
+
+ // It's not cached, so we have to asynchronously fetch it.
+ let value = undefined;
+ let token = this._getBrowserToken(browser);
+ this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, {
+ handleResult(resultPref) { value = resultPref.value; },
+ handleCompletion: function() {
+ if (!token.isCurrent) {
+ this._notifyOnLocationChange(browser);
+ return;
+ }
+ this._applyPrefToZoom(value, browser,
+ this._notifyOnLocationChange.bind(this, browser));
+ }.bind(this)
+ });
+ },
+
+ // update state of zoom type menu item
+
+ updateMenu: function FullZoom_updateMenu() {
+ var menuItem = document.getElementById("toggle_zoom");
+
+ menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
+ },
+
+ // Setting & Pref Manipulation
+
+ /**
+ * Reduces the zoom level of the page in the current browser.
+ */
+ reduce: function FullZoom_reduce() {
+ ZoomManager.reduce();
+ let browser = gBrowser.selectedBrowser;
+ this._ignorePendingZoomAccesses(browser);
+ this._applyZoomToPref(browser);
+ },
+
+ /**
+ * Enlarges the zoom level of the page in the current browser.
+ */
+ enlarge: function FullZoom_enlarge() {
+ ZoomManager.enlarge();
+ let browser = gBrowser.selectedBrowser;
+ this._ignorePendingZoomAccesses(browser);
+ this._applyZoomToPref(browser);
+ },
+
+ /**
+ * Sets the zoom level for the given browser to the given floating
+ * point value, where 1 is the default zoom level.
+ */
+ setZoom(value, browser = gBrowser.selectedBrowser) {
+ ZoomManager.setZoomForBrowser(browser, value);
+ this._ignorePendingZoomAccesses(browser);
+ this._applyZoomToPref(browser);
+ },
+
+ /**
+ * Sets the zoom level of the page in the given browser to the global zoom
+ * level.
+ *
+ * @return A promise which resolves when the zoom reset has been applied.
+ */
+ reset: function FullZoom_reset(browser = gBrowser.selectedBrowser) {
+ let token = this._getBrowserToken(browser);
+ let result = this._getGlobalValue(browser).then(value => {
+ if (token.isCurrent) {
+ ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value);
+ this._ignorePendingZoomAccesses(browser);
+ Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", "");
+ }
+ });
+ this._removePref(browser);
+ return result;
+ },
+
+ /**
+ * Set the zoom level for a given browser.
+ *
+ * Per nsPresContext::setFullZoom, we can set the zoom to its current value
+ * without significant impact on performance, as the setting is only applied
+ * if it differs from the current setting. In fact getting the zoom and then
+ * checking ourselves if it differs costs more.
+ *
+ * And perhaps we should always set the zoom even if it was more expensive,
+ * since nsDocumentViewer::SetTextZoom claims that child documents can have
+ * a different text zoom (although it would be unusual), and it implies that
+ * those child text zooms should get updated when the parent zoom gets set,
+ * and perhaps the same is true for full zoom
+ * (although nsDocumentViewer::SetFullZoom doesn't mention it).
+ *
+ * So when we apply new zoom values to the browser, we simply set the zoom.
+ * We don't check first to see if the new value is the same as the current
+ * one.
+ *
+ * @param aValue The zoom level value.
+ * @param aBrowser The zoom is set in this browser. Required.
+ * @param aCallback If given, it's asynchronously called when complete.
+ */
+ _applyPrefToZoom: function FullZoom__applyPrefToZoom(aValue, aBrowser, aCallback) {
+ if (!this.siteSpecific || gInPrintPreviewMode) {
+ this._executeSoon(aCallback);
+ return;
+ }
+
+ // The browser is sometimes half-destroyed because this method is called
+ // by content pref service callbacks, which themselves can be called at any
+ // time, even after browsers are closed.
+ if (!aBrowser.parentNode || aBrowser.isSyntheticDocument) {
+ this._executeSoon(aCallback);
+ return;
+ }
+
+ if (aValue !== undefined) {
+ ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue));
+ this._ignorePendingZoomAccesses(aBrowser);
+ this._executeSoon(aCallback);
+ return;
+ }
+
+ let token = this._getBrowserToken(aBrowser);
+ this._getGlobalValue(aBrowser).then(value => {
+ if (token.isCurrent) {
+ ZoomManager.setZoomForBrowser(aBrowser, value === undefined ? 1 : value);
+ this._ignorePendingZoomAccesses(aBrowser);
+ }
+ this._executeSoon(aCallback);
+ });
+ },
+
+ /**
+ * Saves the zoom level of the page in the given browser to the content
+ * prefs store.
+ *
+ * @param browser The zoom of this browser will be saved. Required.
+ */
+ _applyZoomToPref: function FullZoom__applyZoomToPref(browser) {
+ Services.obs.notifyObservers(browser, "browser-fullZoom:zoomChange", "");
+ if (!this.siteSpecific ||
+ gInPrintPreviewMode ||
+ browser.isSyntheticDocument)
+ return;
+
+ this._cps2.set(browser.currentURI.spec, this.name,
+ ZoomManager.getZoomForBrowser(browser),
+ this._loadContextFromBrowser(browser), {
+ handleCompletion: function() {
+ this._isNextContentPrefChangeInternal = true;
+ }.bind(this),
+ });
+ },
+
+ /**
+ * Removes from the content prefs store the zoom level of the given browser.
+ *
+ * @param browser The zoom of this browser will be removed. Required.
+ */
+ _removePref: function FullZoom__removePref(browser) {
+ Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", "");
+ if (browser.isSyntheticDocument)
+ return;
+ let ctxt = this._loadContextFromBrowser(browser);
+ this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
+ handleCompletion: function() {
+ this._isNextContentPrefChangeInternal = true;
+ }.bind(this),
+ });
+ },
+
+ // Utilities
+
+ /**
+ * Returns the zoom change token of the given browser. Asynchronous
+ * operations that access the given browser's zoom should use this method to
+ * capture the token before starting and use token.isCurrent to determine if
+ * it's safe to access the zoom when done. If token.isCurrent is false, then
+ * after the async operation started, either the browser's zoom was changed or
+ * the browser was destroyed, and depending on what the operation is doing, it
+ * may no longer be safe to set and get its zoom.
+ *
+ * @param browser The token of this browser will be returned.
+ * @return An object with an "isCurrent" getter.
+ */
+ _getBrowserToken: function FullZoom__getBrowserToken(browser) {
+ let map = this._browserTokenMap;
+ if (!map.has(browser))
+ map.set(browser, 0);
+ return {
+ token: map.get(browser),
+ get isCurrent() {
+ // At this point, the browser may have been destructed and unbound but
+ // its outer ID not removed from the map because outer-window-destroyed
+ // hasn't been received yet. In that case, the browser is unusable, it
+ // has no properties, so return false. Check for this case by getting a
+ // property, say, docShell.
+ return map.get(browser) === this.token && browser.parentNode;
+ },
+ };
+ },
+
+ /**
+ * Returns the browser that the supplied zoom event is associated with.
+ * @param event The ZoomChangeUsingMouseWheel event.
+ * @return The associated browser element, if one exists, otherwise null.
+ */
+ _getTargetedBrowser: function FullZoom__getTargetedBrowser(event) {
+ let target = event.originalTarget;
+
+ // With remote content browsers, the event's target is the browser
+ // we're looking for.
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ if (target instanceof window.XULElement &&
+ target.localName == "browser" &&
+ target.namespaceURI == XUL_NS)
+ return target;
+
+ // With in-process content browsers, the event's target is the content
+ // document.
+ if (target.nodeType == Node.DOCUMENT_NODE)
+ return gBrowser.getBrowserForDocument(target);
+
+ throw new Error("Unexpected ZoomChangeUsingMouseWheel event source");
+ },
+
+ /**
+ * Increments the zoom change token for the given browser so that pending
+ * async operations know that it may be unsafe to access they zoom when they
+ * finish.
+ *
+ * @param browser Pending accesses in this browser will be ignored.
+ */
+ _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(browser) {
+ let map = this._browserTokenMap;
+ map.set(browser, (map.get(browser) || 0) + 1);
+ },
+
+ _ensureValid: function FullZoom__ensureValid(aValue) {
+ // Note that undefined is a valid value for aValue that indicates a known-
+ // not-to-exist value.
+ if (isNaN(aValue))
+ return 1;
+
+ if (aValue < ZoomManager.MIN)
+ return ZoomManager.MIN;
+
+ if (aValue > ZoomManager.MAX)
+ return ZoomManager.MAX;
+
+ return aValue;
+ },
+
+ /**
+ * Gets the global browser.content.full-zoom content preference.
+ *
+ * @param browser The browser pertaining to the zoom.
+ * @returns Promise
+ * Resolves to the preference value when done.
+ */
+ _getGlobalValue: function FullZoom__getGlobalValue(browser) {
+ // * !("_globalValue" in this) => global value not yet cached.
+ // * this._globalValue === undefined => global value known not to exist.
+ // * Otherwise, this._globalValue is a number, the global value.
+ return new Promise(resolve => {
+ if ("_globalValue" in this) {
+ resolve(this._globalValue);
+ return;
+ }
+ let value = undefined;
+ this._cps2.getGlobal(this.name, this._loadContextFromBrowser(browser), {
+ handleResult(pref) { value = pref.value; },
+ handleCompletion: (reason) => {
+ this._globalValue = this._ensureValid(value);
+ resolve(this._globalValue);
+ }
+ });
+ });
+ },
+
+ /**
+ * Gets the load context from the given Browser.
+ *
+ * @param Browser The Browser whose load context will be returned.
+ * @return The nsILoadContext of the given Browser.
+ */
+ _loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) {
+ return browser.loadContext;
+ },
+
+ /**
+ * Asynchronously broadcasts "browser-fullZoom:location-change" so that
+ * listeners can be notified when the zoom levels on those pages change.
+ * The notification is always asynchronous so that observers are guaranteed a
+ * consistent behavior.
+ */
+ _notifyOnLocationChange: function FullZoom__notifyOnLocationChange(browser) {
+ this._executeSoon(function() {
+ Services.obs.notifyObservers(browser, "browser-fullZoom:location-change", "");
+ });
+ },
+
+ _executeSoon: function FullZoom__executeSoon(callback) {
+ if (!callback)
+ return;
+ Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+};
diff --git a/application/basilisk/base/content/browser-fxaccounts.js b/application/basilisk/base/content/browser-fxaccounts.js
new file mode 100644
index 000000000..f718b432a
--- /dev/null
+++ b/application/basilisk/base/content/browser-fxaccounts.js
@@ -0,0 +1,489 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gFxAccounts = {
+
+ SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration",
+
+ _initialized: false,
+ _inCustomizationMode: false,
+ _cachedProfile: null,
+
+ get weave() {
+ delete this.weave;
+ return this.weave = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+ },
+
+ get topics() {
+ // Do all this dance to lazy-load FxAccountsCommon.
+ delete this.topics;
+ return this.topics = [
+ "weave:service:ready",
+ "weave:service:login:change",
+ "weave:service:setup-complete",
+ "weave:service:sync:error",
+ "weave:ui:login:error",
+ "fxa-migration:state-changed",
+ this.FxAccountsCommon.ONLOGIN_NOTIFICATION,
+ this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
+ this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
+ ];
+ },
+
+ get panelUIFooter() {
+ delete this.panelUIFooter;
+ return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
+ },
+
+ get panelUIStatus() {
+ delete this.panelUIStatus;
+ return this.panelUIStatus = document.getElementById("PanelUI-fxa-status");
+ },
+
+ get panelUIAvatar() {
+ delete this.panelUIAvatar;
+ return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar");
+ },
+
+ get panelUILabel() {
+ delete this.panelUILabel;
+ return this.panelUILabel = document.getElementById("PanelUI-fxa-label");
+ },
+
+ get panelUIIcon() {
+ delete this.panelUIIcon;
+ return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon");
+ },
+
+ get strings() {
+ delete this.strings;
+ return this.strings = Services.strings.createBundle(
+ "chrome://browser/locale/accounts.properties"
+ );
+ },
+
+ get loginFailed() {
+ // Referencing Weave.Service will implicitly initialize sync, and we don't
+ // want to force that - so first check if it is ready.
+ let service = Cc["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ if (!service.ready) {
+ return false;
+ }
+ // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
+ // All other login failures are assumed to be transient and should go
+ // away by themselves, so aren't reflected here.
+ return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+ },
+
+ get sendTabToDeviceEnabled() {
+ return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
+ },
+
+ isSendableURI(aURISpec) {
+ if (!aURISpec) {
+ return false;
+ }
+ // Disallow sending tabs with more than 65535 characters.
+ if (aURISpec.length > 65535) {
+ return false;
+ }
+ try {
+ // Filter out un-sendable URIs -- things like local files, object urls, etc.
+ const unsendableRegexp = new RegExp(
+ Services.prefs.getCharPref("services.sync.engine.tabs.filteredUrls"), "i");
+ return !unsendableRegexp.test(aURISpec);
+ } catch (e) {
+ // The preference has been removed, or is an invalid regexp, so we log an
+ // error and treat it as a valid URI -- and the more problematic case is
+ // the length, which we've already addressed.
+ Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
+ return true;
+ }
+ },
+
+ get remoteClients() {
+ return Weave.Service.clientsEngine.remoteClients
+ .sort((a, b) => a.name.localeCompare(b.name));
+ },
+
+ init() {
+ // Bail out if we're already initialized and for pop-up windows.
+ if (this._initialized || !window.toolbar.visible) {
+ return;
+ }
+
+ for (let topic of this.topics) {
+ Services.obs.addObserver(this, topic, false);
+ }
+
+ gNavToolbox.addEventListener("customizationstarting", this);
+ gNavToolbox.addEventListener("customizationending", this);
+
+ EnsureFxAccountsWebChannel();
+ this._initialized = true;
+
+ this.updateUI();
+ },
+
+ uninit() {
+ if (!this._initialized) {
+ return;
+ }
+
+ for (let topic of this.topics) {
+ Services.obs.removeObserver(this, topic);
+ }
+
+ this._initialized = false;
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "fxa-migration:state-changed":
+ this.onMigrationStateChanged(data, subject);
+ break;
+ case this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION:
+ this._cachedProfile = null;
+ // Fallthrough intended
+ default:
+ this.updateUI();
+ break;
+ }
+ },
+
+ onMigrationStateChanged() {
+ // Since we nuked most of the migration code, this notification will fire
+ // once after legacy Sync has been disconnected (and should never fire
+ // again)
+ let nb = window.document.getElementById("global-notificationbox");
+
+ let msg = this.strings.GetStringFromName("autoDisconnectDescription")
+ let signInLabel = this.strings.GetStringFromName("autoDisconnectSignIn.label");
+ let signInAccessKey = this.strings.GetStringFromName("autoDisconnectSignIn.accessKey");
+ let learnMoreLink = this.fxaMigrator.learnMoreLink;
+
+ let buttons = [
+ {
+ label: signInLabel,
+ accessKey: signInAccessKey,
+ callback: () => {
+ this.openPreferences();
+ }
+ }
+ ];
+
+ let fragment = document.createDocumentFragment();
+ let msgNode = document.createTextNode(msg);
+ fragment.appendChild(msgNode);
+ if (learnMoreLink) {
+ let link = document.createElement("label");
+ link.className = "text-link";
+ link.setAttribute("value", learnMoreLink.text);
+ link.href = learnMoreLink.href;
+ fragment.appendChild(link);
+ }
+
+ nb.appendNotification(fragment,
+ this.SYNC_MIGRATION_NOTIFICATION_TITLE,
+ undefined,
+ nb.PRIORITY_WARNING_LOW,
+ buttons);
+
+ // ensure the hamburger menu reflects the newly disconnected state.
+ this.updateAppMenuItem();
+ },
+
+ handleEvent(event) {
+ this._inCustomizationMode = event.type == "customizationstarting";
+ this.updateAppMenuItem();
+ },
+
+ updateUI() {
+ // It's possible someone signed in to FxA after seeing our notification
+ // about "Legacy Sync migration" (which now is actually "Legacy Sync
+ // auto-disconnect") so kill that notification if it still exists.
+ let nb = window.document.getElementById("global-notificationbox");
+ let n = nb.getNotificationWithValue(this.SYNC_MIGRATION_NOTIFICATION_TITLE);
+ if (n) {
+ nb.removeNotification(n, true);
+ }
+
+ this.updateAppMenuItem();
+ },
+
+ // Note that updateAppMenuItem() returns a Promise that's only used by tests.
+ updateAppMenuItem() {
+ let profileInfoEnabled = false;
+ try {
+ profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled");
+ } catch (e) { }
+
+ // Bail out if FxA is disabled.
+ if (!this.weave.fxAccountsEnabled) {
+ return Promise.resolve();
+ }
+
+ this.panelUIFooter.hidden = false;
+
+ // Make sure the button is disabled in customization mode.
+ if (this._inCustomizationMode) {
+ this.panelUIStatus.setAttribute("disabled", "true");
+ this.panelUILabel.setAttribute("disabled", "true");
+ this.panelUIAvatar.setAttribute("disabled", "true");
+ this.panelUIIcon.setAttribute("disabled", "true");
+ } else {
+ this.panelUIStatus.removeAttribute("disabled");
+ this.panelUILabel.removeAttribute("disabled");
+ this.panelUIAvatar.removeAttribute("disabled");
+ this.panelUIIcon.removeAttribute("disabled");
+ }
+
+ let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
+ let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
+ let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
+ // The localization string is for the signed in text, but it's the default text as well
+ let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
+
+ let updateWithUserData = (userData) => {
+ // Window might have been closed while fetching data.
+ if (window.closed) {
+ return;
+ }
+
+ // Reset the button to its original state.
+ this.panelUILabel.setAttribute("label", defaultLabel);
+ this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
+ this.panelUIFooter.removeAttribute("fxastatus");
+ this.panelUIFooter.removeAttribute("fxaprofileimage");
+ this.panelUIAvatar.style.removeProperty("list-style-image");
+ let showErrorBadge = false;
+ if (userData) {
+ // At this point we consider the user as logged-in (but still can be in an error state)
+ if (this.loginFailed) {
+ let tooltipDescription = this.strings.formatStringFromName("reconnectDescription", [userData.email], 1);
+ this.panelUIFooter.setAttribute("fxastatus", "error");
+ this.panelUILabel.setAttribute("label", errorLabel);
+ this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
+ showErrorBadge = true;
+ } else if (!userData.verified) {
+ let tooltipDescription = this.strings.formatStringFromName("verifyDescription", [userData.email], 1);
+ this.panelUIFooter.setAttribute("fxastatus", "error");
+ this.panelUIFooter.setAttribute("unverified", "true");
+ this.panelUILabel.setAttribute("label", unverifiedLabel);
+ this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
+ showErrorBadge = true;
+ } else {
+ this.panelUIFooter.setAttribute("fxastatus", "signedin");
+ this.panelUILabel.setAttribute("label", userData.email);
+ }
+ if (profileInfoEnabled) {
+ this.panelUIFooter.setAttribute("fxaprofileimage", "enabled");
+ }
+ }
+ if (showErrorBadge) {
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
+ } else {
+ gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA);
+ }
+ }
+
+ let updateWithProfile = (profile) => {
+ if (profileInfoEnabled) {
+ if (profile.displayName) {
+ this.panelUILabel.setAttribute("label", profile.displayName);
+ }
+ if (profile.avatar) {
+ this.panelUIFooter.setAttribute("fxaprofileimage", "set");
+ let bgImage = "url(\"" + profile.avatar + "\")";
+ this.panelUIAvatar.style.listStyleImage = bgImage;
+
+ let img = new Image();
+ img.onerror = () => {
+ // Clear the image if it has trouble loading. Since this callback is asynchronous
+ // we check to make sure the image is still the same before we clear it.
+ if (this.panelUIAvatar.style.listStyleImage === bgImage) {
+ this.panelUIFooter.removeAttribute("fxaprofileimage");
+ this.panelUIAvatar.style.removeProperty("list-style-image");
+ }
+ };
+ img.src = profile.avatar;
+ }
+ }
+ }
+
+ return fxAccounts.getSignedInUser().then(userData => {
+ // userData may be null here when the user is not signed-in, but that's expected
+ updateWithUserData(userData);
+ // unverified users cause us to spew log errors fetching an OAuth token
+ // to fetch the profile, so don't even try in that case.
+ if (!userData || !userData.verified || !profileInfoEnabled) {
+ return null; // don't even try to grab the profile.
+ }
+ if (this._cachedProfile) {
+ return this._cachedProfile;
+ }
+ return fxAccounts.getSignedInUserProfile().catch(err => {
+ // Not fetching the profile is sad but the FxA logs will already have noise.
+ return null;
+ });
+ }).then(profile => {
+ if (!profile) {
+ return;
+ }
+ updateWithProfile(profile);
+ this._cachedProfile = profile; // Try to avoid fetching the profile on every UI update
+ }).catch(error => {
+ // This is most likely in tests, were we quickly log users in and out.
+ // The most likely scenario is a user logged out, so reflect that.
+ // Bug 995134 calls for better errors so we could retry if we were
+ // sure this was the failure reason.
+ this.FxAccountsCommon.log.error("Error updating FxA account info", error);
+ updateWithUserData(null);
+ });
+ },
+
+ onMenuPanelCommand() {
+
+ switch (this.panelUIFooter.getAttribute("fxastatus")) {
+ case "signedin":
+ this.openPreferences();
+ break;
+ case "error":
+ if (this.panelUIFooter.getAttribute("unverified")) {
+ this.openPreferences();
+ } else {
+ this.openSignInAgainPage("menupanel");
+ }
+ break;
+ default:
+ this.openPreferences();
+ break;
+ }
+
+ PanelUI.hide();
+ },
+
+ openPreferences() {
+ openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } });
+ },
+
+ openAccountsPage(action, urlParams = {}) {
+ let params = new URLSearchParams();
+ if (action) {
+ params.set("action", action);
+ }
+ for (let name in urlParams) {
+ if (urlParams[name] !== undefined) {
+ params.set(name, urlParams[name]);
+ }
+ }
+ let url = "about:accounts?" + params;
+ switchToTabHavingURI(url, true, {
+ replaceQueryString: true
+ });
+ },
+
+ openSignInAgainPage(entryPoint) {
+ this.openAccountsPage("reauth", { entrypoint: entryPoint });
+ },
+
+ sendTabToDevice(url, clientId, title) {
+ Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
+ },
+
+ populateSendTabToDevicesMenu(devicesPopup, url, title) {
+ // remove existing menu items
+ while (devicesPopup.hasChildNodes()) {
+ devicesPopup.removeChild(devicesPopup.firstChild);
+ }
+
+ const fragment = document.createDocumentFragment();
+
+ const onTargetDeviceCommand = (event) => {
+ let clients = event.target.getAttribute("clientId") ?
+ [event.target.getAttribute("clientId")] :
+ this.remoteClients.map(client => client.id);
+
+ clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
+ }
+
+ function addTargetDevice(clientId, name) {
+ const targetDevice = document.createElement("menuitem");
+ targetDevice.addEventListener("command", onTargetDeviceCommand, true);
+ targetDevice.setAttribute("class", "sendtab-target");
+ targetDevice.setAttribute("clientId", clientId);
+ targetDevice.setAttribute("label", name);
+ fragment.appendChild(targetDevice);
+ }
+
+ const clients = this.remoteClients;
+ for (let client of clients) {
+ addTargetDevice(client.id, client.name);
+ }
+
+ // "All devices" menu item
+ if (clients.length > 1) {
+ const separator = document.createElement("menuseparator");
+ fragment.appendChild(separator);
+ const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
+ addTargetDevice("", allDevicesLabel);
+ }
+
+ devicesPopup.appendChild(fragment);
+ },
+
+ updateTabContextMenu(aPopupMenu, aTargetTab) {
+ if (!this.sendTabToDeviceEnabled) {
+ return;
+ }
+
+ const targetURI = aTargetTab.linkedBrowser.currentURI.spec;
+ const showSendTab = this.remoteClients.length > 0 && this.isSendableURI(targetURI);
+
+ ["context_sendTabToDevice", "context_sendTabToDevice_separator"]
+ .forEach(id => { document.getElementById(id).hidden = !showSendTab });
+ },
+
+ initPageContextMenu(contextMenu) {
+ if (!this.sendTabToDeviceEnabled) {
+ return;
+ }
+
+ const remoteClientPresent = this.remoteClients.length > 0;
+ // showSendLink and showSendPage are mutually exclusive
+ let showSendLink = remoteClientPresent
+ && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
+ const showSendPage = !showSendLink && remoteClientPresent
+ && !(contextMenu.isContentSelected ||
+ contextMenu.onImage || contextMenu.onCanvas ||
+ contextMenu.onVideo || contextMenu.onAudio ||
+ contextMenu.onLink || contextMenu.onTextInput)
+ && this.isSendableURI(contextMenu.browser.currentURI.spec);
+
+ if (showSendLink) {
+ // This isn't part of the condition above since we don't want to try and
+ // send the page if a link is clicked on or selected but is not sendable.
+ showSendLink = this.isSendableURI(contextMenu.linkURL);
+ }
+
+ ["context-sendpagetodevice", "context-sep-sendpagetodevice"]
+ .forEach(id => contextMenu.showItem(id, showSendPage));
+ ["context-sendlinktodevice", "context-sep-sendlinktodevice"]
+ .forEach(id => contextMenu.showItem(id, showSendLink));
+ }
+};
+
+XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function() {
+ return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
+});
+
+XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator",
+ "resource://services-sync/FxaMigrator.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
+ "resource://gre/modules/FxAccountsWebChannel.jsm");
diff --git a/application/basilisk/base/content/browser-gestureSupport.js b/application/basilisk/base/content/browser-gestureSupport.js
new file mode 100644
index 000000000..11b55b3e4
--- /dev/null
+++ b/application/basilisk/base/content/browser-gestureSupport.js
@@ -0,0 +1,1240 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Simple gestures support
+//
+// As per bug #412486, web content must not be allowed to receive any
+// simple gesture events. Multi-touch gesture APIs are in their
+// infancy and we do NOT want to be forced into supporting an API that
+// will probably have to change in the future. (The current Mac OS X
+// API is undocumented and was reverse-engineered.) Until support is
+// implemented in the event dispatcher to keep these events as
+// chrome-only, we must listen for the simple gesture events during
+// the capturing phase and call stopPropagation on every event.
+
+var gGestureSupport = {
+ _currentRotation: 0,
+ _lastRotateDelta: 0,
+ _rotateMomentumThreshold: .75,
+
+ /**
+ * Add or remove mouse gesture event listeners
+ *
+ * @param aAddListener
+ * True to add/init listeners and false to remove/uninit
+ */
+ init: function GS_init(aAddListener) {
+ const gestureEvents = ["SwipeGestureMayStart", "SwipeGestureStart",
+ "SwipeGestureUpdate", "SwipeGestureEnd", "SwipeGesture",
+ "MagnifyGestureStart", "MagnifyGestureUpdate", "MagnifyGesture",
+ "RotateGestureStart", "RotateGestureUpdate", "RotateGesture",
+ "TapGesture", "PressTapGesture"];
+
+ let addRemove = aAddListener ? window.addEventListener :
+ window.removeEventListener;
+
+ for (let event of gestureEvents) {
+ addRemove("Moz" + event, this, true);
+ }
+ },
+
+ /**
+ * Dispatch events based on the type of mouse gesture event. For now, make
+ * sure to stop propagation of every gesture event so that web content cannot
+ * receive gesture events.
+ *
+ * @param aEvent
+ * The gesture event to handle
+ */
+ handleEvent: function GS_handleEvent(aEvent) {
+ if (!Services.prefs.getBoolPref(
+ "dom.debug.propagate_gesture_events_through_content")) {
+ aEvent.stopPropagation();
+ }
+
+ // Create a preference object with some defaults
+ let def = (aThreshold, aLatched) =>
+ ({ threshold: aThreshold, latched: !!aLatched });
+
+ switch (aEvent.type) {
+ case "MozSwipeGestureMayStart":
+ if (this._shouldDoSwipeGesture(aEvent)) {
+ aEvent.preventDefault();
+ }
+ break;
+ case "MozSwipeGestureStart":
+ aEvent.preventDefault();
+ this._setupSwipeGesture();
+ break;
+ case "MozSwipeGestureUpdate":
+ aEvent.preventDefault();
+ this._doUpdate(aEvent);
+ break;
+ case "MozSwipeGestureEnd":
+ aEvent.preventDefault();
+ this._doEnd(aEvent);
+ break;
+ case "MozSwipeGesture":
+ aEvent.preventDefault();
+ this.onSwipe(aEvent);
+ break;
+ case "MozMagnifyGestureStart":
+ aEvent.preventDefault();
+#ifdef XP_WIN
+ let pinchPref = def(25, 0)
+#else
+ let pinchPref = def(150, 1)
+#endif
+ this._setupGesture(aEvent, "pinch", pinchPref, "out", "in");
+ break;
+ case "MozRotateGestureStart":
+ aEvent.preventDefault();
+ this._setupGesture(aEvent, "twist", def(25, 0), "right", "left");
+ break;
+ case "MozMagnifyGestureUpdate":
+ case "MozRotateGestureUpdate":
+ aEvent.preventDefault();
+ this._doUpdate(aEvent);
+ break;
+ case "MozTapGesture":
+ aEvent.preventDefault();
+ this._doAction(aEvent, ["tap"]);
+ break;
+ case "MozRotateGesture":
+ aEvent.preventDefault();
+ this._doAction(aEvent, ["twist", "end"]);
+ break;
+ /* case "MozPressTapGesture":
+ break; */
+ }
+ },
+
+ /**
+ * Called at the start of "pinch" and "twist" gestures to setup all of the
+ * information needed to process the gesture
+ *
+ * @param aEvent
+ * The continual motion start event to handle
+ * @param aGesture
+ * Name of the gesture to handle
+ * @param aPref
+ * Preference object with the names of preferences and defaults
+ * @param aInc
+ * Command to trigger for increasing motion (without gesture name)
+ * @param aDec
+ * Command to trigger for decreasing motion (without gesture name)
+ */
+ _setupGesture: function GS__setupGesture(aEvent, aGesture, aPref, aInc, aDec) {
+ // Try to load user-set values from preferences
+ for (let [pref, def] of Object.entries(aPref))
+ aPref[pref] = this._getPref(aGesture + "." + pref, def);
+
+ // Keep track of the total deltas and latching behavior
+ let offset = 0;
+ let latchDir = aEvent.delta > 0 ? 1 : -1;
+ let isLatched = false;
+
+ // Create the update function here to capture closure state
+ this._doUpdate = function GS__doUpdate(updateEvent) {
+ // Update the offset with new event data
+ offset += updateEvent.delta;
+
+ // Check if the cumulative deltas exceed the threshold
+ if (Math.abs(offset) > aPref["threshold"]) {
+ // Trigger the action if we don't care about latching; otherwise, make
+ // sure either we're not latched and going the same direction of the
+ // initial motion; or we're latched and going the opposite way
+ let sameDir = (latchDir ^ offset) >= 0;
+ if (!aPref["latched"] || (isLatched ^ sameDir)) {
+ this._doAction(updateEvent, [aGesture, offset > 0 ? aInc : aDec]);
+
+ // We must be getting latched or leaving it, so just toggle
+ isLatched = !isLatched;
+ }
+
+ // Reset motion counter to prepare for more of the same gesture
+ offset = 0;
+ }
+ };
+
+ // The start event also contains deltas, so handle an update right away
+ this._doUpdate(aEvent);
+ },
+
+ /**
+ * Checks whether a swipe gesture event can navigate the browser history or
+ * not.
+ *
+ * @param aEvent
+ * The swipe gesture event.
+ * @return true if the swipe event may navigate the history, false othwerwise.
+ */
+ _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) {
+ return this._getCommand(aEvent, ["swipe", "left"])
+ == "Browser:BackOrBackDuplicate" &&
+ this._getCommand(aEvent, ["swipe", "right"])
+ == "Browser:ForwardOrForwardDuplicate";
+ },
+
+ /**
+ * Checks whether we want to start a swipe for aEvent and sets
+ * aEvent.allowedDirections to the right values.
+ *
+ * @param aEvent
+ * The swipe gesture "MayStart" event.
+ * @return true if we're willing to start a swipe for this event, false
+ * otherwise.
+ */
+ _shouldDoSwipeGesture: function GS__shouldDoSwipeGesture(aEvent) {
+ if (!this._swipeNavigatesHistory(aEvent)) {
+ return false;
+ }
+
+ let isVerticalSwipe = false;
+ if (aEvent.direction == aEvent.DIRECTION_UP) {
+ if (gMultiProcessBrowser || content.pageYOffset > 0) {
+ return false;
+ }
+ isVerticalSwipe = true;
+ } else if (aEvent.direction == aEvent.DIRECTION_DOWN) {
+ if (gMultiProcessBrowser || content.pageYOffset < content.scrollMaxY) {
+ return false;
+ }
+ isVerticalSwipe = true;
+ }
+ if (isVerticalSwipe) {
+ // Vertical overscroll has been temporarily disabled until bug 939480 is
+ // fixed.
+ return false;
+ }
+
+ let canGoBack = gHistorySwipeAnimation.canGoBack();
+ let canGoForward = gHistorySwipeAnimation.canGoForward();
+ let isLTR = gHistorySwipeAnimation.isLTR;
+
+ if (canGoBack) {
+ aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_LEFT :
+ aEvent.DIRECTION_RIGHT;
+ }
+ if (canGoForward) {
+ aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_RIGHT :
+ aEvent.DIRECTION_LEFT;
+ }
+
+ return true;
+ },
+
+ /**
+ * Sets up swipe gestures. This includes setting up swipe animations for the
+ * gesture, if enabled.
+ *
+ * @param aEvent
+ * The swipe gesture start event.
+ * @return true if swipe gestures could successfully be set up, false
+ * othwerwise.
+ */
+ _setupSwipeGesture: function GS__setupSwipeGesture() {
+ gHistorySwipeAnimation.startAnimation(false);
+
+ this._doUpdate = function GS__doUpdate(aEvent) {
+ gHistorySwipeAnimation.updateAnimation(aEvent.delta);
+ };
+
+ this._doEnd = function GS__doEnd(aEvent) {
+ gHistorySwipeAnimation.swipeEndEventReceived();
+
+ this._doUpdate = function() {};
+ this._doEnd = function() {};
+ }
+ },
+
+ /**
+ * Generator producing the powerset of the input array where the first result
+ * is the complete set and the last result (before StopIteration) is empty.
+ *
+ * @param aArray
+ * Source array containing any number of elements
+ * @yield Array that is a subset of the input array from full set to empty
+ */
+ _power: function* GS__power(aArray) {
+ // Create a bitmask based on the length of the array
+ let num = 1 << aArray.length;
+ while (--num >= 0) {
+ // Only select array elements where the current bit is set
+ yield aArray.reduce(function(aPrev, aCurr, aIndex) {
+ if (num & 1 << aIndex)
+ aPrev.push(aCurr);
+ return aPrev;
+ }, []);
+ }
+ },
+
+ /**
+ * Determine what action to do for the gesture based on which keys are
+ * pressed and which commands are set, and execute the command.
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aGesture
+ * Array of gesture name parts (to be joined by periods)
+ * @return Name of the executed command. Returns null if no command is
+ * found.
+ */
+ _doAction: function GS__doAction(aEvent, aGesture) {
+ let command = this._getCommand(aEvent, aGesture);
+ return command && this._doCommand(aEvent, command);
+ },
+
+ /**
+ * Determine what action to do for the gesture based on which keys are
+ * pressed and which commands are set
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aGesture
+ * Array of gesture name parts (to be joined by periods)
+ */
+ _getCommand: function GS__getCommand(aEvent, aGesture) {
+ // Create an array of pressed keys in a fixed order so that a command for
+ // "meta" is preferred over "ctrl" when both buttons are pressed (and a
+ // command for both don't exist)
+ let keyCombos = [];
+ for (let key of ["shift", "alt", "ctrl", "meta"]) {
+ if (aEvent[key + "Key"])
+ keyCombos.push(key);
+ }
+
+ // Try each combination of key presses in decreasing order for commands
+ for (let subCombo of this._power(keyCombos)) {
+ // Convert a gesture and pressed keys into the corresponding command
+ // action where the preference has the gesture before "shift" before
+ // "alt" before "ctrl" before "meta" all separated by periods
+ let command;
+ try {
+ command = this._getPref(aGesture.concat(subCombo).join("."));
+ } catch (e) {}
+
+ if (command)
+ return command;
+ }
+ return null;
+ },
+
+ /**
+ * Execute the specified command.
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aCommand
+ * Name of the command found for the event's keys and gesture.
+ */
+ _doCommand: function GS__doCommand(aEvent, aCommand) {
+ let node = document.getElementById(aCommand);
+ if (node) {
+ if (node.getAttribute("disabled") != "true") {
+ let cmdEvent = document.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent("command", true, true, window, 0,
+ aEvent.ctrlKey, aEvent.altKey,
+ aEvent.shiftKey, aEvent.metaKey, aEvent);
+ node.dispatchEvent(cmdEvent);
+ }
+
+ } else {
+ goDoCommand(aCommand);
+ }
+ },
+
+ /**
+ * Handle continual motion events. This function will be set by
+ * _setupGesture or _setupSwipe.
+ *
+ * @param aEvent
+ * The continual motion update event to handle
+ */
+ _doUpdate(aEvent) {},
+
+ /**
+ * Handle gesture end events. This function will be set by _setupSwipe.
+ *
+ * @param aEvent
+ * The gesture end event to handle
+ */
+ _doEnd(aEvent) {},
+
+ /**
+ * Convert the swipe gesture into a browser action based on the direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ */
+ onSwipe: function GS_onSwipe(aEvent) {
+ // Figure out which one (and only one) direction was triggered
+ for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) {
+ if (aEvent.direction == aEvent["DIRECTION_" + dir]) {
+ this._coordinateSwipeEventWithAnimation(aEvent, dir);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Process a swipe event based on the given direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) {
+ this._doAction(aEvent, ["swipe", aDir.toLowerCase()]);
+ },
+
+ /**
+ * Coordinates the swipe event with the swipe animation, if any.
+ * If an animation is currently running, the swipe event will be
+ * processed once the animation stops. This will guarantee a fluid
+ * motion of the animation.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ _coordinateSwipeEventWithAnimation:
+ function GS__coordinateSwipeEventWithAnimation(aEvent, aDir) {
+ if ((gHistorySwipeAnimation.isAnimationRunning()) &&
+ (aDir == "RIGHT" || aDir == "LEFT")) {
+ gHistorySwipeAnimation.processSwipeEvent(aEvent, aDir);
+ } else {
+ this.processSwipeEvent(aEvent, aDir);
+ }
+ },
+
+ /**
+ * Get a gesture preference or use a default if it doesn't exist
+ *
+ * @param aPref
+ * Name of the preference to load under the gesture branch
+ * @param aDef
+ * Default value if the preference doesn't exist
+ */
+ _getPref: function GS__getPref(aPref, aDef) {
+ // Preferences branch under which all gestures preferences are stored
+ const branch = "browser.gesture.";
+
+ try {
+ // Determine what type of data to load based on default value's type
+ let type = typeof aDef;
+ let getFunc = "Char";
+ if (type == "boolean")
+ getFunc = "Bool";
+ else if (type == "number")
+ getFunc = "Int";
+ return gPrefService["get" + getFunc + "Pref"](branch + aPref);
+ } catch (e) {
+ return aDef;
+ }
+ },
+
+ /**
+ * Perform rotation for ImageDocuments
+ *
+ * @param aEvent
+ * The MozRotateGestureUpdate event triggering this call
+ */
+ rotate(aEvent) {
+ if (!(content.document instanceof ImageDocument))
+ return;
+
+ let contentElement = content.document.body.firstElementChild;
+ if (!contentElement)
+ return;
+ // If we're currently snapping, cancel that snap
+ if (contentElement.classList.contains("completeRotation"))
+ this._clearCompleteRotation();
+
+ this.rotation = Math.round(this.rotation + aEvent.delta);
+ contentElement.style.transform = "rotate(" + this.rotation + "deg)";
+ this._lastRotateDelta = aEvent.delta;
+ },
+
+ /**
+ * Perform a rotation end for ImageDocuments
+ */
+ rotateEnd() {
+ if (!(content.document instanceof ImageDocument))
+ return;
+
+ let contentElement = content.document.body.firstElementChild;
+ if (!contentElement)
+ return;
+
+ let transitionRotation = 0;
+
+ // The reason that 360 is allowed here is because when rotating between
+ // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong
+ // direction around--spinning wildly.
+ if (this.rotation <= 45)
+ transitionRotation = 0;
+ else if (this.rotation > 45 && this.rotation <= 135)
+ transitionRotation = 90;
+ else if (this.rotation > 135 && this.rotation <= 225)
+ transitionRotation = 180;
+ else if (this.rotation > 225 && this.rotation <= 315)
+ transitionRotation = 270;
+ else
+ transitionRotation = 360;
+
+ // If we're going fast enough, and we didn't already snap ahead of rotation,
+ // then snap ahead of rotation to simulate momentum
+ if (this._lastRotateDelta > this._rotateMomentumThreshold &&
+ this.rotation > transitionRotation)
+ transitionRotation += 90;
+ else if (this._lastRotateDelta < -1 * this._rotateMomentumThreshold &&
+ this.rotation < transitionRotation)
+ transitionRotation -= 90;
+
+ // Only add the completeRotation class if it is is necessary
+ if (transitionRotation != this.rotation) {
+ contentElement.classList.add("completeRotation");
+ contentElement.addEventListener("transitionend", this._clearCompleteRotation);
+ }
+
+ contentElement.style.transform = "rotate(" + transitionRotation + "deg)";
+ this.rotation = transitionRotation;
+ },
+
+ /**
+ * Gets the current rotation for the ImageDocument
+ */
+ get rotation() {
+ return this._currentRotation;
+ },
+
+ /**
+ * Sets the current rotation for the ImageDocument
+ *
+ * @param aVal
+ * The new value to take. Can be any value, but it will be bounded to
+ * 0 inclusive to 360 exclusive.
+ */
+ set rotation(aVal) {
+ this._currentRotation = aVal % 360;
+ if (this._currentRotation < 0)
+ this._currentRotation += 360;
+ return this._currentRotation;
+ },
+
+ /**
+ * When the location/tab changes, need to reload the current rotation for the
+ * image
+ */
+ restoreRotationState() {
+ // Bug 863514 - Make gesture support work in electrolysis
+ if (gMultiProcessBrowser)
+ return;
+
+ if (!(content.document instanceof ImageDocument))
+ return;
+
+ let contentElement = content.document.body.firstElementChild;
+ let transformValue = content.window.getComputedStyle(contentElement, null)
+ .transform;
+
+ if (transformValue == "none") {
+ this.rotation = 0;
+ return;
+ }
+
+ // transformValue is a rotation matrix--split it and do mathemagic to
+ // obtain the real rotation value
+ transformValue = transformValue.split("(")[1]
+ .split(")")[0]
+ .split(",");
+ this.rotation = Math.round(Math.atan2(transformValue[1], transformValue[0]) *
+ (180 / Math.PI));
+ },
+
+ /**
+ * Removes the transition rule by removing the completeRotation class
+ */
+ _clearCompleteRotation() {
+ let contentElement = content.document &&
+ content.document instanceof ImageDocument &&
+ content.document.body &&
+ content.document.body.firstElementChild;
+ if (!contentElement)
+ return;
+ contentElement.classList.remove("completeRotation");
+ contentElement.removeEventListener("transitionend", this._clearCompleteRotation);
+ },
+};
+
+// History Swipe Animation Support (bug 678392)
+var gHistorySwipeAnimation = {
+
+ active: false,
+ isLTR: false,
+
+ /**
+ * Initializes the support for history swipe animations, if it is supported
+ * by the platform/configuration.
+ */
+ init: function HSA_init() {
+ if (!this._isSupported())
+ return;
+
+ this.active = false;
+ this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)");
+ this._trackedSnapshots = [];
+ this._startingIndex = -1;
+ this._historyIndex = -1;
+ this._boxWidth = -1;
+ this._boxHeight = -1;
+ this._maxSnapshots = this._getMaxSnapshots();
+ this._lastSwipeDir = "";
+ this._direction = "horizontal";
+
+ // We only want to activate history swipe animations if we store snapshots.
+ // If we don't store any, we handle horizontal swipes without animations.
+ if (this._maxSnapshots > 0) {
+ this.active = true;
+ gBrowser.addEventListener("pagehide", this);
+ gBrowser.addEventListener("pageshow", this);
+ gBrowser.addEventListener("popstate", this);
+ gBrowser.addEventListener("DOMModalDialogClosed", this);
+ gBrowser.tabContainer.addEventListener("TabClose", this);
+ }
+ },
+
+ /**
+ * Uninitializes the support for history swipe animations.
+ */
+ uninit: function HSA_uninit() {
+ gBrowser.removeEventListener("pagehide", this);
+ gBrowser.removeEventListener("pageshow", this);
+ gBrowser.removeEventListener("popstate", this);
+ gBrowser.removeEventListener("DOMModalDialogClosed", this);
+ gBrowser.tabContainer.removeEventListener("TabClose", this);
+
+ this.active = false;
+ this.isLTR = false;
+ },
+
+ /**
+ * Starts the swipe animation and handles fast swiping (i.e. a swipe animation
+ * is already in progress when a new one is initiated).
+ *
+ * @param aIsVerticalSwipe
+ * Whether we're dealing with a vertical swipe or not.
+ */
+ startAnimation: function HSA_startAnimation(aIsVerticalSwipe) {
+ this._direction = aIsVerticalSwipe ? "vertical" : "horizontal";
+
+ if (this.isAnimationRunning()) {
+ // If this is a horizontal scroll, or if this is a vertical scroll that
+ // was started while a horizontal scroll was still running, handle it as
+ // as a fast swipe. In the case of the latter scenario, this allows us to
+ // start the vertical animation without first loading the final page, or
+ // taking another snapshot. If vertical scrolls are initiated repeatedly
+ // without prior horizontal scroll we skip this and restart the animation
+ // from 0.
+ if (this._direction == "horizontal" || this._lastSwipeDir != "") {
+ gBrowser.stop();
+ this._lastSwipeDir = "RELOAD"; // just ensure that != ""
+ this._canGoBack = this.canGoBack();
+ this._canGoForward = this.canGoForward();
+ this._handleFastSwiping();
+ }
+ this.updateAnimation(0);
+ } else {
+ // Get the session history from SessionStore.
+ let updateSessionHistory = sessionHistory => {
+ this._startingIndex = sessionHistory.index;
+ this._historyIndex = this._startingIndex;
+ this._canGoBack = this.canGoBack();
+ this._canGoForward = this.canGoForward();
+ if (this.active) {
+ this._addBoxes();
+ this._takeSnapshot();
+ this._installPrevAndNextSnapshots();
+ this._lastSwipeDir = "";
+ }
+ this.updateAnimation(0);
+ }
+ SessionStore.getSessionHistory(gBrowser.selectedTab, updateSessionHistory);
+ }
+ },
+
+ /**
+ * Stops the swipe animation.
+ */
+ stopAnimation: function HSA_stopAnimation() {
+ gHistorySwipeAnimation._removeBoxes();
+ this._historyIndex = this._getCurrentHistoryIndex();
+ },
+
+ /**
+ * Updates the animation between two pages in history.
+ *
+ * @param aVal
+ * A floating point value that represents the progress of the
+ * swipe gesture.
+ */
+ updateAnimation: function HSA_updateAnimation(aVal) {
+ if (!this.isAnimationRunning()) {
+ return;
+ }
+
+ // We use the following value to decrease the bounce effect when scrolling
+ // to the top or bottom of the page, or when swiping back/forward past the
+ // browsing history. This value was determined experimentally.
+ let dampValue = 4;
+ if (this._direction == "vertical") {
+ this._prevBox.collapsed = true;
+ this._nextBox.collapsed = true;
+ this._positionBox(this._curBox, -1 * aVal / dampValue);
+ } else if ((aVal >= 0 && this.isLTR) ||
+ (aVal <= 0 && !this.isLTR)) {
+ let tempDampValue = 1;
+ if (this._canGoBack) {
+ this._prevBox.collapsed = false;
+ } else {
+ tempDampValue = dampValue;
+ this._prevBox.collapsed = true;
+ }
+
+ // The current page is pushed to the right (LTR) or left (RTL),
+ // the intention is to go back.
+ // If there is a page to go back to, it should show in the background.
+ this._positionBox(this._curBox, aVal / tempDampValue);
+
+ // The forward page should be pushed offscreen all the way to the right.
+ this._positionBox(this._nextBox, 1);
+ } else if (this._canGoForward) {
+ // The intention is to go forward. If there is a page to go forward to,
+ // it should slide in from the right (LTR) or left (RTL).
+ // Otherwise, the current page should slide to the left (LTR) or
+ // right (RTL) and the backdrop should appear in the background.
+ // For the backdrop to be visible in that case, the previous page needs
+ // to be hidden (if it exists).
+ this._nextBox.collapsed = false;
+ let offset = this.isLTR ? 1 : -1;
+ this._positionBox(this._curBox, 0);
+ this._positionBox(this._nextBox, offset + aVal);
+ } else {
+ this._prevBox.collapsed = true;
+ this._positionBox(this._curBox, aVal / dampValue);
+ }
+ },
+
+ _getCurrentHistoryIndex() {
+ return SessionStore.getSessionHistory(gBrowser.selectedTab).index;
+ },
+
+ /**
+ * Event handler for events relevant to the history swipe animation.
+ *
+ * @param aEvent
+ * An event to process.
+ */
+ handleEvent: function HSA_handleEvent(aEvent) {
+ let browser = gBrowser.selectedBrowser;
+ switch (aEvent.type) {
+ case "TabClose":
+ let browserForTab = gBrowser.getBrowserForTab(aEvent.target);
+ this._removeTrackedSnapshot(-1, browserForTab);
+ break;
+ case "DOMModalDialogClosed":
+ this.stopAnimation();
+ break;
+ case "pageshow":
+ if (aEvent.target == browser.contentDocument) {
+ this.stopAnimation();
+ }
+ break;
+ case "popstate":
+ if (aEvent.target == browser.contentDocument.defaultView) {
+ this.stopAnimation();
+ }
+ break;
+ case "pagehide":
+ if (aEvent.target == browser.contentDocument) {
+ // Take and compress a snapshot of a page whenever it's about to be
+ // navigated away from. We already have a snapshot of the page if an
+ // animation is running, so we're left with compressing it.
+ if (!this.isAnimationRunning()) {
+ this._takeSnapshot();
+ }
+ this._compressSnapshotAtCurrentIndex();
+ }
+ break;
+ }
+ },
+
+ /**
+ * Checks whether the history swipe animation is currently running or not.
+ *
+ * @return true if the animation is currently running, false otherwise.
+ */
+ isAnimationRunning: function HSA_isAnimationRunning() {
+ return !!this._container;
+ },
+
+ /**
+ * Process a swipe event based on the given direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ processSwipeEvent: function HSA_processSwipeEvent(aEvent, aDir) {
+ if (aDir == "RIGHT")
+ this._historyIndex += this.isLTR ? 1 : -1;
+ else if (aDir == "LEFT")
+ this._historyIndex += this.isLTR ? -1 : 1;
+ else
+ return;
+ this._lastSwipeDir = aDir;
+ },
+
+ /**
+ * Checks if there is a page in the browser history to go back to.
+ *
+ * @return true if there is a previous page in history, false otherwise.
+ */
+ canGoBack: function HSA_canGoBack() {
+ if (this.isAnimationRunning())
+ return this._doesIndexExistInHistory(this._historyIndex - 1);
+ return gBrowser.webNavigation.canGoBack;
+ },
+
+ /**
+ * Checks if there is a page in the browser history to go forward to.
+ *
+ * @return true if there is a next page in history, false otherwise.
+ */
+ canGoForward: function HSA_canGoForward() {
+ if (this.isAnimationRunning())
+ return this._doesIndexExistInHistory(this._historyIndex + 1);
+ return gBrowser.webNavigation.canGoForward;
+ },
+
+ /**
+ * Used to notify the history swipe animation that the OS sent a swipe end
+ * event and that we should navigate to the page that the user swiped to, if
+ * any. This will also result in the animation overlay to be torn down.
+ */
+ swipeEndEventReceived: function HSA_swipeEndEventReceived() {
+ // Update the session history before continuing.
+ let updateSessionHistory = sessionHistory => {
+ if (this._lastSwipeDir != "" && this._historyIndex != this._startingIndex)
+ this._navigateToHistoryIndex();
+ else
+ this.stopAnimation();
+ }
+ SessionStore.getSessionHistory(gBrowser.selectedTab, updateSessionHistory);
+ },
+
+ /**
+ * Checks whether a particular index exists in the browser history or not.
+ *
+ * @param aIndex
+ * The index to check for availability for in the history.
+ * @return true if the index exists in the browser history, false otherwise.
+ */
+ _doesIndexExistInHistory: function HSA__doesIndexExistInHistory(aIndex) {
+ try {
+ return SessionStore.getSessionHistory(gBrowser.selectedTab).entries[aIndex] != null;
+ } catch (ex) {
+ return false;
+ }
+ },
+
+ /**
+ * Navigates to the index in history that is currently being tracked by
+ * |this|.
+ */
+ _navigateToHistoryIndex: function HSA__navigateToHistoryIndex() {
+ if (this._doesIndexExistInHistory(this._historyIndex))
+ gBrowser.webNavigation.gotoIndex(this._historyIndex);
+ else
+ this.stopAnimation();
+ },
+
+ /**
+ * Checks to see if history swipe animations are supported by this
+ * platform/configuration.
+ *
+ * return true if supported, false otherwise.
+ */
+ _isSupported: function HSA__isSupported() {
+ return window.matchMedia("(-moz-swipe-animation-enabled)").matches;
+ },
+
+ /**
+ * Handle fast swiping (i.e. a swipe animation is already in
+ * progress when a new one is initiated). This will swap out the snapshots
+ * used in the previous animation with the appropriate new ones.
+ */
+ _handleFastSwiping: function HSA__handleFastSwiping() {
+ this._installCurrentPageSnapshot(null);
+ this._installPrevAndNextSnapshots();
+ },
+
+ /**
+ * Adds the boxes that contain the snapshots used during the swipe animation.
+ */
+ _addBoxes: function HSA__addBoxes() {
+ let browserStack =
+ document.getAnonymousElementByAttribute(gBrowser.getNotificationBox(),
+ "class", "browserStack");
+ this._container = this._createElement("historySwipeAnimationContainer",
+ "stack");
+ browserStack.appendChild(this._container);
+
+ this._prevBox = this._createElement("historySwipeAnimationPreviousPage",
+ "box");
+ this._container.appendChild(this._prevBox);
+
+ this._curBox = this._createElement("historySwipeAnimationCurrentPage",
+ "box");
+ this._container.appendChild(this._curBox);
+
+ this._nextBox = this._createElement("historySwipeAnimationNextPage",
+ "box");
+ this._container.appendChild(this._nextBox);
+
+ // Cache width and height.
+ this._boxWidth = this._curBox.getBoundingClientRect().width;
+ this._boxHeight = this._curBox.getBoundingClientRect().height;
+ },
+
+ /**
+ * Removes the boxes.
+ */
+ _removeBoxes: function HSA__removeBoxes() {
+ this._curBox = null;
+ this._prevBox = null;
+ this._nextBox = null;
+ if (this._container)
+ this._container.parentNode.removeChild(this._container);
+ this._container = null;
+ this._boxWidth = -1;
+ this._boxHeight = -1;
+ },
+
+ /**
+ * Creates an element with a given identifier and tag name.
+ *
+ * @param aID
+ * An identifier to create the element with.
+ * @param aTagName
+ * The name of the tag to create the element for.
+ * @return the newly created element.
+ */
+ _createElement: function HSA__createElement(aID, aTagName) {
+ let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let element = document.createElementNS(XULNS, aTagName);
+ element.id = aID;
+ return element;
+ },
+
+ /**
+ * Moves a given box to a given X coordinate position.
+ *
+ * @param aBox
+ * The box element to position.
+ * @param aPosition
+ * The position (in X coordinates) to move the box element to.
+ */
+ _positionBox: function HSA__positionBox(aBox, aPosition) {
+ let transform = "";
+
+ if (this._direction == "vertical")
+ transform = "translateY(" + this._boxHeight * aPosition + "px)";
+ else
+ transform = "translateX(" + this._boxWidth * aPosition + "px)";
+
+ aBox.style.transform = transform;
+ },
+
+ /**
+ * Verifies that we're ready to take snapshots based on the global pref and
+ * the current index in history.
+ *
+ * @return true if we're ready to take snapshots, false otherwise.
+ */
+ _readyToTakeSnapshots: function HSA__readyToTakeSnapshots() {
+ return (this._maxSnapshots >= 1 && this._getCurrentHistoryIndex() >= 0);
+ },
+
+ /**
+ * Takes a snapshot of the page the browser is currently on.
+ */
+ _takeSnapshot: function HSA__takeSnapshot() {
+ if (!this._readyToTakeSnapshots()) {
+ return;
+ }
+
+ let canvas = null;
+
+ let browser = gBrowser.selectedBrowser;
+ let r = browser.getBoundingClientRect();
+ canvas = document.createElementNS("http://www.w3.org/1999/xhtml",
+ "canvas");
+ canvas.mozOpaque = true;
+ let scale = window.devicePixelRatio;
+ canvas.width = r.width * scale;
+ canvas.height = r.height * scale;
+ let ctx = canvas.getContext("2d");
+ let zoom = browser.markupDocumentViewer.fullZoom * scale;
+ ctx.scale(zoom, zoom);
+ ctx.drawWindow(browser.contentWindow,
+ 0, 0, canvas.width / zoom, canvas.height / zoom, "white",
+ ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW |
+ ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
+ ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
+
+ TelemetryStopwatch.start("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
+ try {
+ this._installCurrentPageSnapshot(canvas);
+ this._assignSnapshotToCurrentBrowser(canvas);
+ } finally {
+ TelemetryStopwatch.finish("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
+ }
+ },
+
+ /**
+ * Retrieves the maximum number of snapshots that should be kept in memory.
+ * This limit is a global limit and is valid across all open tabs.
+ */
+ _getMaxSnapshots: function HSA__getMaxSnapshots() {
+ return gPrefService.getIntPref("browser.snapshots.limit");
+ },
+
+ /**
+ * Adds a snapshot to the list and initiates the compression of said snapshot.
+ * Once the compression is completed, it will replace the uncompressed
+ * snapshot in the list.
+ *
+ * @param aCanvas
+ * The snapshot to add to the list and compress.
+ */
+ _assignSnapshotToCurrentBrowser:
+ function HSA__assignSnapshotToCurrentBrowser(aCanvas) {
+ let browser = gBrowser.selectedBrowser;
+ let currIndex = this._getCurrentHistoryIndex();
+
+ this._removeTrackedSnapshot(currIndex, browser);
+ this._addSnapshotRefToArray(currIndex, browser);
+
+ if (!("snapshots" in browser))
+ browser.snapshots = [];
+ let snapshots = browser.snapshots;
+ // Temporarily store the canvas as the compressed snapshot.
+ // This avoids a blank page if the user swipes quickly
+ // between pages before the compression could complete.
+ snapshots[currIndex] = {
+ image: aCanvas,
+ scale: window.devicePixelRatio
+ };
+ },
+
+ /**
+ * Compresses the HTMLCanvasElement that's stored at the current history
+ * index in the snapshot array and stores the compressed image in its place.
+ */
+ _compressSnapshotAtCurrentIndex:
+ function HSA__compressSnapshotAtCurrentIndex() {
+ if (!this._readyToTakeSnapshots()) {
+ // We didn't take a snapshot earlier because we weren't ready to, so
+ // there's nothing to compress.
+ return;
+ }
+
+ TelemetryStopwatch.start("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
+ try {
+ let browser = gBrowser.selectedBrowser;
+ let snapshots = browser.snapshots;
+ let currIndex = _getCurrentHistoryIndex();
+
+ // Kick off snapshot compression.
+ let canvas = snapshots[currIndex].image;
+ canvas.toBlob(function(aBlob) {
+ if (snapshots[currIndex]) {
+ snapshots[currIndex].image = aBlob;
+ }
+ }, "image/png"
+ );
+ } finally {
+ TelemetryStopwatch.finish("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
+ }
+ },
+
+ /**
+ * Removes a snapshot identified by the browser and index in the array of
+ * snapshots for that browser, if present. If no snapshot could be identified
+ * the method simply returns without taking any action. If aIndex is negative,
+ * all snapshots for a particular browser will be removed.
+ *
+ * @param aIndex
+ * The index in history of the new snapshot, or negative value if all
+ * snapshots for a browser should be removed.
+ * @param aBrowser
+ * The browser the new snapshot was taken in.
+ */
+ _removeTrackedSnapshot: function HSA__removeTrackedSnapshot(aIndex, aBrowser) {
+ let arr = this._trackedSnapshots;
+ let requiresExactIndexMatch = aIndex >= 0;
+ for (let i = 0; i < arr.length; i++) {
+ if ((arr[i].browser == aBrowser) &&
+ (aIndex < 0 || aIndex == arr[i].index)) {
+ delete aBrowser.snapshots[arr[i].index];
+ arr.splice(i, 1);
+ if (requiresExactIndexMatch)
+ return; // Found and removed the only element.
+ i--; // Make sure to revisit the index that we just removed an
+ // element at.
+ }
+ }
+ },
+
+ /**
+ * Adds a new snapshot reference for a given index and browser to the array
+ * of references to tracked snapshots.
+ *
+ * @param aIndex
+ * The index in history of the new snapshot.
+ * @param aBrowser
+ * The browser the new snapshot was taken in.
+ */
+ _addSnapshotRefToArray:
+ function HSA__addSnapshotRefToArray(aIndex, aBrowser) {
+ let id = { index: aIndex,
+ browser: aBrowser };
+ let arr = this._trackedSnapshots;
+ arr.unshift(id);
+
+ while (arr.length > this._maxSnapshots) {
+ let lastElem = arr[arr.length - 1];
+ delete lastElem.browser.snapshots[lastElem.index].image;
+ delete lastElem.browser.snapshots[lastElem.index];
+ arr.splice(-1, 1);
+ }
+ },
+
+ /**
+ * Converts a compressed blob to an Image object. In some situations
+ * (especially during fast swiping) aBlob may still be a canvas, not a
+ * compressed blob. In this case, we simply return the canvas.
+ *
+ * @param aBlob
+ * The compressed blob to convert, or a canvas if a blob compression
+ * couldn't complete before this method was called.
+ * @return A new Image object representing the converted blob.
+ */
+ _convertToImg: function HSA__convertToImg(aBlob) {
+ if (!aBlob)
+ return null;
+
+ // Return aBlob if it's still a canvas and not a compressed blob yet.
+ if (aBlob instanceof HTMLCanvasElement)
+ return aBlob;
+
+ let img = new Image();
+ let url = "";
+ try {
+ url = URL.createObjectURL(aBlob);
+ img.onload = function() {
+ URL.revokeObjectURL(url);
+ };
+ } finally {
+ img.src = url;
+ }
+ return img;
+ },
+
+ /**
+ * Scales the background of a given box element (which uses a given snapshot
+ * as background) based on a given scale factor.
+ * @param aSnapshot
+ * The snapshot that is used as background of aBox.
+ * @param aScale
+ * The scale factor to use.
+ * @param aBox
+ * The box element that uses aSnapshot as background.
+ */
+ _scaleSnapshot: function HSA__scaleSnapshot(aSnapshot, aScale, aBox) {
+ if (aSnapshot && aScale != 1 && aBox) {
+ if (aSnapshot instanceof HTMLCanvasElement) {
+ aBox.style.backgroundSize =
+ aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
+ } else {
+ // snapshot is instanceof HTMLImageElement
+ aSnapshot.addEventListener("load", function() {
+ aBox.style.backgroundSize =
+ aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
+ });
+ }
+ }
+ },
+
+ /**
+ * Sets the snapshot of the current page to the snapshot passed as parameter,
+ * or to the one previously stored for the current index in history if the
+ * parameter is null.
+ *
+ * @param aCanvas
+ * The snapshot to set the current page to. If this parameter is null,
+ * the previously stored snapshot for this index (if any) will be used.
+ */
+ _installCurrentPageSnapshot:
+ function HSA__installCurrentPageSnapshot(aCanvas) {
+ let currSnapshot = aCanvas;
+ let scale = window.devicePixelRatio;
+ if (!currSnapshot) {
+ let snapshots = gBrowser.selectedBrowser.snapshots || {};
+ let currIndex = this._historyIndex;
+ if (currIndex in snapshots) {
+ currSnapshot = this._convertToImg(snapshots[currIndex].image);
+ scale = snapshots[currIndex].scale;
+ }
+ }
+ this._scaleSnapshot(currSnapshot, scale, this._curBox ? this._curBox :
+ null);
+ document.mozSetImageElement("historySwipeAnimationCurrentPageSnapshot",
+ currSnapshot);
+ },
+
+ /**
+ * Sets the snapshots of the previous and next pages to the snapshots
+ * previously stored for their respective indeces.
+ */
+ _installPrevAndNextSnapshots:
+ function HSA__installPrevAndNextSnapshots() {
+ let snapshots = gBrowser.selectedBrowser.snapshots || [];
+ let currIndex = this._historyIndex;
+ let prevIndex = currIndex - 1;
+ let prevSnapshot = null;
+ if (prevIndex in snapshots) {
+ prevSnapshot = this._convertToImg(snapshots[prevIndex].image);
+ this._scaleSnapshot(prevSnapshot, snapshots[prevIndex].scale,
+ this._prevBox);
+ }
+ document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot",
+ prevSnapshot);
+
+ let nextIndex = currIndex + 1;
+ let nextSnapshot = null;
+ if (nextIndex in snapshots) {
+ nextSnapshot = this._convertToImg(snapshots[nextIndex].image);
+ this._scaleSnapshot(nextSnapshot, snapshots[nextIndex].scale,
+ this._nextBox);
+ }
+ document.mozSetImageElement("historySwipeAnimationNextPageSnapshot",
+ nextSnapshot);
+ },
+};
diff --git a/application/basilisk/base/content/browser-media.js b/application/basilisk/base/content/browser-media.js
new file mode 100644
index 000000000..df76df9c0
--- /dev/null
+++ b/application/basilisk/base/content/browser-media.js
@@ -0,0 +1,351 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var gEMEHandler = {
+ get uiEnabled() {
+#ifdef MOZ_EME
+ let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled");
+ // Force-disable on WinXP:
+ if (navigator.platform.toLowerCase().startsWith("win")) {
+ emeUIEnabled = emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6;
+ }
+ return emeUIEnabled;
+#else
+ return false;
+#endif
+ },
+ ensureEMEEnabled(browser, keySystem) {
+ Services.prefs.setBoolPref("media.eme.enabled", true);
+ if (keySystem &&
+ keySystem == "com.widevine.alpha" &&
+ Services.prefs.getPrefType("media.gmp-widevinecdm.enabled") &&
+ !Services.prefs.getBoolPref("media.gmp-widevinecdm.enabled")) {
+ Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", true);
+ }
+ browser.reload();
+ },
+ isKeySystemVisible(keySystem) {
+ if (!keySystem) {
+ return false;
+ }
+ if (keySystem == "com.widevine.alpha" &&
+ Services.prefs.getPrefType("media.gmp-widevinecdm.visible")) {
+ return Services.prefs.getBoolPref("media.gmp-widevinecdm.visible");
+ }
+ return true;
+ },
+ getLearnMoreLink(msgId) {
+ let text = gNavigatorBundle.getString("emeNotifications." + msgId + ".learnMoreLabel");
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ return "" +
+ text + " ";
+ },
+ receiveMessage({target: browser, data: data}) {
+ let parsedData;
+ try {
+ parsedData = JSON.parse(data);
+ } catch (ex) {
+ Cu.reportError("Malformed EME video message with data: " + data);
+ return;
+ }
+ let {status: status, keySystem: keySystem} = parsedData;
+ // Don't need to show if disabled or keysystem not visible.
+ if (!this.uiEnabled || !this.isKeySystemVisible(keySystem)) {
+ return;
+ }
+
+ let notificationId;
+ let buttonCallback;
+ let params = [];
+ switch (status) {
+ case "available":
+ case "cdm-created":
+ // Only show the chain icon for proprietary CDMs. Clearkey is not one.
+ if (keySystem != "org.w3.clearkey") {
+ this.showPopupNotificationForSuccess(browser, keySystem);
+ }
+ // ... and bail!
+ return;
+
+ case "api-disabled":
+ case "cdm-disabled":
+ notificationId = "drmContentDisabled";
+ buttonCallback = gEMEHandler.ensureEMEEnabled.bind(gEMEHandler, browser, keySystem)
+ params = [this.getLearnMoreLink(notificationId)];
+ break;
+
+ case "cdm-insufficient-version":
+ notificationId = "drmContentCDMInsufficientVersion";
+ params = [this._brandShortName];
+ break;
+
+ case "cdm-not-installed":
+ notificationId = "drmContentCDMInstalling";
+ params = [this._brandShortName];
+ break;
+
+ case "cdm-not-supported":
+ // Not to pop up user-level notification because they cannot do anything
+ // about it.
+ return;
+ default:
+ Cu.reportError(new Error("Unknown message ('" + status + "') dealing with EME key request: " + data));
+ return;
+ }
+
+ this.showNotificationBar(browser, notificationId, keySystem, params, buttonCallback);
+ },
+ showNotificationBar(browser, notificationId, keySystem, labelParams, callback) {
+ let box = gBrowser.getNotificationBox(browser);
+ if (box.getNotificationWithValue(notificationId)) {
+ return;
+ }
+
+ let msgPrefix = "emeNotifications." + notificationId + ".";
+ let msgId = msgPrefix + "message";
+
+ let message = labelParams.length ?
+ gNavigatorBundle.getFormattedString(msgId, labelParams) :
+ gNavigatorBundle.getString(msgId);
+
+ let buttons = [];
+ if (callback) {
+ let btnLabelId = msgPrefix + "button.label";
+ let btnAccessKeyId = msgPrefix + "button.accesskey";
+ buttons.push({
+ label: gNavigatorBundle.getString(btnLabelId),
+ accessKey: gNavigatorBundle.getString(btnAccessKeyId),
+ callback
+ });
+ }
+
+ let iconURL = "chrome://browser/skin/drm-icon.svg#chains-black";
+
+ // Do a little dance to get rich content into the notification:
+ let fragment = document.createDocumentFragment();
+ let descriptionContainer = document.createElement("description");
+ descriptionContainer.innerHTML = message;
+ while (descriptionContainer.childNodes.length) {
+ fragment.appendChild(descriptionContainer.childNodes[0]);
+ }
+
+ box.appendNotification(fragment, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM,
+ buttons);
+ },
+ showPopupNotificationForSuccess(browser, keySystem) {
+ // We're playing EME content! Remove any "we can't play because..." messages.
+ var box = gBrowser.getNotificationBox(browser);
+ ["drmContentDisabled",
+ "drmContentCDMInstalling"
+ ].forEach(function(value) {
+ var notification = box.getNotificationWithValue(value);
+ if (notification)
+ box.removeNotification(notification);
+ });
+
+ // Don't bother creating it if it's already there:
+ if (PopupNotifications.getNotification("drmContentPlaying", browser)) {
+ return;
+ }
+
+ let msgPrefix = "emeNotifications.drmContentPlaying.";
+ let msgId = msgPrefix + "message2";
+ let btnLabelId = msgPrefix + "button.label";
+ let btnAccessKeyId = msgPrefix + "button.accesskey";
+
+ let message = gNavigatorBundle.getFormattedString(msgId, [this._brandShortName]);
+ let anchorId = "eme-notification-icon";
+ let firstPlayPref = "browser.eme.ui.firstContentShown";
+ if (!Services.prefs.getPrefType(firstPlayPref) ||
+ !Services.prefs.getBoolPref(firstPlayPref)) {
+ document.getElementById(anchorId).setAttribute("firstplay", "true");
+ Services.prefs.setBoolPref(firstPlayPref, true);
+ } else {
+ document.getElementById(anchorId).removeAttribute("firstplay");
+ }
+
+ let mainAction = {
+ label: gNavigatorBundle.getString(btnLabelId),
+ accessKey: gNavigatorBundle.getString(btnAccessKeyId),
+ callback() { openPreferences("paneContent"); },
+ dismiss: true
+ };
+ let options = {
+ dismissed: true,
+ eventCallback: aTopic => aTopic == "swapping",
+ learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content",
+ };
+ PopupNotifications.show(browser, "drmContentPlaying", message, anchorId, mainAction, null, options);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener])
+};
+
+XPCOMUtils.defineLazyGetter(gEMEHandler, "_brandShortName", function() {
+ return document.getElementById("bundle_brand").getString("brandShortName");
+});
+
+const TELEMETRY_DDSTAT_SHOWN = 0;
+const TELEMETRY_DDSTAT_SHOWN_FIRST = 1;
+const TELEMETRY_DDSTAT_CLICKED = 2;
+const TELEMETRY_DDSTAT_CLICKED_FIRST = 3;
+const TELEMETRY_DDSTAT_SOLVED = 4;
+
+let gDecoderDoctorHandler = {
+ getLabelForNotificationBox(type) {
+#ifdef XP_WIN
+ if (type == "adobe-cdm-not-found" || type == "adobe-cdm-not-activated") {
+ return gNavigatorBundle.getString("decoder.noCodecs.message");
+ }
+#endif
+
+#ifndef XP_MACOSX
+ if (type == "platform-decoder-not-found") {
+#ifdef XP_WIN
+ return gNavigatorBundle.getString("decoder.noHWAcceleration.message");
+#elif XP_LINUX
+ return gNavigatorBundle.getString("decoder.noCodecsLinux.message");
+#endif
+ }
+#endif
+ if (type == "cannot-initialize-pulseaudio") {
+ return gNavigatorBundle.getString("decoder.noPulseAudio.message");
+ }
+#ifdef XP_LINUX
+ if (type == "unsupported-libavcodec") {
+ return gNavigatorBundle.getString("decoder.unsupportedLibavcodec.message");
+ }
+#endif
+ return "";
+ },
+
+ getSumoForLearnHowButton(type) {
+#ifdef XP_WIN
+ return "fix-video-audio-problems-firefox-windows";
+#else
+ if (type == "cannot-initialize-pulseaudio") {
+ return "fix-common-audio-and-video-issues";
+ }
+
+ return "";
+#endif
+ },
+
+ receiveMessage({target: browser, data: data}) {
+ let box = gBrowser.getNotificationBox(browser);
+ let notificationId = "decoder-doctor-notification";
+ if (box.getNotificationWithValue(notificationId)) {
+ return;
+ }
+
+ let parsedData;
+ try {
+ parsedData = JSON.parse(data);
+ } catch (ex) {
+ Cu.reportError("Malformed Decoder Doctor message with data: " + data);
+ return;
+ }
+ // parsedData (the result of parsing the incoming 'data' json string)
+ // contains analysis information from Decoder Doctor:
+ // - 'type' is the type of issue, it determines which text to show in the
+ // infobar.
+ // - 'decoderDoctorReportId' is the Decoder Doctor issue identifier, to be
+ // used here as key for the telemetry (counting infobar displays,
+ // "Learn how" buttons clicks, and resolutions) and for the prefs used
+ // to store at-issue formats.
+ // - 'formats' contains a comma-separated list of formats (or key systems)
+ // that suffer the issue. These are kept in a pref, which the backend
+ // uses to later find when an issue is resolved.
+ // - 'isSolved' is true when the notification actually indicates the
+ // resolution of that issue, to be reported as telemetry.
+ let {type, isSolved, decoderDoctorReportId, formats} = parsedData;
+ type = type.toLowerCase();
+ // Error out early on invalid ReportId
+ if (!(/^\w+$/mi).test(decoderDoctorReportId)) {
+ return
+ }
+ let title = gDecoderDoctorHandler.getLabelForNotificationBox(type);
+ if (!title) {
+ return;
+ }
+
+ // We keep the list of formats in prefs for the sake of the decoder itself,
+ // which reads it to determine when issues get solved for these formats.
+ // (Writing prefs from e10s content is now allowed.)
+ let formatsPref = "media.decoder-doctor." + decoderDoctorReportId + ".formats";
+ let buttonClickedPref = "media.decoder-doctor." + decoderDoctorReportId + ".button-clicked";
+ let histogram =
+ Services.telemetry.getKeyedHistogramById("DECODER_DOCTOR_INFOBAR_STATS");
+
+ let formatsInPref = Services.prefs.getPrefType(formatsPref) &&
+ Services.prefs.getCharPref(formatsPref);
+
+ if (!isSolved) {
+ if (!formats) {
+ Cu.reportError("Malformed Decoder Doctor unsolved message with no formats");
+ return;
+ }
+ if (!formatsInPref) {
+ Services.prefs.setCharPref(formatsPref, formats);
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SHOWN_FIRST);
+ } else {
+ // Split existing formats into an array of strings.
+ let existing = formatsInPref.split(",").map(x => x.trim());
+ // Keep given formats that were not already recorded.
+ let newbies = formats.split(",").map(x => x.trim())
+ .filter(x => !existing.includes(x));
+ // And rewrite pref with the added new formats (if any).
+ if (newbies.length) {
+ Services.prefs.setCharPref(formatsPref,
+ existing.concat(newbies).join(", "));
+ }
+ }
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SHOWN);
+
+ let buttons = [];
+ let sumo = gDecoderDoctorHandler.getSumoForLearnHowButton(type);
+ if (sumo) {
+ buttons.push({
+ label: gNavigatorBundle.getString("decoder.noCodecs.button"),
+ accessKey: gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ callback() {
+ let clickedInPref = Services.prefs.getPrefType(buttonClickedPref) &&
+ Services.prefs.getBoolPref(buttonClickedPref);
+ if (!clickedInPref) {
+ Services.prefs.setBoolPref(buttonClickedPref, true);
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED_FIRST);
+ }
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED);
+
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ openUILinkIn(baseURL + sumo, "tab");
+ }
+ });
+ }
+
+ box.appendNotification(
+ title,
+ notificationId,
+ "", // This uses the info icon as specified below.
+ box.PRIORITY_INFO_LOW,
+ buttons
+ );
+ } else if (formatsInPref) {
+ // Issue is solved, and prefs haven't been cleared yet, meaning it's the
+ // first time we get this resolution -> Clear prefs and report telemetry.
+ Services.prefs.clearUserPref(formatsPref);
+ Services.prefs.clearUserPref(buttonClickedPref);
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SOLVED);
+ }
+ },
+}
+
+window.getGroupMessageManager("browsers").addMessageListener("DecoderDoctor:Notification", gDecoderDoctorHandler);
+window.getGroupMessageManager("browsers").addMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler);
+window.addEventListener("unload", function() {
+ window.getGroupMessageManager("browsers").removeMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler);
+ window.getGroupMessageManager("browsers").removeMessageListener("DecoderDoctor:Notification", gDecoderDoctorHandler);
+});
diff --git a/application/basilisk/base/content/browser-menubar.inc b/application/basilisk/base/content/browser-menubar.inc
new file mode 100644
index 000000000..3fc098755
--- /dev/null
+++ b/application/basilisk/base/content/browser-menubar.inc
@@ -0,0 +1,527 @@
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifdef XP_MACOSX
+
+#endif
+
+
diff --git a/application/basilisk/base/content/browser-places.js b/application/basilisk/base/content/browser-places.js
new file mode 100644
index 000000000..963d36c3a
--- /dev/null
+++ b/application/basilisk/base/content/browser-places.js
@@ -0,0 +1,2018 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var StarUI = {
+ _itemId: -1,
+ uri: null,
+ _batching: false,
+ _isNewBookmark: false,
+ _isComposing: false,
+ _autoCloseTimer: 0,
+ // The autoclose timer is diasbled if the user interacts with the
+ // popup, such as making a change through typing or clicking on
+ // the popup.
+ _autoCloseTimerEnabled: true,
+
+ _element(aID) {
+ return document.getElementById(aID);
+ },
+
+ // Edit-bookmark panel
+ get panel() {
+ delete this.panel;
+ var element = this._element("editBookmarkPanel");
+ // initially the panel is hidden
+ // to avoid impacting startup / new window performance
+ element.hidden = false;
+ element.addEventListener("keypress", this);
+ element.addEventListener("mousedown", this);
+ element.addEventListener("mouseout", this);
+ element.addEventListener("mousemove", this);
+ element.addEventListener("compositionstart", this);
+ element.addEventListener("compositionend", this);
+ element.addEventListener("input", this);
+ element.addEventListener("popuphidden", this);
+ element.addEventListener("popupshown", this);
+ return this.panel = element;
+ },
+
+ // Array of command elements to disable when the panel is opened.
+ get _blockedCommands() {
+ delete this._blockedCommands;
+ return this._blockedCommands =
+ ["cmd_close", "cmd_closeWindow"].map(id => this._element(id));
+ },
+
+ _blockCommands: function SU__blockCommands() {
+ this._blockedCommands.forEach(function(elt) {
+ // make sure not to permanently disable this item (see bug 409155)
+ if (elt.hasAttribute("wasDisabled"))
+ return;
+ if (elt.getAttribute("disabled") == "true") {
+ elt.setAttribute("wasDisabled", "true");
+ } else {
+ elt.setAttribute("wasDisabled", "false");
+ elt.setAttribute("disabled", "true");
+ }
+ });
+ },
+
+ _restoreCommandsState: function SU__restoreCommandsState() {
+ this._blockedCommands.forEach(function(elt) {
+ if (elt.getAttribute("wasDisabled") != "true")
+ elt.removeAttribute("disabled");
+ elt.removeAttribute("wasDisabled");
+ });
+ },
+
+ // nsIDOMEventListener
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "mousemove":
+ clearTimeout(this._autoCloseTimer);
+ // The autoclose timer is not disabled on generic mouseout
+ // because the user may not have actually interacted with the popup.
+ break;
+ case "popuphidden":
+ clearTimeout(this._autoCloseTimer);
+ if (aEvent.originalTarget == this.panel) {
+ if (!this._element("editBookmarkPanelContent").hidden)
+ this.quitEditMode();
+
+ if (this._anchorToolbarButton) {
+ this._anchorToolbarButton.removeAttribute("open");
+ this._anchorToolbarButton = null;
+ }
+ this._restoreCommandsState();
+ this._itemId = -1;
+ if (this._batching)
+ this.endBatch();
+
+ if (this._uriForRemoval) {
+ if (this._isNewBookmark) {
+ if (!PlacesUtils.useAsyncTransactions) {
+ PlacesUtils.transactionManager.undoTransaction();
+ break;
+ }
+ PlacesTransactions().undo().catch(Cu.reportError);
+ break;
+ }
+ // Remove all bookmarks for the bookmark's url, this also removes
+ // the tags for the url.
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval);
+ for (let itemId of itemIds) {
+ let txn = new PlacesRemoveItemTransaction(itemId);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ break;
+ }
+
+ PlacesTransactions.RemoveBookmarksForUrls([this._uriForRemoval])
+ .transact().catch(Cu.reportError);
+ }
+ }
+ break;
+ case "keypress":
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimerEnabled = false;
+
+ if (aEvent.defaultPrevented) {
+ // The event has already been consumed inside of the panel.
+ break;
+ }
+
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_ESCAPE:
+ this.panel.hidePopup();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ if (aEvent.target.classList.contains("expander-up") ||
+ aEvent.target.classList.contains("expander-down") ||
+ aEvent.target.id == "editBMPanel_newFolderButton" ||
+ aEvent.target.id == "editBookmarkPanelRemoveButton") {
+ // XXX Why is this necessary? The defaultPrevented check should
+ // be enough.
+ break;
+ }
+ this.panel.hidePopup();
+ break;
+ // This case is for catching character-generating keypresses
+ case 0:
+ let accessKey = document.getElementById("key_close");
+ if (eventMatchesKey(aEvent, accessKey)) {
+ this.panel.hidePopup();
+ }
+ break;
+ }
+ break;
+ case "compositionend":
+ // After composition is committed, "mouseout" or something can set
+ // auto close timer.
+ this._isComposing = false;
+ break;
+ case "compositionstart":
+ if (aEvent.defaultPrevented) {
+ // If the composition was canceled, nothing to do here.
+ break;
+ }
+ this._isComposing = true;
+ // Explicit fall-through, during composition, panel shouldn't be
+ // hidden automatically.
+ case "input":
+ // Might have edited some text without keyboard events nor composition
+ // events. Fall-through to cancel auto close in such case.
+ case "mousedown":
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimerEnabled = false;
+ break;
+ case "mouseout":
+ if (!this._autoCloseTimerEnabled) {
+ // Don't autoclose the popup if the user has made a selection
+ // or keypress and then subsequently mouseout.
+ break;
+ }
+ // Explicit fall-through
+ case "popupshown":
+ // Don't handle events for descendent elements.
+ if (aEvent.target != aEvent.currentTarget) {
+ break;
+ }
+ // auto-close if new and not interacted with
+ if (this._isNewBookmark && !this._isComposing) {
+ // 3500ms matches the timeout that Pocket uses in
+ // browser/extensions/pocket/content/panels/js/saved.js
+ let delay = 3500;
+ if (this._closePanelQuickForTesting) {
+ delay /= 10;
+ }
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimer = setTimeout(() => {
+ if (!this.panel.mozMatchesSelector(":hover")) {
+ this.panel.hidePopup();
+ }
+ }, delay);
+ this._autoCloseTimerEnabled = true;
+ }
+ break;
+ }
+ },
+
+ _overlayLoaded: false,
+ _overlayLoading: false,
+ showEditBookmarkPopup: Task.async(function* (aNode, aAnchorElement, aPosition, aIsNewBookmark) {
+ // Slow double-clicks (not true double-clicks) shouldn't
+ // cause the panel to flicker.
+ if (this.panel.state == "showing" ||
+ this.panel.state == "open") {
+ return;
+ }
+
+ this._isNewBookmark = aIsNewBookmark;
+ this._uriForRemoval = "";
+ // TODO: Deprecate this once async transactions are enabled and the legacy
+ // transactions code is gone (bug 1131491) - we don't want addons to to use
+ // the completeNodeLikeObjectForItemId, so it's better if they keep passing
+ // the item-id for now).
+ if (typeof(aNode) == "number") {
+ let itemId = aNode;
+ if (PlacesUIUtils.useAsyncTransactions) {
+ let guid = yield PlacesUtils.promiseItemGuid(itemId);
+ aNode = yield PlacesUIUtils.promiseNodeLike(guid);
+ } else {
+ aNode = { itemId };
+ yield PlacesUIUtils.completeNodeLikeObjectForItemId(aNode);
+ }
+ }
+
+ // Performance: load the overlay the first time the panel is opened
+ // (see bug 392443).
+ if (this._overlayLoading)
+ return;
+
+ if (this._overlayLoaded) {
+ this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
+ return;
+ }
+
+ this._overlayLoading = true;
+ document.loadOverlay(
+ "chrome://browser/content/places/editBookmarkOverlay.xul",
+ (function(aSubject, aTopic, aData) {
+ // Move the header (star, title, button) into the grid,
+ // so that it aligns nicely with the other items (bug 484022).
+ let header = this._element("editBookmarkPanelHeader");
+ let rows = this._element("editBookmarkPanelGrid").lastChild;
+ rows.insertBefore(header, rows.firstChild);
+ header.hidden = false;
+
+ this._overlayLoading = false;
+ this._overlayLoaded = true;
+ this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
+ }).bind(this)
+ );
+ }),
+
+ _doShowEditBookmarkPanel: Task.async(function* (aNode, aAnchorElement, aPosition) {
+ if (this.panel.state != "closed")
+ return;
+
+ this._blockCommands(); // un-done in the popuphidden handler
+
+ this._element("editBookmarkPanelTitle").value =
+ this._isNewBookmark ?
+ gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
+ gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle");
+
+ // No description; show the Done, Remove;
+ this._element("editBookmarkPanelDescription").textContent = "";
+ this._element("editBookmarkPanelBottomButtons").hidden = false;
+ this._element("editBookmarkPanelContent").hidden = false;
+
+ // The label of the remove button differs if the URI is bookmarked
+ // multiple times.
+ let bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
+ let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
+ let label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length);
+ this._element("editBookmarkPanelRemoveButton").label = label;
+
+ // unset the unstarred state, if set
+ this._element("editBookmarkPanelStarIcon").removeAttribute("unstarred");
+
+ this._itemId = aNode.itemId;
+ this.beginBatch();
+
+ if (aAnchorElement) {
+ // Set the open=true attribute if the anchor is a
+ // descendent of a toolbarbutton.
+ let parent = aAnchorElement.parentNode;
+ while (parent) {
+ if (parent.localName == "toolbarbutton") {
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ if (parent) {
+ this._anchorToolbarButton = parent;
+ parent.setAttribute("open", "true");
+ }
+ }
+ let panel = this.panel;
+ let target = panel;
+ if (target.parentNode) {
+ // By targeting the panel's parent and using a capturing listener, we
+ // can have our listener called before others waiting for the panel to
+ // be shown (which probably expect the panel to be fully initialized)
+ target = target.parentNode;
+ }
+ target.addEventListener("popupshown", function shownListener(event) {
+ if (event.target == panel) {
+ target.removeEventListener("popupshown", shownListener, true);
+
+ gEditItemOverlay.initPanel({ node: aNode
+ , hiddenRows: ["description", "location",
+ "loadInSidebar", "keyword"]
+ , focusedElement: "preferred"});
+ }
+ }, true);
+
+ this.panel.openPopup(aAnchorElement, aPosition);
+ }),
+
+ panelShown:
+ function SU_panelShown(aEvent) {
+ if (aEvent.target == this.panel) {
+ if (this._element("editBookmarkPanelContent").hidden) {
+ // Note this isn't actually used anymore, we should remove this
+ // once we decide not to bring back the page bookmarked notification
+ this.panel.focus();
+ }
+ }
+ },
+
+ quitEditMode: function SU_quitEditMode() {
+ this._element("editBookmarkPanelContent").hidden = true;
+ this._element("editBookmarkPanelBottomButtons").hidden = true;
+ gEditItemOverlay.uninitPanel(true);
+ },
+
+ removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
+ this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
+ this.panel.hidePopup();
+ },
+
+ // Matching the way it is used in the Library, editBookmarkOverlay implements
+ // an instant-apply UI, having no batched-Undo/Redo support.
+ // However, in this context (the Star UI) we have a Cancel button whose
+ // expected behavior is to undo all the operations done in the panel.
+ // Sometime in the future this needs to be reimplemented using a
+ // non-instant apply code path, but for the time being, we patch-around
+ // editBookmarkOverlay so that all of the actions done in the panel
+ // are treated by PlacesTransactions as a single batch. To do so,
+ // we start a PlacesTransactions batch when the star UI panel is shown, and
+ // we keep the batch ongoing until the panel is hidden.
+ _batchBlockingDeferred: null,
+ beginBatch() {
+ if (this._batching)
+ return;
+ if (PlacesUIUtils.useAsyncTransactions) {
+ this._batchBlockingDeferred = PromiseUtils.defer();
+ PlacesTransactions.batch(function* () {
+ yield this._batchBlockingDeferred.promise;
+ }.bind(this));
+ } else {
+ PlacesUtils.transactionManager.beginBatch(null);
+ }
+ this._batching = true;
+ },
+
+ endBatch() {
+ if (!this._batching)
+ return;
+
+ if (PlacesUIUtils.useAsyncTransactions) {
+ this._batchBlockingDeferred.resolve();
+ this._batchBlockingDeferred = null;
+ } else {
+ PlacesUtils.transactionManager.endBatch(false);
+ }
+ this._batching = false;
+ }
+};
+
+var PlacesCommandHook = {
+ /**
+ * Adds a bookmark to the page loaded in the given browser.
+ *
+ * @param aBrowser
+ * a element.
+ * @param [optional] aParent
+ * The folder in which to create a new bookmark if the page loaded in
+ * aBrowser isn't bookmarked yet, defaults to the unfiled root.
+ * @param [optional] aShowEditUI
+ * whether or not to show the edit-bookmark UI for the bookmark item
+ */
+ bookmarkPage: Task.async(function* (aBrowser, aParent, aShowEditUI) {
+ if (PlacesUIUtils.useAsyncTransactions) {
+ yield this._bookmarkPagePT(aBrowser, aParent, aShowEditUI);
+ return;
+ }
+
+ var uri = aBrowser.currentURI;
+ var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
+ let isNewBookmark = itemId == -1;
+ if (isNewBookmark) {
+ // Bug 1148838 - Make this code work for full page plugins.
+ var title;
+ var description;
+ var charset;
+
+ let docInfo = yield this._getPageDetails(aBrowser);
+
+ try {
+ title = docInfo.isErrorPage ? PlacesUtils.history.getPageTitle(uri)
+ : aBrowser.contentTitle;
+ title = title || uri.spec;
+ description = docInfo.description;
+ charset = aBrowser.characterSet;
+ } catch (e) { }
+
+ if (aShowEditUI && isNewBookmark) {
+ // If we bookmark the page here but open right into a cancelable
+ // state (i.e. new bookmark in Library), start batching here so
+ // all of the actions can be undone in a single undo step.
+ StarUI.beginBatch();
+ }
+
+ var parent = aParent !== undefined ?
+ aParent : PlacesUtils.unfiledBookmarksFolderId;
+ var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
+ var txn = new PlacesCreateBookmarkTransaction(uri, parent,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title, null, [descAnno]);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ itemId = txn.item.id;
+ // Set the character-set.
+ if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
+ PlacesUtils.setCharsetForURI(uri, charset);
+ }
+
+ // Revert the contents of the location bar
+ gURLBar.handleRevert();
+
+ // If it was not requested to open directly in "edit" mode, we are done.
+ if (!aShowEditUI)
+ return;
+
+ // Try to dock the panel to:
+ // 1. the bookmarks menu button
+ // 2. the identity icon
+ // 3. the content area
+ if (BookmarkingUI.anchor) {
+ StarUI.showEditBookmarkPopup(itemId, BookmarkingUI.anchor,
+ "bottomcenter topright", isNewBookmark);
+ return;
+ }
+
+ let identityIcon = document.getElementById("identity-icon");
+ if (isElementVisible(identityIcon)) {
+ StarUI.showEditBookmarkPopup(itemId, identityIcon,
+ "bottomcenter topright", isNewBookmark);
+ } else {
+ StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap", isNewBookmark);
+ }
+ }),
+
+ // TODO: Replace bookmarkPage code with this function once legacy
+ // transactions are removed.
+ _bookmarkPagePT: Task.async(function* (aBrowser, aParentId, aShowEditUI) {
+ let url = new URL(aBrowser.currentURI.spec);
+ let info = yield PlacesUtils.bookmarks.fetch({ url });
+ let isNewBookmark = !info;
+ if (!info) {
+ let parentGuid = aParentId !== undefined ?
+ yield PlacesUtils.promiseItemGuid(aParentId) :
+ PlacesUtils.bookmarks.unfiledGuid;
+ info = { url, parentGuid };
+ // Bug 1148838 - Make this code work for full page plugins.
+ let description = null;
+ let charset = null;
+
+ let docInfo = yield this._getPageDetails(aBrowser);
+
+ try {
+ info.title = docInfo.isErrorPage ?
+ (yield PlacesUtils.promisePlaceInfo(aBrowser.currentURI)).title :
+ aBrowser.contentTitle;
+ info.title = info.title || url.href;
+ description = docInfo.description;
+ charset = aBrowser.characterSet;
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ if (aShowEditUI && isNewBookmark) {
+ // If we bookmark the page here but open right into a cancelable
+ // state (i.e. new bookmark in Library), start batching here so
+ // all of the actions can be undone in a single undo step.
+ StarUI.beginBatch();
+ }
+
+ if (description) {
+ info.annotations = [{ name: PlacesUIUtils.DESCRIPTION_ANNO
+ , value: description }];
+ }
+
+ info.guid = yield PlacesTransactions.NewBookmark(info).transact();
+
+ // Set the character-set
+ if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
+ PlacesUtils.setCharsetForURI(makeURI(url.href), charset);
+ }
+
+ // Revert the contents of the location bar
+ gURLBar.handleRevert();
+
+ // If it was not requested to open directly in "edit" mode, we are done.
+ if (!aShowEditUI)
+ return;
+
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(info);
+
+ // Try to dock the panel to:
+ // 1. the bookmarks menu button
+ // 2. the identity icon
+ // 3. the content area
+ if (BookmarkingUI.anchor) {
+ StarUI.showEditBookmarkPopup(node, BookmarkingUI.anchor,
+ "bottomcenter topright", isNewBookmark);
+ return;
+ }
+
+ let identityIcon = document.getElementById("identity-icon");
+ if (isElementVisible(identityIcon)) {
+ StarUI.showEditBookmarkPopup(node, identityIcon,
+ "bottomcenter topright", isNewBookmark);
+ } else {
+ StarUI.showEditBookmarkPopup(node, aBrowser, "overlap", isNewBookmark);
+ }
+ }),
+
+ _getPageDetails(browser) {
+ return new Promise(resolve => {
+ let mm = browser.messageManager;
+ mm.addMessageListener("Bookmarks:GetPageDetails:Result", function listener(msg) {
+ mm.removeMessageListener("Bookmarks:GetPageDetails:Result", listener);
+ resolve(msg.data);
+ });
+
+ mm.sendAsyncMessage("Bookmarks:GetPageDetails", { })
+ });
+ },
+
+ /**
+ * Adds a bookmark to the page loaded in the current tab.
+ */
+ bookmarkCurrentPage: function PCH_bookmarkCurrentPage(aShowEditUI, aParent) {
+ this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI);
+ },
+
+ /**
+ * Adds a bookmark to the page targeted by a link.
+ * @param aParent
+ * The folder in which to create a new bookmark if aURL isn't
+ * bookmarked.
+ * @param aURL (string)
+ * the address of the link target
+ * @param aTitle
+ * The link text
+ * @param [optional] aDescription
+ * The linked page description, if available
+ */
+ bookmarkLink: Task.async(function* (aParentId, aURL, aTitle, aDescription = "") {
+ let node = yield PlacesUIUtils.fetchNodeLike({ url: aURL });
+ if (node) {
+ PlacesUIUtils.showBookmarkDialog({ action: "edit"
+ , node
+ }, window.top);
+ return;
+ }
+
+ let ip = new InsertionPoint(aParentId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "bookmark"
+ , uri: makeURI(aURL)
+ , title: aTitle
+ , description: aDescription
+ , defaultInsertionPoint: ip
+ , hiddenRows: [ "description"
+ , "location"
+ , "loadInSidebar"
+ , "keyword" ]
+ }, window.top);
+ }),
+
+ /**
+ * List of nsIURI objects characterizing the tabs currently open in the
+ * browser, modulo pinned tabs. The URIs will be in the order in which their
+ * corresponding tabs appeared and duplicates are discarded.
+ */
+ get uniqueCurrentPages() {
+ let uniquePages = {};
+ let URIs = [];
+
+ gBrowser.visibleTabs.forEach(tab => {
+ let browser = tab.linkedBrowser;
+ let uri = browser.currentURI;
+ let title = browser.contentTitle || tab.label;
+ let spec = uri.spec;
+ if (!tab.pinned && !(spec in uniquePages)) {
+ uniquePages[spec] = null;
+ URIs.push({ uri, title });
+ }
+ });
+ return URIs;
+ },
+
+ /**
+ * Adds a folder with bookmarks to all of the currently open tabs in this
+ * window.
+ */
+ bookmarkCurrentPages: function PCH_bookmarkCurrentPages() {
+ let pages = this.uniqueCurrentPages;
+ if (pages.length > 1) {
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "folder"
+ , URIList: pages
+ , hiddenRows: [ "description" ]
+ }, window);
+ }
+ },
+
+ /**
+ * Updates disabled state for the "Bookmark All Tabs" command.
+ */
+ updateBookmarkAllTabsCommand:
+ function PCH_updateBookmarkAllTabsCommand() {
+ // There's nothing to do in non-browser windows.
+ if (window.location.href != getBrowserURL())
+ return;
+
+ // Disable "Bookmark All Tabs" if there are less than two
+ // "unique current pages".
+ goSetCommandEnabled("Browser:BookmarkAllTabs",
+ this.uniqueCurrentPages.length >= 2);
+ },
+
+ /**
+ * Adds a Live Bookmark to a feed associated with the current page.
+ * @param url
+ * The nsIURI of the page the feed was attached to
+ * @title title
+ * The title of the feed. Optional.
+ * @subtitle subtitle
+ * A short description of the feed. Optional.
+ */
+ addLiveBookmark: Task.async(function *(url, feedTitle, feedSubtitle) {
+ let toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+
+ let feedURI = makeURI(url);
+ let title = feedTitle || gBrowser.contentTitle;
+ let description = feedSubtitle;
+ if (!description) {
+ description = (yield this._getPageDetails(gBrowser.selectedBrowser)).description;
+ }
+
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "livemark"
+ , feedURI
+ , siteURI: gBrowser.currentURI
+ , title
+ , description
+ , defaultInsertionPoint: toolbarIP
+ , hiddenRows: [ "feedLocation"
+ , "siteLocation"
+ , "description" ]
+ }, window);
+ }),
+
+ /**
+ * Opens the Places Organizer.
+ * @param aLeftPaneRoot
+ * The query to select in the organizer window - options
+ * are: History, AllBookmarks, BookmarksMenu, BookmarksToolbar,
+ * UnfiledBookmarks, Tags and Downloads.
+ */
+ showPlacesOrganizer: function PCH_showPlacesOrganizer(aLeftPaneRoot) {
+ var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
+ // Due to bug 528706, getMostRecentWindow can return closed windows.
+ if (!organizer || organizer.closed) {
+ // No currently open places window, so open one with the specified mode.
+ openDialog("chrome://browser/content/places/places.xul",
+ "", "chrome,toolbar=yes,dialog=no,resizable", aLeftPaneRoot);
+ } else {
+ organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
+ organizer.focus();
+ }
+ }
+};
+
+XPCOMUtils.defineLazyModuleGetter(this, "RecentlyClosedTabsAndWindowsMenuUtils",
+ "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm");
+
+// View for the history menu.
+function HistoryMenu(aPopupShowingEvent) {
+ // Workaround for Bug 610187. The sidebar does not include all the Places
+ // views definitions, and we don't need them there.
+ // Defining the prototype inheritance in the prototype itself would cause
+ // browser.js to halt on "PlacesMenu is not defined" error.
+ this.__proto__.__proto__ = PlacesMenu.prototype;
+ PlacesMenu.call(this, aPopupShowingEvent,
+ "place:sort=4&maxResults=15");
+}
+
+HistoryMenu.prototype = {
+ _getClosedTabCount() {
+ // SessionStore doesn't track the hidden window, so just return zero then.
+ if (window == Services.appShell.hiddenDOMWindow) {
+ return 0;
+ }
+
+ return SessionStore.getClosedTabCount(window);
+ },
+
+ toggleRecentlyClosedTabs: function HM_toggleRecentlyClosedTabs() {
+ // enable/disable the Recently Closed Tabs sub menu
+ var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0];
+
+ // no restorable tabs, so disable menu
+ if (this._getClosedTabCount() == 0)
+ undoMenu.setAttribute("disabled", true);
+ else
+ undoMenu.removeAttribute("disabled");
+ },
+
+ /**
+ * Populate when the history menu is opened
+ */
+ populateUndoSubmenu: function PHM_populateUndoSubmenu() {
+ var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0];
+ var undoPopup = undoMenu.firstChild;
+
+ // remove existing menu items
+ while (undoPopup.hasChildNodes())
+ undoPopup.removeChild(undoPopup.firstChild);
+
+ // no restorable tabs, so make sure menu is disabled, and return
+ if (this._getClosedTabCount() == 0) {
+ undoMenu.setAttribute("disabled", true);
+ return;
+ }
+
+ // enable menu
+ undoMenu.removeAttribute("disabled");
+
+ // populate menu
+ let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment(window, "menuitem");
+ undoPopup.appendChild(tabsFragment);
+ },
+
+ toggleRecentlyClosedWindows: function PHM_toggleRecentlyClosedWindows() {
+ // enable/disable the Recently Closed Windows sub menu
+ var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0];
+
+ // no restorable windows, so disable menu
+ if (SessionStore.getClosedWindowCount() == 0)
+ undoMenu.setAttribute("disabled", true);
+ else
+ undoMenu.removeAttribute("disabled");
+ },
+
+ /**
+ * Populate when the history menu is opened
+ */
+ populateUndoWindowSubmenu: function PHM_populateUndoWindowSubmenu() {
+ let undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0];
+ let undoPopup = undoMenu.firstChild;
+
+ // remove existing menu items
+ while (undoPopup.hasChildNodes())
+ undoPopup.removeChild(undoPopup.firstChild);
+
+ // no restorable windows, so make sure menu is disabled, and return
+ if (SessionStore.getClosedWindowCount() == 0) {
+ undoMenu.setAttribute("disabled", true);
+ return;
+ }
+
+ // enable menu
+ undoMenu.removeAttribute("disabled");
+
+ // populate menu
+ let windowsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment(window, "menuitem");
+ undoPopup.appendChild(windowsFragment);
+ },
+
+ toggleTabsFromOtherComputers: function PHM_toggleTabsFromOtherComputers() {
+ // Enable/disable the Tabs From Other Computers menu. Some of the menus handled
+ // by HistoryMenu do not have this menuitem.
+ let menuitem = this._rootElt.getElementsByClassName("syncTabsMenuItem")[0];
+ if (!menuitem)
+ return;
+
+ if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) {
+ menuitem.setAttribute("hidden", true);
+ return;
+ }
+
+ menuitem.setAttribute("hidden", false);
+ },
+
+ _onPopupShowing: function HM__onPopupShowing(aEvent) {
+ PlacesMenu.prototype._onPopupShowing.apply(this, arguments);
+
+ // Don't handle events for submenus.
+ if (aEvent.target != aEvent.currentTarget)
+ return;
+
+ this.toggleRecentlyClosedTabs();
+ this.toggleRecentlyClosedWindows();
+ this.toggleTabsFromOtherComputers();
+ },
+
+ _onCommand: function HM__onCommand(aEvent) {
+ let placesNode = aEvent.target._placesNode;
+ if (placesNode) {
+ if (!PrivateBrowsingUtils.isWindowPrivate(window))
+ PlacesUIUtils.markPageAsTyped(placesNode.uri);
+ openUILink(placesNode.uri, aEvent, { ignoreAlt: true });
+ }
+ }
+};
+
+/**
+ * Functions for handling events in the Bookmarks Toolbar and menu.
+ */
+var BookmarksEventHandler = {
+ /**
+ * Handler for click event for an item in the bookmarks toolbar or menu.
+ * Menus and submenus from the folder buttons bubble up to this handler.
+ * Left-click is handled in the onCommand function.
+ * When items are middle-clicked (or clicked with modifier), open in tabs.
+ * If the click came through a menu, close the menu.
+ * @param aEvent
+ * DOMEvent for the click
+ * @param aView
+ * The places view which aEvent should be associated with.
+ */
+ onClick: function BEH_onClick(aEvent, aView) {
+ // Only handle middle-click or left-click with modifiers.
+ let modifKey;
+#ifdef XP_MACOSX
+ modifKey = aEvent.metaKey || aEvent.shiftKey
+#else
+ modifKey = aEvent.ctrlKey || aEvent.shiftKey
+#endif
+
+ if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey))
+ return;
+
+ var target = aEvent.originalTarget;
+ // If this event bubbled up from a menu or menuitem, close the menus.
+ // Do this before opening tabs, to avoid hiding the open tabs confirm-dialog.
+ if (target.localName == "menu" || target.localName == "menuitem") {
+ for (let node = target.parentNode; node; node = node.parentNode) {
+ if (node.localName == "menupopup")
+ node.hidePopup();
+ else if (node.localName != "menu" &&
+ node.localName != "hbox" &&
+ node.localName != "vbox" )
+ break;
+ }
+ }
+
+ if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) {
+ // Don't open the root folder in tabs when the empty area on the toolbar
+ // is middle-clicked or when a non-bookmark item except for Open in Tabs)
+ // in a bookmarks menupopup is middle-clicked.
+ if (target.localName == "menu" || target.localName == "toolbarbutton")
+ PlacesUIUtils.openContainerNodeInTabs(target._placesNode, aEvent, aView);
+ } else if (aEvent.button == 1) {
+ // left-clicks with modifier are already served by onCommand
+ this.onCommand(aEvent, aView);
+ }
+ },
+
+ /**
+ * Handler for command event for an item in the bookmarks toolbar.
+ * Menus and submenus from the folder buttons bubble up to this handler.
+ * Opens the item.
+ * @param aEvent
+ * DOMEvent for the command
+ * @param aView
+ * The places view which aEvent should be associated with.
+ */
+ onCommand: function BEH_onCommand(aEvent, aView) {
+ var target = aEvent.originalTarget;
+ if (target._placesNode)
+ PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView);
+ },
+
+ fillInBHTooltip: function BEH_fillInBHTooltip(aDocument, aEvent) {
+ var node;
+ var cropped = false;
+ var targetURI;
+
+ if (aDocument.tooltipNode.localName == "treechildren") {
+ var tree = aDocument.tooltipNode.parentNode;
+ var tbo = tree.treeBoxObject;
+ var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
+ if (cell.row == -1)
+ return false;
+ node = tree.view.nodeForTreeIndex(cell.row);
+ cropped = tbo.isCellCropped(cell.row, cell.col);
+ } else {
+ // Check whether the tooltipNode is a Places node.
+ // In such a case use it, otherwise check for targetURI attribute.
+ var tooltipNode = aDocument.tooltipNode;
+ if (tooltipNode._placesNode)
+ node = tooltipNode._placesNode;
+ else {
+ // This is a static non-Places node.
+ targetURI = tooltipNode.getAttribute("targetURI");
+ }
+ }
+
+ if (!node && !targetURI)
+ return false;
+
+ // Show node.label as tooltip's title for non-Places nodes.
+ var title = node ? node.title : tooltipNode.label;
+
+ // Show URL only for Places URI-nodes or nodes with a targetURI attribute.
+ var url;
+ if (targetURI || PlacesUtils.nodeIsURI(node))
+ url = targetURI || node.uri;
+
+ // Show tooltip for containers only if their title is cropped.
+ if (!cropped && !url)
+ return false;
+
+ var tooltipTitle = aDocument.getElementById("bhtTitleText");
+ tooltipTitle.hidden = (!title || (title == url));
+ if (!tooltipTitle.hidden)
+ tooltipTitle.textContent = title;
+
+ var tooltipUrl = aDocument.getElementById("bhtUrlText");
+ tooltipUrl.hidden = !url;
+ if (!tooltipUrl.hidden)
+ tooltipUrl.value = url;
+
+ // Show tooltip.
+ return true;
+ }
+};
+
+// Handles special drag and drop functionality for Places menus that are not
+// part of a Places view (e.g. the bookmarks menu in the menubar).
+var PlacesMenuDNDHandler = {
+ _springLoadDelayMs: 350,
+ _closeDelayMs: 500,
+ _loadTimer: null,
+ _closeTimer: null,
+ _closingTimerNode: null,
+
+ /**
+ * Called when the user enters the element during a drag.
+ * @param event
+ * The DragEnter event that spawned the opening.
+ */
+ onDragEnter: function PMDH_onDragEnter(event) {
+ // Opening menus in a Places popup is handled by the view itself.
+ if (!this._isStaticContainer(event.target))
+ return;
+
+ // If we re-enter the same menu or anchor before the close timer runs out,
+ // we should ensure that we do not close:
+ if (this._closeTimer && this._closingTimerNode === event.currentTarget) {
+ this._closeTimer.cancel();
+ this._closingTimerNode = null;
+ this._closeTimer = null;
+ }
+
+ PlacesControllerDragHelper.currentDropTarget = event.target;
+ let popup = event.target.lastChild;
+ if (this._loadTimer || popup.state === "showing" || popup.state === "open")
+ return;
+
+ this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._loadTimer.initWithCallback(() => {
+ this._loadTimer = null;
+ popup.setAttribute("autoopened", "true");
+ popup.showPopup(popup);
+ }, this._springLoadDelayMs, Ci.nsITimer.TYPE_ONE_SHOT);
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Handles dragleave on the element.
+ */
+ onDragLeave: function PMDH_onDragLeave(event) {
+ // Handle menu-button separate targets.
+ if (event.relatedTarget === event.currentTarget ||
+ (event.relatedTarget &&
+ event.relatedTarget.parentNode === event.currentTarget))
+ return;
+
+ // Closing menus in a Places popup is handled by the view itself.
+ if (!this._isStaticContainer(event.target))
+ return;
+
+ PlacesControllerDragHelper.currentDropTarget = null;
+ let popup = event.target.lastChild;
+
+ if (this._loadTimer) {
+ this._loadTimer.cancel();
+ this._loadTimer = null;
+ }
+ this._closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._closingTimerNode = event.currentTarget;
+ this._closeTimer.initWithCallback(function() {
+ this._closeTimer = null;
+ this._closingTimerNode = null;
+ let node = PlacesControllerDragHelper.currentDropTarget;
+ let inHierarchy = false;
+ while (node && !inHierarchy) {
+ inHierarchy = node == event.target;
+ node = node.parentNode;
+ }
+ if (!inHierarchy && popup && popup.hasAttribute("autoopened")) {
+ popup.removeAttribute("autoopened");
+ popup.hidePopup();
+ }
+ }, this._closeDelayMs, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /**
+ * Determines if a XUL element represents a static container.
+ * @returns true if the element is a container element (menu or
+ *` menu-toolbarbutton), false otherwise.
+ */
+ _isStaticContainer: function PMDH__isContainer(node) {
+ let isMenu = node.localName == "menu" ||
+ (node.localName == "toolbarbutton" &&
+ (node.getAttribute("type") == "menu" ||
+ node.getAttribute("type") == "menu-button"));
+ let isStatic = !("_placesNode" in node) && node.lastChild &&
+ node.lastChild.hasAttribute("placespopup") &&
+ !node.parentNode.hasAttribute("placespopup");
+ return isMenu && isStatic;
+ },
+
+ /**
+ * Called when the user drags over the element.
+ * @param event
+ * The DragOver event.
+ */
+ onDragOver: function PMDH_onDragOver(event) {
+ let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+ if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer))
+ event.preventDefault();
+
+ event.stopPropagation();
+ },
+
+ /**
+ * Called when the user drops on the element.
+ * @param event
+ * The Drop event.
+ */
+ onDrop: function PMDH_onDrop(event) {
+ // Put the item at the end of bookmark menu.
+ let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+ PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
+ PlacesControllerDragHelper.currentDropTarget = null;
+ event.stopPropagation();
+ }
+};
+
+/**
+ * This object handles the initialization and uninitialization of the bookmarks
+ * toolbar.
+ */
+var PlacesToolbarHelper = {
+ _place: "place:folder=TOOLBAR",
+
+ get _viewElt() {
+ return document.getElementById("PlacesToolbar");
+ },
+
+ get _placeholder() {
+ return document.getElementById("bookmarks-toolbar-placeholder");
+ },
+
+ init: function PTH_init(forceToolbarOverflowCheck) {
+ let viewElt = this._viewElt;
+ if (!viewElt || viewElt._placesView)
+ return;
+
+ // CustomizableUI.addListener is idempotent, so we can safely
+ // call this multiple times.
+ CustomizableUI.addListener(this);
+
+ // If the bookmarks toolbar item is:
+ // - not in a toolbar, or;
+ // - the toolbar is collapsed, or;
+ // - the toolbar is hidden some other way:
+ // don't initialize. Also, there is no need to initialize the toolbar if
+ // customizing, because that will happen when the customization is done.
+ let toolbar = this._getParentToolbar(viewElt);
+ if (!toolbar || toolbar.collapsed || this._isCustomizing ||
+ getComputedStyle(toolbar, "").display == "none")
+ return;
+
+ new PlacesToolbar(this._place);
+ if (forceToolbarOverflowCheck) {
+ viewElt._placesView.updateOverflowStatus();
+ }
+ this._shouldWrap = false;
+ this._setupPlaceholder();
+ },
+
+ uninit: function PTH_uninit() {
+ CustomizableUI.removeListener(this);
+ },
+
+ customizeStart: function PTH_customizeStart() {
+ try {
+ let viewElt = this._viewElt;
+ if (viewElt && viewElt._placesView)
+ viewElt._placesView.uninit();
+ } finally {
+ this._isCustomizing = true;
+ }
+ this._shouldWrap = this._getShouldWrap();
+ },
+
+ customizeChange: function PTH_customizeChange() {
+ this._setupPlaceholder();
+ },
+
+ _setupPlaceholder: function PTH_setupPlaceholder() {
+ let placeholder = this._placeholder;
+ if (!placeholder) {
+ return;
+ }
+
+ let shouldWrapNow = this._getShouldWrap();
+ if (this._shouldWrap != shouldWrapNow) {
+ if (shouldWrapNow) {
+ placeholder.setAttribute("wrap", "true");
+ } else {
+ placeholder.removeAttribute("wrap");
+ }
+ this._shouldWrap = shouldWrapNow;
+ }
+ },
+
+ customizeDone: function PTH_customizeDone() {
+ this._isCustomizing = false;
+ this.init(true);
+ },
+
+ _getShouldWrap: function PTH_getShouldWrap() {
+ let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
+ let area = placement && placement.area;
+ let areaType = area && CustomizableUI.getAreaType(area);
+ return !area || CustomizableUI.TYPE_MENU_PANEL == areaType;
+ },
+
+ onPlaceholderCommand() {
+ let widgetGroup = CustomizableUI.getWidget("personal-bookmarks");
+ let widget = widgetGroup.forWindow(window);
+ if (widget.overflowed ||
+ widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
+ PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar");
+ }
+ },
+
+ _getParentToolbar(element) {
+ while (element) {
+ if (element.localName == "toolbar") {
+ return element;
+ }
+ element = element.parentNode;
+ }
+ return null;
+ },
+
+ onWidgetUnderflow(aNode, aContainer) {
+ // The view gets broken by being removed and reinserted by the overflowable
+ // toolbar, so we have to force an uninit and reinit.
+ let win = aNode.ownerGlobal;
+ if (aNode.id == "personal-bookmarks" && win == window) {
+ this._resetView();
+ }
+ },
+
+ onWidgetAdded(aWidgetId, aArea, aPosition) {
+ if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) {
+ // It's possible (with the "Add to Menu", "Add to Toolbar" context
+ // options) that the Places Toolbar Items have been moved without
+ // letting us prepare and handle it with with customizeStart and
+ // customizeDone. If that's the case, we need to reset the views
+ // since they're probably broken from the DOM reparenting.
+ this._resetView();
+ }
+ },
+
+ _resetView() {
+ if (this._viewElt) {
+ // It's possible that the placesView might not exist, and we need to
+ // do a full init. This could happen if the Bookmarks Toolbar Items are
+ // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar"
+ // context menu option, outside of customize mode.
+ if (this._viewElt._placesView) {
+ this._viewElt._placesView.uninit();
+ }
+ this.init(true);
+ }
+ },
+};
+
+/**
+ * Handles the bookmarks menu-button in the toolbar.
+ */
+
+var BookmarkingUI = {
+ BOOKMARK_BUTTON_ID: "bookmarks-menu-button",
+ BOOKMARK_BUTTON_SHORTCUT: "addBookmarkAsKb",
+ get button() {
+ delete this.button;
+ let widgetGroup = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID);
+ return this.button = widgetGroup.forWindow(window).node;
+ },
+
+ /* Can't make this a self-deleting getter because it's anonymous content
+ * and might lose/regain bindings at some point. */
+ get star() {
+ return document.getAnonymousElementByAttribute(this.button, "anonid",
+ "button");
+ },
+
+ get anchor() {
+ if (!this._shouldUpdateStarState()) {
+ return null;
+ }
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
+ .forWindow(window);
+ if (widget.overflowed)
+ return widget.anchor;
+
+ let star = this.star;
+ return star ? document.getAnonymousElementByAttribute(star, "class",
+ "toolbarbutton-icon")
+ : null;
+ },
+
+ get notifier() {
+ delete this.notifier;
+ return this.notifier = document.getElementById("bookmarked-notification-anchor");
+ },
+
+ get dropmarkerNotifier() {
+ delete this.dropmarkerNotifier;
+ return this.dropmarkerNotifier = document.getElementById("bookmarked-notification-dropmarker-anchor");
+ },
+
+ get broadcaster() {
+ delete this.broadcaster;
+ let broadcaster = document.getElementById("bookmarkThisPageBroadcaster");
+ return this.broadcaster = broadcaster;
+ },
+
+ STATUS_UPDATING: -1,
+ STATUS_UNSTARRED: 0,
+ STATUS_STARRED: 1,
+ get status() {
+ if (!this._shouldUpdateStarState()) {
+ return this.STATUS_UNSTARRED;
+ }
+ if (this._pendingStmt)
+ return this.STATUS_UPDATING;
+ return this.button.hasAttribute("starred") ? this.STATUS_STARRED
+ : this.STATUS_UNSTARRED;
+ },
+
+ get _starredTooltip() {
+ delete this._starredTooltip;
+ return this._starredTooltip =
+ this._getFormattedTooltip("starButtonOn.tooltip2");
+ },
+
+ get _unstarredTooltip() {
+ delete this._unstarredTooltip;
+ return this._unstarredTooltip =
+ this._getFormattedTooltip("starButtonOff.tooltip2");
+ },
+
+ _getFormattedTooltip(strId) {
+ let args = [];
+ let shortcut = document.getElementById(this.BOOKMARK_BUTTON_SHORTCUT);
+ if (shortcut)
+ args.push(ShortcutUtils.prettifyShortcut(shortcut));
+ return gNavigatorBundle.getFormattedString(strId, args);
+ },
+
+ /**
+ * The type of the area in which the button is currently located.
+ * When in the panel, we don't update the button's icon.
+ */
+ _currentAreaType: null,
+ _shouldUpdateStarState() {
+ return this._currentAreaType == CustomizableUI.TYPE_TOOLBAR;
+ },
+
+ /**
+ * The popup contents must be updated when the user customizes the UI, or
+ * changes the personal toolbar collapsed status. In such a case, any needed
+ * change should be handled in the popupshowing helper, for performance
+ * reasons.
+ */
+ _popupNeedsUpdate: true,
+ onToolbarVisibilityChange: function BUI_onToolbarVisibilityChange() {
+ this._popupNeedsUpdate = true;
+ },
+
+ onPopupShowing: function BUI_onPopupShowing(event) {
+ // Don't handle events for submenus.
+ if (event.target != event.currentTarget)
+ return;
+
+ // Ideally this code would never be reached, but if you click the outer
+ // button's border, some cpp code for the menu button's so-called XBL binding
+ // decides to open the popup even though the dropmarker is invisible.
+ if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
+ this._showSubview();
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
+ .forWindow(window);
+ if (widget.overflowed) {
+ // Don't open a popup in the overflow popup, rather just open the Library.
+ event.preventDefault();
+ widget.node.removeAttribute("closemenu");
+ PlacesCommandHook.showPlacesOrganizer("BookmarksMenu");
+ return;
+ }
+
+ this._initRecentBookmarks(document.getElementById("BMB_recentBookmarks"),
+ "subviewbutton");
+
+ if (!this._popupNeedsUpdate)
+ return;
+ this._popupNeedsUpdate = false;
+
+ let popup = event.target;
+ let getPlacesAnonymousElement =
+ aAnonId => document.getAnonymousElementByAttribute(popup.parentNode,
+ "placesanonid",
+ aAnonId);
+
+ let viewToolbarMenuitem = getPlacesAnonymousElement("view-toolbar");
+ if (viewToolbarMenuitem) {
+ // Update View bookmarks toolbar checkbox menuitem.
+ viewToolbarMenuitem.classList.add("subviewbutton");
+ let personalToolbar = document.getElementById("PersonalToolbar");
+ viewToolbarMenuitem.setAttribute("checked", !personalToolbar.collapsed);
+ }
+ },
+
+ attachPlacesView(event, node) {
+ // If the view is already there, bail out early.
+ if (node.parentNode._placesView)
+ return;
+
+ new PlacesMenu(event, "place:folder=BOOKMARKS_MENU", {
+ extraClasses: {
+ entry: "subviewbutton",
+ footer: "panel-subview-footer"
+ },
+ insertionPoint: ".panel-subview-footer"
+ });
+ },
+
+ RECENTLY_BOOKMARKED_PREF: "browser.bookmarks.showRecentlyBookmarked",
+
+ _initRecentBookmarks(aHeaderItem, aExtraCSSClass) {
+ this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass);
+
+ // Add observers and listeners and remove them again when the menupopup closes.
+
+ let bookmarksMenu = aHeaderItem.parentNode;
+ let placesContextMenu = document.getElementById("placesContext");
+
+ let prefObserver = () => {
+ this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass);
+ };
+
+ this._recentlyBookmarkedObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ Ci.nsISupportsWeakReference
+ ])
+ };
+ this._recentlyBookmarkedObserver.onItemRemoved = () => {
+ // Update the menu when a bookmark has been removed.
+ // The native menubar on Mac doesn't support live update, so this won't
+ // work there.
+ this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass);
+ };
+
+ let updatePlacesContextMenu = (shouldHidePrefUI = false) => {
+ let prefEnabled = !shouldHidePrefUI && Services.prefs.getBoolPref(this.RECENTLY_BOOKMARKED_PREF);
+ let showItem = document.getElementById("placesContext_showRecentlyBookmarked");
+ let hideItem = document.getElementById("placesContext_hideRecentlyBookmarked");
+ let separator = document.getElementById("placesContext_recentlyBookmarkedSeparator");
+ showItem.hidden = shouldHidePrefUI || prefEnabled;
+ hideItem.hidden = shouldHidePrefUI || !prefEnabled;
+ separator.hidden = shouldHidePrefUI;
+ if (!shouldHidePrefUI) {
+ // Move to the bottom of the menu.
+ separator.parentNode.appendChild(separator);
+ showItem.parentNode.appendChild(showItem);
+ hideItem.parentNode.appendChild(hideItem);
+ }
+ };
+
+ let onPlacesContextMenuShowing = event => {
+ if (event.target == event.currentTarget) {
+ let triggerPopup = event.target.triggerNode;
+ while (triggerPopup && triggerPopup.localName != "menupopup") {
+ triggerPopup = triggerPopup.parentNode;
+ }
+ let shouldHidePrefUI = triggerPopup != bookmarksMenu;
+ updatePlacesContextMenu(shouldHidePrefUI);
+ }
+ };
+
+ let onBookmarksMenuHidden = event => {
+ if (event.target == event.currentTarget) {
+ updatePlacesContextMenu(true);
+
+ Services.prefs.removeObserver(this.RECENTLY_BOOKMARKED_PREF, prefObserver);
+ PlacesUtils.bookmarks.removeObserver(this._recentlyBookmarkedObserver);
+ this._recentlyBookmarkedObserver = null;
+ if (placesContextMenu) {
+ placesContextMenu.removeEventListener("popupshowing", onPlacesContextMenuShowing);
+ }
+ bookmarksMenu.removeEventListener("popuphidden", onBookmarksMenuHidden);
+ }
+ };
+
+ Services.prefs.addObserver(this.RECENTLY_BOOKMARKED_PREF, prefObserver, false);
+ PlacesUtils.bookmarks.addObserver(this._recentlyBookmarkedObserver, true);
+
+ // The context menu doesn't exist in non-browser windows on Mac
+ if (placesContextMenu) {
+ placesContextMenu.addEventListener("popupshowing", onPlacesContextMenuShowing);
+ }
+
+ bookmarksMenu.addEventListener("popuphidden", onBookmarksMenuHidden);
+ },
+
+ _populateRecentBookmarks(aHeaderItem, aExtraCSSClass = "") {
+ while (aHeaderItem.nextSibling &&
+ aHeaderItem.nextSibling.localName == "menuitem") {
+ aHeaderItem.nextSibling.remove();
+ }
+
+ let shouldShow = Services.prefs.getBoolPref(this.RECENTLY_BOOKMARKED_PREF);
+ let separator = aHeaderItem.previousSibling;
+ aHeaderItem.hidden = !shouldShow;
+ separator.hidden = !shouldShow;
+
+ if (!shouldShow) {
+ return;
+ }
+
+ const kMaxResults = 5;
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.excludeQueries = true;
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ options.sortingMode = options.SORT_BY_DATEADDED_DESCENDING;
+ options.maxResults = kMaxResults;
+ let query = PlacesUtils.history.getNewQuery();
+
+ let sh = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ let loadingPrincipal = sh.serializeToString(document.nodePrincipal);
+
+ let fragment = document.createDocumentFragment();
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = node.uri;
+ let title = node.title;
+ let icon = node.icon;
+
+ let item =
+ document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "menuitem");
+ item.setAttribute("label", title || uri);
+ item.setAttribute("targetURI", uri);
+ item.setAttribute("simulated-places-node", true);
+ item.setAttribute("class", "menuitem-iconic menuitem-with-favicon bookmark-item " +
+ aExtraCSSClass);
+ if (icon) {
+ item.setAttribute("image", icon);
+ item.setAttribute("loadingprincipal", loadingPrincipal);
+ }
+ item._placesNode = node;
+ fragment.appendChild(item);
+ }
+ root.containerOpen = false;
+ aHeaderItem.parentNode.insertBefore(fragment, aHeaderItem.nextSibling);
+ },
+
+ showRecentlyBookmarked() {
+ Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, true);
+ },
+
+ hideRecentlyBookmarked() {
+ Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, false);
+ },
+
+ _updateCustomizationState: function BUI__updateCustomizationState() {
+ let placement = CustomizableUI.getPlacementOfWidget(this.BOOKMARK_BUTTON_ID);
+ this._currentAreaType = placement && CustomizableUI.getAreaType(placement.area);
+ },
+
+ _uninitView: function BUI__uninitView() {
+ // When an element with a placesView attached is removed and re-inserted,
+ // XBL reapplies the binding causing any kind of issues and possible leaks,
+ // so kill current view and let popupshowing generate a new one.
+ if (this.button._placesView)
+ this.button._placesView.uninit();
+ // Also uninit the main menubar placesView, since it would have the same
+ // issues.
+ let menubar = document.getElementById("bookmarksMenu");
+ if (menubar && menubar._placesView)
+ menubar._placesView.uninit();
+
+ // We have to do the same thing for the "special" views underneath the
+ // the bookmarks menu.
+ const kSpecialViewNodeIDs = ["BMB_bookmarksToolbar", "BMB_unsortedBookmarks"];
+ for (let viewNodeID of kSpecialViewNodeIDs) {
+ let elem = document.getElementById(viewNodeID);
+ if (elem && elem._placesView) {
+ elem._placesView.uninit();
+ }
+ }
+ },
+
+ onCustomizeStart: function BUI_customizeStart(aWindow) {
+ if (aWindow == window) {
+ this._uninitView();
+ this._isCustomizing = true;
+ }
+ },
+
+ onWidgetAdded: function BUI_widgetAdded(aWidgetId) {
+ if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetRemoved: function BUI_widgetRemoved(aWidgetId) {
+ if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetReset: function BUI_widgetReset(aNode, aContainer) {
+ if (aNode == this.button) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode, aContainer) {
+ if (aNode == this.button) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ _onWidgetWasMoved: function BUI_widgetWasMoved() {
+ let usedToUpdateStarState = this._shouldUpdateStarState();
+ this._updateCustomizationState();
+ if (!usedToUpdateStarState && this._shouldUpdateStarState()) {
+ this.updateStarState();
+ } else if (usedToUpdateStarState && !this._shouldUpdateStarState()) {
+ this._updateStar();
+ }
+ // If we're moved outside of customize mode, we need to uninit
+ // our view so it gets reconstructed.
+ if (!this._isCustomizing) {
+ this._uninitView();
+ }
+ },
+
+ onCustomizeEnd: function BUI_customizeEnd(aWindow) {
+ if (aWindow == window) {
+ this._isCustomizing = false;
+ this.onToolbarVisibilityChange();
+ }
+ },
+
+ init() {
+ CustomizableUI.addListener(this);
+ this._updateCustomizationState();
+ },
+
+ _hasBookmarksObserver: false,
+ _itemIds: [],
+ uninit: function BUI_uninit() {
+ this._updateBookmarkPageMenuItem(true);
+ CustomizableUI.removeListener(this);
+
+ this._uninitView();
+
+ if (this._hasBookmarksObserver) {
+ PlacesUtils.removeLazyBookmarkObserver(this);
+ }
+
+ if (this._pendingStmt) {
+ this._pendingStmt.cancel();
+ delete this._pendingStmt;
+ }
+ },
+
+ onLocationChange: function BUI_onLocationChange() {
+ if (this._uri && gBrowser.currentURI.equals(this._uri)) {
+ return;
+ }
+ this.updateStarState();
+ },
+
+ updateStarState: function BUI_updateStarState() {
+ // Reset tracked values.
+ this._uri = gBrowser.currentURI;
+ this._itemIds = [];
+
+ if (this._pendingStmt) {
+ this._pendingStmt.cancel();
+ delete this._pendingStmt;
+ }
+
+ this._pendingStmt = PlacesUtils.asyncGetBookmarkIds(this._uri, (aItemIds, aURI) => {
+ // Safety check that the bookmarked URI equals the tracked one.
+ if (!aURI.equals(this._uri)) {
+ Components.utils.reportError("BookmarkingUI did not receive current URI");
+ return;
+ }
+
+ // It's possible that onItemAdded gets called before the async statement
+ // calls back. For such an edge case, retain all unique entries from both
+ // arrays.
+ this._itemIds = this._itemIds.filter(
+ id => !aItemIds.includes(id)
+ ).concat(aItemIds);
+
+ this._updateStar();
+
+ // Start observing bookmarks if needed.
+ if (!this._hasBookmarksObserver) {
+ try {
+ PlacesUtils.addLazyBookmarkObserver(this);
+ this._hasBookmarksObserver = true;
+ } catch (ex) {
+ Components.utils.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex);
+ }
+ }
+
+ delete this._pendingStmt;
+ });
+ },
+
+ _updateStar: function BUI__updateStar() {
+ if (!this._shouldUpdateStarState()) {
+ if (this.broadcaster.hasAttribute("starred")) {
+ this.broadcaster.removeAttribute("starred");
+ this.broadcaster.removeAttribute("buttontooltiptext");
+ }
+ return;
+ }
+
+ if (this._itemIds.length > 0) {
+ this.broadcaster.setAttribute("starred", "true");
+ this.broadcaster.setAttribute("buttontooltiptext", this._starredTooltip);
+ if (this.button.getAttribute("overflowedItem") == "true") {
+ this.button.setAttribute("label", this._starButtonOverflowedStarredLabel);
+ }
+ } else {
+ this.broadcaster.removeAttribute("starred");
+ this.broadcaster.setAttribute("buttontooltiptext", this._unstarredTooltip);
+ if (this.button.getAttribute("overflowedItem") == "true") {
+ this.button.setAttribute("label", this._starButtonOverflowedLabel);
+ }
+ }
+ },
+
+ /**
+ * forceReset is passed when we're destroyed and the label should go back
+ * to the default (Bookmark This Page) for OS X.
+ */
+ _updateBookmarkPageMenuItem: function BUI__updateBookmarkPageMenuItem(forceReset) {
+ let isStarred = !forceReset && this._itemIds.length > 0;
+ let label = isStarred ? "editlabel" : "bookmarklabel";
+ if (this.broadcaster) {
+ this.broadcaster.setAttribute("label", this.broadcaster.getAttribute(label));
+ }
+ },
+
+ onMainMenuPopupShowing: function BUI_onMainMenuPopupShowing(event) {
+ // Don't handle events for submenus.
+ if (event.target != event.currentTarget)
+ return;
+
+ this._updateBookmarkPageMenuItem();
+ PlacesCommandHook.updateBookmarkAllTabsCommand();
+ this._initRecentBookmarks(document.getElementById("menu_recentBookmarks"));
+ },
+
+ _showBookmarkedNotification: function BUI_showBookmarkedNotification() {
+ function getCenteringTransformForRects(rectToPosition, referenceRect) {
+ let topDiff = referenceRect.top - rectToPosition.top;
+ let leftDiff = referenceRect.left - rectToPosition.left;
+ let heightDiff = referenceRect.height - rectToPosition.height;
+ let widthDiff = referenceRect.width - rectToPosition.width;
+ return [(leftDiff + .5 * widthDiff) + "px", (topDiff + .5 * heightDiff) + "px"];
+ }
+
+ if (this._notificationTimeout) {
+ clearTimeout(this._notificationTimeout);
+ }
+
+ if (this.notifier.style.transform == "") {
+ // Get all the relevant nodes and computed style objects
+ let dropmarker = document.getAnonymousElementByAttribute(this.button, "anonid", "dropmarker");
+ let dropmarkerIcon = document.getAnonymousElementByAttribute(dropmarker, "class", "dropmarker-icon");
+ let dropmarkerStyle = getComputedStyle(dropmarkerIcon);
+
+ // Check for RTL and get bounds
+ let isRTL = getComputedStyle(this.button).direction == "rtl";
+ let buttonRect = this.button.getBoundingClientRect();
+ let notifierRect = this.notifier.getBoundingClientRect();
+ let dropmarkerRect = dropmarkerIcon.getBoundingClientRect();
+ let dropmarkerNotifierRect = this.dropmarkerNotifier.getBoundingClientRect();
+
+ // Compute, but do not set, transform for star icon
+ let [translateX, translateY] = getCenteringTransformForRects(notifierRect, buttonRect);
+ let starIconTransform = "translate(" + translateX + ", " + translateY + ")";
+ if (isRTL) {
+ starIconTransform += " scaleX(-1)";
+ }
+
+ // Compute, but do not set, transform for dropmarker
+ [translateX, translateY] = getCenteringTransformForRects(dropmarkerNotifierRect, dropmarkerRect);
+ let dropmarkerTransform = "translate(" + translateX + ", " + translateY + ")";
+
+ // Do all layout invalidation in one go:
+ this.notifier.style.transform = starIconTransform;
+ this.dropmarkerNotifier.style.transform = dropmarkerTransform;
+
+ let dropmarkerAnimationNode = this.dropmarkerNotifier.firstChild;
+ dropmarkerAnimationNode.style.MozImageRegion = dropmarkerStyle.MozImageRegion;
+ dropmarkerAnimationNode.style.listStyleImage = dropmarkerStyle.listStyleImage;
+ }
+
+ let isInOverflowPanel = this.button.getAttribute("overflowedItem") == "true";
+ if (!isInOverflowPanel) {
+ this.notifier.setAttribute("notification", "finish");
+ this.button.setAttribute("notification", "finish");
+ this.dropmarkerNotifier.setAttribute("notification", "finish");
+ }
+
+ this._notificationTimeout = setTimeout( () => {
+ this.notifier.removeAttribute("notification");
+ this.dropmarkerNotifier.removeAttribute("notification");
+ this.button.removeAttribute("notification");
+
+ this.dropmarkerNotifier.style.transform = "";
+ this.notifier.style.transform = "";
+ }, 1000);
+ },
+
+ _showSubview() {
+ let view = document.getElementById("PanelUI-bookmarks");
+ view.addEventListener("ViewShowing", this);
+ view.addEventListener("ViewHiding", this);
+ let anchor = document.getElementById(this.BOOKMARK_BUTTON_ID);
+ anchor.setAttribute("closemenu", "none");
+ PanelUI.showSubView("PanelUI-bookmarks", anchor,
+ CustomizableUI.AREA_PANEL);
+ },
+
+ onCommand: function BUI_onCommand(aEvent) {
+ if (aEvent.target != aEvent.currentTarget) {
+ return;
+ }
+
+ // Handle special case when the button is in the panel.
+ let isBookmarked = this._itemIds.length > 0;
+
+ if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
+ this._showSubview();
+ return;
+ }
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
+ .forWindow(window);
+ if (widget.overflowed) {
+ // Close the overflow panel because the Edit Bookmark panel will appear.
+ widget.node.removeAttribute("closemenu");
+ }
+
+ // Ignore clicks on the star if we are updating its state.
+ if (!this._pendingStmt) {
+ if (!isBookmarked)
+ this._showBookmarkedNotification();
+ PlacesCommandHook.bookmarkCurrentPage(true);
+ }
+ },
+
+ onCurrentPageContextPopupShowing() {
+ this._updateBookmarkPageMenuItem();
+ },
+
+ handleEvent: function BUI_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "ViewShowing":
+ this.onPanelMenuViewShowing(aEvent);
+ break;
+ case "ViewHiding":
+ this.onPanelMenuViewHiding(aEvent);
+ break;
+ }
+ },
+
+ onPanelMenuViewShowing: function BUI_onViewShowing(aEvent) {
+ this._updateBookmarkPageMenuItem();
+ // Update checked status of the toolbar toggle.
+ let viewToolbar = document.getElementById("panelMenu_viewBookmarksToolbar");
+ let personalToolbar = document.getElementById("PersonalToolbar");
+ if (personalToolbar.collapsed)
+ viewToolbar.removeAttribute("checked");
+ else
+ viewToolbar.setAttribute("checked", "true");
+ // Get all statically placed buttons to supply them with keyboard shortcuts.
+ let staticButtons = viewToolbar.parentNode.getElementsByTagName("toolbarbutton");
+ for (let i = 0, l = staticButtons.length; i < l; ++i)
+ CustomizableUI.addShortcut(staticButtons[i]);
+ // Setup the Places view.
+ this._panelMenuView = new PlacesPanelMenuView("place:folder=BOOKMARKS_MENU",
+ "panelMenu_bookmarksMenu",
+ "panelMenu_bookmarksMenu", {
+ extraClasses: {
+ entry: "subviewbutton",
+ footer: "panel-subview-footer"
+ }
+ });
+ aEvent.target.removeEventListener("ViewShowing", this);
+ },
+
+ onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) {
+ this._panelMenuView.uninit();
+ delete this._panelMenuView;
+ aEvent.target.removeEventListener("ViewHiding", this);
+ },
+
+ onPanelMenuViewCommand: function BUI_onPanelMenuViewCommand(aEvent, aView) {
+ let target = aEvent.originalTarget;
+ if (!target._placesNode)
+ return;
+ if (PlacesUtils.nodeIsContainer(target._placesNode))
+ PlacesCommandHook.showPlacesOrganizer([ "BookmarksMenu", target._placesNode.itemId ]);
+ else
+ PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView);
+ PanelUI.hide();
+ },
+
+ // nsINavBookmarkObserver
+ onItemAdded: function BUI_onItemAdded(aItemId, aParentId, aIndex, aItemType,
+ aURI) {
+ if (aURI && aURI.equals(this._uri)) {
+ // If a new bookmark has been added to the tracked uri, register it.
+ if (!this._itemIds.includes(aItemId)) {
+ this._itemIds.push(aItemId);
+ // Only need to update the UI if it wasn't marked as starred before:
+ if (this._itemIds.length == 1) {
+ this._updateStar();
+ }
+ }
+ }
+ },
+
+ onItemRemoved: function BUI_onItemRemoved(aItemId) {
+ let index = this._itemIds.indexOf(aItemId);
+ // If one of the tracked bookmarks has been removed, unregister it.
+ if (index != -1) {
+ this._itemIds.splice(index, 1);
+ // Only need to update the UI if the page is no longer starred
+ if (this._itemIds.length == 0) {
+ this._updateStar();
+ }
+ }
+ },
+
+ onItemChanged: function BUI_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue) {
+ if (aProperty == "uri") {
+ let index = this._itemIds.indexOf(aItemId);
+ // If the changed bookmark was tracked, check if it is now pointing to
+ // a different uri and unregister it.
+ if (index != -1 && aNewValue != this._uri.spec) {
+ this._itemIds.splice(index, 1);
+ // Only need to update the UI if the page is no longer starred
+ if (this._itemIds.length == 0) {
+ this._updateStar();
+ }
+ } else if (index == -1 && aNewValue == this._uri.spec) {
+ // If another bookmark is now pointing to the tracked uri, register it.
+ this._itemIds.push(aItemId);
+ // Only need to update the UI if it wasn't marked as starred before:
+ if (this._itemIds.length == 1) {
+ this._updateStar();
+ }
+ }
+ }
+ },
+
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onBeforeItemRemoved() {},
+ onItemVisited() {},
+ onItemMoved() {},
+
+ // CustomizableUI events:
+ _starButtonLabel: null,
+ get _starButtonOverflowedLabel() {
+ delete this._starButtonOverflowedLabel;
+ return this._starButtonOverflowedLabel =
+ gNavigatorBundle.getString("starButtonOverflowed.label");
+ },
+ get _starButtonOverflowedStarredLabel() {
+ delete this._starButtonOverflowedStarredLabel;
+ return this._starButtonOverflowedStarredLabel =
+ gNavigatorBundle.getString("starButtonOverflowedStarred.label");
+ },
+ onWidgetOverflow(aNode, aContainer) {
+ let win = aNode.ownerGlobal;
+ if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window)
+ return;
+
+ let currentLabel = aNode.getAttribute("label");
+ if (!this._starButtonLabel)
+ this._starButtonLabel = currentLabel;
+
+ if (currentLabel == this._starButtonLabel) {
+ let desiredLabel = this._itemIds.length > 0 ? this._starButtonOverflowedStarredLabel
+ : this._starButtonOverflowedLabel;
+ aNode.setAttribute("label", desiredLabel);
+ }
+ },
+
+ onWidgetUnderflow(aNode, aContainer) {
+ let win = aNode.ownerGlobal;
+ if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window)
+ return;
+
+ // The view gets broken by being removed and reinserted. Uninit
+ // here so popupshowing will generate a new one:
+ this._uninitView();
+
+ if (aNode.getAttribute("label") != this._starButtonLabel)
+ aNode.setAttribute("label", this._starButtonLabel);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ ])
+};
+
+var AutoShowBookmarksToolbar = {
+ init() {
+ Services.obs.addObserver(this, "autoshow-bookmarks-toolbar", false);
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "autoshow-bookmarks-toolbar");
+ },
+
+ observe(subject, topic, data) {
+ let toolbar = document.getElementById("PersonalToolbar");
+ if (!toolbar.collapsed)
+ return;
+
+ let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
+ let area = placement && placement.area;
+ if (area != CustomizableUI.AREA_BOOKMARKS)
+ return;
+
+ setToolbarVisibility(toolbar, true);
+ }
+};
diff --git a/application/basilisk/base/content/browser-plugins.js b/application/basilisk/base/content/browser-plugins.js
new file mode 100644
index 000000000..630ab0b27
--- /dev/null
+++ b/application/basilisk/base/content/browser-plugins.js
@@ -0,0 +1,544 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gPluginHandler = {
+ PREF_SESSION_PERSIST_MINUTES: "plugin.sessionPermissionNow.intervalInMinutes",
+ PREF_PERSISTENT_DAYS: "plugin.persistentPermissionAlways.intervalInDays",
+ MESSAGES: [
+ "PluginContent:ShowClickToPlayNotification",
+ "PluginContent:RemoveNotification",
+ "PluginContent:UpdateHiddenPluginUI",
+ "PluginContent:HideNotificationBar",
+ "PluginContent:InstallSinglePlugin",
+ "PluginContent:ShowPluginCrashedNotification",
+ "PluginContent:SubmitReport",
+ "PluginContent:LinkClickCallback",
+ ],
+
+ init() {
+ const mm = window.messageManager;
+ for (let msg of this.MESSAGES) {
+ mm.addMessageListener(msg, this);
+ }
+ window.addEventListener("unload", this);
+ },
+
+ uninit() {
+ const mm = window.messageManager;
+ for (let msg of this.MESSAGES) {
+ mm.removeMessageListener(msg, this);
+ }
+ window.removeEventListener("unload", this);
+ },
+
+ handleEvent(event) {
+ if (event.type == "unload") {
+ this.uninit();
+ }
+ },
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "PluginContent:ShowClickToPlayNotification":
+ this.showClickToPlayNotification(msg.target, msg.data.plugins, msg.data.showNow,
+ msg.principal, msg.data.location);
+ break;
+ case "PluginContent:RemoveNotification":
+ this.removeNotification(msg.target, msg.data.name);
+ break;
+ case "PluginContent:UpdateHiddenPluginUI":
+ this.updateHiddenPluginUI(msg.target, msg.data.haveInsecure, msg.data.actions,
+ msg.principal, msg.data.location);
+ break;
+ case "PluginContent:HideNotificationBar":
+ this.hideNotificationBar(msg.target, msg.data.name);
+ break;
+ case "PluginContent:InstallSinglePlugin":
+ this.installSinglePlugin(msg.data.pluginInfo);
+ break;
+ case "PluginContent:ShowPluginCrashedNotification":
+ this.showPluginCrashedNotification(msg.target, msg.data.messageString,
+ msg.data.pluginID);
+ break;
+ case "PluginContent:SubmitReport":
+ // Nothing to do here.
+ break;
+ case "PluginContent:LinkClickCallback":
+ switch (msg.data.name) {
+ case "managePlugins":
+ case "openHelpPage":
+ case "openPluginUpdatePage":
+ this[msg.data.name](msg.data.pluginTag);
+ break;
+ }
+ break;
+ default:
+ Cu.reportError("gPluginHandler did not expect to handle message " + msg.name);
+ break;
+ }
+ },
+
+ // Callback for user clicking on a disabled plugin
+ managePlugins() {
+ BrowserOpenAddonsMgr("addons://list/plugin");
+ },
+
+ // Callback for user clicking on the link in a click-to-play plugin
+ // (where the plugin has an update)
+ openPluginUpdatePage(pluginTag) {
+ let url = Services.blocklist.getPluginInfoURL(pluginTag);
+ if (!url) {
+ url = Services.blocklist.getPluginBlocklistURL(pluginTag);
+ }
+ openUILinkIn(url, "tab");
+ },
+
+ submitReport: function submitReport(runID, keyVals, submitURLOptIn) {
+ return;
+ },
+
+ // Callback for user clicking a "reload page" link
+ reloadPage(browser) {
+ browser.reload();
+ },
+
+ // Callback for user clicking the help icon
+ openHelpPage() {
+ openHelpLink("plugin-crashed", false);
+ },
+
+ _clickToPlayNotificationEventCallback: function PH_ctpEventCallback(event) {
+ if (event == "showing") {
+ Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_SHOWN")
+ .add(!this.options.primaryPlugin);
+ // Histograms always start at 0, even though our data starts at 1
+ let histogramCount = this.options.pluginData.size - 1;
+ if (histogramCount > 4) {
+ histogramCount = 4;
+ }
+ Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_PLUGIN_COUNT")
+ .add(histogramCount);
+ } else if (event == "dismissed") {
+ // Once the popup is dismissed, clicking the icon should show the full
+ // list again
+ this.options.primaryPlugin = null;
+ }
+ },
+
+ /**
+ * Called from the plugin doorhanger to set the new permissions for a plugin
+ * and activate plugins if necessary.
+ * aNewState should be either "allownow" "allowalways" or "block"
+ */
+ _updatePluginPermission(aNotification, aPluginInfo, aNewState) {
+ let permission;
+ let expireType;
+ let expireTime;
+ let histogram =
+ Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_USER_ACTION");
+
+ // Update the permission manager.
+ // Also update the current state of pluginInfo.fallbackType so that
+ // subsequent opening of the notification shows the current state.
+ switch (aNewState) {
+ case "allownow":
+ permission = Ci.nsIPermissionManager.ALLOW_ACTION;
+ expireType = Ci.nsIPermissionManager.EXPIRE_SESSION;
+ expireTime = Date.now() + Services.prefs.getIntPref(this.PREF_SESSION_PERSIST_MINUTES) * 60 * 1000;
+ histogram.add(0);
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
+ break;
+
+ case "allowalways":
+ permission = Ci.nsIPermissionManager.ALLOW_ACTION;
+ expireType = Ci.nsIPermissionManager.EXPIRE_TIME;
+ expireTime = Date.now() +
+ Services.prefs.getIntPref(this.PREF_PERSISTENT_DAYS) * 24 * 60 * 60 * 1000;
+ histogram.add(1);
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
+ break;
+
+ case "block":
+ permission = Ci.nsIPermissionManager.PROMPT_ACTION;
+ expireType = Ci.nsIPermissionManager.EXPIRE_NEVER;
+ expireTime = 0;
+ histogram.add(2);
+ switch (aPluginInfo.blocklistState) {
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE;
+ break;
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE;
+ break;
+ default:
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY;
+ }
+ break;
+
+ // In case a plugin has already been allowed in another tab, the "continue allowing" button
+ // shouldn't change any permissions but should run the plugin-enablement code below.
+ case "continue":
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
+ break;
+ default:
+ Cu.reportError(Error("Unexpected plugin state: " + aNewState));
+ return;
+ }
+
+ let browser = aNotification.browser;
+ if (aNewState != "continue") {
+ let principal = aNotification.options.principal;
+ Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString,
+ permission, expireType, expireTime);
+ aPluginInfo.pluginPermissionType = expireType;
+ }
+
+ browser.messageManager.sendAsyncMessage("BrowserPlugins:ActivatePlugins", {
+ pluginInfo: aPluginInfo,
+ newState: aNewState,
+ });
+ },
+
+ showClickToPlayNotification(browser, plugins, showNow,
+ principal, location) {
+ // It is possible that we've received a message from the frame script to show
+ // a click to play notification for a principal that no longer matches the one
+ // that the browser's content now has assigned (ie, the browser has browsed away
+ // after the message was sent, but before the message was received). In that case,
+ // we should just ignore the message.
+ if (!principal.equals(browser.contentPrincipal)) {
+ return;
+ }
+
+ // Data URIs, when linked to from some page, inherit the principal of that
+ // page. That means that we also need to compare the actual locations to
+ // ensure we aren't getting a message from a Data URI that we're no longer
+ // looking at.
+ let receivedURI = BrowserUtils.makeURI(location);
+ if (!browser.documentURI.equalsExceptRef(receivedURI)) {
+ return;
+ }
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", browser);
+
+ // If this is a new notification, create a pluginData map, otherwise append
+ let pluginData;
+ if (notification) {
+ pluginData = notification.options.pluginData;
+ } else {
+ pluginData = new Map();
+ }
+
+ for (var pluginInfo of plugins) {
+ if (pluginData.has(pluginInfo.permissionString)) {
+ continue;
+ }
+
+ // If a block contains an infoURL, we should always prefer that to the default
+ // URL that we construct in-product, even for other blocklist types.
+ let url = Services.blocklist.getPluginInfoURL(pluginInfo.pluginTag);
+
+ if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
+ if (!url) {
+ url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag);
+ }
+ } else {
+ url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "clicktoplay";
+ }
+ pluginInfo.detailsLink = url;
+
+ pluginData.set(pluginInfo.permissionString, pluginInfo);
+ }
+
+ let primaryPluginPermission = null;
+ if (showNow) {
+ primaryPluginPermission = plugins[0].permissionString;
+ }
+
+ if (notification) {
+ // Don't modify the notification UI while it's on the screen, that would be
+ // jumpy and might allow clickjacking.
+ if (showNow) {
+ notification.options.primaryPlugin = primaryPluginPermission;
+ notification.reshow();
+ browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
+ }
+ return;
+ }
+
+ let options = {
+ dismissed: !showNow,
+ persistent: showNow,
+ eventCallback: this._clickToPlayNotificationEventCallback,
+ primaryPlugin: primaryPluginPermission,
+ pluginData,
+ principal,
+ };
+ PopupNotifications.show(browser, "click-to-play-plugins",
+ "", "plugins-notification-icon",
+ null, null, options);
+ browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
+ },
+
+ removeNotification(browser, name) {
+ let notification = PopupNotifications.getNotification(name, browser);
+ if (notification)
+ PopupNotifications.remove(notification);
+ },
+
+ hideNotificationBar(browser, name) {
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.getNotificationWithValue(name);
+ if (notification)
+ notificationBox.removeNotification(notification, true);
+ },
+
+ updateHiddenPluginUI(browser, haveInsecure, actions,
+ principal, location) {
+ let origin = principal.originNoSuffix;
+
+ // It is possible that we've received a message from the frame script to show
+ // the hidden plugin notification for a principal that no longer matches the one
+ // that the browser's content now has assigned (ie, the browser has browsed away
+ // after the message was sent, but before the message was received). In that case,
+ // we should just ignore the message.
+ if (!principal.equals(browser.contentPrincipal)) {
+ return;
+ }
+
+ // Data URIs, when linked to from some page, inherit the principal of that
+ // page. That means that we also need to compare the actual locations to
+ // ensure we aren't getting a message from a Data URI that we're no longer
+ // looking at.
+ let receivedURI = BrowserUtils.makeURI(location);
+ if (!browser.documentURI.equalsExceptRef(receivedURI)) {
+ return;
+ }
+
+ // Set up the icon
+ document.getElementById("plugins-notification-icon").classList.
+ toggle("plugin-blocked", haveInsecure);
+
+ // Now configure the notification bar
+ let notificationBox = gBrowser.getNotificationBox(browser);
+
+ function hideNotification() {
+ let n = notificationBox.getNotificationWithValue("plugin-hidden");
+ if (n) {
+ notificationBox.removeNotification(n, true);
+ }
+ }
+
+ // There are three different cases when showing an infobar:
+ // 1. A single type of plugin is hidden on the page. Show the UI for that
+ // plugin.
+ // 2a. Multiple types of plugins are hidden on the page. Show the multi-UI
+ // with the vulnerable styling.
+ // 2b. Multiple types of plugins are hidden on the page, but none are
+ // vulnerable. Show the nonvulnerable multi-UI.
+ function showNotification() {
+ let n = notificationBox.getNotificationWithValue("plugin-hidden");
+ if (n) {
+ // If something is already shown, just keep it
+ return;
+ }
+
+ Services.telemetry.getHistogramById("PLUGINS_INFOBAR_SHOWN").
+ add(true);
+
+ let message;
+ // Icons set directly cannot be manipulated using moz-image-region, so
+ // we use CSS classes instead.
+ let brand = document.getElementById("bundle_brand").getString("brandShortName");
+
+ if (actions.length == 1) {
+ let pluginInfo = actions[0];
+ let pluginName = pluginInfo.pluginName;
+
+ switch (pluginInfo.fallbackType) {
+ case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
+ message = gNavigatorBundle.getFormattedString(
+ "pluginActivateNew.message",
+ [pluginName, origin]);
+ break;
+ case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
+ message = gNavigatorBundle.getFormattedString(
+ "pluginActivateOutdated.message",
+ [pluginName, origin, brand]);
+ break;
+ case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
+ message = gNavigatorBundle.getFormattedString(
+ "pluginActivateVulnerable.message",
+ [pluginName, origin, brand]);
+ }
+ } else {
+ // Multi-plugin
+ message = gNavigatorBundle.getFormattedString(
+ "pluginActivateMultiple.message", [origin]);
+ }
+
+ let buttons = [
+ {
+ label: gNavigatorBundle.getString("pluginContinueBlocking.label"),
+ accessKey: gNavigatorBundle.getString("pluginContinueBlocking.accesskey"),
+ callback() {
+ Services.telemetry.getHistogramById("PLUGINS_INFOBAR_BLOCK").
+ add(true);
+
+ Services.perms.addFromPrincipal(principal,
+ "plugin-hidden-notification",
+ Services.perms.DENY_ACTION);
+ }
+ },
+ {
+ label: gNavigatorBundle.getString("pluginActivateTrigger.label"),
+ accessKey: gNavigatorBundle.getString("pluginActivateTrigger.accesskey"),
+ callback() {
+ Services.telemetry.getHistogramById("PLUGINS_INFOBAR_ALLOW").
+ add(true);
+
+ let curNotification =
+ PopupNotifications.getNotification("click-to-play-plugins",
+ browser);
+ if (curNotification) {
+ curNotification.reshow();
+ }
+ }
+ }
+ ];
+ n = notificationBox.
+ appendNotification(message, "plugin-hidden", null,
+ notificationBox.PRIORITY_INFO_HIGH, buttons);
+ if (haveInsecure) {
+ n.classList.add("pluginVulnerable");
+ }
+ }
+
+ if (actions.length == 0) {
+ hideNotification();
+ } else {
+ let notificationPermission = Services.perms.testPermissionFromPrincipal(
+ principal, "plugin-hidden-notification");
+ if (notificationPermission == Ci.nsIPermissionManager.DENY_ACTION) {
+ hideNotification();
+ } else {
+ showNotification();
+ }
+ }
+ },
+
+ contextMenuCommand(browser, plugin, command) {
+ browser.messageManager.sendAsyncMessage("BrowserPlugins:ContextMenuCommand",
+ { command }, { plugin });
+ },
+
+ // Crashed-plugin observer. Notified once per plugin crash, before events
+ // are dispatched to individual plugin instances.
+ NPAPIPluginCrashed(subject, topic, data) {
+ let propertyBag = subject;
+ if (!(propertyBag instanceof Ci.nsIPropertyBag2) ||
+ !(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
+ !propertyBag.hasKey("runID") ||
+ !propertyBag.hasKey("pluginName")) {
+ Cu.reportError("A NPAPI plugin crashed, but the properties of this plugin " +
+ "cannot be read.");
+ return;
+ }
+
+ let runID = propertyBag.getPropertyAsUint32("runID");
+ let uglyPluginName = propertyBag.getPropertyAsAString("pluginName");
+ let pluginName = BrowserUtils.makeNicePluginName(uglyPluginName);
+ let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
+
+ // If we don't have a minidumpID, we can't (or didn't) submit anything.
+ // This can happen if the plugin is killed from the task manager.
+ let state = "noSubmit";
+#ifdef MOZ_CRASHREPORTER
+ if (!gCrashReporter.enabled) {
+ // This state tells the user that crash reporting is disabled, so we
+ // cannot send a report.
+ state = "noSubmit";
+ } else if (!pluginDumpID) {
+ // This state tells the user that there is no crash report available.
+ state = "noReport";
+ } else {
+ // This state asks the user to submit a crash report.
+ state = "please";
+ }
+#endif
+
+ let mm = window.getGroupMessageManager("browsers");
+ mm.broadcastAsyncMessage("BrowserPlugins:NPAPIPluginProcessCrashed",
+ { pluginName, runID, state });
+ },
+
+ /**
+ * Shows a plugin-crashed notification bar for a browser that has had an
+ * invisiable NPAPI plugin crash, or a GMP plugin crash.
+ *
+ * @param browser
+ * The browser to show the notification for.
+ * @param messageString
+ * The string to put in the notification bar
+ * @param pluginID
+ * The unique-per-process identifier for the NPAPI plugin or GMP.
+ * For a GMP, this is the pluginID. For NPAPI plugins (where "pluginID"
+ * means something different), this is the runID.
+ */
+ showPluginCrashedNotification(browser, messageString, pluginID) {
+ // If there's already an existing notification bar, don't do anything.
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.getNotificationWithValue("plugin-crashed");
+ if (notification) {
+ return;
+ }
+
+ // Configure the notification bar
+ let priority = notificationBox.PRIORITY_WARNING_MEDIUM;
+ let iconURL = "chrome://mozapps/skin/plugins/notifyPluginCrashed.png";
+ let reloadLabel = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.label");
+ let reloadKey = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.accesskey");
+
+ let buttons = [{
+ label: reloadLabel,
+ accessKey: reloadKey,
+ popup: null,
+ callback() { browser.reload(); },
+ }];
+
+#ifdef MOZ_CRASHREPORTER
+ if (PluginCrashReporter.hasCrashReport(pluginID)) {
+ let submitLabel = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.label");
+ let submitKey = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.accesskey");
+ let submitButton = {
+ label: submitLabel,
+ accessKey: submitKey,
+ popup: null,
+ callback: () => {
+ PluginCrashReporter.submitCrashReport(pluginID);
+ },
+ };
+
+ buttons.push(submitButton);
+ }
+#endif
+
+ notification = notificationBox.appendNotification(messageString, "plugin-crashed",
+ iconURL, priority, buttons);
+
+ // Add the "learn more" link.
+ let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let link = notification.ownerDocument.createElementNS(XULNS, "label");
+ link.className = "text-link";
+ link.setAttribute("value", gNavigatorBundle.getString("crashedpluginsMessage.learnMore"));
+ let crashurl = formatURL("app.support.baseURL", true);
+ crashurl += "plugin-crashed-notificationbar";
+ link.href = crashurl;
+ let description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText");
+ description.appendChild(link);
+ },
+};
+
+gPluginHandler.init();
diff --git a/application/basilisk/base/content/browser-refreshblocker.js b/application/basilisk/base/content/browser-refreshblocker.js
new file mode 100644
index 000000000..2c6f2da97
--- /dev/null
+++ b/application/basilisk/base/content/browser-refreshblocker.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * If the user has opted into blocking refresh and redirect attempts by
+ * default, this handles showing the notification to the user which
+ * gives them the option to let the refresh or redirect proceed.
+ */
+var RefreshBlocker = {
+ init() {
+ gBrowser.addEventListener("RefreshBlocked", this);
+ },
+
+ uninit() {
+ gBrowser.removeEventListener("RefreshBlocked", this);
+ },
+
+ handleEvent(event) {
+ if (event.type == "RefreshBlocked") {
+ this.block(event.originalTarget, event.detail);
+ }
+ },
+
+ /**
+ * Shows the blocked refresh / redirect notification for some browser.
+ *
+ * @param browser ()
+ * The browser that had the refresh blocked. This will be the browser
+ * for which we'll show the notification on.
+ * @param data (object)
+ * An object with the following properties:
+ *
+ * URI (string)
+ * The URI that a page is attempting to refresh or redirect to.
+ *
+ * delay (int)
+ * The delay (in milliseconds) before the page was going to reload
+ * or redirect.
+ *
+ * sameURI (bool)
+ * true if we're refreshing the page. false if we're redirecting.
+ *
+ * outerWindowID (int)
+ * The outerWindowID of the frame that requested the refresh or
+ * redirect.
+ */
+ block(browser, data) {
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+ let message =
+ gNavigatorBundle.getFormattedString(data.sameURI ? "refreshBlocked.refreshLabel"
+ : "refreshBlocked.redirectLabel",
+ [brandShortName]);
+
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.getNotificationWithValue("refresh-blocked");
+
+ if (notification) {
+ notification.label = message;
+ } else {
+ let refreshButtonText =
+ gNavigatorBundle.getString("refreshBlocked.goButton");
+ let refreshButtonAccesskey =
+ gNavigatorBundle.getString("refreshBlocked.goButton.accesskey");
+
+ let buttons = [{
+ label: refreshButtonText,
+ accessKey: refreshButtonAccesskey,
+ callback() {
+ if (browser.messageManager) {
+ browser.messageManager.sendAsyncMessage("RefreshBlocker:Refresh", data);
+ }
+ }
+ }];
+
+ notificationBox.appendNotification(message, "refresh-blocked",
+ "chrome://browser/skin/Info.png",
+ notificationBox.PRIORITY_INFO_MEDIUM,
+ buttons);
+ }
+ }
+};
diff --git a/application/basilisk/base/content/browser-safebrowsing.js b/application/basilisk/base/content/browser-safebrowsing.js
new file mode 100644
index 000000000..b8b5976e3
--- /dev/null
+++ b/application/basilisk/base/content/browser-safebrowsing.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gSafeBrowsing = {
+
+ setReportPhishingMenu() {
+ // In order to detect whether or not we're at the phishing warning
+ // page, we have to check the documentURI instead of the currentURI.
+ // This is because when the DocShell loads an error page, the
+ // currentURI stays at the original target, while the documentURI
+ // will point to the internal error page we loaded instead.
+ var docURI = gBrowser.selectedBrowser.documentURI;
+ var isPhishingPage =
+ docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked");
+
+ // Show/hide the appropriate menu item.
+ document.getElementById("menu_HelpPopup_reportPhishingtoolmenu")
+ .hidden = isPhishingPage;
+ document.getElementById("menu_HelpPopup_reportPhishingErrortoolmenu")
+ .hidden = !isPhishingPage;
+
+ var broadcasterId = isPhishingPage
+ ? "reportPhishingErrorBroadcaster"
+ : "reportPhishingBroadcaster";
+
+ var broadcaster = document.getElementById(broadcasterId);
+ if (!broadcaster)
+ return;
+
+ // Now look at the currentURI to learn which page we were trying
+ // to browse to.
+ let uri = gBrowser.currentURI;
+ if (uri && (uri.schemeIs("http") || uri.schemeIs("https")))
+ broadcaster.removeAttribute("disabled");
+ else
+ broadcaster.setAttribute("disabled", true);
+ },
+
+ /**
+ * Used to report a phishing page or a false positive
+ * @param name String One of "Phish", "Error", "Malware" or "MalwareError"
+ * @return String the report phishing URL.
+ */
+ getReportURL(name) {
+ return SafeBrowsing.getReportURL(name, gBrowser.currentURI);
+ }
+}
diff --git a/application/basilisk/base/content/browser-sets.inc b/application/basilisk/base/content/browser-sets.inc
new file mode 100644
index 000000000..7c066f410
--- /dev/null
+++ b/application/basilisk/base/content/browser-sets.inc
@@ -0,0 +1,382 @@
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifdef XP_UNIX
+#ifndef XP_MACOSX
+#define XP_GNOME 1
+#endif
+#endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifdef XP_MACOSX
+
+#endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifdef E10S_TESTING_ONLY
+
+#endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifndef XP_MACOSX
+
+#endif
+
+#
+# Search Command Key Logic works like this:
+#
+# Unix: Ctrl+K (cross platform binding)
+# Ctrl+J (in case of emacs Ctrl-K conflict)
+# Mac: Cmd+K (cross platform binding)
+# Cmd+Opt+F (platform convention)
+# Win: Ctrl+K (cross platform binding)
+# Ctrl+E (IE compat)
+#
+# We support Ctrl+K on all platforms now and advertise it in the menu since it is
+# our standard - it is a "safe" choice since it is near no harmful keys like "W" as
+# "E" is. People mourning the loss of Ctrl+K for emacs compat can switch their GTK
+# system setting to use emacs emulation, and we should respect it. Focus-Search-Box
+# is a fundamental keybinding and we are maintaining a XP binding so that it is easy
+# for people to switch to Linux.
+#
+
+#ifdef XP_MACOSX
+
+#endif
+#ifdef XP_WIN
+
+#endif
+#ifdef XP_GNOME
+
+
+#else
+
+#endif
+
+
+
+
+
+
+
+
+#ifdef XP_UNIX
+
+#else
+
+#endif
+
+
+
+
+
+
+
+
+#ifndef XP_MACOSX
+
+
+#else
+
+
+#endif
+#ifdef XP_UNIX
+
+
+#endif
+
+
+#ifndef XP_MACOSX
+
+
+
+#else
+
+
+
+#endif
+
+
+
+
+#ifndef XP_WIN
+
+#endif
+
+
+
+#ifdef XP_MACOSX
+
+#endif
+
+
+
+
+# Accel+Shift+A-F are reserved on GTK
+#ifndef MOZ_WIDGET_GTK
+
+
+#else
+
+#endif
+
+#ifdef XP_WIN
+# Cmd+I is conventially mapped to Info on MacOS X, thus it should not be
+# overridden for other purposes there.
+
+#endif
+
+
+
+#ifdef XP_MACOSX
+
+#endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifdef XP_MACOSX
+
+
+#elifdef XP_UNIX
+
+#endif
+
+#ifdef FULL_BROWSER_WINDOW
+
+#endif
+
+
+#ifdef XP_GNOME
+#define NUM_SELECT_TAB_MODIFIER alt
+#else
+#define NUM_SELECT_TAB_MODIFIER accel
+#endif
+
+#expand
+#expand
+#expand
+#expand
+#expand
+#expand
+#expand
+#expand
+#expand
+
+
+
+# Used by baseMenuOverlay
+#ifdef XP_MACOSX
+
+#endif
+
diff --git a/application/basilisk/base/content/browser-sidebar.js b/application/basilisk/base/content/browser-sidebar.js
new file mode 100644
index 000000000..76128ecd8
--- /dev/null
+++ b/application/basilisk/base/content/browser-sidebar.js
@@ -0,0 +1,337 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * SidebarUI controls showing and hiding the browser sidebar.
+ *
+ * @note
+ * Some of these methods take a commandID argument - we expect to find a
+ * xul:broadcaster element with the specified ID.
+ * The following attributes on that element may be used and/or modified:
+ * - id (required) the string to match commandID. The convention
+ * is to use this naming scheme: 'viewSidebar'.
+ * - sidebarurl (required) specifies the URL to load in this sidebar.
+ * - sidebartitle or label (in that order) specify the title to
+ * display on the sidebar.
+ * - checked indicates whether the sidebar is currently displayed.
+ * Note that toggleSidebar updates this attribute when
+ * it changes the sidebar's visibility.
+ * - group this attribute must be set to "sidebar".
+ */
+var SidebarUI = {
+ browser: null,
+
+ _box: null,
+ _title: null,
+ _splitter: null,
+
+ init() {
+ this._box = document.getElementById("sidebar-box");
+ this.browser = document.getElementById("sidebar");
+ this._title = document.getElementById("sidebar-title");
+ this._splitter = document.getElementById("sidebar-splitter");
+
+ if (!this.adoptFromWindow(window.opener)) {
+ let commandID = this._box.getAttribute("sidebarcommand");
+ if (commandID) {
+ let command = document.getElementById(commandID);
+ if (command) {
+ this._delayedLoad = true;
+ this._box.hidden = false;
+ this._splitter.hidden = false;
+ command.setAttribute("checked", "true");
+ } else {
+ // Remove the |sidebarcommand| attribute, because the element it
+ // refers to no longer exists, so we should assume this sidebar
+ // panel has been uninstalled. (249883)
+ this._box.removeAttribute("sidebarcommand");
+ }
+ }
+ }
+ },
+
+ uninit() {
+ let enumerator = Services.wm.getEnumerator(null);
+ enumerator.getNext();
+ if (!enumerator.hasMoreElements()) {
+ document.persist("sidebar-box", "sidebarcommand");
+ document.persist("sidebar-box", "width");
+ document.persist("sidebar-box", "src");
+ document.persist("sidebar-title", "value");
+ }
+ },
+
+ /**
+ * Try and adopt the status of the sidebar from another window.
+ * @param {Window} sourceWindow - Window to use as a source for sidebar status.
+ * @return true if we adopted the state, or false if the caller should
+ * initialize the state itself.
+ */
+ adoptFromWindow(sourceWindow) {
+ // No source window, or it being closed, or not chrome, or in a different
+ // private-browsing context means we can't adopt.
+ if (!sourceWindow || sourceWindow.closed ||
+ !sourceWindow.document.documentURIObject.schemeIs("chrome") ||
+ PrivateBrowsingUtils.isWindowPrivate(window) != PrivateBrowsingUtils.isWindowPrivate(sourceWindow)) {
+ return false;
+ }
+
+ // If the opener had a sidebar, open the same sidebar in our window.
+ // The opener can be the hidden window too, if we're coming from the state
+ // where no windows are open, and the hidden window has no sidebar box.
+ let sourceUI = sourceWindow.SidebarUI;
+ if (!sourceUI || !sourceUI._box) {
+ // no source UI or no _box means we also can't adopt the state.
+ return false;
+ }
+ if (sourceUI._box.hidden) {
+ // just hidden means we have adopted the hidden state.
+ return true;
+ }
+
+ let commandID = sourceUI._box.getAttribute("sidebarcommand");
+ let commandElem = document.getElementById(commandID);
+
+ // dynamically generated sidebars will fail this check, but we still
+ // consider it adopted.
+ if (!commandElem) {
+ return true;
+ }
+
+ this._title.setAttribute("value",
+ sourceUI._title.getAttribute("value"));
+ this._box.setAttribute("width", sourceUI._box.boxObject.width);
+
+ this._box.setAttribute("sidebarcommand", commandID);
+ // Note: we're setting 'src' on this._box, which is a , not on
+ // the