From 24d9b9243c691520e5f8f019e0dcdce13996f66f Mon Sep 17 00:00:00 2001 From: pbz Date: Thu, 29 Oct 2020 13:43:54 +0000 Subject: [PATCH] Bug 1565574 - Added protocol handler permission dialog and updated app chooser dialog. r=Gijs,fluent-reviewers - Added a new permission dialog shown when the caller does not have permission to open a protocol - Updated the appChooser dialog for the new UX - Updated and moved l10n strings to fluent (fluent migration in the following patch) Differential Revision: https://phabricator.services.mozilla.com/D94149 --- .../chrome/mozapps/handling/handling.dtd | 10 - .../mozapps/handling/handling.properties | 15 -- .../en-US/toolkit/global/handlerDialog.ftl | 73 ++++- toolkit/locales/jar.mn | 2 - .../content/{dialog.js => appChooser.js} | 251 ++++++++---------- .../mozapps/handling/content/appChooser.xhtml | 50 ++++ toolkit/mozapps/handling/content/dialog.xhtml | 65 ----- toolkit/mozapps/handling/content/handler.css | 24 ++ .../handling/content/permissionDialog.js | 183 +++++++++++++ .../handling/content/permissionDialog.xhtml | 41 +++ toolkit/mozapps/handling/jar.mn | 8 +- .../themes/osx/mozapps/handling/handling.css | 5 - .../windows/mozapps/handling/handling.css | 5 - 13 files changed, 479 insertions(+), 253 deletions(-) delete mode 100644 toolkit/locales/en-US/chrome/mozapps/handling/handling.dtd delete mode 100644 toolkit/locales/en-US/chrome/mozapps/handling/handling.properties rename toolkit/mozapps/handling/content/{dialog.js => appChooser.js} (61%) create mode 100644 toolkit/mozapps/handling/content/appChooser.xhtml delete mode 100644 toolkit/mozapps/handling/content/dialog.xhtml create mode 100644 toolkit/mozapps/handling/content/permissionDialog.js create mode 100644 toolkit/mozapps/handling/content/permissionDialog.xhtml diff --git a/toolkit/locales/en-US/chrome/mozapps/handling/handling.dtd b/toolkit/locales/en-US/chrome/mozapps/handling/handling.dtd deleted file mode 100644 index fe7a52566c96..000000000000 --- a/toolkit/locales/en-US/chrome/mozapps/handling/handling.dtd +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/toolkit/locales/en-US/chrome/mozapps/handling/handling.properties b/toolkit/locales/en-US/chrome/mozapps/handling/handling.properties deleted file mode 100644 index 462875bcecbd..000000000000 --- a/toolkit/locales/en-US/chrome/mozapps/handling/handling.properties +++ /dev/null @@ -1,15 +0,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/. - -protocol.title=Launch Application -protocol.description=This link needs to be opened with an application. -protocol.choices.label=Send to: -protocol.checkbox.label=Remember my choice for %S links. -protocol.checkbox.accesskey=R -protocol.checkbox.extra=This can be changed in %S’s preferences. - -# Displayed under the name of a protocol handler in the Launch Application dialog. -privatebrowsing.disabled.label=Disabled in Private Windows - -choose.application.title=Another Application… diff --git a/toolkit/locales/en-US/toolkit/global/handlerDialog.ftl b/toolkit/locales/en-US/toolkit/global/handlerDialog.ftl index 1767ecba8224..f1c01b4f3ca4 100644 --- a/toolkit/locales/en-US/toolkit/global/handlerDialog.ftl +++ b/toolkit/locales/en-US/toolkit/global/handlerDialog.ftl @@ -2,12 +2,73 @@ # 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/. +## Permission Dialog +## Variables: +## $host - the hostname that is initiating the request +## $scheme - the type of link that's being opened. +## $appName - Name of the application that will be opened. + +permission-dialog-description = + Allow this site to open the { $scheme } link? + +permission-dialog-description-host = + Allow { $host } to open the { $scheme } link? + +permission-dialog-description-app = + Allow this site to open the { $scheme } link with { $appName }? + +permission-dialog-description-host-app = + Allow { $host } to open the { $scheme } link with { $appName }? + + # Please keep the emphasis around the hostname and scheme (ie the # `` HTML tags). Please also keep the hostname as close to the start # of the sentence as your language's grammar allows. -# -# Variables: -# $host - the hostname that is initiating the request -# $scheme - the type of link that's being opened. -handler-dialog-host = - { $host } wants to open a { $scheme } link. +permission-dialog-remember = + Always allow { $host } to open { $scheme } links + +permission-dialog-btn-open-link = + .label = Open Link + .accessKey = O + +permission-dialog-btn-choose-app = + .label = Choose Application + .accessKey = A + +permission-dialog-unset-description = You’ll need to choose an application. + +permission-dialog-set-change-app-link = Choose a different application. + + +## Chooser dialog +## Variables: +## $scheme - the type of link that's being opened. + +chooser-window = + .title = Choose Application + .style = min-width: 26em; min-height: 26em; + +chooser-dialog = + .buttonlabelaccept = Open Link + .buttonaccesskeyaccept = O + +chooser-dialog-description = Choose an application to open the { $scheme } link. + +# Please keep the emphasis around the scheme (ie the `` HTML tags). +chooser-dialog-remember = + Always use this application to open { $scheme } links + +chooser-dialog-remember-extra = { + PLATFORM() -> + [windows] This can be changed in { -brand-short-name }’s options. + *[other] This can be changed in { -brand-short-name }’s preferences. + } + +choose-other-app-description = Choose other Application +choose-app-btn = + .label = Choose… + .accessKey = C +choose-other-app-window-title = Another Application… + +# Displayed under the name of a protocol handler in the Launch Application dialog. +choose-dialog-privatebrowsing-disabled = Disabled in Private Windows diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn index e0136f5833b9..c881c12a0790 100644 --- a/toolkit/locales/jar.mn +++ b/toolkit/locales/jar.mn @@ -71,8 +71,6 @@ #ifndef MOZ_FENNEC locale/@AB_CD@/mozapps/extensions/extensions.properties (%chrome/mozapps/extensions/extensions.properties) #endif - locale/@AB_CD@/mozapps/handling/handling.dtd (%chrome/mozapps/handling/handling.dtd) - locale/@AB_CD@/mozapps/handling/handling.properties (%chrome/mozapps/handling/handling.properties) locale/@AB_CD@/mozapps/profile/profileSelection.properties (%chrome/mozapps/profile/profileSelection.properties) #ifndef MOZ_FENNEC locale/@AB_CD@/mozapps/update/updates.properties (%chrome/mozapps/update/updates.properties) diff --git a/toolkit/mozapps/handling/content/dialog.js b/toolkit/mozapps/handling/content/appChooser.js similarity index 61% rename from toolkit/mozapps/handling/content/dialog.js rename to toolkit/mozapps/handling/content/appChooser.js index 264438889867..afb2de27b8e9 100644 --- a/toolkit/mozapps/handling/content/dialog.js +++ b/toolkit/mozapps/handling/content/appChooser.js @@ -2,41 +2,13 @@ * 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/. */ -/** - * This dialog builds its content based on arguments passed into it. - * window.arguments[0]: - * The title of the dialog. - * window.arguments[1]: - * The url of the image that appears to the left of the description text - * window.arguments[2]: - * The text of the description that will appear above the choices the user - * can choose from. - * window.arguments[3]: - * The text of the label directly above the choices the user can choose from. - * window.arguments[4]: - * This is the text to be placed in the label for the checkbox. If no text is - * passed (ie, it's an empty string), the checkbox will be hidden. - * window.arguments[5]: - * The accesskey for the checkbox - * window.arguments[6]: - * This is the text that is displayed below the checkbox when it is checked. - * window.arguments[7]: - * This is the nsIHandlerInfo that gives us all our precious information. - * window.arguments[8]: - * This is the nsIURI that we are being brought up for in the first place. - * window.arguments[9]: - * This is the nsIPrincipal that has triggered the dialog; may be null. - * window.arguments[10]: - * The browsingContext from which the request originates; may be null. - */ - -const { EnableDelayHelper } = ChromeUtils.import( - "resource://gre/modules/SharedPromptUtils.jsm" -); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { PrivateBrowsingUtils } = ChromeUtils.import( "resource://gre/modules/PrivateBrowsingUtils.jsm" ); +const { EnableDelayHelper } = ChromeUtils.import( + "resource://gre/modules/SharedPromptUtils.jsm" +); class MozHandler extends window.MozElements.MozRichlistitem { static get markup() { @@ -82,97 +54,62 @@ let loadPromise = new Promise(resolve => { window.addEventListener("load", resolve, { once: true }); }); -var dialog = { - // Member Variables - - _handlerInfo: null, - _URI: null, - _itemChoose: null, - _okButton: null, - _browsingContext: null, - _buttonDisabled: true, - - // Methods - +let dialog = { /** * This function initializes the content of the dialog. */ initialize: function initialize() { - this._handlerInfo = window.arguments[7].QueryInterface(Ci.nsIHandlerInfo); - this._URI = window.arguments[8].QueryInterface(Ci.nsIURI); - let principal = window.arguments[9]?.QueryInterface(Ci.nsIPrincipal); - this._browsingContext = window.arguments[10]; - let usePrivateBrowsing = false; - if (this._browsingContext) { - usePrivateBrowsing = this._browsingContext.usePrivateBrowsing; - } + let args = window.arguments[0].wrappedJSObject || window.arguments[0]; + let { handler, outArgs, usePrivateBrowsing, enableButtonDelay } = args; + + this._handlerInfo = handler.QueryInterface(Ci.nsIHandlerInfo); + this._outArgs = outArgs; this.isPrivate = usePrivateBrowsing || (window.opener && PrivateBrowsingUtils.isWindowPrivate(window.opener)); + this._dialog = document.querySelector("dialog"); this._itemChoose = document.getElementById("item-choose"); - this._okButton = document.getElementById("handling").getButton("extra1"); + this._rememberCheck = document.getElementById("remember"); - var description = { - image: document.getElementById("description-image"), - text: document.getElementById("description-text"), - }; - var options = document.getElementById("item-action-text"); - var checkbox = { - desc: document.getElementById("remember"), - text: document.getElementById("remember-text"), - }; + let rememberLabel = document.getElementById("remember-label"); + document.l10n.setAttributes(rememberLabel, "chooser-dialog-remember", { + scheme: this._handlerInfo.type, + }); - // Setting values - document.title = window.arguments[0]; - description.image.src = window.arguments[1]; - description.text.textContent = window.arguments[2]; - options.value = window.arguments[3]; - checkbox.desc.label = window.arguments[4]; - checkbox.desc.accessKey = window.arguments[5]; - checkbox.text.textContent = window.arguments[6]; + // Register event listener for the checkbox hint. + this._rememberCheck.addEventListener("change", () => this.onCheck()); - if (principal && principal.isContentPrincipal) { - let hostContainer = document.getElementById("originating-host"); - document.l10n.pauseObserving(); - document.l10n.setAttributes(hostContainer, "handler-dialog-host", { - host: principal.exposablePrePath, - scheme: this._URI.scheme, - }); - document.mozSubdialogReady = document.l10n - .translateElements([hostContainer]) - .then(() => window.sizeToContent()); - document.l10n.resumeObserving(); - hostContainer.parentNode.removeAttribute("hidden"); - } + let description = document.getElementById("description"); + document.l10n.setAttributes(description, "chooser-dialog-description", { + scheme: this._handlerInfo.type, + }); - // Hide stuff that needs to be hidden - if (!checkbox.desc.label) { - checkbox.desc.hidden = true; - } + document.addEventListener("dialogaccept", () => { + this.onAccept(); + }); // UI is ready, lets populate our list this.populateList(); - // Explicitly not an 'accept' button to avoid having `enter` accept the dialog. - document.addEventListener("dialogextra1", () => { - this.onOK(); - }); - document.addEventListener("dialogaccept", e => { - e.preventDefault(); + + document.mozSubdialogReady = document.l10n.ready.then(() => { + window.sizeToContent(); }); - this._delayHelper = new EnableDelayHelper({ - disableDialog: () => { - this._buttonDisabled = true; - this.updateOKButton(); - }, - enableDialog: () => { - this._buttonDisabled = false; - this.updateOKButton(); - }, - focusTarget: window, - }); + if (enableButtonDelay) { + this._delayHelper = new EnableDelayHelper({ + disableDialog: () => { + this._acceptBtnDisabled = true; + this.updateAcceptButton(); + }, + enableDialog: () => { + this._acceptBtnDisabled = false; + this.updateAcceptButton(); + }, + focusTarget: window, + }); + } }, /** @@ -218,12 +155,10 @@ var dialog = { if (this.isPrivate) { let policy = WebExtensionPolicy.getByURI(uri); if (policy && !policy.privateBrowsingAllowed) { - var bundle = document.getElementById("base-strings"); - var disabledLabel = bundle.getString( - "privatebrowsing.disabled.label" - ); elm.setAttribute("disabled", true); - elm.setAttribute("description", disabledLabel); + this.getPrivateBrowsingDisabledLabel().then(label => { + elm.setAttribute("description", label); + }); if (app == preferredHandler) { preferredHandler = null; } @@ -262,7 +197,7 @@ var dialog = { let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService( Ci.nsIGIOService ); - var gioApps = gIOSvc.getAppsForURIScheme(this._URI.scheme); + var gioApps = gIOSvc.getAppsForURIScheme(this._handlerInfo.type); for (let handler of gioApps.enumerate(Ci.nsIHandlerApp)) { // OS handler share the same name, it's most likely the same app, skipping... if (handler.name == this._handlerInfo.defaultDescription) { @@ -295,9 +230,8 @@ var dialog = { /** * Brings up a filepicker and allows a user to choose an application. */ - chooseApplication: function chooseApplication() { - var bundle = document.getElementById("base-strings"); - var title = bundle.getString("choose.application.title"); + async chooseApplication() { + let title = await this.getChooseAppWindowTitle(); var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); fp.init(window, title, Ci.nsIFilePicker.modeOpen); @@ -342,43 +276,51 @@ var dialog = { /** * Function called when the OK button is pressed. */ - onOK: function onOK() { - if (this._buttonDisabled) { - return; - } - var checkbox = document.getElementById("remember"); - if (!checkbox.hidden) { - // We need to make sure that the default is properly set now - if (this.selectedItem.obj) { - // default OS handler doesn't have this property - this._handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; - this._handlerInfo.preferredApplicationHandler = this.selectedItem.obj; - } else { - this._handlerInfo.preferredAction = Ci.nsIHandlerInfo.useSystemDefault; - } - } - this._handlerInfo.alwaysAskBeforeHandling = !checkbox.checked; - - var hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService( - Ci.nsIHandlerService - ); - hs.store(this._handlerInfo); - - this._handlerInfo.launchWithURI(this._URI, this._browsingContext); - window.close(); + onAccept() { + this.updateHandlerData(this._rememberCheck.checked); + this._outArgs.setProperty("openHandler", true); }, /** - * Determines if the OK button should be disabled or not + * Determines if the accept button should be disabled or not */ - updateOKButton: function updateOKButton() { - this._okButton.disabled = this._itemChoose.selected || this._buttonDisabled; + updateAcceptButton() { + this._dialog.setAttribute( + "buttondisabledaccept", + this._acceptBtnDisabled || this._itemChoose.selected + ); + }, + + /** + * Update the handler info to reflect the user choice. + * @param {boolean} skipAsk - Whether we should persist the application + * choice and skip asking next time. + */ + updateHandlerData(skipAsk) { + // We need to make sure that the default is properly set now + if (this.selectedItem.obj) { + // default OS handler doesn't have this property + this._outArgs.setProperty( + "preferredAction", + Ci.nsIHandlerInfo.useHelperApp + ); + this._outArgs.setProperty( + "preferredApplicationHandler", + this.selectedItem.obj + ); + } else { + this._outArgs.setProperty( + "preferredAction", + Ci.nsIHandlerInfo.useSystemDefault + ); + } + this._outArgs.setProperty("alwaysAskBeforeHandling", !skipAsk); }, /** * Updates the UI based on the checkbox being checked or not. */ - onCheck: function onCheck() { + onCheck() { if (document.getElementById("remember").checked) { document.getElementById("remember-text").setAttribute("visible", "true"); } else { @@ -393,7 +335,7 @@ var dialog = { if (this.selectedItem == this._itemChoose) { this.chooseApplication(); } else { - this.onOK(); + this._dialog.acceptDialog(); } }, @@ -406,6 +348,31 @@ var dialog = { return document.getElementById("items").selectedItem; }, set selectedItem(aItem) { - return (document.getElementById("items").selectedItem = aItem); + document.getElementById("items").selectedItem = aItem; + }, + + /** + * Lazy l10n getter for the title of the app chooser window + */ + async getChooseAppWindowTitle() { + if (!this._chooseAppWindowTitle) { + this._chooseAppWindowTitle = await document.l10n.formatValues([ + "choose-other-app-window-title", + ]); + } + return this._chooseAppWindowTitle; + }, + + /** + * Lazy l10n getter for handler menu items which are disabled due to private + * browsing. + */ + async getPrivateBrowsingDisabledLabel() { + if (!this._privateBrowsingDisabledLabel) { + this._privateBrowsingDisabledLabel = await document.l10n.formatValues([ + "choose-dialog-privatebrowsing-disabled", + ]); + } + return this._privateBrowsingDisabledLabel; }, }; diff --git a/toolkit/mozapps/handling/content/appChooser.xhtml b/toolkit/mozapps/handling/content/appChooser.xhtml new file mode 100644 index 000000000000..c96362f1e4d1 --- /dev/null +++ b/toolkit/mozapps/handling/content/appChooser.xhtml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + +