| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/extensions/api/file_system/consent_provider_impl.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/extensions/api/file_system/request_file_system_notification.h" |
| #include "chrome/browser/profiles/profiles_state.h" |
| #include "chrome/browser/ui/extensions/extensions_dialogs.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/app_window/app_window.h" |
| #include "extensions/browser/app_window/app_window_registry.h" |
| #include "extensions/browser/extensions_browser_client.h" |
| #include "extensions/browser/kiosk/kiosk_delegate.h" |
| #include "extensions/common/api/file_system.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/manifest_handlers/kiosk_mode_info.h" |
| #include "ui/base/mojom/dialog_button.mojom.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chromeos/ash/components/file_manager/app_id.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // List of allowlisted component apps and extensions by their ids for |
| // chrome.fileSystem.requestFileSystem. |
| const char* const kRequestFileSystemComponentAllowlist[] = { |
| #if BUILDFLAG(IS_CHROMEOS) |
| file_manager::kFileManagerAppId, file_manager::kImageLoaderExtensionId, |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| // TODO(henryhsu,b/110126438): Remove this extension id, and add it only |
| // for tests. |
| "pkplfbidichfdicaijlchgnapepdginl" // Testing extensions. |
| }; |
| |
| ui::mojom::DialogButton g_auto_dialog_button_for_test = |
| ui::mojom::DialogButton::kNone; |
| |
| // Gets a WebContents instance handle for a current window of a platform app |
| // with |app_id|. If not found, then returns NULL. |
| content::WebContents* GetWebContentsForAppId(Profile* profile, |
| const std::string& app_id) { |
| AppWindowRegistry* const registry = AppWindowRegistry::Get(profile); |
| DCHECK(registry); |
| AppWindow* const app_window = registry->GetCurrentAppWindowForApp(app_id); |
| return app_window ? app_window->web_contents() : nullptr; |
| } |
| |
| // Converts the clicked button to a consent result and passes it via the |
| // |callback|. |
| void DialogResultToConsent( |
| file_system_api::ConsentProviderImpl::ConsentCallback callback, |
| ui::mojom::DialogButton button) { |
| switch (button) { |
| case ui::mojom::DialogButton::kNone: |
| std::move(callback).Run(ConsentProvider::CONSENT_IMPOSSIBLE); |
| break; |
| case ui::mojom::DialogButton::kOk: |
| std::move(callback).Run(ConsentProvider::CONSENT_GRANTED); |
| break; |
| // The following is wired to both Cancel and Close callbacks. |
| case ui::mojom::DialogButton::kCancel: |
| std::move(callback).Run(ConsentProvider::CONSENT_REJECTED); |
| break; |
| } |
| } |
| |
| } // namespace |
| |
| namespace file_system_api { |
| |
| /******** ConsentProviderImpl::DelegateInterface ********/ |
| |
| ConsentProviderImpl::DelegateInterface::DelegateInterface() = default; |
| ConsentProviderImpl::DelegateInterface::~DelegateInterface() = default; |
| |
| /******** ConsentProviderImpl ********/ |
| |
| ConsentProviderImpl::ConsentProviderImpl( |
| std::unique_ptr<DelegateInterface> delegate) |
| : delegate_(std::move(delegate)) { |
| DCHECK(delegate_); |
| } |
| |
| ConsentProviderImpl::~ConsentProviderImpl() = default; |
| |
| void ConsentProviderImpl::RequestConsent(content::RenderFrameHost* host, |
| const Extension& extension, |
| const std::string& volume_id, |
| const std::string& volume_label, |
| bool writable, |
| ConsentCallback callback) { |
| DCHECK(IsGrantable(extension)); |
| |
| // If an allowlisted component, then no need to ask or inform the user. |
| if (extension.location() == mojom::ManifestLocation::kComponent && |
| delegate_->IsAllowlistedComponent(extension)) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), CONSENT_GRANTED)); |
| return; |
| } |
| |
| // If auto-launched kiosk app, then no need to ask user either, but show the |
| // notification. |
| if (delegate_->IsAutoLaunched(extension)) { |
| delegate_->ShowNotification(extension.id(), extension.name(), volume_id, |
| volume_label, writable); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), CONSENT_GRANTED)); |
| return; |
| } |
| |
| // If it's a kiosk app running in manual-launch kiosk session, then show the |
| // confirmation dialog. |
| if (KioskModeInfo::IsKioskOnly(&extension) && |
| profiles::IsChromeAppKioskSession()) { |
| delegate_->ShowDialog( |
| host, extension.id(), extension.name(), volume_id, volume_label, |
| writable, base::BindOnce(&DialogResultToConsent, std::move(callback))); |
| return; |
| } |
| |
| NOTREACHED() << "Cannot request consent for non-grantable extensions."; |
| } |
| |
| bool ConsentProviderImpl::IsGrantable(const Extension& extension) { |
| // Only kiosk apps in kiosk sessions can use file system API. |
| // Additionally it is enabled for allowlisted component extensions and apps. |
| const bool is_allowlisted_component = |
| delegate_->IsAllowlistedComponent(extension); |
| |
| const bool is_running_in_kiosk_session = |
| KioskModeInfo::IsKioskOnly(&extension) && |
| profiles::IsChromeAppKioskSession(); |
| |
| return is_allowlisted_component || is_running_in_kiosk_session; |
| } |
| |
| /******** ConsentProviderDelegate ********/ |
| |
| ConsentProviderDelegate::ConsentProviderDelegate(Profile* profile) |
| : profile_(profile) { |
| DCHECK(profile_); |
| profile_observation_.Observe(profile_); |
| } |
| |
| ConsentProviderDelegate::~ConsentProviderDelegate() = default; |
| |
| // static |
| void ConsentProviderDelegate::SetAutoDialogButtonForTest( |
| ui::mojom::DialogButton button) { |
| g_auto_dialog_button_for_test = button; |
| } |
| |
| void ConsentProviderDelegate::OnProfileWillBeDestroyed(Profile* profile) { |
| DCHECK_EQ(profile_, profile); |
| profile_observation_.Reset(); |
| profile_ = nullptr; |
| } |
| |
| void ConsentProviderDelegate::ShowDialog( |
| content::RenderFrameHost* host, |
| const extensions::ExtensionId& extension_id, |
| const std::string& extension_name, |
| const std::string& volume_id, |
| const std::string& volume_label, |
| bool writable, |
| file_system_api::ConsentProviderImpl::ShowDialogCallback callback) { |
| DCHECK(host); |
| // Reject if |profile_| is gone. |
| if (!profile_) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), ui::mojom::DialogButton::kNone)); |
| return; |
| } |
| |
| content::WebContents* web_contents = nullptr; |
| |
| // Find an app window to host the dialog. |
| content::WebContents* const foreground_contents = |
| content::WebContents::FromRenderFrameHost(host); |
| if (AppWindowRegistry::Get(profile_)->GetAppWindowForWebContents( |
| foreground_contents)) { |
| web_contents = foreground_contents; |
| } |
| |
| // If there is no web contents handle, then the method is most probably |
| // executed from a background page. |
| if (!web_contents) |
| web_contents = GetWebContentsForAppId(profile_, extension_id); |
| |
| if (!web_contents) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), ui::mojom::DialogButton::kNone)); |
| return; |
| } |
| |
| // Short circuit the user consent dialog for tests. This is far from a pretty |
| // code design. |
| if (g_auto_dialog_button_for_test != ui::mojom::DialogButton::kNone) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), |
| /*result=*/g_auto_dialog_button_for_test)); |
| return; |
| } |
| |
| ShowRequestFileSystemDialog(web_contents, extension_name, |
| volume_label.empty() ? volume_id : volume_label, |
| writable, std::move(callback)); |
| } |
| |
| void ConsentProviderDelegate::ShowNotification( |
| const extensions::ExtensionId& extension_id, |
| const std::string& extension_name, |
| const std::string& volume_id, |
| const std::string& volume_label, |
| bool writable) { |
| // Skip if |profile_| is gone. |
| if (!profile_) |
| return; |
| |
| ShowNotificationForAutoGrantedRequestFileSystem(profile_, extension_id, |
| extension_name, volume_id, |
| volume_label, writable); |
| } |
| |
| bool ConsentProviderDelegate::IsAutoLaunched(const Extension& extension) { |
| return ExtensionsBrowserClient::Get() |
| ->GetKioskDelegate() |
| ->IsAutoLaunchedKioskApp(extension.id()); |
| } |
| |
| bool ConsentProviderDelegate::IsAllowlistedComponent( |
| const Extension& extension) { |
| for (auto* allowlisted_id : kRequestFileSystemComponentAllowlist) { |
| if (extension.id().compare(allowlisted_id) == 0) |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace file_system_api |
| } // namespace extensions |