blob: 2f7c189c5852a5ab55d56bb34f7ae505341d7884 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stdint.h>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/optional.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_storage_monitor.h"
#include "chrome/browser/extensions/extension_storage_monitor_factory.h"
#include "chrome/browser/notifications/notification_display_service_tester.h"
#include "chrome/browser/ui/extensions/app_launch_params.h"
#include "chrome/browser/ui/extensions/application_launch.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_dialog_auto_confirm.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/common/constants.h"
#include "extensions/common/value_builder.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
#include "ui/message_center/public/cpp/notification.h"
namespace extensions {
namespace {
const int kInitialUsageThreshold = 500;
const char kWriteDataApp[] = "storage_monitor/write_data";
std::unique_ptr<KeyedService> CreateExtensionStorageMonitorInstance(
content::BrowserContext* context) {
return std::make_unique<ExtensionStorageMonitor>(
Profile::FromBrowserContext(context));
}
class NotificationObserver {
public:
NotificationObserver(NotificationDisplayServiceTester* display_service,
const std::string& target_notification)
: display_service_(display_service),
target_notification_id_(target_notification) {
// Don't count old notifications.
display_service_->RemoveAllNotifications(
NotificationHandler::Type::TRANSIENT, false);
}
~NotificationObserver() {
display_service_->SetNotificationAddedClosure(base::RepeatingClosure());
}
bool HasReceivedNotification() const {
return !!display_service_->GetNotification(target_notification_id_);
}
// Runs the message loop and returns true if a notification is received.
// Immediately returns true if a notification has already been received.
bool WaitForNotification() {
if (HasReceivedNotification())
return true;
waiting_ = true;
display_service_->SetNotificationAddedClosure(base::BindRepeating(
&NotificationObserver::OnNotificationAdded, base::Unretained(this)));
content::RunMessageLoop();
waiting_ = false;
return HasReceivedNotification();
}
private:
void OnNotificationAdded() {
if (waiting_ && HasReceivedNotification())
base::RunLoop::QuitCurrentWhenIdleDeprecated();
}
NotificationDisplayServiceTester* display_service_;
std::string target_notification_id_;
bool waiting_ = false;
};
} // namespace
class ExtensionStorageMonitorTest : public ExtensionBrowserTest {
public:
ExtensionStorageMonitorTest() = default;
protected:
// ExtensionBrowserTest overrides:
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
display_service_ =
std::make_unique<NotificationDisplayServiceTester>(profile());
host_resolver()->AddRule("*", "127.0.0.1");
InitStorageMonitor();
}
ExtensionStorageMonitor* monitor() {
CHECK(storage_monitor_);
return storage_monitor_.get();
}
int64_t GetInitialExtensionThreshold() {
CHECK(storage_monitor_);
return storage_monitor_->initial_extension_threshold_;
}
void DisableForInstalledExtensions() {
CHECK(storage_monitor_);
storage_monitor_->enable_for_all_extensions_ = false;
}
const Extension* InitWriteDataApp() {
base::FilePath path = test_data_dir_.AppendASCII(kWriteDataApp);
const Extension* extension = InstallExtension(path, 1);
EXPECT_TRUE(extension);
return extension;
}
const Extension* CreateHostedApp(const std::string& name,
GURL app_url,
std::vector<std::string> permissions) {
auto dir = std::make_unique<TestExtensionDir>();
url::Replacements<char> clear_port;
clear_port.ClearPort();
DictionaryBuilder manifest;
manifest.Set("name", name)
.Set("version", "1.0")
.Set("manifest_version", 2)
.Set(
"app",
DictionaryBuilder()
.Set("urls",
ListBuilder()
.Append(app_url.ReplaceComponents(clear_port).spec())
.Build())
.Set("launch",
DictionaryBuilder().Set("web_url", app_url.spec()).Build())
.Build());
ListBuilder permissions_builder;
for (const std::string& permission : permissions)
permissions_builder.Append(permission);
manifest.Set("permissions", permissions_builder.Build());
dir->WriteManifest(manifest.ToJSON());
const Extension* extension = LoadExtension(dir->UnpackedPath());
EXPECT_TRUE(extension);
temp_dirs_.push_back(std::move(dir));
return extension;
}
std::string GetNotificationId(const std::string& extension_id) {
return monitor()->GetNotificationId(extension_id);
}
bool IsStorageNotificationEnabled(const std::string& extension_id) {
return monitor()->IsStorageNotificationEnabled(extension_id);
}
int64_t GetNextStorageThreshold(const std::string& extension_id) {
return monitor()->GetNextStorageThreshold(extension_id);
}
void WriteBytesExpectingNotification(const Extension* extension,
int num_bytes,
const char* filesystem = "PERSISTENT") {
int64_t previous_threshold = GetNextStorageThreshold(extension->id());
WriteBytes(extension, num_bytes, filesystem, true);
ASSERT_GT(GetNextStorageThreshold(extension->id()), previous_threshold);
}
void WriteBytesNotExpectingNotification(
const Extension* extension,
int num_bytes,
const char* filesystem = "PERSISTENT") {
WriteBytes(extension, num_bytes, filesystem, false);
}
void SimulateProfileShutdown() {
// Setting a testing factory function deletes the current
// ExtensionStorageMonitor; see KeyedServiceFactory::SetTestingFactory().
ExtensionStorageMonitorFactory::GetInstance()->SetTestingFactoryAndUse(
profile(), base::BindRepeating(&CreateExtensionStorageMonitorInstance));
InitStorageMonitor();
}
void InitStorageMonitor() {
EXPECT_FALSE(storage_monitor_);
storage_monitor_ =
ExtensionStorageMonitor::Get(profile())->weak_ptr_factory_.GetWeakPtr();
ASSERT_TRUE(storage_monitor_);
// Override thresholds so that we don't have to write a huge amount of data
// to trigger notifications in these tests.
storage_monitor_->enable_for_all_extensions_ = true;
storage_monitor_->initial_extension_threshold_ = kInitialUsageThreshold;
// To ensure storage events are dispatched from QuotaManager immediately.
storage_monitor_->observer_rate_ = base::TimeDelta();
}
// Write bytes for a hosted app page that's loaded the script:
// //chrome/test/data/extensions/storage_monitor/hosted_apps/common.js
void WriteBytesForHostedApp(const Extension* extension,
int num_bytes,
const std::string& filesystem) {
content::WebContents* web_contents = OpenApplication(AppLaunchParams(
profile(), extension, LAUNCH_CONTAINER_TAB,
WindowOpenDisposition::SINGLETON_TAB, extensions::SOURCE_TEST));
ASSERT_TRUE(WaitForLoadStop(web_contents));
std::string result;
const char* script = R"(
HostedAppWriteData(%s, %d)
.then(() => domAutomationController.send('write_done'))
.catch(e => domAutomationController.send('write_error: ' + e));
)";
ASSERT_TRUE(ExecuteScriptAndExtractString(
web_contents, base::StringPrintf(script, filesystem.c_str(), num_bytes),
&result));
ASSERT_EQ("write_done", result);
}
// Write bytes for the extension loaded from:
// //chrome/test/data/extensions/storage_monitor/write_data
void WriteBytesForExtension(const Extension* extension, int num_bytes) {
ExtensionTestMessageListener launched_listener("launched", true);
ExtensionTestMessageListener write_complete_listener("write_complete",
false);
OpenApplication(AppLaunchParams(profile(), extension, LAUNCH_CONTAINER_NONE,
WindowOpenDisposition::NEW_WINDOW,
extensions::SOURCE_TEST));
ASSERT_TRUE(launched_listener.WaitUntilSatisfied());
// Instruct the app to write |num_bytes| of data.
launched_listener.Reply(base::NumberToString(num_bytes));
ASSERT_TRUE(write_complete_listener.WaitUntilSatisfied());
}
// Write a number of bytes to persistent storage.
void WriteBytes(const Extension* extension,
int num_bytes,
const std::string& filesystem,
bool expected_notification) {
NotificationObserver notification_observer(
display_service_.get(), GetNotificationId(extension->id()));
if (extension->is_hosted_app()) {
WriteBytesForHostedApp(extension, num_bytes, filesystem);
} else {
ASSERT_EQ("PERSISTENT", filesystem) << "Not implemented in the js code.";
WriteBytesForExtension(extension, num_bytes);
}
if (expected_notification) {
ASSERT_TRUE(notification_observer.WaitForNotification());
} else {
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(notification_observer.HasReceivedNotification());
}
}
base::WeakPtr<ExtensionStorageMonitor> storage_monitor_;
std::unique_ptr<NotificationDisplayServiceTester> display_service_;
std::vector<std::unique_ptr<TestExtensionDir>> temp_dirs_;
};
// Control - No notifications should be shown if usage remains under the
// threshold.
IN_PROC_BROWSER_TEST_F(ExtensionStorageMonitorTest, UnderThreshold) {
const Extension* extension = InitWriteDataApp();
ASSERT_TRUE(extension);
WriteBytesNotExpectingNotification(extension, 1);
}
// Ensure a notification is shown when usage reaches the first threshold.
IN_PROC_BROWSER_TEST_F(ExtensionStorageMonitorTest, ExceedInitialThreshold) {
const Extension* extension = InitWriteDataApp();
ASSERT_TRUE(extension);
WriteBytesExpectingNotification(extension, GetInitialExtensionThreshold());
}
// Ensure a notification is shown when usage immediately exceeds double the
// first threshold.
IN_PROC_BROWSER_TEST_F(ExtensionStorageMonitorTest, DoubleInitialThreshold) {
const Extension* extension = InitWriteDataApp();
ASSERT_TRUE(extension);
WriteBytesExpectingNotification(extension,
GetInitialExtensionThreshold() * 2);
}
// Ensure that notifications are not fired if the next threshold has not been
// reached.
IN_PROC_BROWSER_TEST_F(ExtensionStorageMonitorTest, ThrottleNotifications) {
const Extension* extension = InitWriteDataApp();
ASSERT_TRUE(extension);
// Exceed the first threshold.
WriteBytesExpectingNotification(extension, GetInitialExtensionThreshold());
// Stay within the next threshold.
WriteBytesNotExpectingNotification(extension, 1);
}
// Verify that notifications are disabled when the user clicks the action button
// in the notification.
IN_PROC_BROWSER_TEST_F(ExtensionStorageMonitorTest, UserDisabledNotifications) {
const Extension* extension = InitWriteDataApp();
ASSERT_TRUE(extension);
WriteBytesExpectingNotification(extension, GetInitialExtensionThreshold());
EXPECT_TRUE(IsStorageNotificationEnabled(extension->id()));
// Fake clicking the notification button to disable notifications.
display_service_->GetNotification(GetNotificationId(extension->id()))
->delegate()
->Click(ExtensionStorageMonitor::BUTTON_DISABLE_NOTIFICATION,
base::nullopt);
EXPECT_FALSE(IsStorageNotificationEnabled(extension->id()));
// Expect to receive no further notifications when usage continues to
// increase.
int64_t next_threshold = GetNextStorageThreshold(extension->id());
int64_t next_data_size = next_threshold - GetInitialExtensionThreshold();
ASSERT_GT(next_data_size, 0);
WriteBytesNotExpectingNotification(extension, next_data_size);
}
// Ensure that monitoring is disabled for installed extensions if
// |enable_for_all_extensions_| is false. This test can be removed if monitoring
// is eventually enabled for all extensions.
IN_PROC_BROWSER_TEST_F(ExtensionStorageMonitorTest,
DisableForInstalledExtensions) {
DisableForInstalledExtensions();
const Extension* extension = InitWriteDataApp();
ASSERT_TRUE(extension);
WriteBytesNotExpectingNotification(extension, GetInitialExtensionThreshold());
}
// Regression test for https://crbug.com/716426
IN_PROC_BROWSER_TEST_F(ExtensionStorageMonitorTest,
HostedAppTemporaryFilesystem) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(
"chromium.org", "/extensions/storage_monitor/hosted_apps/one/index.html");
const Extension* app =
CreateHostedApp("Hosted App", url, {"unlimitedStorage"});
EXPECT_NO_FATAL_FAILURE(WriteBytesExpectingNotification(
app, GetInitialExtensionThreshold(), "TEMPORARY"));
EXPECT_NO_FATAL_FAILURE(WriteBytesNotExpectingNotification(
app, GetInitialExtensionThreshold(), "PERSISTENT"));
EXPECT_NO_FATAL_FAILURE(WriteBytesExpectingNotification(
app, GetInitialExtensionThreshold(), "TEMPORARY"));
// Bug 716426 was a shutdown crash due to not removing a
// storage::StorageObserver registration before deleting the observer. To
// recreate that scenario, first disable the app (which leaks the observer
// registration), then simulate the step of profile exit where we delete the
// StorageObserver.
DisableExtension(app->id());
SimulateProfileShutdown();
// Now generate more storage activity for the hosted app's temporary
// filesystem. Note that it's not a hosted app anymore -- it's just a webpage.
// Bug 716426 caused this to crash the browser.
EXPECT_NO_FATAL_FAILURE(WriteBytesNotExpectingNotification(
app, GetInitialExtensionThreshold(), "TEMPORARY"));
}
// Exercises the case where two hosted apps are same-origin but have non-
// overlapping extents. Disabling one should not suppress storage monitoring for
// the other.
IN_PROC_BROWSER_TEST_F(ExtensionStorageMonitorTest, TwoHostedAppsInSameOrigin) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url1 = embedded_test_server()->GetURL(
"chromium.org", "/extensions/storage_monitor/hosted_apps/one/index.html");
const Extension* app1 = CreateHostedApp("App 1", url1, {"unlimitedStorage"});
GURL url2 = embedded_test_server()->GetURL(
"chromium.org", "/extensions/storage_monitor/hosted_apps/two/index.html");
const Extension* app2 = CreateHostedApp("App 2", url2, {"unlimitedStorage"});
EXPECT_EQ(url1.GetOrigin(), url2.GetOrigin());
EXPECT_NO_FATAL_FAILURE(
WriteBytesExpectingNotification(app1, GetInitialExtensionThreshold()));
EXPECT_NO_FATAL_FAILURE(WriteBytesExpectingNotification(
app2, GetInitialExtensionThreshold() * 2));
// Disable app2. We should still be monitoring the origin on behalf of app1.
DisableExtension(app2->id());
// Writing a bunch of data in app1 should trigger the warning.
EXPECT_NO_FATAL_FAILURE(WriteBytesExpectingNotification(
app1, GetInitialExtensionThreshold() * 4));
}
// Verify that notifications are disabled when the user clicks the action button
// in the notification.
// Flaky: https://crbug.com/617801
IN_PROC_BROWSER_TEST_F(ExtensionStorageMonitorTest,
DISABLED_UninstallExtension) {
const Extension* extension = InitWriteDataApp();
ASSERT_TRUE(extension);
WriteBytesExpectingNotification(extension, GetInitialExtensionThreshold());
// Fake clicking the notification button to uninstall and accepting the
// uninstall.
ScopedTestDialogAutoConfirm scoped_autoconfirm(
ScopedTestDialogAutoConfirm::ACCEPT);
TestExtensionRegistryObserver observer(ExtensionRegistry::Get(profile()),
extension->id());
display_service_->GetNotification(GetNotificationId(extension->id()))
->delegate()
->Click(ExtensionStorageMonitor::BUTTON_UNINSTALL, base::nullopt);
observer.WaitForExtensionUninstalled();
}
} // namespace extensions