From: uazo Date: Fri, 8 Apr 2022 11:04:04 +0000 Subject: Add lifetime options for permissions Indicate the session mode for content-settings by using the constraint `content_settings::SessionModel` as UserSession when setting the value, and also make use of an expiration time value. This is used in Chromium for `ClientHints` but it is generally possible to use this functionality when a specific value needs to be persisted by origin. All content settings of this type are not saved on disk (except for the `Forever` option), allowing user to reset the status each time application is restarted. There are 4 main areas affected to introduce the functionality: * components/content_settings A new `content_settings::LifetimeMode` enum value is defined to specify the user's choice (Always, OnlyThisTime, UntilOriginClosed, UntilBrowserClosed). Enumeration is also generated for java by adding it in `content_settings_enums_javagen` (gn). This is mainly used in `content_settings_utils.cc` to create a specialised `content_settings::ContentSettingConstraints` that is then used in `SetContentSettingDefaultScope()` by `PermissionContextBase::UpdateContentSetting`. Existing Chromium data structures do not provide a specific property to define a choice which is instead encoded through the `ContentSettingConstraints`; this approach is already used in other parts of the Chromium codebase so it is not novel here. Therefore, `content_settings::GetConstraintSessionExpiration()` and `content_settings::IsConstraintSessionExpiration()` manage the lifetime modes of the session content-settings. The modification also adds the session pattern to the ContentSettingPatternSource so that it is available for the UI. * components/permissions Lifetime support is added to the permissions; most of the changes are caused by the fact that it is necessary to report the value selected by the user from the Java UI managed by `components/browser_ui` up to `PermissionContextBase::UpdateContentSetting()`, without necessarily having to modify all requests that are not related to geolocation/camera/microphone. The approach used is a new `PermissionRequest::PermissionDecidedCallbackWithLifetime` used by an overload of `PermissionContextBase::CreatePermissionRequest` so that options are present only for the specific content-settings (see `PermissionDialogModel.java`). For other permissions no behaviour is changed (see `PermissionDialogDelegate::Accept`); for geolocation it was necessary to act directly in the specific context, because, unlike microphone/camera, the content-setting value is inserted in its specific method (`FinishNotifyPermissionSet`, that calls the callback), even if the class always derives from `PermissionContextBase`. * components/page_info Some changes needed to see in the summary of the `page_info` the text "(only this session)" (aka `page_info_android_permission_session_permission`) through adding a new property "is_user_session" in `PageInfoPermissionEntry` (Java). * components/browser_ui Changes to the Settings UI to show "(only this session)" in the specific content-setting. The same view is used both in the settings and in the page_info. For the management of `UntilOriginClosed` the logic used by flag `kOneTimeGeolocationPermission` was used; this flag is active only in the desktop (files `last_tab_standing_tracker_*`). It is a class that manages a list of the active origins and allows to perform operations when all the tabs relating to that origin have been closed, in this case deleting the session content settings of `UntilOriginClosed`. See also: https://github.com/bromite/bromite/issues/1549 Original License: GPL-2.0-or-later - https://spdx.org/licenses/GPL-2.0-or-later.html License: GPL-3.0-only - https://spdx.org/licenses/GPL-3.0-only.html --- .../permissions/last_tab_standing_tracker.cc | 33 ++++++++ .../one_time_permissions_tracker.cc | 34 +++++++- .../one_time_permissions_tracker.h | 5 +- .../one_time_permissions_tracker_factory.cc | 2 +- .../views/permissions/chip/chip_controller.cc | 2 +- ...exclusive_access_permission_prompt_view.cc | 2 +- .../permission_prompt_bubble_base_view.cc | 2 +- .../site_settings/SingleWebsiteSettings.java | 9 ++ .../android/website_preference_bridge.cc | 2 +- .../strings/android/browser_ui_strings.grd | 5 ++ .../core/browser/content_settings_utils.cc | 44 ++++++++-- .../core/browser/content_settings_utils.h | 6 ++ .../core/common/content_settings_enums.mojom | 9 ++ .../page_info/PageInfoController.java | 4 +- .../PermissionParamsListBuilder.java | 12 ++- .../android/page_info_controller_android.cc | 10 ++- components/page_info/page_info.cc | 2 + components/page_info/page_info.h | 1 + .../permissions/PermissionDialogDelegate.java | 13 +++ .../PermissionDialogModelFactory.java | 83 ++++++++++++++++++- .../embedded_permission_prompt_android.cc | 2 +- .../embedded_permission_prompt_android.h | 2 +- .../permission_dialog_delegate.cc | 24 +++++- .../permission_dialog_delegate.h | 1 + .../permission_prompt_android.cc | 10 ++- .../permission_prompt_android.h | 3 +- .../android/permissions_android_strings.grd | 17 ++++ .../geolocation_permission_context_android.cc | 25 ++++-- .../geolocation_permission_context_android.h | 7 ++ .../embedded_permission_prompt_flow_model.cc | 2 +- .../permissions/permission_context_base.cc | 58 +++++++++++-- .../permissions/permission_context_base.h | 25 +++++- components/permissions/permission_prompt.h | 3 +- components/permissions/permission_request.cc | 43 +++++++++- components/permissions/permission_request.h | 17 +++- .../permissions/permission_request_manager.cc | 50 ++++++----- .../permissions/permission_request_manager.h | 10 ++- 37 files changed, 510 insertions(+), 69 deletions(-) diff --git a/chrome/browser/permissions/last_tab_standing_tracker.cc b/chrome/browser/permissions/last_tab_standing_tracker.cc --- a/chrome/browser/permissions/last_tab_standing_tracker.cc +++ b/chrome/browser/permissions/last_tab_standing_tracker.cc @@ -7,6 +7,32 @@ #include "base/observer_list.h" #include "url/gurl.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings_utils.h" +#include "components/permissions/permissions_client.h" + +namespace { + // Remove all sessions content setting by origin and type + void RemoveSessionSettings(HostContentSettingsMap* content_settings, + const url::Origin& origin, + ContentSettingsType type) { + ContentSettingsForOneType session_settings = + content_settings->GetSettingsForOneType( + type, content_settings::mojom::SessionModel::USER_SESSION); + + GURL url = origin.GetURL(); + for (ContentSettingPatternSource& entry : session_settings) { + if (content_settings::IsConstraintSessionExpiration(entry, + content_settings::mojom::LifetimeMode::UNTIL_ORIGIN_CLOSED) && + entry.primary_pattern.Matches(url)) { + content_settings->SetWebsiteSettingCustomScope( + entry.primary_pattern, entry.secondary_pattern, + type, base::Value()); + } + } + } +} + LastTabStandingTracker::LastTabStandingTracker(content::BrowserContext* context) : context_(context) {} @@ -56,4 +82,11 @@ void LastTabStandingTracker::WebContentsUnloadedOrigin( for (auto& observer : observer_list_) { observer.OnLastPageFromOriginClosed(origin); } + HostContentSettingsMap* content_settings = + permissions::PermissionsClient::Get()->GetSettingsMap(context_); + RemoveSessionSettings(content_settings, origin, ContentSettingsType::GEOLOCATION); + RemoveSessionSettings(content_settings, origin, ContentSettingsType::GEOLOCATION_WITH_OPTIONS); + RemoveSessionSettings(content_settings, origin, ContentSettingsType::MEDIASTREAM_MIC); + RemoveSessionSettings(content_settings, origin, ContentSettingsType::MEDIASTREAM_CAMERA); + } } diff --git a/chrome/browser/permissions/one_time_permissions_tracker.cc b/chrome/browser/permissions/one_time_permissions_tracker.cc --- a/chrome/browser/permissions/one_time_permissions_tracker.cc +++ b/chrome/browser/permissions/one_time_permissions_tracker.cc @@ -20,8 +20,34 @@ #include "components/permissions/permission_util.h" #include "content/public/browser/visibility.h" #include "url/gurl.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings_utils.h" +#include "components/permissions/permissions_client.h" + +namespace { + // Remove all sessions content setting by origin and type + void RemoveSessionSettings(HostContentSettingsMap* content_settings, + const url::Origin& origin, + ContentSettingsType type) { + ContentSettingsForOneType session_settings = + content_settings->GetSettingsForOneType( + type, content_settings::mojom::SessionModel::USER_SESSION); + + GURL url = origin.GetURL(); + for (ContentSettingPatternSource& entry : session_settings) { + if (content_settings::IsConstraintSessionExpiration(entry, + content_settings::mojom::LifetimeMode::UNTIL_ORIGIN_CLOSED) && + entry.primary_pattern.Matches(url)) { + content_settings->SetWebsiteSettingCustomScope( + entry.primary_pattern, entry.secondary_pattern, + type, base::Value()); + } + } + } +} -OneTimePermissionsTracker::OneTimePermissionsTracker() = default; +OneTimePermissionsTracker::OneTimePermissionsTracker(content::BrowserContext* context) + : context_(context) {} OneTimePermissionsTracker::~OneTimePermissionsTracker() = default; OneTimePermissionsTracker::OriginTrackEntry::OriginTrackEntry() = default; @@ -260,6 +286,12 @@ void OneTimePermissionsTracker::NotifyLastPageFromOriginClosed( for (auto& observer : observer_list_) { observer.OnLastPageFromOriginClosed(origin); } + HostContentSettingsMap* content_settings = + permissions::PermissionsClient::Get()->GetSettingsMap(context_); + RemoveSessionSettings(content_settings, origin, ContentSettingsType::GEOLOCATION); + RemoveSessionSettings(content_settings, origin, ContentSettingsType::GEOLOCATION_WITH_OPTIONS); + RemoveSessionSettings(content_settings, origin, ContentSettingsType::MEDIASTREAM_MIC); + RemoveSessionSettings(content_settings, origin, ContentSettingsType::MEDIASTREAM_CAMERA); } bool OneTimePermissionsTracker::ShouldIgnoreOrigin(const url::Origin& origin) { diff --git a/chrome/browser/permissions/one_time_permissions_tracker.h b/chrome/browser/permissions/one_time_permissions_tracker.h --- a/chrome/browser/permissions/one_time_permissions_tracker.h +++ b/chrome/browser/permissions/one_time_permissions_tracker.h @@ -13,6 +13,7 @@ #include "base/timer/timer.h" #include "chrome/browser/permissions/one_time_permissions_tracker_observer.h" #include "components/content_settings/core/common/content_settings_pattern.h" +#include "chrome/browser/profiles/profile.h" #include "components/content_settings/core/common/content_settings_types.h" #include "components/keyed_service/core/keyed_service.h" #include "content/public/browser/visibility.h" @@ -27,7 +28,7 @@ class OneTimePermissionsTracker : public KeyedService { void (OneTimePermissionsTracker::*)(const url::Origin&); public: - OneTimePermissionsTracker(); + OneTimePermissionsTracker(content::BrowserContext* context); ~OneTimePermissionsTracker() override; OneTimePermissionsTracker(const OneTimePermissionsTracker&) = delete; @@ -148,7 +149,7 @@ class OneTimePermissionsTracker : public KeyedService { base::ObserverList observer_list_; std::map origin_tracker_; - + raw_ptr context_; base::WeakPtrFactory weak_factory_{this}; }; diff --git a/chrome/browser/permissions/one_time_permissions_tracker_factory.cc b/chrome/browser/permissions/one_time_permissions_tracker_factory.cc --- a/chrome/browser/permissions/one_time_permissions_tracker_factory.cc +++ b/chrome/browser/permissions/one_time_permissions_tracker_factory.cc @@ -44,5 +44,5 @@ bool OneTimePermissionsTrackerFactory::ServiceIsCreatedWithBrowserContext() std::unique_ptr OneTimePermissionsTrackerFactory::BuildServiceInstanceForBrowserContext( content::BrowserContext* context) const { - return std::make_unique(); + return std::make_unique(context); } diff --git a/chrome/browser/ui/views/permissions/chip/chip_controller.cc b/chrome/browser/ui/views/permissions/chip/chip_controller.cc --- a/chrome/browser/ui/views/permissions/chip/chip_controller.cc +++ b/chrome/browser/ui/views/permissions/chip/chip_controller.cc @@ -235,7 +235,7 @@ void ChipController::OnWidgetDestroyed(views::Widget* widget) { break; case permissions::PermissionAction::GRANTED_ONCE: active_chip_permission_request_manager_.value()->AcceptThisTime( - /*prompt_options=*/std::monostate()); + /*prompt_options=*/std::monostate(), content_settings::mojom::LifetimeMode::ONLY_THIS_TIME); break; case permissions::PermissionAction::DENIED: active_chip_permission_request_manager_.value()->Deny( diff --git a/chrome/browser/ui/views/permissions/exclusive_access_permission_prompt_view.cc b/chrome/browser/ui/views/permissions/exclusive_access_permission_prompt_view.cc --- a/chrome/browser/ui/views/permissions/exclusive_access_permission_prompt_view.cc +++ b/chrome/browser/ui/views/permissions/exclusive_access_permission_prompt_view.cc @@ -118,7 +118,7 @@ void ExclusiveAccessPermissionPromptView::RunButtonCallback(int button_id) { request_type(), GetPermissionActionString(button), record_browser_always_active_value()); if (button == ButtonType::kAllowThisTime) { - delegate_->AcceptThisTime(/*prompt_options=*/std::monostate()); + delegate_->AcceptThisTime(/*prompt_options=*/std::monostate(), content_settings::mojom::LifetimeMode::ONLY_THIS_TIME); } else if (button == ButtonType::kAlwaysAllow) { delegate_->Accept(/*prompt_options=*/std::monostate()); } else if (button == ButtonType::kNeverAllow) { diff --git a/chrome/browser/ui/views/permissions/permission_prompt_bubble_base_view.cc b/chrome/browser/ui/views/permissions/permission_prompt_bubble_base_view.cc --- a/chrome/browser/ui/views/permissions/permission_prompt_bubble_base_view.cc +++ b/chrome/browser/ui/views/permissions/permission_prompt_bubble_base_view.cc @@ -321,7 +321,7 @@ void PermissionPromptBubbleBaseView::RunButtonCallback(int button_id) { delegate_->Accept(/*prompt_options=*/std::monostate()); return; case PermissionDialogButton::kAcceptOnce: - delegate_->AcceptThisTime(/*prompt_options=*/std::monostate()); + delegate_->AcceptThisTime(/*prompt_options=*/std::monostate(), content_settings::mojom::LifetimeMode::ONLY_THIS_TIME); return; case PermissionDialogButton::kDeny: #if BUILDFLAG(IS_CHROMEOS) diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java --- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java +++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java @@ -724,6 +724,11 @@ public class SingleWebsiteSettings extends BaseSiteSettingsFragment } @RequiresNonNull({"mSite"}) + private boolean isSessionPermission(@ContentSettingsType.EnumType int type) { + return mSite.getPermissionInfo(type) != null && + mSite.getPermissionInfo(type).getSessionModel() == SessionModel.USER_SESSION; + } + private void setUpClearDataPreference() { ClearWebsiteStorage preference = findPreference(PREF_CLEAR_DATA); long usage = mSite.getTotalUsage(); @@ -1404,6 +1409,10 @@ public class SingleWebsiteSettings extends BaseSiteSettingsFragment .getDefaultColor()); } } + if (preference instanceof ChromeSwitchPreference && isSessionPermission(contentType)) { + ((ChromeSwitchPreference)preference).setSummary(preference.getSummary() + " " + + getString(R.string.page_info_android_permission_session_permission)); + } } /** diff --git a/components/browser_ui/site_settings/android/website_preference_bridge.cc b/components/browser_ui/site_settings/android/website_preference_bridge.cc --- a/components/browser_ui/site_settings/android/website_preference_bridge.cc +++ b/components/browser_ui/site_settings/android/website_preference_bridge.cc @@ -238,7 +238,7 @@ void GetOrigins(JNIEnv* env, seen_origins.push_back(origin); insertionFunc(env, static_cast(content_type), list, ConvertOriginToJavaString(env, origin), jembedder, - /*is_embargoed=*/true, /*is_one_time=*/false); + /*is_embargoed=*/true, 0); } } } diff --git a/components/browser_ui/strings/android/browser_ui_strings.grd b/components/browser_ui/strings/android/browser_ui_strings.grd --- a/components/browser_ui/strings/android/browser_ui_strings.grd +++ b/components/browser_ui/strings/android/browser_ui_strings.grd @@ -629,6 +629,11 @@ URL truncated + + (only this session) + + Ad privacy diff --git a/components/content_settings/core/browser/content_settings_utils.cc b/components/content_settings/core/browser/content_settings_utils.cc --- a/components/content_settings/core/browser/content_settings_utils.cc +++ b/components/content_settings/core/browser/content_settings_utils.cc @@ -182,6 +182,42 @@ bool IsConstraintPersistent(const ContentSettingConstraints& constraints) { return constraints.session_model() == mojom::SessionModel::DURABLE; } +ContentSettingConstraints GetConstraintSessionExpiration(content_settings::mojom::LifetimeMode lifetime_mode) { + int lifetime; + base::Time now; + if (lifetime_mode == content_settings::mojom::LifetimeMode::ONLY_THIS_TIME) { + // note: this content settings will be discarded immediately + // 1h is used as a magic constant to identify the one-time lifetime mode + lifetime = 1; + } else if (lifetime_mode == content_settings::mojom::LifetimeMode::UNTIL_ORIGIN_CLOSED) { + now = base::Time::Now(); + lifetime = 24; + } else { + lifetime = 0; + } + ContentSettingConstraints c(now); + c.set_lifetime(base::Hours(lifetime)); + c.set_session_model(mojom::SessionModel::USER_SESSION); + return c; +} + +bool IsConstraintSessionExpiration(const ContentSettingPatternSource& source, + content_settings::mojom::LifetimeMode lifetime_mode) { + if (source.metadata.session_model() != content_settings::mojom::SessionModel::USER_SESSION) + return false; + + mojom::LifetimeMode type; + if (source.metadata.lifetime() == base::Hours(24)) { + type = content_settings::mojom::LifetimeMode::UNTIL_ORIGIN_CLOSED; + } else if (source.metadata.expiration() == (base::Time() + base::Hours(1))) { + type = content_settings::mojom::LifetimeMode::ONLY_THIS_TIME; + } else { + type = content_settings::mojom::LifetimeMode::UNTIL_BROWSER_CLOSED; + } + + return lifetime_mode == type; +} + bool CanTrackLastVisit(ContentSettingsType type) { DCHECK(WebsiteSettingsRegistry::GetInstance()->Get(type)) << type; @@ -267,10 +303,6 @@ const std::vector& GetTypesWithTemporaryGrants() { ContentSettingsType::CAPTURED_SURFACE_CONTROL, #endif ContentSettingsType::KEYBOARD_LOCK, - ContentSettingsType::GEOLOCATION, - ContentSettingsType::GEOLOCATION_WITH_OPTIONS, - ContentSettingsType::MEDIASTREAM_MIC, - ContentSettingsType::MEDIASTREAM_CAMERA, ContentSettingsType::SENSORS, ContentSettingsType::HAND_TRACKING, ContentSettingsType::SMART_CARD_DATA, @@ -287,10 +319,6 @@ const std::vector& GetTypesWithTemporaryGrantsInHcsm() { ContentSettingsType::CAPTURED_SURFACE_CONTROL, #endif ContentSettingsType::KEYBOARD_LOCK, - ContentSettingsType::GEOLOCATION, - ContentSettingsType::GEOLOCATION_WITH_OPTIONS, - ContentSettingsType::MEDIASTREAM_MIC, - ContentSettingsType::MEDIASTREAM_CAMERA, ContentSettingsType::SENSORS, ContentSettingsType::HAND_TRACKING, ContentSettingsType::AR, diff --git a/components/content_settings/core/browser/content_settings_utils.h b/components/content_settings/core/browser/content_settings_utils.h --- a/components/content_settings/core/browser/content_settings_utils.h +++ b/components/content_settings/core/browser/content_settings_utils.h @@ -78,6 +78,12 @@ bool IsConstraintPersistent(const ContentSettingConstraints& constraints); // Returns whether the given type supports tracking last_visit timestamps. bool CanTrackLastVisit(ContentSettingsType type); +ContentSettingConstraints GetConstraintSessionExpiration(mojom::LifetimeMode lifetime_mode); + +bool IsConstraintSessionExpiration( + const ContentSettingPatternSource& source, + mojom::LifetimeMode lifetime_mode); + // Get a timestamp with week-precision. base::Time GetCoarseVisitedTime(base::Time time); diff --git a/components/content_settings/core/common/content_settings_enums.mojom b/components/content_settings/core/common/content_settings_enums.mojom --- a/components/content_settings/core/common/content_settings_enums.mojom +++ b/components/content_settings/core/common/content_settings_enums.mojom @@ -63,6 +63,15 @@ enum SessionModel { ONE_TIME = 3, }; +// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.components.content_settings +// GENERATED_JAVA_CLASS_NAME_OVERRIDE: LifetimeMode +enum LifetimeMode { + ALWAYS = 99, + ONLY_THIS_TIME = 1, + UNTIL_ORIGIN_CLOSED = 2, + UNTIL_BROWSER_CLOSED = 0, +}; + // Identifies the content settings provider of a content setting. // GENERATED_JAVA_ENUM_PACKAGE: org.chromium.components.content_settings // GENERATED_JAVA_CLASS_NAME_OVERRIDE: ProviderType diff --git a/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoController.java b/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoController.java --- a/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoController.java +++ b/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoController.java @@ -384,9 +384,9 @@ public class PageInfoController */ @CalledByNative private void addPermissionSection( - String name, String nameMidSentence, int type, boolean allowed, boolean requested) { + String name, String nameMidSentence, int type, boolean allowed, boolean requested, boolean is_user_session) { mPermissionParamsListBuilder.addPermissionEntry( - name, nameMidSentence, type, allowed, requested); + name, nameMidSentence, type, allowed, requested, is_user_session); } /** Update the permissions view based on the contents of mDisplayedPermissions. */ diff --git a/components/page_info/android/java/src/org/chromium/components/page_info/PermissionParamsListBuilder.java b/components/page_info/android/java/src/org/chromium/components/page_info/PermissionParamsListBuilder.java --- a/components/page_info/android/java/src/org/chromium/components/page_info/PermissionParamsListBuilder.java +++ b/components/page_info/android/java/src/org/chromium/components/page_info/PermissionParamsListBuilder.java @@ -47,8 +47,8 @@ public class PermissionParamsListBuilder { } public void addPermissionEntry( - String name, String nameMidSentence, int type, boolean allowed, boolean requested) { - mEntries.add(new PageInfoPermissionEntry(name, nameMidSentence, type, allowed, requested)); + String name, String nameMidSentence, int type, boolean allowed, boolean requested, boolean is_user_session) { + mEntries.add(new PageInfoPermissionEntry(name, nameMidSentence, type, allowed, requested, is_user_session)); } public void clearPermissionEntries() { @@ -85,6 +85,10 @@ public class PermissionParamsListBuilder { } else { warningTextResource = R.string.page_info_android_permission_blocked; } + if (permission.is_user_session) { + warningTextResource = + R.string.page_info_android_permission_session_permission; + } } } @@ -115,14 +119,16 @@ public class PermissionParamsListBuilder { public final int type; public final boolean allowed; public final boolean requested; + public final boolean is_user_session; PageInfoPermissionEntry( - String name, String nameMidSentence, int type, boolean allowed, boolean requested) { + String name, String nameMidSentence, int type, boolean allowed, boolean requested, boolean is_user_session) { this.name = name; this.nameMidSentence = nameMidSentence; this.type = type; this.allowed = allowed; this.requested = requested; + this.is_user_session = is_user_session; } @Override diff --git a/components/page_info/android/page_info_controller_android.cc b/components/page_info/android/page_info_controller_android.cc --- a/components/page_info/android/page_info_controller_android.cc +++ b/components/page_info/android/page_info_controller_android.cc @@ -219,6 +219,8 @@ void PageInfoControllerAndroid::SetPermissionInfo( // being requested. This is needed for the Clapper experiment // (crbug.com/458351800) and (crbug.com/463333225). bool requested_notifications = false; + std::map + user_specified_settings_is_user_session; for (const auto& permission : permission_info_list) { if (std::ranges::contains(permissions_to_display, permission.type)) { @@ -231,6 +233,8 @@ void PageInfoControllerAndroid::SetPermissionInfo( user_specified_settings_to_display[permission.type] = info->delegate().IsAnyPermissionAllowed(*setting_to_display); + user_specified_settings_is_user_session[permission.type] = + permission.is_user_session; } // Notifications permission can have the setting to display as DEFAULT @@ -257,7 +261,8 @@ void PageInfoControllerAndroid::SetPermissionInfo( ConvertUTF16ToJavaString(env, setting_title_mid_sentence), static_cast(permission), user_specified_settings_to_display[permission], - requested_notifications); + requested_notifications, + user_specified_settings_is_user_session[permission]); } } @@ -270,7 +275,8 @@ void PageInfoControllerAndroid::SetPermissionInfo( env, controller_jobject_, ConvertUTF16ToJavaString(env, object_title), ConvertUTF16ToJavaString(env, object_title), static_cast(chosen_object->ui_info->content_settings_type), - static_cast(CONTENT_SETTING_ALLOW), requested_notifications); + static_cast(CONTENT_SETTING_ALLOW), requested_notifications, + /* is_user_session */ false); } Java_PageInfoController_updatePermissionDisplay(env, controller_jobject_); diff --git a/components/page_info/page_info.cc b/components/page_info/page_info.cc --- a/components/page_info/page_info.cc +++ b/components/page_info/page_info.cc @@ -1314,6 +1314,8 @@ void PageInfo::PopulatePermissionInfo(PermissionInfo& permission_info, permission_info.is_one_time = (info.metadata.session_model() == content_settings::mojom::SessionModel::ONE_TIME); + permission_info.is_user_session = + (info.metadata.session_model() == content_settings::mojom::SessionModel::USER_SESSION); auto* setting_info = content_settings::PermissionSettingsRegistry::GetInstance()->Get( diff --git a/components/page_info/page_info.h b/components/page_info/page_info.h --- a/components/page_info/page_info.h +++ b/components/page_info/page_info.h @@ -150,6 +150,7 @@ class PageInfo : private content_settings::CookieControlsObserver, content_settings::SettingSource::kNone; // Whether the permission is a one-time grant. bool is_one_time = false; + bool is_user_session = false; // Only set for settings that can have multiple permissions for different // embedded origins. std::optional requesting_origin; diff --git a/components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogDelegate.java b/components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogDelegate.java --- a/components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogDelegate.java +++ b/components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogDelegate.java @@ -17,6 +17,7 @@ import org.chromium.build.annotations.NullMarked; import org.chromium.build.annotations.Nullable; import org.chromium.ui.base.DeviceFormFactor; import org.chromium.ui.base.WindowAndroid; +import org.chromium.components.content_settings.LifetimeMode; import java.util.ArrayList; import java.util.List; @@ -68,6 +69,9 @@ public class PermissionDialogDelegate { // Prompt(screen) variant we want to display on the dialog. private @EmbeddedPromptVariant int mEmbeddedPromptVariant; + /** Lifetime option selected by the user. */ + private int mSelectedLifetimeOption = LifetimeMode.ALWAYS; + /** * Defines a (potentially empty) list of ranges represented as pairs of , * which shall be used by the UI to format the specified ranges as bold text. @@ -145,6 +149,15 @@ public class PermissionDialogDelegate { PermissionDialogDelegateJni.get().acceptThisTime(mNativeDelegatePtr); } + public void setSelectedLifetimeOption(int idx) { + mSelectedLifetimeOption = idx; + } + + @CalledByNative + public int getSelectedLifetimeOption() { + return mSelectedLifetimeOption; + } + public void onDeny() { assert mNativeDelegatePtr != 0; PermissionDialogDelegateJni.get().deny(mNativeDelegatePtr); diff --git a/components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogModelFactory.java b/components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogModelFactory.java --- a/components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogModelFactory.java +++ b/components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogModelFactory.java @@ -14,6 +14,18 @@ import org.chromium.ui.UiUtils; import org.chromium.ui.modaldialog.ModalDialogProperties; import org.chromium.ui.modelutil.PropertyModel; +import java.util.Arrays; +import java.util.List; +import android.view.ViewGroup.LayoutParams; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; +import org.chromium.base.ApiCompatibilityUtils; +import org.chromium.ui.base.ViewUtils; +import org.chromium.components.content_settings.ContentSettingsType; +import org.chromium.components.content_settings.LifetimeMode; + /** This class creates the model for the permission dialog. */ @NullMarked class PermissionDialogModelFactory { @@ -77,7 +89,76 @@ class PermissionDialogModelFactory { ModalDialogProperties.ButtonStyles.PRIMARY_FILLED_NEGATIVE_OUTLINE) .with(ModalDialogProperties.CHANGE_CUSTOM_VIEW_OR_BUTTONS, true); } - return builder.build(); + + PropertyModel pm = builder.build(); + int[] types = delegate.getContentSettingsTypes(); + if (contains(types, ContentSettingsType.GEOLOCATION) || + contains(types, ContentSettingsType.GEOLOCATION_WITH_OPTIONS) || + contains(types, ContentSettingsType.MEDIASTREAM_MIC) || + contains(types, ContentSettingsType.MEDIASTREAM_CAMERA)) + { + LinearLayout layout = (LinearLayout) customView; + + // Create a text label before the lifetime selector. + TextView lifetimeOptionsText = new TextView(context); + lifetimeOptionsText.setText(context.getString( + org.chromium.components.permissions.R.string.session_permissions_title)); + lifetimeOptionsText.setTextAppearance( + lifetimeOptionsText.getContext(), R.style.TextAppearance_TextMedium_Primary); + + LinearLayout.LayoutParams lifetimeOptionsTextLayoutParams = + new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + lifetimeOptionsTextLayoutParams.setMargins(0, 0, 0, ViewUtils.dpToPx(context, 8)); + lifetimeOptionsText.setLayoutParams(lifetimeOptionsTextLayoutParams); + layout.addView(lifetimeOptionsText); + + // Create radio buttons with lifetime options. + RadioGroup radioGroup = new RadioGroup(context); + + RadioButton radioButton = new RadioButton(context); + radioButton.setText(context.getString( + org.chromium.components.permissions.R.string.session_permissions_only_this_this)); + radioButton.setId(LifetimeMode.ONLY_THIS_TIME); + radioGroup.addView(radioButton); + + radioButton = new RadioButton(context); + radioButton.setText(context.getString( + org.chromium.components.permissions.R.string.session_permissions_until_page_close)); + radioButton.setId(LifetimeMode.UNTIL_ORIGIN_CLOSED); + radioGroup.addView(radioButton); + + radioButton = new RadioButton(context); + radioButton.setText(context.getString( + org.chromium.components.permissions.R.string.session_permissions_until_browser_close)); + radioButton.setId(LifetimeMode.UNTIL_BROWSER_CLOSED); + radioGroup.addView(radioButton); + + radioButton = new RadioButton(context); + radioButton.setText(context.getString( + org.chromium.components.permissions.R.string.session_permissions_forever)); + radioButton.setId(LifetimeMode.ALWAYS); + radioGroup.addView(radioButton); + + radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + delegate.setSelectedLifetimeOption(checkedId); + } + }); + radioGroup.check(1); + layout.addView(radioGroup); + } + + return pm; + } + + private static boolean contains(final int[] array, final int key) { + int length = array.length; + for(int i = 0; i < length; i++) { + if (array[i] == key) + return true; + } + return false; } public static ModalDialogProperties.ModalDialogButtonSpec[] getButtonSpecs( diff --git a/components/permissions/android/permission_prompt/embedded_permission_prompt_android.cc b/components/permissions/android/permission_prompt/embedded_permission_prompt_android.cc --- a/components/permissions/android/permission_prompt/embedded_permission_prompt_android.cc +++ b/components/permissions/android/permission_prompt/embedded_permission_prompt_android.cc @@ -126,7 +126,7 @@ void EmbeddedPermissionPromptAndroid::Acknowledge( } void EmbeddedPermissionPromptAndroid::AcceptThisTime( - const PromptOptions& prompt_options) { + const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode lifetimeOption) { prompt_model_->PrecalculateVariantsForMetrics(); prompt_model_->RecordPermissionActionUKM( permissions::ElementAnchoredBubbleAction::kGrantedOnce); diff --git a/components/permissions/android/permission_prompt/embedded_permission_prompt_android.h b/components/permissions/android/permission_prompt/embedded_permission_prompt_android.h --- a/components/permissions/android/permission_prompt/embedded_permission_prompt_android.h +++ b/components/permissions/android/permission_prompt/embedded_permission_prompt_android.h @@ -45,7 +45,7 @@ class EmbeddedPermissionPromptAndroid : public PermissionPromptAndroid { const override; void Dismiss(const PromptOptions& prompt_options) override; void Accept(const PromptOptions& prompt_options) override; - void AcceptThisTime(const PromptOptions& prompt_options) override; + void AcceptThisTime(const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode lifetimeOption) override; void Acknowledge(const PromptOptions& prompt_options) override; void Deny(const PromptOptions& prompt_options) override; void Resumed() override; diff --git a/components/permissions/android/permission_prompt/permission_dialog_delegate.cc b/components/permissions/android/permission_prompt/permission_dialog_delegate.cc --- a/components/permissions/android/permission_prompt/permission_dialog_delegate.cc +++ b/components/permissions/android/permission_prompt/permission_dialog_delegate.cc @@ -136,6 +136,11 @@ void PermissionDialogJavaDelegate::UpdateDialog() { static_cast(permission_prompt_->GetEmbeddedPromptVariant())); } +int PermissionDialogJavaDelegate::GetSelectedLifetimeOption() { + JNIEnv* env = base::android::AttachCurrentThread(); + return Java_PermissionDialogDelegate_getSelectedLifetimeOption(env, j_delegate_); +} + // static std::unique_ptr PermissionDialogDelegate::Create( content::WebContents* web_contents, @@ -164,12 +169,22 @@ PermissionDialogDelegate::CreateForTesting( void PermissionDialogDelegate::Accept(JNIEnv* env) { CHECK(permission_prompt_); + content_settings::mojom::LifetimeMode lifetimeOption = + static_cast( + java_delegate_->GetSelectedLifetimeOption()); + if (lifetimeOption != content_settings::mojom::LifetimeMode::ALWAYS) { + permission_prompt_->AcceptThisTime(prompt_options_, lifetimeOption); + return; + } permission_prompt_->Accept(prompt_options_); } void PermissionDialogDelegate::AcceptThisTime(JNIEnv* env) { CHECK(permission_prompt_); - permission_prompt_->AcceptThisTime(prompt_options_); + content_settings::mojom::LifetimeMode lifetimeOption = + static_cast( + java_delegate_->GetSelectedLifetimeOption()); + permission_prompt_->AcceptThisTime(prompt_options_, lifetimeOption); } void PermissionDialogDelegate::Acknowledge(JNIEnv* env) { @@ -179,6 +194,13 @@ void PermissionDialogDelegate::Acknowledge(JNIEnv* env) { void PermissionDialogDelegate::Deny(JNIEnv* env) { CHECK(permission_prompt_); + content_settings::mojom::LifetimeMode lifetimeOption = + static_cast( + java_delegate_->GetSelectedLifetimeOption()); + if (lifetimeOption != content_settings::mojom::LifetimeMode::ALWAYS) { + permission_prompt_->DenyThisTime(prompt_options_, lifetimeOption); + return; + } permission_prompt_->Deny(prompt_options_); } diff --git a/components/permissions/android/permission_prompt/permission_dialog_delegate.h b/components/permissions/android/permission_prompt/permission_dialog_delegate.h --- a/components/permissions/android/permission_prompt/permission_dialog_delegate.h +++ b/components/permissions/android/permission_prompt/permission_dialog_delegate.h @@ -46,6 +46,7 @@ class PermissionDialogJavaDelegate { const favicon_base::FaviconRawBitmapResult& favicon_result); virtual void DismissDialog(); + virtual int GetSelectedLifetimeOption(); virtual void UpdateDialog(); diff --git a/components/permissions/android/permission_prompt/permission_prompt_android.cc b/components/permissions/android/permission_prompt/permission_prompt_android.cc --- a/components/permissions/android/permission_prompt/permission_prompt_android.cc +++ b/components/permissions/android/permission_prompt/permission_prompt_android.cc @@ -86,14 +86,20 @@ void PermissionPromptAndroid::Accept(const PromptOptions& prompt_options) { } void PermissionPromptAndroid::AcceptThisTime( - const PromptOptions& prompt_options) { - delegate_->AcceptThisTime(prompt_options); + const PromptOptions& prompt_options, + content_settings::mojom::LifetimeMode lifetimeOption) { + delegate_->AcceptThisTime(prompt_options, lifetimeOption); } void PermissionPromptAndroid::Deny(const PromptOptions& prompt_options) { delegate_->Deny(prompt_options); } +void PermissionPromptAndroid::DenyThisTime(const PromptOptions& prompt_options, + content_settings::mojom::LifetimeMode lifetimeOption) { + delegate_->DenyThisTime(prompt_options, lifetimeOption); +} + void PermissionPromptAndroid::Ignore(const PromptOptions& prompt_options) { delegate_->Ignore(prompt_options); } diff --git a/components/permissions/android/permission_prompt/permission_prompt_android.h b/components/permissions/android/permission_prompt/permission_prompt_android.h --- a/components/permissions/android/permission_prompt/permission_prompt_android.h +++ b/components/permissions/android/permission_prompt/permission_prompt_android.h @@ -57,10 +57,11 @@ class PermissionPromptAndroid : public PermissionPrompt { const; virtual void Dismiss(const PromptOptions& prompt_options); virtual void Accept(const PromptOptions& prompt_options); - virtual void AcceptThisTime(const PromptOptions& prompt_options); + virtual void AcceptThisTime(const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode lifetimeOption); virtual void Acknowledge(const PromptOptions& prompt_options) {} virtual void Deny(const PromptOptions& prompt_options); virtual void Ignore(const PromptOptions& prompt_options); + virtual void DenyThisTime(const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode lifetimeOption); virtual void Resumed() {} virtual void SystemSettingsShown() {} virtual void SystemPermissionResolved(bool accepted) {} diff --git a/components/permissions/android/permissions_android_strings.grd b/components/permissions/android/permissions_android_strings.grd --- a/components/permissions/android/permissions_android_strings.grd +++ b/components/permissions/android/permissions_android_strings.grd @@ -272,6 +272,23 @@ Unknown or unsupported device (%1$sA1:B2:C3:D4:E5:F6) + + + Remember my decision + + + Only this time + + + Until all pages of this origin are closed + + + Until Bromite is closed + + + Forever + + %1$sitem_name (%2$sitem id) diff --git a/components/permissions/contexts/geolocation_permission_context_android.cc b/components/permissions/contexts/geolocation_permission_context_android.cc --- a/components/permissions/contexts/geolocation_permission_context_android.cc +++ b/components/permissions/contexts/geolocation_permission_context_android.cc @@ -213,7 +213,17 @@ void GeolocationPermissionContextAndroid::NotifyPermissionSet( bool persist, const permissions::PermissionPromptDecision& decision) { DCHECK(decision.is_final); + NotifyPermissionSetWithLifetime(request_data, + std::move(callback), persist, decision, + content_settings::mojom::LifetimeMode::ALWAYS); +} +void GeolocationPermissionContextAndroid::NotifyPermissionSetWithLifetime( + const PermissionRequestData& request_data, + BrowserPermissionCallback callback, + bool persist, + const permissions::PermissionPromptDecision& decision, + content_settings::mojom::LifetimeMode lifetime_option) { bool is_default_search = IsRequestingOriginDSE(request_data.requesting_origin); if (decision.overall_decision == PermissionDecision::kAllow && @@ -233,6 +243,7 @@ void GeolocationPermissionContextAndroid::NotifyPermissionSet( .overall_decision = PermissionDecision::kDeny, .prompt_options = decision.prompt_options, .is_final = true}, + lifetime_option, request_data.embedded_permission_request_descriptor.Clone()); return; } @@ -258,6 +269,7 @@ void GeolocationPermissionContextAndroid::NotifyPermissionSet( .overall_decision = PermissionDecision::kDeny, .prompt_options = decision.prompt_options, .is_final = true}, + lifetime_option, request_data.embedded_permission_request_descriptor.Clone()); return; } @@ -270,13 +282,13 @@ void GeolocationPermissionContextAndroid::NotifyPermissionSet( base::BindOnce( &GeolocationPermissionContextAndroid::OnLocationSettingsDialogShown, weak_factory_.GetWeakPtr(), request_data.requesting_origin, - request_data.embedding_origin, persist, decision)); + request_data.embedding_origin, persist, decision, lifetime_option)); return; } FinishNotifyPermissionSet( request_data.id, request_data.requesting_origin, - request_data.embedding_origin, std::move(callback), persist, decision, + request_data.embedding_origin, std::move(callback), persist, decision, lifetime_option, request_data.embedded_permission_request_descriptor.Clone()); } @@ -459,6 +471,7 @@ void GeolocationPermissionContextAndroid::OnLocationSettingsDialogShown( const GURL& embedding_origin, bool persist, const permissions::PermissionPromptDecision& decision, + content_settings::mojom::LifetimeMode lifetime_option, LocationSettingsDialogOutcome prompt_outcome) { bool is_default_search = IsRequestingOriginDSE(requesting_origin); PermissionPromptDecision result_decision = decision; @@ -477,7 +490,7 @@ void GeolocationPermissionContextAndroid::OnLocationSettingsDialogShown( FinishNotifyPermissionSet( location_settings_dialog_request_id_, requesting_origin, embedding_origin, - std::move(location_settings_dialog_callback_), persist, result_decision); + std::move(location_settings_dialog_callback_), persist, result_decision, lifetime_option); location_settings_dialog_request_id_ = PermissionRequestID(content::GlobalRenderFrameHostId(0, 0), @@ -491,6 +504,7 @@ void GeolocationPermissionContextAndroid::FinishNotifyPermissionSet( BrowserPermissionCallback callback, bool persist, const permissions::PermissionPromptDecision& decision, + content_settings::mojom::LifetimeMode lifetime_option, blink::mojom::EmbeddedPermissionRequestDescriptorPtr embedded_permission_request_descriptor) { PermissionRequestData request_data( @@ -502,8 +516,9 @@ void GeolocationPermissionContextAndroid::FinishNotifyPermissionSet( requesting_origin, embedding_origin); request_data.embedded_permission_request_descriptor = std::move(embedded_permission_request_descriptor); - GeolocationPermissionContext::NotifyPermissionSet( - request_data, std::move(callback), persist, decision); + GeolocationPermissionContext::NotifyPermissionSetImpl( + request_data, std::move(callback), persist, decision, + /*use_lifetime_option*/ true, lifetime_option); } void GeolocationPermissionContextAndroid::SetLocationSettingsForTesting( diff --git a/components/permissions/contexts/geolocation_permission_context_android.h b/components/permissions/contexts/geolocation_permission_context_android.h --- a/components/permissions/contexts/geolocation_permission_context_android.h +++ b/components/permissions/contexts/geolocation_permission_context_android.h @@ -98,6 +98,11 @@ class GeolocationPermissionContextAndroid BrowserPermissionCallback callback, bool persist, const permissions::PermissionPromptDecision& decision) override; + void NotifyPermissionSetWithLifetime(const PermissionRequestData& request_data, + BrowserPermissionCallback callback, + bool persist, + const permissions::PermissionPromptDecision& decision, + content_settings::mojom::LifetimeMode lifetime_option) override; content::PermissionResult UpdatePermissionStatusWithDeviceStatus( content::WebContents* web_contents, content::PermissionResult result, @@ -143,6 +148,7 @@ class GeolocationPermissionContextAndroid const GURL& embedding_origin, bool persist, const permissions::PermissionPromptDecision& decision, + content_settings::mojom::LifetimeMode lifetime_option, LocationSettingsDialogOutcome prompt_outcome); void FinishNotifyPermissionSet( @@ -152,6 +158,7 @@ class GeolocationPermissionContextAndroid BrowserPermissionCallback callback, bool persist, const permissions::PermissionPromptDecision& decision, + content_settings::mojom::LifetimeMode lifetime_option, blink::mojom::EmbeddedPermissionRequestDescriptorPtr embedded_permission_request_descriptor = nullptr); diff --git a/components/permissions/embedded_permission_prompt_flow_model.cc b/components/permissions/embedded_permission_prompt_flow_model.cc --- a/components/permissions/embedded_permission_prompt_flow_model.cc +++ b/components/permissions/embedded_permission_prompt_flow_model.cc @@ -297,7 +297,7 @@ void EmbeddedPermissionPromptFlowModel::SetDelegateAction( delegate_->Accept(prompt_options); break; case DelegateAction::kAllowThisTime: - delegate_->AcceptThisTime(prompt_options); + delegate_->AcceptThisTime(prompt_options, content_settings::mojom::LifetimeMode::ONLY_THIS_TIME); break; case DelegateAction::kDeny: delegate_->Deny(prompt_options); diff --git a/components/permissions/permission_context_base.cc b/components/permissions/permission_context_base.cc --- a/components/permissions/permission_context_base.cc +++ b/components/permissions/permission_context_base.cc @@ -321,6 +321,17 @@ PermissionContextBase::CreatePermissionRequest( std::move(request_finished_callback), UsesAutomaticEmbargo()); } +std::unique_ptr +PermissionContextBase::CreatePermissionRequest( + content::WebContents* web_contents, + std::unique_ptr request_data, + PermissionRequest::PermissionDecidedCallbackWithLifetime permission_decided_callback, + base::OnceClosure request_finished_callback) const { + return std::make_unique( + std::move(request_data), std::move(permission_decided_callback), + std::move(request_finished_callback), UsesAutomaticEmbargo()); +} + bool PermissionContextBase::UsesAutomaticEmbargo() const { return true; } @@ -666,7 +677,8 @@ void PermissionContextBase::DecidePermission( void PermissionContextBase::PermissionDecided( const permissions::PermissionPromptDecision& decision, - const PermissionRequestData& request_data) { + const PermissionRequestData& request_data, + content_settings::mojom::LifetimeMode lifetime_option) { UserMadePermissionDecision(request_data.id, request_data.requesting_origin, request_data.embedding_origin, decision.overall_decision); @@ -690,10 +702,10 @@ void PermissionContextBase::PermissionDecided( // Check if `request` has `BrowserPermissionCallback`. The call back might be // missing if a permission prompt was preignored and we already notified an // origin about it. - NotifyPermissionSet(request_data, + NotifyPermissionSetWithLifetime(request_data, request->second.second ? std::move(request->second.second) : base::DoNothing(), - persist, decision); + persist, decision, lifetime_option); } content::BrowserContext* PermissionContextBase::browser_context() const { @@ -776,6 +788,29 @@ void PermissionContextBase::NotifyPermissionSet( BrowserPermissionCallback callback, bool persist, const permissions::PermissionPromptDecision& decision) { + NotifyPermissionSetImpl(request_data, std::move(callback), + persist, decision, /*use_lifetime_option*/ false, + content_settings::mojom::LifetimeMode::ALWAYS); +} + +void PermissionContextBase::NotifyPermissionSetWithLifetime( + const PermissionRequestData& request_data, + BrowserPermissionCallback callback, + bool persist, + const permissions::PermissionPromptDecision& decision, + content_settings::mojom::LifetimeMode lifetime_option) { + NotifyPermissionSetImpl(request_data, std::move(callback), + persist, decision, /*use_lifetime_option*/ true, + lifetime_option); +} + +void PermissionContextBase::NotifyPermissionSetImpl( + const PermissionRequestData& request_data, + BrowserPermissionCallback callback, + bool persist, + const permissions::PermissionPromptDecision& decision, + bool use_lifetime_option, + content_settings::mojom::LifetimeMode lifetime_option) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); // Note that rfh may be null, see crbug.com/426909787. auto* rfh = content::RenderFrameHost::FromID( @@ -791,9 +826,9 @@ void PermissionContextBase::NotifyPermissionSet( if (persist) { // Clone new value, because we need it again for the callback. - UpdateSetting( + UpdateSettingImpl( request_data, new_value, - decision.overall_decision == PermissionDecision::kAllowThisTime); + decision.overall_decision == PermissionDecision::kAllowThisTime, use_lifetime_option, lifetime_option); } if (decision.is_final) { @@ -853,6 +888,17 @@ void PermissionContextBase::UpdateSetting( const PermissionRequestData& request_data, PermissionSetting setting, bool is_one_time) { + UpdateSettingImpl(request_data, std::move(setting), + is_one_time, + /*use_lifetime_option*/ false, + content_settings::mojom::LifetimeMode::ALWAYS); +} + +void PermissionContextBase::UpdateSettingImpl(const PermissionRequestData& request_data, + PermissionSetting setting, + bool is_one_time, + bool use_lifetime_option, + content_settings::mojom::LifetimeMode lifetime_option) { DCHECK_EQ(request_data.requesting_origin, request_data.requesting_origin.DeprecatedGetOriginAsURL()); DCHECK_EQ(request_data.embedding_origin, @@ -862,6 +908,8 @@ void PermissionContextBase::UpdateSetting( constraints.set_session_model( is_one_time ? content_settings::mojom::SessionModel::ONE_TIME : content_settings::mojom::SessionModel::DURABLE); + if (use_lifetime_option) + constraints = content_settings::GetConstraintSessionExpiration(lifetime_option); // The unused permissions module in Safety check will revoke unused site // permissions after a finite amount of time if the permission can be revoked. diff --git a/components/permissions/permission_context_base.h b/components/permissions/permission_context_base.h --- a/components/permissions/permission_context_base.h +++ b/components/permissions/permission_context_base.h @@ -205,6 +205,17 @@ class PermissionContextBase : public content_settings::Observer { // Updates stored setting if persist is set, updates tab indicators // and runs the callback to finish the request. + void NotifyPermissionSetImpl(const PermissionRequestData& request_data, + BrowserPermissionCallback callback, + bool persist, + const permissions::PermissionPromptDecision& decision, + bool use_lifetime_option, + content_settings::mojom::LifetimeMode lifetime_option); + virtual void NotifyPermissionSetWithLifetime(const PermissionRequestData& request_data, + BrowserPermissionCallback callback, + bool persist, + const permissions::PermissionPromptDecision& decision, + content_settings::mojom::LifetimeMode lifetime_option); virtual void NotifyPermissionSet( const PermissionRequestData& request_data, BrowserPermissionCallback callback, @@ -221,6 +232,11 @@ class PermissionContextBase : public content_settings::Observer { // Store the decided permission state. Virtual since the permission might be // stored with different restrictions (for example for desktop notifications). + void UpdateSettingImpl(const PermissionRequestData& request_data, + PermissionSetting setting, + bool is_one_time, + bool use_lifetime_option, + content_settings::mojom::LifetimeMode lifetime_option); virtual void UpdateSetting(const PermissionRequestData& request_data, PermissionSetting setting, bool is_one_time); @@ -250,6 +266,12 @@ class PermissionContextBase : public content_settings::Observer { PermissionRequest::PermissionDecidedCallback permission_decided_callback, base::OnceClosure request_finished_callback) const; + virtual std::unique_ptr CreatePermissionRequest( + content::WebContents* web_contents, + std::unique_ptr request_data, + PermissionRequest::PermissionDecidedCallbackWithLifetime permission_decided_callback, + base::OnceClosure request_finished_callback) const; + // Implementors can override this method to avoid using automatic embargo. virtual bool UsesAutomaticEmbargo() const; @@ -303,7 +325,8 @@ class PermissionContextBase : public content_settings::Observer { // This is the callback for PermissionRequest and is called once the user // allows/blocks/dismisses a permission prompt. void PermissionDecided(const permissions::PermissionPromptDecision& decision, - const PermissionRequestData& request_data); + const PermissionRequestData& request_data, + content_settings::mojom::LifetimeMode lifetime_option); raw_ptr browser_context_; const ContentSettingsType content_settings_type_; diff --git a/components/permissions/permission_prompt.h b/components/permissions/permission_prompt.h --- a/components/permissions/permission_prompt.h +++ b/components/permissions/permission_prompt.h @@ -70,8 +70,9 @@ class PermissionPrompt { virtual GURL GetEmbeddingOrigin() const = 0; virtual void Accept(const PromptOptions& prompt_options) = 0; - virtual void AcceptThisTime(const PromptOptions& prompt_options) = 0; + virtual void AcceptThisTime(const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode lifetime_option) = 0; virtual void Deny(const PromptOptions& prompt_options) = 0; + virtual void DenyThisTime(const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode lifetime_option) = 0; virtual void Dismiss(const PromptOptions& prompt_options) = 0; virtual void Ignore(const PromptOptions& prompt_options) = 0; virtual void SwitchToLoudPrompt() = 0; diff --git a/components/permissions/permission_request.cc b/components/permissions/permission_request.cc --- a/components/permissions/permission_request.cc +++ b/components/permissions/permission_request.cc @@ -49,6 +49,16 @@ PermissionRequest::PermissionRequest( request_finished_callback_(std::move(request_finished_callback)), uses_automatic_embargo_(uses_automatic_embargo) {} +PermissionRequest::PermissionRequest( + std::unique_ptr request_data, + PermissionDecidedCallbackWithLifetime permission_decided_callback, + base::OnceClosure request_finished_callback, + bool uses_automatic_embargo) + : data_(std::move(request_data)), + permission_decided_callback_withlifetime_(std::move(permission_decided_callback)), + request_finished_callback_(std::move(request_finished_callback)), + uses_automatic_embargo_(uses_automatic_embargo) {} + PermissionRequest::~PermissionRequest() { std::move(request_finished_callback_).Run(); } @@ -511,7 +521,19 @@ PermissionRequest::GetGeolocationPromptType() const { } void PermissionRequest::PermissionGranted(const PromptOptions& prompt_options, - bool is_one_time) { + bool is_one_time, + content_settings::mojom::LifetimeMode lifetime_option) { + if (permission_decided_callback_withlifetime_) { + std::move(permission_decided_callback_withlifetime_) + .Run(PermissionPromptDecision{.overall_decision = + is_one_time + ? PermissionDecision::kAllowThisTime + : PermissionDecision::kAllow, + .prompt_options = prompt_options, + .is_final = true}, + /*request_data=*/*data_, lifetime_option); + return; + } std::move(permission_decided_callback_) .Run(PermissionPromptDecision{.overall_decision = is_one_time @@ -522,7 +544,16 @@ void PermissionRequest::PermissionGranted(const PromptOptions& prompt_options, /*request_data=*/*data_); } -void PermissionRequest::PermissionDenied() { +void PermissionRequest::PermissionDenied(content_settings::mojom::LifetimeMode lifetime_option) { + if (permission_decided_callback_withlifetime_) { + std::move(permission_decided_callback_withlifetime_) + .Run(PermissionPromptDecision{.overall_decision = + PermissionDecision::kDeny, + .prompt_options = std::monostate(), + .is_final = true}, + /*request_data=*/*data_, lifetime_option); + return; + } std::move(permission_decided_callback_) .Run(PermissionPromptDecision{.overall_decision = PermissionDecision::kDeny, @@ -532,6 +563,14 @@ void PermissionRequest::PermissionDenied() { } void PermissionRequest::Cancelled(bool is_final_decision) { + if (permission_decided_callback_withlifetime_) { + permission_decided_callback_withlifetime_.Run( + PermissionPromptDecision{.overall_decision = PermissionDecision::kNone, + .prompt_options = std::monostate(), + .is_final = is_final_decision}, + /*request_data=*/*data_, content_settings::mojom::LifetimeMode::ALWAYS); + return; + } if (permission_decided_callback_) { permission_decided_callback_.Run( PermissionPromptDecision{.overall_decision = PermissionDecision::kNone, diff --git a/components/permissions/permission_request.h b/components/permissions/permission_request.h --- a/components/permissions/permission_request.h +++ b/components/permissions/permission_request.h @@ -54,6 +54,11 @@ class PermissionRequest { base::RepeatingCallback; + using PermissionDecidedCallbackWithLifetime = base::RepeatingCallback; + // `permission_decided_callback` is called when the permission request is // resolved by the user (see comment on PermissionDecidedCallback above). // `request_finished_callback` is called when the permission request is being @@ -68,6 +73,11 @@ class PermissionRequest { base::OnceClosure request_finished_callback = base::DoNothing(), bool uses_automatic_embargo = true); + PermissionRequest(std::unique_ptr request_data, + PermissionDecidedCallbackWithLifetime permission_decided_callback, + base::OnceClosure request_finished_callback = base::DoNothing(), + bool uses_automatic_embargo = true); + PermissionRequest(const PermissionRequest&) = delete; PermissionRequest& operator=(const PermissionRequest&) = delete; @@ -187,10 +197,10 @@ class PermissionRequest { // If |is_one_time| is true the permission will last until all tabs of // |origin| are closed or navigated away from, and then the permission will // automatically expire after 1 day. - void PermissionGranted(const PromptOptions& prompt_options, bool is_one_time); + void PermissionGranted(const PromptOptions& prompt_options, bool is_one_time, content_settings::mojom::LifetimeMode lifetime_option); // Called when the user has denied the requested permission. - void PermissionDenied(); + void PermissionDenied(content_settings::mojom::LifetimeMode lifetime_option); // Called when the user has cancelled the permission request. This // corresponds to a denial, but is segregated in case the context needs to @@ -254,6 +264,9 @@ class PermissionRequest { // Called once a decision is made about the permission. PermissionDecidedCallback permission_decided_callback_; + // Called once a decision is made about the permission (with lifetime option). + PermissionDecidedCallbackWithLifetime permission_decided_callback_withlifetime_; + // Called when the request is finished to perform bookkeeping tasks. base::OnceClosure request_finished_callback_; diff --git a/components/permissions/permission_request_manager.cc b/components/permissions/permission_request_manager.cc --- a/components/permissions/permission_request_manager.cc +++ b/components/permissions/permission_request_manager.cc @@ -212,12 +212,12 @@ void PermissionRequestManager::AddRequest( if (base::CommandLine::ForCurrentProcess()->HasSwitch( switches::kDenyPermissionPrompts)) { - request->PermissionDenied(); + request->PermissionDenied(content_settings::mojom::LifetimeMode::ALWAYS); return; } if (display::Screen::Get()->IsHeadless()) { - request->PermissionDenied(); + request->PermissionDenied(content_settings::mojom::LifetimeMode::ALWAYS); return; } @@ -296,9 +296,9 @@ void PermissionRequestManager::AddRequest( .selected_accuracy = GeolocationAccuracy::kPrecise}) : std::monostate(); if (should_auto_approve_request == PermissionAction::GRANTED) { - request->PermissionGranted(prompt_options, /*is_one_time=*/false); + request->PermissionGranted(prompt_options, /*is_one_time=*/false, content_settings::mojom::LifetimeMode::UNTIL_ORIGIN_CLOSED); } else if (should_auto_approve_request == PermissionAction::GRANTED_ONCE) { - request->PermissionGranted(prompt_options, /*is_one_time=*/true); + request->PermissionGranted(prompt_options, /*is_one_time=*/true, content_settings::mojom::LifetimeMode::UNTIL_ORIGIN_CLOSED); } return; } @@ -638,7 +638,8 @@ void PermissionRequestManager::Accept(const PromptOptions& prompt_options) { StorePermissionActionForUMA(request->requesting_origin(), request->request_type(), action); PermissionGrantedIncludingDuplicates(request.get(), prompt_options, - /*is_one_time=*/false); + /*is_one_time=*/false, + content_settings::mojom::LifetimeMode::ALWAYS); #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) std::optional content_settings_type = @@ -657,7 +658,7 @@ void PermissionRequestManager::Accept(const PromptOptions& prompt_options) { } void PermissionRequestManager::AcceptThisTime( - const PromptOptions& prompt_options) { + const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode mode) { CHECK_EQ(std::holds_alternative(prompt_options), requests_[0]->GetContentSettingsType() == ContentSettingsType::GEOLOCATION_WITH_OPTIONS) @@ -676,7 +677,7 @@ void PermissionRequestManager::AcceptThisTime( StorePermissionActionForUMA(request->requesting_origin(), request->request_type(), action); PermissionGrantedIncludingDuplicates(request.get(), prompt_options, - /*is_one_time=*/true); + /*is_one_time=*/true, mode); } NotifyRequestDecided(action); @@ -688,6 +689,16 @@ void PermissionRequestManager::Deny(const PromptOptions& prompt_options) { requests_[0]->GetContentSettingsType() == ContentSettingsType::GEOLOCATION_WITH_OPTIONS); + Deny_(prompt_options, content_settings::mojom::LifetimeMode::ALWAYS); +} + +void PermissionRequestManager::DenyThisTime(const PromptOptions& prompt_options, + content_settings::mojom::LifetimeMode mode) { + Deny_(prompt_options, mode); +} + +void PermissionRequestManager::Deny_(const PromptOptions& prompt_options, + content_settings::mojom::LifetimeMode lifetime_option) { if (ignore_callbacks_from_prompt_) { return; } @@ -708,7 +719,7 @@ void PermissionRequestManager::Deny(const PromptOptions& prompt_options) { for (const auto& request : requests_) { StorePermissionActionForUMA(request->requesting_origin(), request->request_type(), action); - PermissionDeniedIncludingDuplicates(request.get()); + PermissionDeniedIncludingDuplicates(request.get(), lifetime_option); } NotifyRequestDecided(action); @@ -1462,32 +1473,33 @@ PermissionRequestManager::VisitDuplicateRequests( void PermissionRequestManager::PermissionGrantedIncludingDuplicates( PermissionRequest* request, const PromptOptions& prompt_options, - bool is_one_time) { + bool is_one_time, + content_settings::mojom::LifetimeMode lifetime_option) { CHECK(RequestExistsExactlyOnce(request, pending_permission_requests_, requests_)) << "Only requests in [pending_permission_]requests_ can have duplicates"; - request->PermissionGranted(prompt_options, is_one_time); + request->PermissionGranted(prompt_options, is_one_time, lifetime_option); VisitDuplicateRequests( base::BindRepeating( - [](const PromptOptions& prompt_options, bool is_one_time, + [](const PromptOptions& prompt_options, bool is_one_time, content_settings::mojom::LifetimeMode lifetime_option, const std::unique_ptr& request) { - request->PermissionGranted(prompt_options, is_one_time); + request->PermissionGranted(prompt_options, is_one_time, lifetime_option); }, - prompt_options, is_one_time), + prompt_options, is_one_time, lifetime_option), request); } void PermissionRequestManager::PermissionDeniedIncludingDuplicates( - PermissionRequest* request) { + PermissionRequest* request, content_settings::mojom::LifetimeMode lifetime_option) { CHECK(RequestExistsExactlyOnce(request, pending_permission_requests_, requests_)) << "Only requests in [pending_permission_]requests_ can have duplicates"; - request->PermissionDenied(); + request->PermissionDenied(lifetime_option); VisitDuplicateRequests( base::BindRepeating( - [](const std::unique_ptr& request) { - request->PermissionDenied(); - }), + [](content_settings::mojom::LifetimeMode lifetime_option, const std::unique_ptr& request) { + request->PermissionDenied(lifetime_option); + }, lifetime_option), request); } @@ -1785,7 +1797,7 @@ void PermissionRequestManager::DoAutoResponseForTesting() { } switch (auto_response_for_test_) { case ACCEPT_ONCE: - AcceptThisTime(auto_response_prompt_options_for_test_); + AcceptThisTime(auto_response_prompt_options_for_test_, content_settings::mojom::LifetimeMode::ONLY_THIS_TIME); break; case ACCEPT_ALL: Accept(auto_response_prompt_options_for_test_); diff --git a/components/permissions/permission_request_manager.h b/components/permissions/permission_request_manager.h --- a/components/permissions/permission_request_manager.h +++ b/components/permissions/permission_request_manager.h @@ -188,8 +188,10 @@ class PermissionRequestManager GURL GetRequestingOrigin() const override; GURL GetEmbeddingOrigin() const override; void Accept(const PromptOptions& prompt_options) override; - void AcceptThisTime(const PromptOptions& prompt_options) override; + void AcceptThisTime(const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode lifetime_option) override; void Deny(const PromptOptions& prompt_options) override; + void Deny_(const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode lifetime_option); + void DenyThisTime(const PromptOptions& prompt_options, content_settings::mojom::LifetimeMode lifetime_option) override; void Dismiss(const PromptOptions& prompt_options) override; void Ignore(const PromptOptions& prompt_options) override; void FinalizeCurrentRequests() override; @@ -448,9 +450,11 @@ class PermissionRequestManager // Calls PermissionGranted on a request and all its duplicates. void PermissionGrantedIncludingDuplicates(PermissionRequest* request, const PromptOptions& prompt_options, - bool is_one_time); + bool is_one_time, + content_settings::mojom::LifetimeMode lifetime_option); // Calls PermissionDenied on a request and all its duplicates. - void PermissionDeniedIncludingDuplicates(PermissionRequest* request); + void PermissionDeniedIncludingDuplicates(PermissionRequest* request, + content_settings::mojom::LifetimeMode lifetime_option); // Calls Cancelled on a request and all its duplicates. void CancelRequestIncludingDuplicates(PermissionRequest* request, bool is_final_decision = true); --