| // Copyright 2012 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/background/background_contents_service.h" |
| |
| #include <utility> |
| |
| #include "base/compiler_specific.h" |
| #include "base/functional/bind.h" |
| #include "base/location.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/observer_list.h" |
| #include "base/one_shot_event.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/apps/platform_apps/app_load_service.h" |
| #include "chrome/browser/background/background_contents_service_factory.h" |
| #include "chrome/browser/background/background_contents_service_observer.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/notifications/notification_common.h" |
| #include "chrome/browser/notifications/notification_display_service.h" |
| #include "chrome/browser/notifications/notification_display_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_tabstrip.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "content/public/browser/site_instance.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/extension_host.h" |
| #include "extensions/browser/extension_registrar.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/image_loader.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_id.h" |
| #include "extensions/common/extension_set.h" |
| #include "extensions/common/icons/extension_icon_set.h" |
| #include "extensions/common/manifest_handlers/background_info.h" |
| #include "extensions/common/manifest_handlers/icons_handler.h" |
| #include "extensions/grit/extensions_browser_resources.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/message_center/public/cpp/notification.h" |
| #include "ui/message_center/public/cpp/notification_delegate.h" |
| #include "ui/message_center/public/cpp/notification_types.h" |
| #include "ui/message_center/public/cpp/notifier_id.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "ash/constants/notifier_catalogs.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| using content::SiteInstance; |
| using content::WebContents; |
| using extensions::BackgroundInfo; |
| using extensions::Extension; |
| using extensions::UnloadedExtensionReason; |
| |
| namespace { |
| |
| const char kCrashedNotificationPrefix[] = "app.background.crashed."; |
| const char kNotifierId[] = "app.background.crashed"; |
| bool g_disable_close_balloon_for_testing = false; |
| |
| void CloseBalloon(const std::string& extension_id, Profile* profile) { |
| if (g_disable_close_balloon_for_testing) |
| return; |
| |
| NotificationDisplayServiceFactory::GetForProfile(profile)->Close( |
| NotificationHandler::Type::TRANSIENT, |
| kCrashedNotificationPrefix + extension_id); |
| } |
| |
| // Delegate for the app/extension crash notification balloon. Restarts the |
| // app/extension when the balloon is clicked. |
| class CrashNotificationDelegate : public message_center::NotificationDelegate { |
| public: |
| CrashNotificationDelegate(Profile* profile, const Extension* extension) |
| : profile_(profile), |
| is_hosted_app_(extension->is_hosted_app()), |
| is_platform_app_(extension->is_platform_app()), |
| extension_id_(extension->id()) {} |
| |
| CrashNotificationDelegate(const CrashNotificationDelegate&) = delete; |
| CrashNotificationDelegate& operator=(const CrashNotificationDelegate&) = |
| delete; |
| |
| void Click(const std::optional<int>& button_index, |
| const std::optional<std::u16string>& reply) override { |
| // Pass arguments by value as HandleClick() might destroy *this. |
| HandleClick(is_hosted_app_, is_platform_app_, extension_id_, profile_); |
| // *this might be destroyed now, do not access any members anymore! |
| } |
| |
| private: |
| ~CrashNotificationDelegate() override = default; |
| |
| // Static to prevent accidental use of members as *this might get destroyed. |
| static void HandleClick(bool is_hosted_app, |
| bool is_platform_app, |
| std::string extension_id, |
| Profile* profile) { |
| // http://crbug.com/247790 involves a crash notification balloon being |
| // clicked while the extension isn't in the TERMINATED state. In that case, |
| // any of the "reload" methods called below can unload the extension, which |
| // indirectly destroys the CrashNotificationDelegate, invalidating all its |
| // member variables. Make sure to pass arguments by value when adding new |
| // ones to this method. |
| if (is_hosted_app) { |
| // There can be a race here: user clicks the balloon, and simultaneously |
| // reloads the sad tab for the app. So we check here to be safe before |
| // loading the background page. |
| BackgroundContentsService* service = |
| BackgroundContentsServiceFactory::GetForProfile(profile); |
| if (!service->GetAppBackgroundContents(extension_id)) |
| service->LoadBackgroundContentsForExtension(extension_id); |
| } else if (is_platform_app) { |
| apps::AppLoadService::Get(profile)->RestartApplication(extension_id); |
| } else { |
| extensions::ExtensionRegistrar::Get(profile)->ReloadExtension( |
| extension_id); |
| } |
| |
| CloseBalloon(extension_id, profile); |
| } |
| |
| // This dangling raw_ptr occurred in: |
| // unit_tests: |
| // BackgroundContentsServiceNotificationTest.TestShowBalloonShutdown |
| // https://ci.chromium.org/ui/p/chromium/builders/try/linux-rel/1427454/test-results?q=ExactID%3Aninja%3A%2F%2Fchrome%2Ftest%3Aunit_tests%2FBackgroundContentsServiceNotificationTest.TestShowBalloonShutdown+VHash%3A728d3f3a440b40c1 |
| raw_ptr<Profile, FlakyDanglingUntriaged> profile_; |
| bool is_hosted_app_; |
| bool is_platform_app_; |
| extensions::ExtensionId extension_id_; |
| }; |
| |
| void ReloadExtension(const std::string& extension_id, Profile* profile) { |
| if (g_browser_process->IsShuttingDown() || |
| !g_browser_process->profile_manager()->IsValidProfile(profile)) { |
| return; |
| } |
| |
| auto* extension_registrar = extensions::ExtensionRegistrar::Get(profile); |
| auto* extension_registry = extensions::ExtensionRegistry::Get(profile); |
| if (!extension_registrar || !extension_registry) { |
| return; |
| } |
| |
| if (!extension_registry->terminated_extensions().GetByID(extension_id)) { |
| // Either the app/extension was uninstalled by policy or it has since |
| // been restarted successfully by someone else (the user). |
| return; |
| } |
| extension_registrar->ReloadExtension(extension_id); |
| } |
| |
| } // namespace |
| |
| // Keys for the information we store about individual BackgroundContents in |
| // prefs. There is one top-level base::Value::Dict (stored at |
| // prefs::kRegisteredBackgroundContents). Information about each |
| // BackgroundContents is stored under that top-level base::Value::Dict, keyed |
| // by the parent application ID for easy lookup. |
| // |
| // kRegisteredBackgroundContents: |
| // base::Value::Dict { |
| // <appid_1>: { "url": <url1>, "name": <frame_name> }, |
| // <appid_2>: { "url": <url2>, "name": <frame_name> }, |
| // ... etc ... |
| // } |
| const char kUrlKey[] = "url"; |
| const char kFrameNameKey[] = "name"; |
| |
| // Defines the backoff policy used for attempting to reload extensions. |
| const net::BackoffEntry::Policy kExtensionReloadBackoffPolicy = { |
| 0, // Initial errors to ignore before applying backoff. |
| 3000, // Initial delay: 3 seconds. |
| 2, // Multiply factor. |
| 0.1, // Fuzzing percentage. |
| -1, // Maximum backoff time: -1 for no maximum. |
| -1, // Entry lifetime: -1 to never discard. |
| false, // Whether to always use initial delay. No-op as there are |
| // no initial errors to ignore. |
| }; |
| |
| int BackgroundContentsService::restart_delay_in_ms_ = 3000; // 3 seconds. |
| |
| BackgroundContentsService::BackgroundContentsService(Profile* profile) |
| : profile_(profile) { |
| // Don't load/store preferences if the parent profile is incognito. |
| if (!profile->IsOffTheRecord()) |
| prefs_ = profile->GetPrefs(); |
| |
| // Listen for events to tell us when to load/unload persisted background |
| // contents. |
| StartObserving(); |
| } |
| |
| BackgroundContentsService::~BackgroundContentsService() { |
| for (auto& observer : observers_) |
| observer.OnBackgroundContentsServiceDestroying(); |
| } |
| |
| // static |
| void BackgroundContentsService:: |
| SetRestartDelayForForceInstalledAppsAndExtensionsForTesting( |
| int restart_delay_in_ms) { |
| restart_delay_in_ms_ = restart_delay_in_ms; |
| } |
| |
| // static |
| std::string |
| BackgroundContentsService::GetNotificationDelegateIdForExtensionForTesting( |
| const std::string& extension_id) { |
| return kCrashedNotificationPrefix + extension_id; |
| } |
| |
| // static |
| void BackgroundContentsService::DisableCloseBalloonForTesting( |
| bool disable_close_balloon_for_testing) { |
| g_disable_close_balloon_for_testing = disable_close_balloon_for_testing; |
| } |
| |
| void BackgroundContentsService::ShowBalloonForTesting( |
| const extensions::Extension* extension) { |
| ShowBalloon(extension); |
| } |
| |
| std::vector<BackgroundContents*> |
| BackgroundContentsService::GetBackgroundContents() const { |
| std::vector<BackgroundContents*> contents; |
| for (auto it = contents_map_.begin(); it != contents_map_.end(); ++it) |
| contents.push_back(it->second.contents.get()); |
| return contents; |
| } |
| |
| void BackgroundContentsService::StartObserving() { |
| // On startup, load our background pages after extension-apps have loaded. |
| extensions::ExtensionSystem::Get(profile_)->ready().Post( |
| FROM_HERE, |
| base::BindOnce(&BackgroundContentsService::OnExtensionSystemReady, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| extension_host_registry_observation_.Observe( |
| extensions::ExtensionHostRegistry::Get(profile_)); |
| |
| // Listen for extension uninstall, load, unloaded notification. |
| extension_registry_observation_.Observe( |
| extensions::ExtensionRegistry::Get(profile_)); |
| } |
| |
| void BackgroundContentsService::OnExtensionSystemReady() { |
| LoadBackgroundContentsFromManifests(); |
| LoadBackgroundContentsFromPrefs(); |
| SendChangeNotification(); |
| } |
| |
| void BackgroundContentsService::OnExtensionHostRenderProcessGone( |
| content::BrowserContext* browser_context, |
| extensions::ExtensionHost* extension_host) { |
| // The notification may be for the on/off-the-record pair to this object's |
| // `profile_`; since the BackgroundContentsService has its own instance in |
| // incognito, only consider notifications from our exact context. |
| if (browser_context != profile_) |
| return; |
| |
| TRACE_EVENT0("browser,startup", |
| "BackgroundContentsService::OnExtensionHostRenderProcessGone"); |
| HandleExtensionCrashed(extension_host->extension()); |
| } |
| |
| void BackgroundContentsService::OnExtensionLoaded( |
| content::BrowserContext* browser_context, |
| const extensions::Extension* extension) { |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| if (extension->is_hosted_app() && |
| BackgroundInfo::HasBackgroundPage(extension)) { |
| // If there is a background page specified in the manifest for a hosted |
| // app, then blow away registered urls in the pref. |
| ShutdownAssociatedBackgroundContents(extension->id()); |
| |
| extensions::ExtensionSystem* extension_system = |
| extensions::ExtensionSystem::Get(profile); |
| |
| if (extension_system->is_ready()) { |
| // Now load the manifest-specified background page. If service isn't |
| // ready, then the background page will be loaded from the |
| // EXTENSIONS_READY callback. |
| LoadBackgroundContents(BackgroundInfo::GetBackgroundURL(extension), |
| "background", extension->id()); |
| } |
| } |
| |
| // If there is an existing BackoffEntry for the extension, clear it if |
| // the component extension stays loaded for 60 seconds. This avoids the |
| // situation of effectively disabling an extension for the entire browser |
| // session if there was a periodic crash (sometimes caused by another source). |
| if (extensions::Manifest::IsComponentLocation(extension->location())) { |
| ComponentExtensionBackoffEntryMap::const_iterator it = |
| component_backoff_map_.find(extension->id()); |
| if (it != component_backoff_map_.end()) { |
| net::BackoffEntry* entry = component_backoff_map_[extension->id()].get(); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&BackgroundContentsService::MaybeClearBackoffEntry, |
| weak_ptr_factory_.GetWeakPtr(), extension->id(), |
| entry->failure_count()), |
| base::Seconds(60)); |
| } |
| } |
| |
| // Close the crash notification balloon for the app/extension, if any. |
| CloseBalloon(extension->id(), profile); |
| SendChangeNotification(); |
| } |
| |
| void BackgroundContentsService::OnExtensionUnloaded( |
| content::BrowserContext* browser_context, |
| const extensions::Extension* extension, |
| extensions::UnloadedExtensionReason reason) { |
| switch (reason) { |
| case UnloadedExtensionReason::DISABLE: // Fall through. |
| case UnloadedExtensionReason::TERMINATE: // Fall through. |
| case UnloadedExtensionReason::UNINSTALL: // Fall through. |
| case UnloadedExtensionReason::BLOCKLIST: // Fall through. |
| case UnloadedExtensionReason::LOCK_ALL: // Fall through. |
| case UnloadedExtensionReason::MIGRATED_TO_COMPONENT: // Fall through. |
| case UnloadedExtensionReason::PROFILE_SHUTDOWN: |
| ShutdownAssociatedBackgroundContents(extension->id()); |
| SendChangeNotification(); |
| return; |
| case UnloadedExtensionReason::UPDATE: { |
| // If there is a manifest specified background page, then shut it down |
| // here, since if the updated extension still has the background page, |
| // then it will be loaded from LOADED callback. Otherwise, leave |
| // BackgroundContents in place. |
| // We don't call SendChangeNotification here - it will be generated |
| // from the LOADED callback. |
| if (BackgroundInfo::HasBackgroundPage(extension)) |
| ShutdownAssociatedBackgroundContents(extension->id()); |
| return; |
| case UnloadedExtensionReason::UNDEFINED: |
| // Fall through to undefined case. |
| break; |
| } |
| } |
| NOTREACHED() << "Undefined UnloadedExtensionReason."; |
| } |
| |
| void BackgroundContentsService::OnExtensionUninstalled( |
| content::BrowserContext* browser_context, |
| const extensions::Extension* extension, |
| extensions::UninstallReason reason) { |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| // Make sure the extension-crash balloons are removed when the extension is |
| // uninstalled/reloaded. We cannot do this from UNLOADED since a crashed |
| // extension is unloaded immediately after the crash, not when user reloads or |
| // uninstalls the extension. |
| CloseBalloon(extension->id(), profile); |
| } |
| |
| void BackgroundContentsService::RestartForceInstalledExtensionOnCrash( |
| const Extension* extension) { |
| int restart_delay = restart_delay_in_ms_; |
| |
| // If the extension was a component extension, use exponential backoff when |
| // attempting to reload. |
| if (extensions::Manifest::IsComponentLocation(extension->location())) { |
| ComponentExtensionBackoffEntryMap::const_iterator it = |
| component_backoff_map_.find(extension->id()); |
| |
| // Create a BackoffEntry if this is the first time we try to reload this |
| // particular extension. |
| if (it == component_backoff_map_.end()) { |
| std::unique_ptr<net::BackoffEntry> backoff_entry( |
| new net::BackoffEntry(&kExtensionReloadBackoffPolicy)); |
| component_backoff_map_.insert( |
| std::pair<extensions::ExtensionId, |
| std::unique_ptr<net::BackoffEntry>>( |
| extension->id(), std::move(backoff_entry))); |
| } |
| |
| net::BackoffEntry* entry = component_backoff_map_[extension->id()].get(); |
| entry->InformOfRequest(false); |
| restart_delay = entry->GetTimeUntilRelease().InMilliseconds(); |
| } |
| |
| // Ugly implementation detail: ExtensionService listens to the same |
| // notification that can lead us here, and asynchronously marks the extension |
| // as terminated (with an effective delay of 0) to allow other systems to |
| // receive the notification. However, that means we need to ensure this task |
| // gets ran *after* that, so that the extension is in the terminated set by |
| // the time we try to reload it (even if this service gets the notification |
| // first). |
| // |
| // TODO(devlin): This would be unnecessary if we listened to the |
| // OnExtensionUnloaded() notification and checked the unload reason. |
| DCHECK_GT(restart_delay, 0); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, base::BindOnce(&ReloadExtension, extension->id(), profile_), |
| base::Milliseconds(restart_delay)); |
| } |
| |
| // Loads all background contents whose urls have been stored in prefs. |
| void BackgroundContentsService::LoadBackgroundContentsFromPrefs() { |
| if (!prefs_) |
| return; |
| const base::Value::Dict& contents = |
| prefs_->GetDict(prefs::kRegisteredBackgroundContents); |
| extensions::ExtensionRegistry* extension_registry = |
| extensions::ExtensionRegistry::Get(profile_); |
| DCHECK(extension_registry); |
| for (const auto [extension_id, _] : contents) { |
| // Check to make sure that the parent extension is still enabled. |
| const Extension* extension = |
| extension_registry->enabled_extensions().GetByID(extension_id); |
| if (!extension) { |
| // Normally, we shouldn't reach here - it shouldn't be possible for an app |
| // to become uninstalled without the associated BackgroundContents being |
| // unregistered via the EXTENSIONS_UNLOADED notification. However, this |
| // is possible if there's e.g. a crash before we could save our prefs or |
| // the user deletes the extension files manually rather than uninstalling |
| // it. |
| LOG(ERROR) << "No extension found for BackgroundContents - id = " |
| << extension_id; |
| // Don't cancel out of our loop, just ignore this BackgroundContents and |
| // load the next one. |
| continue; |
| } |
| LoadBackgroundContentsFromDictionary(extension_id, contents); |
| } |
| } |
| |
| void BackgroundContentsService::SendChangeNotification() { |
| for (auto& observer : observers_) |
| observer.OnBackgroundContentsServiceChanged(); |
| } |
| |
| void BackgroundContentsService::MaybeClearBackoffEntry( |
| const std::string extension_id, |
| int expected_failure_count) { |
| ComponentExtensionBackoffEntryMap::const_iterator it = |
| component_backoff_map_.find(extension_id); |
| if (it == component_backoff_map_.end()) |
| return; |
| |
| net::BackoffEntry* entry = component_backoff_map_[extension_id].get(); |
| |
| // Only remove the BackoffEntry if there has has been no failure for |
| // |extension_id| since loading. |
| if (entry->failure_count() == expected_failure_count) |
| component_backoff_map_.erase(it); |
| } |
| |
| void BackgroundContentsService::LoadBackgroundContentsForExtension( |
| const std::string& extension_id) { |
| // First look if the manifest specifies a background page. |
| const Extension* extension = extensions::ExtensionRegistry::Get(profile_) |
| ->enabled_extensions() |
| .GetByID(extension_id); |
| DCHECK(!extension || extension->is_hosted_app()); |
| if (extension && BackgroundInfo::HasBackgroundPage(extension)) { |
| LoadBackgroundContents(BackgroundInfo::GetBackgroundURL(extension), |
| "background", extension->id()); |
| return; |
| } |
| |
| // Now look in the prefs. |
| if (!prefs_) |
| return; |
| const base::Value::Dict& contents = |
| prefs_->GetDict(prefs::kRegisteredBackgroundContents); |
| LoadBackgroundContentsFromDictionary(extension_id, contents); |
| } |
| |
| void BackgroundContentsService::LoadBackgroundContentsFromDictionary( |
| const std::string& extension_id, |
| const base::Value::Dict& contents) { |
| extensions::ExtensionService* extensions_service = |
| extensions::ExtensionSystem::Get(profile_)->extension_service(); |
| DCHECK(extensions_service); |
| |
| const base::Value::Dict* dict = contents.FindDict(extension_id); |
| if (!dict) |
| return; |
| |
| const std::string* maybe_frame_name = dict->FindString(kUrlKey); |
| const std::string* maybe_url = dict->FindString(kFrameNameKey); |
| std::string frame_name = maybe_frame_name ? *maybe_frame_name : std::string(); |
| std::string url = maybe_url ? *maybe_url : std::string(); |
| |
| LoadBackgroundContents(GURL(url), frame_name, extension_id); |
| } |
| |
| void BackgroundContentsService::LoadBackgroundContentsFromManifests() { |
| for (const scoped_refptr<const extensions::Extension>& extension : |
| extensions::ExtensionRegistry::Get(profile_)->enabled_extensions()) { |
| if (extension->is_hosted_app() && |
| BackgroundInfo::HasBackgroundPage(extension.get())) { |
| LoadBackgroundContents(BackgroundInfo::GetBackgroundURL(extension.get()), |
| "background", extension->id()); |
| } |
| } |
| } |
| |
| void BackgroundContentsService::LoadBackgroundContents( |
| const GURL& url, |
| const std::string& frame_name, |
| const std::string& application_id) { |
| // We are depending on the fact that we will initialize before any user |
| // actions or session restore can take place, so no BackgroundContents should |
| // be running yet for the passed application_id. |
| DCHECK(!GetAppBackgroundContents(application_id)); |
| DCHECK(!application_id.empty()); |
| DCHECK(url.is_valid()); |
| DVLOG(1) << "Loading background content url: " << url; |
| |
| BackgroundContents* contents = CreateBackgroundContents( |
| SiteInstance::CreateForURL(profile_, url), nullptr, true, frame_name, |
| application_id, content::StoragePartitionConfig::CreateDefault(profile_), |
| nullptr); |
| |
| contents->CreateRendererSoon(url); |
| } |
| |
| BackgroundContents* BackgroundContentsService::CreateBackgroundContents( |
| scoped_refptr<SiteInstance> site, |
| content::RenderFrameHost* opener, |
| bool is_new_browsing_instance, |
| const std::string& frame_name, |
| const std::string& application_id, |
| const content::StoragePartitionConfig& partition_config, |
| content::SessionStorageNamespace* session_storage_namespace) { |
| auto contents = std::make_unique<BackgroundContents>( |
| std::move(site), opener, is_new_browsing_instance, this, partition_config, |
| session_storage_namespace); |
| BackgroundContents* contents_ptr = contents.get(); |
| AddBackgroundContents(std::move(contents), application_id, frame_name); |
| |
| // Register the BackgroundContents internally, then send out a notification |
| // to external listeners. |
| BackgroundContentsOpenedDetails details = {contents_ptr, raw_ref(frame_name), |
| raw_ref(application_id)}; |
| for (auto& observer : observers_) |
| observer.OnBackgroundContentsOpened(details); |
| |
| // A new background contents has been created - notify our listeners. |
| SendChangeNotification(); |
| return contents_ptr; |
| } |
| |
| void BackgroundContentsService::DeleteBackgroundContents( |
| BackgroundContents* contents) { |
| contents_map_.erase(GetParentApplicationId(contents)); |
| SendChangeNotification(); |
| } |
| |
| void BackgroundContentsService::RegisterBackgroundContents( |
| BackgroundContents* background_contents) { |
| DCHECK(IsTracked(background_contents)); |
| if (!prefs_) |
| return; |
| |
| // We store the first URL we receive for a given application. If there's |
| // already an entry for this application, no need to do anything. |
| // TODO(atwilson): Verify that this is the desired behavior based on developer |
| // feedback (http://crbug.com/47118). |
| ScopedDictPrefUpdate update(prefs_, prefs::kRegisteredBackgroundContents); |
| base::Value::Dict& pref = update.Get(); |
| const std::string& appid = GetParentApplicationId(background_contents); |
| if (pref.FindDict(appid)) { |
| return; |
| } |
| |
| // No entry for this application yet, so add one. |
| base::Value::Dict dict; |
| dict.Set(kUrlKey, background_contents->GetURL().spec()); |
| dict.Set(kFrameNameKey, contents_map_[appid].frame_name); |
| pref.Set(appid, std::move(dict)); |
| } |
| |
| bool BackgroundContentsService::HasRegisteredBackgroundContents( |
| const std::string& app_id) { |
| if (!prefs_) |
| return false; |
| const base::Value::Dict& contents = |
| prefs_->GetDict(prefs::kRegisteredBackgroundContents); |
| return contents.Find(app_id); |
| } |
| |
| void BackgroundContentsService::UnregisterBackgroundContents( |
| BackgroundContents* background_contents) { |
| if (!prefs_) |
| return; |
| DCHECK(IsTracked(background_contents)); |
| const std::string& appid = GetParentApplicationId(background_contents); |
| ScopedDictPrefUpdate update(prefs_, prefs::kRegisteredBackgroundContents); |
| update->Remove(appid); |
| } |
| |
| void BackgroundContentsService::ShutdownAssociatedBackgroundContents( |
| const std::string& appid) { |
| BackgroundContents* contents = GetAppBackgroundContents(appid); |
| if (contents) { |
| UnregisterBackgroundContents(contents); |
| // Background contents destructor shuts down the renderer. |
| DeleteBackgroundContents(contents); |
| } |
| } |
| |
| void BackgroundContentsService::AddBackgroundContents( |
| std::unique_ptr<BackgroundContents> contents, |
| const std::string& application_id, |
| const std::string& frame_name) { |
| // Add the passed object to our list. |
| DCHECK(!application_id.empty()); |
| BackgroundContentsInfo& info = contents_map_[application_id]; |
| info.contents = std::move(contents); |
| info.frame_name = frame_name; |
| |
| CloseBalloon(application_id, profile_); |
| } |
| |
| // Used by test code and debug checks to verify whether a given |
| // BackgroundContents is being tracked by this instance. |
| bool BackgroundContentsService::IsTracked( |
| BackgroundContents* background_contents) const { |
| return !GetParentApplicationId(background_contents).empty(); |
| } |
| |
| void BackgroundContentsService::AddObserver( |
| BackgroundContentsServiceObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void BackgroundContentsService::RemoveObserver( |
| BackgroundContentsServiceObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| BackgroundContents* BackgroundContentsService::GetAppBackgroundContents( |
| const std::string& application_id) { |
| BackgroundContentsMap::const_iterator it = contents_map_.find(application_id); |
| return (it != contents_map_.end()) ? it->second.contents.get() : nullptr; |
| } |
| |
| const std::string& BackgroundContentsService::GetParentApplicationId( |
| BackgroundContents* contents) const { |
| for (auto it = contents_map_.begin(); it != contents_map_.end(); ++it) { |
| if (contents == it->second.contents.get()) |
| return it->first; |
| } |
| return base::EmptyString(); |
| } |
| |
| void BackgroundContentsService::AddWebContents( |
| std::unique_ptr<WebContents> new_contents, |
| const GURL& target_url, |
| WindowOpenDisposition disposition, |
| const blink::mojom::WindowFeatures& window_features, |
| bool* was_blocked) { |
| Browser* browser = chrome::FindLastActiveWithProfile( |
| Profile::FromBrowserContext(new_contents->GetBrowserContext())); |
| if (browser) { |
| chrome::AddWebContents(browser, nullptr, std::move(new_contents), |
| target_url, disposition, window_features); |
| } |
| } |
| |
| void BackgroundContentsService::OnBackgroundContentsNavigated( |
| BackgroundContents* contents) { |
| DCHECK(IsTracked(contents)); |
| // Do not register in the pref if the extension has a manifest-specified |
| // background page. |
| const std::string& appid = GetParentApplicationId(contents); |
| extensions::ExtensionRegistry* extension_registry = |
| extensions::ExtensionRegistry::Get(profile_); |
| const Extension* extension = |
| extension_registry->enabled_extensions().GetByID(appid); |
| if (extension && BackgroundInfo::HasBackgroundPage(extension)) |
| return; |
| RegisterBackgroundContents(contents); |
| } |
| |
| void BackgroundContentsService::OnBackgroundContentsTerminated( |
| BackgroundContents* contents) { |
| HandleExtensionCrashed(extensions::ExtensionRegistry::Get(profile_) |
| ->enabled_extensions() |
| .GetByID(GetParentApplicationId(contents))); |
| DeleteBackgroundContents(contents); |
| } |
| |
| void BackgroundContentsService::OnBackgroundContentsClosed( |
| BackgroundContents* contents) { |
| DCHECK(IsTracked(contents)); |
| UnregisterBackgroundContents(contents); |
| DeleteBackgroundContents(contents); |
| for (auto& observer : observers_) |
| observer.OnBackgroundContentsClosed(); |
| } |
| |
| void BackgroundContentsService::Shutdown() { |
| contents_map_.clear(); |
| } |
| |
| void BackgroundContentsService::HandleExtensionCrashed( |
| const extensions::Extension* extension) { |
| // When the extensions crash, notify the user about it and restart the crashed |
| // contents. |
| if (!extension) |
| return; |
| |
| const bool force_installed = |
| extensions::Manifest::IsComponentLocation(extension->location()) || |
| extensions::Manifest::IsPolicyLocation(extension->location()); |
| if (!force_installed) { |
| ShowBalloon(extension); |
| } else { |
| // Restart the extension. |
| RestartForceInstalledExtensionOnCrash(extension); |
| } |
| } |
| |
| void BackgroundContentsService::NotificationImageReady( |
| const std::string extension_name, |
| const std::string extension_id, |
| const std::u16string message, |
| scoped_refptr<message_center::NotificationDelegate> delegate, |
| const gfx::Image& icon) { |
| NotificationDisplayService* notification_service = |
| NotificationDisplayServiceFactory::GetForProfile(profile_); |
| CHECK(notification_service); |
| |
| if (g_browser_process->IsShuttingDown()) { |
| return; |
| } |
| |
| gfx::Image notification_icon(icon); |
| if (notification_icon.IsEmpty()) { |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| notification_icon = rb.GetImageNamed(IDR_EXTENSION_DEFAULT_ICON); |
| } |
| |
| // Origin URL must be different from the crashed extension to avoid the |
| // conflict. NotificationSystemObserver will cancel all notifications from |
| // the same origin when OnExtensionUnloaded() is called. |
| std::string id = kCrashedNotificationPrefix + extension_id; |
| message_center::Notification notification( |
| message_center::NOTIFICATION_TYPE_SIMPLE, id, std::u16string(), message, |
| ui::ImageModel::FromImage(notification_icon), std::u16string(), |
| GURL("chrome://extension-crash"), |
| #if BUILDFLAG(IS_CHROMEOS) |
| message_center::NotifierId( |
| message_center::NotifierType::SYSTEM_COMPONENT, kNotifierId, |
| ash::NotificationCatalogName::kBackgroundCrash), |
| #else |
| message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT, |
| kNotifierId), |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| {}, delegate); |
| notification_service->Display(NotificationHandler::Type::TRANSIENT, |
| notification, |
| /*metadata=*/nullptr); |
| } |
| |
| // Show a popup notification balloon with a crash message for a given app/ |
| // extension. |
| void BackgroundContentsService::ShowBalloon(const Extension* extension) { |
| const std::u16string message = l10n_util::GetStringFUTF16( |
| extension->is_app() ? IDS_BACKGROUND_CRASHED_APP_BALLOON_MESSAGE |
| : IDS_BACKGROUND_CRASHED_EXTENSION_BALLOON_MESSAGE, |
| base::UTF8ToUTF16(extension->name())); |
| extension_misc::ExtensionIcons size(extension_misc::EXTENSION_ICON_LARGE); |
| extensions::ExtensionResource resource = |
| extensions::IconsInfo::GetIconResource(extension, size, |
| ExtensionIconSet::Match::kSmaller); |
| // We can't just load the image in the Observe method below because, despite |
| // what this method is called, it may call the callback synchronously. |
| // However, it's possible that the extension went away during the interim, |
| // so we'll bind all the pertinent data here. |
| extensions::ImageLoader::Get(profile_)->LoadImageAsync( |
| extension, resource, gfx::Size(size, size), |
| base::BindOnce(&BackgroundContentsService::NotificationImageReady, |
| weak_ptr_factory_.GetWeakPtr(), extension->name(), |
| extension->id(), message, |
| base::MakeRefCounted<CrashNotificationDelegate>( |
| profile_, extension))); |
| } |
| |
| BackgroundContentsService::BackgroundContentsInfo::BackgroundContentsInfo() = |
| default; |
| BackgroundContentsService::BackgroundContentsInfo::~BackgroundContentsInfo() = |
| default; |