diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index 8056e8b478a5..c391a4c93f31 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -230,6 +230,8 @@ pref("extensions.compatability.locales.buildid", "0"); /* Don't let XPIProvider install distribution add-ons; we do our own thing on mobile. */ pref("extensions.installDistroAddons", false); +pref("extensions.webextPermissionPrompts", true); + // Add-on content security policies. pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;"); pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';"); diff --git a/mobile/android/app/src/main/res/drawable/ic_extension.xml b/mobile/android/app/src/main/res/drawable/ic_extension.xml new file mode 100644 index 000000000000..ad192170b456 --- /dev/null +++ b/mobile/android/app/src/main/res/drawable/ic_extension.xml @@ -0,0 +1,4 @@ + + + diff --git a/mobile/android/app/src/main/res/layout/extension_permissions_dialog.xml b/mobile/android/app/src/main/res/layout/extension_permissions_dialog.xml new file mode 100644 index 000000000000..06949b88e3d0 --- /dev/null +++ b/mobile/android/app/src/main/res/layout/extension_permissions_dialog.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java index 857029896da9..7598b7a1cb20 100644 --- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java +++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java @@ -89,6 +89,7 @@ import org.mozilla.gecko.delegates.ScreenshotDelegate; import org.mozilla.gecko.distribution.Distribution; import org.mozilla.gecko.distribution.DistributionStoreCallback; import org.mozilla.gecko.dlc.DownloadContentService; +import org.mozilla.gecko.extensions.ExtensionPermissionsHelper; import org.mozilla.gecko.firstrun.FirstrunAnimationContainer; import org.mozilla.gecko.gfx.DynamicToolbarAnimator; import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason; @@ -315,6 +316,8 @@ public class BrowserApp extends GeckoApp private AccountsHelper mAccountsHelper; + private ExtensionPermissionsHelper mExtensionPermissionsHelper; + // The tab to be selected on editing mode exit. private Integer mTargetTabForEditingMode; @@ -818,6 +821,7 @@ public class BrowserApp extends GeckoApp mSharedPreferencesHelper = new SharedPreferencesHelper(appContext); mReadingListHelper = new ReadingListHelper(appContext, profile); mAccountsHelper = new AccountsHelper(appContext, profile); + mExtensionPermissionsHelper = new ExtensionPermissionsHelper(this); if (AppConstants.MOZ_ANDROID_BEAM) { NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this); @@ -1536,6 +1540,11 @@ public class BrowserApp extends GeckoApp mAccountsHelper = null; } + if (mExtensionPermissionsHelper != null) { + mExtensionPermissionsHelper.uninit(); + mExtensionPermissionsHelper = null; + } + mSearchEngineManager.unregisterListeners(); EventDispatcher.getInstance().unregisterGeckoThreadListener(this, diff --git a/mobile/android/base/java/org/mozilla/gecko/extensions/ExtensionPermissionsHelper.java b/mobile/android/base/java/org/mozilla/gecko/extensions/ExtensionPermissionsHelper.java new file mode 100644 index 000000000000..5fe483997e02 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/extensions/ExtensionPermissionsHelper.java @@ -0,0 +1,85 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.extensions; + +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.util.BundleEventListener; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.GeckoBundle; +import org.mozilla.gecko.util.ResourceDrawableUtils; +import org.mozilla.gecko.R; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +public class ExtensionPermissionsHelper implements BundleEventListener { + private final Context mContext; + + public ExtensionPermissionsHelper(Context context) { + mContext = context; + + EventDispatcher.getInstance().registerUiThreadListener(this, + "Extension:PermissionPrompt"); + } + + public void uninit() { + EventDispatcher.getInstance().unregisterUiThreadListener(this, + "Extension:PermissionPrompt"); + } + + @Override // BundleEventListener + public void handleMessage(final String event, final GeckoBundle message, + final EventCallback callback) { + if ("Extension:PermissionPrompt".equals(event)) { + final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + + final View view = LayoutInflater.from(mContext) + .inflate(R.layout.extension_permissions_dialog, null); + builder.setView(view); + + final TextView headerText = (TextView) view.findViewById(R.id.extension_permission_header); + headerText.setText(message.getString("header")); + + final TextView bodyText = (TextView) view.findViewById(R.id.extension_permission_body); + bodyText.setText(message.getString("message")); + + builder.setPositiveButton(message.getString("acceptText"), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + callback.sendSuccess(true); + } + }); + builder.setNegativeButton(message.getString("cancelText"), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + callback.sendSuccess(false); + } + }); + + final String iconUrl = message.getString("icon"); + if ("DEFAULT".equals(iconUrl)) { + headerText.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_extension, 0, 0, 0); + } else { + ResourceDrawableUtils.getDrawable(mContext, iconUrl, new ResourceDrawableUtils.BitmapLoader() { + @Override + public void onBitmapFound(final Drawable d) { + headerText.setCompoundDrawablesWithIntrinsicBounds(d, null, null, null); + } + }); + } + + final AlertDialog dialog = builder.create(); + dialog.show(); + } + } +} diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index dabe3328da17..9bc4ac74ea7f 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -628,6 +628,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [ 'DynamicToolbar.java', 'EditBookmarkDialog.java', 'Experiments.java', + 'extensions/ExtensionPermissionsHelper.java', 'FilePicker.java', 'FilePickerResultHandler.java', 'FindInPageBar.java', diff --git a/mobile/android/chrome/content/ExtensionPermissions.js b/mobile/android/chrome/content/ExtensionPermissions.js new file mode 100644 index 000000000000..6c358f2532f6 --- /dev/null +++ b/mobile/android/chrome/content/ExtensionPermissions.js @@ -0,0 +1,68 @@ +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData", + "resource://gre/modules/Extension.jsm"); + +var ExtensionPermissions = { + // Prepare the strings needed for a permission notification. + _prepareStrings(info) { + let appName = Strings.brand.GetStringFromName("brandShortName"); + let info2 = Object.assign({appName, addonName: info.addon.name}, info); + let strings = ExtensionData.formatPermissionStrings(info2, Strings.browser); + + // We dump the main body of the dialog into a big android + // TextView. Build a big string with the full contents here. + let message = ""; + if (strings.msgs.length > 0) { + message = [strings.listIntro, ...strings.msgs.map(s => `\u2022 ${s}`)].join("\n"); + } + + return { + header: strings.header || strings.text, + message, + acceptText: strings.acceptText, + cancelText: strings.cancelText, + }; + }, + + // Prepare an icon for a permission notification + _prepareIcon(iconURL) { + // We can render pngs with ResourceDrawableUtils + if (iconURL.endsWith(".png")) { + return iconURL; + } + + // If we can't render an icon, show the default + return "DEFAULT"; + }, + + async observe(subject, topic, data) { + switch (topic) { + case "webextension-permission-prompt": { + let {target, info} = subject.wrappedJSObject; + + let details = this._prepareStrings(info); + details.icon = this._prepareIcon(info.icon); + details.type = "Extension:PermissionPrompt"; + let accepted = await EventDispatcher.instance.sendRequestForResult(details); + + if (accepted) { + info.resolve(); + } else { + info.reject(); + } + break; + } + + case "webextension-update-permissions": + // To be implemented in bug 1391579, just auto-approve until then + subject.wrappedJSObject.resolve(); + break; + + case "webextension-optional-permission-prompt": + // To be implemented in bug 1392176, just auto-approve until then + subject.wrappedJSObject.resolve(true); + break; + } + }, +}; diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 286d7b2f4b1e..d1d6906e2a07 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -151,6 +151,10 @@ lazilyLoadedBrowserScripts.forEach(function (aScript) { var lazilyLoadedObserverScripts = [ ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"], ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"], + ["ExtensionPermissions", ["webextension-permission-prompt", + "webextension-update-permissions", + "webextension-optional-permission-prompt"], + "chrome://browser/content/ExtensionPermissions.js"], ]; lazilyLoadedObserverScripts.forEach(function (aScript) { diff --git a/mobile/android/chrome/jar.mn b/mobile/android/chrome/jar.mn index 791f0ec94992..5a433ab8dccb 100644 --- a/mobile/android/chrome/jar.mn +++ b/mobile/android/chrome/jar.mn @@ -60,6 +60,7 @@ chrome.jar: #ifndef RELEASE_OR_BETA content/WebcompatReporter.js (content/WebcompatReporter.js) #endif + content/ExtensionPermissions.js (content/ExtensionPermissions.js) % content branding %content/branding/ diff --git a/mobile/android/locales/en-US/chrome/browser.properties b/mobile/android/locales/en-US/chrome/browser.properties index cb4ca4c0af1d..2a0b2abf56fa 100644 --- a/mobile/android/locales/en-US/chrome/browser.properties +++ b/mobile/android/locales/en-US/chrome/browser.properties @@ -109,6 +109,69 @@ xpinstallDisabledMessageLocked=Software installation has been disabled by your s xpinstallDisabledMessage2=Software installation is currently disabled. Press Enable and try again. xpinstallDisabledButton=Enable +# LOCALIZATION NOTE (webextPerms.header) +# This string is used as a header in the webextension permissions dialog, +# %S is replaced with the localized name of the extension being installed. +# See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612 +# for an example of the full dialog. +# Note, this string will be used as raw markup. Avoid characters like <, >, & +webextPerms.header=Add %S? + +# LOCALIZATION NOTE (webextPerms.listIntro) +# This string will be followed by a list of permissions requested +# by the webextension. +webextPerms.listIntro=It requires your permission to: +webextPerms.add.label=Add +webextPerms.add.accessKey=A +webextPerms.cancel.label=Cancel +webextPerms.cancel.accessKey=C + +webextPerms.description.bookmarks=Read and modify bookmarks +webextPerms.description.browserSettings=Read and modify browser settings +webextPerms.description.clipboardRead=Get data from the clipboard +webextPerms.description.clipboardWrite=Input data to the clipboard +webextPerms.description.downloads=Download files and read and modify the browser’s download history +webextPerms.description.geolocation=Access your location +webextPerms.description.history=Access browsing history +webextPerms.description.management=Monitor extension usage and manage themes +# LOCALIZATION NOTE (webextPerms.description.nativeMessaging) +# %S will be replaced with the name of the application +webextPerms.description.nativeMessaging=Exchange messages with programs other than %S +webextPerms.description.notifications=Display notifications to you +webextPerms.description.privacy=Read and modify privacy settings +webextPerms.description.sessions=Access recently closed tabs +webextPerms.description.tabs=Access browser tabs +webextPerms.description.topSites=Access browsing history +webextPerms.description.unlimitedStorage=Store unlimited amount of client-side data +webextPerms.description.webNavigation=Access browser activity during navigation + +webextPerms.hostDescription.allUrls=Access your data for all websites + +# LOCALIZATION NOTE (webextPerms.hostDescription.wildcard) +# %S will be replaced by the DNS domain for which a webextension +# is requesting access (e.g., mozilla.org) +webextPerms.hostDescription.wildcard=Access your data for sites in the %S domain + +# LOCALIZATION NOTE (webextPerms.hostDescription.tooManyWildcards): +# Semi-colon list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# #1 will be replaced by an integer indicating the number of additional +# domains for which this webextension is requesting permission. +webextPerms.hostDescription.tooManyWildcards=Access your data in #1 other domain;Access your data in #1 other domains + +# LOCALIZATION NOTE (webextPerms.hostDescription.oneSite) +# %S will be replaced by the DNS host name for which a webextension +# is requesting access (e.g., www.mozilla.org) +webextPerms.hostDescription.oneSite=Access your data for %S + +# LOCALIZATION NOTE (webextPerms.hostDescription.tooManySites) +# Semi-colon list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# #1 will be replaced by an integer indicating the number of additional +# hosts for which this webextension is requesting permission. +webextPerms.hostDescription.tooManySites=Access your data on #1 other site;Access your data on #1 other sites + + # Site Identity identity.identified.verifier=Verified by: %S identity.identified.verified_by_you=You have added a security exception for this site