|  | // 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 "base/file_util.h" | 
|  | #include "base/files/file.h" | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/files/scoped_temp_dir.h" | 
|  | #include "base/memory/scoped_ptr.h" | 
|  | #include "base/message_loop/message_loop.h" | 
|  | #include "base/message_loop/message_loop_proxy.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/time/time.h" | 
|  | #include "content/public/test/mock_special_storage_policy.h" | 
|  | #include "net/base/net_errors.h" | 
|  | #include "net/base/test_completion_callback.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  | #include "third_party/sqlite/sqlite3.h" | 
|  | #include "webkit/browser/database/database_tracker.h" | 
|  | #include "webkit/browser/quota/quota_manager_proxy.h" | 
|  | #include "webkit/common/database/database_identifier.h" | 
|  |  | 
|  | using base::ASCIIToUTF16; | 
|  | using storage::DatabaseConnections; | 
|  | using storage::DatabaseTracker; | 
|  | using storage::OriginInfo; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | const char kOrigin1Url[] = "http://origin1"; | 
|  | const char kOrigin2Url[] = "http://protected_origin2"; | 
|  |  | 
|  | class TestObserver : public storage::DatabaseTracker::Observer { | 
|  | public: | 
|  | TestObserver() | 
|  | : new_notification_received_(false), | 
|  | observe_size_changes_(true), | 
|  | observe_scheduled_deletions_(true) { | 
|  | } | 
|  | TestObserver(bool observe_size_changes, bool observe_scheduled_deletions) | 
|  | : new_notification_received_(false), | 
|  | observe_size_changes_(observe_size_changes), | 
|  | observe_scheduled_deletions_(observe_scheduled_deletions) { | 
|  | } | 
|  |  | 
|  | virtual ~TestObserver() {} | 
|  | virtual void OnDatabaseSizeChanged(const std::string& origin_identifier, | 
|  | const base::string16& database_name, | 
|  | int64 database_size) OVERRIDE { | 
|  | if (!observe_size_changes_) | 
|  | return; | 
|  | new_notification_received_ = true; | 
|  | origin_identifier_ = origin_identifier; | 
|  | database_name_ = database_name; | 
|  | database_size_ = database_size; | 
|  | } | 
|  | virtual void OnDatabaseScheduledForDeletion( | 
|  | const std::string& origin_identifier, | 
|  | const base::string16& database_name) OVERRIDE { | 
|  | if (!observe_scheduled_deletions_) | 
|  | return; | 
|  | new_notification_received_ = true; | 
|  | origin_identifier_ = origin_identifier; | 
|  | database_name_ = database_name; | 
|  | } | 
|  | bool DidReceiveNewNotification() { | 
|  | bool temp_new_notification_received = new_notification_received_; | 
|  | new_notification_received_ = false; | 
|  | return temp_new_notification_received; | 
|  | } | 
|  | std::string GetNotificationOriginIdentifier() { | 
|  | return origin_identifier_; | 
|  | } | 
|  | base::string16 GetNotificationDatabaseName() { return database_name_; } | 
|  | int64 GetNotificationDatabaseSize() { return database_size_; } | 
|  |  | 
|  | private: | 
|  | bool new_notification_received_; | 
|  | bool observe_size_changes_; | 
|  | bool observe_scheduled_deletions_; | 
|  | std::string origin_identifier_; | 
|  | base::string16 database_name_; | 
|  | int64 database_size_; | 
|  | }; | 
|  |  | 
|  | void CheckNotificationReceived(TestObserver* observer, | 
|  | const std::string& expected_origin_identifier, | 
|  | const base::string16& expected_database_name, | 
|  | int64 expected_database_size) { | 
|  | EXPECT_TRUE(observer->DidReceiveNewNotification()); | 
|  | EXPECT_EQ(expected_origin_identifier, | 
|  | observer->GetNotificationOriginIdentifier()); | 
|  | EXPECT_EQ(expected_database_name, | 
|  | observer->GetNotificationDatabaseName()); | 
|  | EXPECT_EQ(expected_database_size, | 
|  | observer->GetNotificationDatabaseSize()); | 
|  | } | 
|  |  | 
|  | class TestQuotaManagerProxy : public storage::QuotaManagerProxy { | 
|  | public: | 
|  | TestQuotaManagerProxy() | 
|  | : QuotaManagerProxy(NULL, NULL), | 
|  | registered_client_(NULL) { | 
|  | } | 
|  |  | 
|  | virtual void RegisterClient(storage::QuotaClient* client) OVERRIDE { | 
|  | EXPECT_FALSE(registered_client_); | 
|  | registered_client_ = client; | 
|  | } | 
|  |  | 
|  | virtual void NotifyStorageAccessed(storage::QuotaClient::ID client_id, | 
|  | const GURL& origin, | 
|  | storage::StorageType type) OVERRIDE { | 
|  | EXPECT_EQ(storage::QuotaClient::kDatabase, client_id); | 
|  | EXPECT_EQ(storage::kStorageTypeTemporary, type); | 
|  | accesses_[origin] += 1; | 
|  | } | 
|  |  | 
|  | virtual void NotifyStorageModified(storage::QuotaClient::ID client_id, | 
|  | const GURL& origin, | 
|  | storage::StorageType type, | 
|  | int64 delta) OVERRIDE { | 
|  | EXPECT_EQ(storage::QuotaClient::kDatabase, client_id); | 
|  | EXPECT_EQ(storage::kStorageTypeTemporary, type); | 
|  | modifications_[origin].first += 1; | 
|  | modifications_[origin].second += delta; | 
|  | } | 
|  |  | 
|  | // Not needed for our tests. | 
|  | virtual void NotifyOriginInUse(const GURL& origin) OVERRIDE {} | 
|  | virtual void NotifyOriginNoLongerInUse(const GURL& origin) OVERRIDE {} | 
|  | virtual void SetUsageCacheEnabled(storage::QuotaClient::ID client_id, | 
|  | const GURL& origin, | 
|  | storage::StorageType type, | 
|  | bool enabled) OVERRIDE {} | 
|  | virtual void GetUsageAndQuota( | 
|  | base::SequencedTaskRunner* original_task_runner, | 
|  | const GURL& origin, | 
|  | storage::StorageType type, | 
|  | const GetUsageAndQuotaCallback& callback) OVERRIDE {} | 
|  |  | 
|  | void SimulateQuotaManagerDestroyed() { | 
|  | if (registered_client_) { | 
|  | registered_client_->OnQuotaManagerDestroyed(); | 
|  | registered_client_ = NULL; | 
|  | } | 
|  | } | 
|  |  | 
|  | bool WasAccessNotified(const GURL& origin) { | 
|  | return accesses_[origin] != 0; | 
|  | } | 
|  |  | 
|  | bool WasModificationNotified(const GURL& origin, int64 amount) { | 
|  | return modifications_[origin].first != 0 && | 
|  | modifications_[origin].second == amount; | 
|  | } | 
|  |  | 
|  | void reset() { | 
|  | accesses_.clear(); | 
|  | modifications_.clear(); | 
|  | } | 
|  |  | 
|  | storage::QuotaClient* registered_client_; | 
|  |  | 
|  | // Map from origin to count of access notifications. | 
|  | std::map<GURL, int> accesses_; | 
|  |  | 
|  | // Map from origin to <count, sum of deltas> | 
|  | std::map<GURL, std::pair<int, int64> > modifications_; | 
|  |  | 
|  | protected: | 
|  | virtual ~TestQuotaManagerProxy() { | 
|  | EXPECT_FALSE(registered_client_); | 
|  | } | 
|  | }; | 
|  |  | 
|  |  | 
|  | bool EnsureFileOfSize(const base::FilePath& file_path, int64 length) { | 
|  | base::File file(file_path, | 
|  | base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_WRITE); | 
|  | if (!file.IsValid()) | 
|  | return false; | 
|  | return file.SetLength(length); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | // We declare a helper class, and make it a friend of DatabaseTracker using | 
|  | // the FORWARD_DECLARE_TEST macro, and we implement all tests we want to run as | 
|  | // static methods of this class. Then we make our TEST() targets call these | 
|  | // static functions. This allows us to run each test in normal mode and | 
|  | // incognito mode without writing the same code twice. | 
|  | class DatabaseTracker_TestHelper_Test { | 
|  | public: | 
|  | static void TestDeleteOpenDatabase(bool incognito_mode) { | 
|  | // Initialize the tracker database. | 
|  | base::ScopedTempDir temp_dir; | 
|  | ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); | 
|  | scoped_refptr<MockSpecialStoragePolicy> special_storage_policy = | 
|  | new MockSpecialStoragePolicy; | 
|  | special_storage_policy->AddProtected(GURL(kOrigin2Url)); | 
|  | scoped_refptr<DatabaseTracker> tracker( | 
|  | new DatabaseTracker(temp_dir.path(), | 
|  | incognito_mode, | 
|  | special_storage_policy.get(), | 
|  | NULL, | 
|  | NULL)); | 
|  |  | 
|  | // Create and open three databases. | 
|  | int64 database_size = 0; | 
|  | const std::string kOrigin1 = | 
|  | storage::GetIdentifierFromOrigin(GURL(kOrigin1Url)); | 
|  | const std::string kOrigin2 = | 
|  | storage::GetIdentifierFromOrigin(GURL(kOrigin2Url)); | 
|  | const base::string16 kDB1 = ASCIIToUTF16("db1"); | 
|  | const base::string16 kDB2 = ASCIIToUTF16("db2"); | 
|  | const base::string16 kDB3 = ASCIIToUTF16("db3"); | 
|  | const base::string16 kDescription = ASCIIToUTF16("database_description"); | 
|  |  | 
|  | tracker->DatabaseOpened(kOrigin1, kDB1, kDescription, 0, | 
|  | &database_size); | 
|  | tracker->DatabaseOpened(kOrigin2, kDB2, kDescription, 0, | 
|  | &database_size); | 
|  | tracker->DatabaseOpened(kOrigin2, kDB3, kDescription, 0, | 
|  | &database_size); | 
|  |  | 
|  | EXPECT_TRUE(base::CreateDirectory( | 
|  | tracker->DatabaseDirectory().Append(base::FilePath::FromUTF16Unsafe( | 
|  | tracker->GetOriginDirectory(kOrigin1))))); | 
|  | EXPECT_TRUE(base::CreateDirectory( | 
|  | tracker->DatabaseDirectory().Append(base::FilePath::FromUTF16Unsafe( | 
|  | tracker->GetOriginDirectory(kOrigin2))))); | 
|  | EXPECT_EQ(1, base::WriteFile( | 
|  | tracker->GetFullDBFilePath(kOrigin1, kDB1), "a", 1)); | 
|  | EXPECT_EQ(2, base::WriteFile( | 
|  | tracker->GetFullDBFilePath(kOrigin2, kDB2), "aa", 2)); | 
|  | EXPECT_EQ(3, base::WriteFile( | 
|  | tracker->GetFullDBFilePath(kOrigin2, kDB3), "aaa", 3)); | 
|  | tracker->DatabaseModified(kOrigin1, kDB1); | 
|  | tracker->DatabaseModified(kOrigin2, kDB2); | 
|  | tracker->DatabaseModified(kOrigin2, kDB3); | 
|  |  | 
|  | // Delete db1. Should also delete origin1. | 
|  | TestObserver observer; | 
|  | tracker->AddObserver(&observer); | 
|  | net::TestCompletionCallback callback; | 
|  | int result = tracker->DeleteDatabase(kOrigin1, kDB1, callback.callback()); | 
|  | EXPECT_EQ(net::ERR_IO_PENDING, result); | 
|  | ASSERT_FALSE(callback.have_result()); | 
|  | EXPECT_TRUE(observer.DidReceiveNewNotification()); | 
|  | EXPECT_EQ(kOrigin1, observer.GetNotificationOriginIdentifier()); | 
|  | EXPECT_EQ(kDB1, observer.GetNotificationDatabaseName()); | 
|  | tracker->DatabaseClosed(kOrigin1, kDB1); | 
|  | result = callback.GetResult(result); | 
|  | EXPECT_EQ(net::OK, result); | 
|  | EXPECT_FALSE(base::PathExists( | 
|  | tracker->DatabaseDirectory().AppendASCII(kOrigin1))); | 
|  |  | 
|  | // Recreate db1. | 
|  | tracker->DatabaseOpened(kOrigin1, kDB1, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_TRUE(base::CreateDirectory( | 
|  | tracker->DatabaseDirectory().Append(base::FilePath::FromUTF16Unsafe( | 
|  | tracker->GetOriginDirectory(kOrigin1))))); | 
|  | EXPECT_EQ(1, base::WriteFile( | 
|  | tracker->GetFullDBFilePath(kOrigin1, kDB1), "a", 1)); | 
|  | tracker->DatabaseModified(kOrigin1, kDB1); | 
|  |  | 
|  | // Setup file modification times.  db1 and db2 are modified now, db3 three | 
|  | // days ago. | 
|  | base::Time now = base::Time::Now(); | 
|  | EXPECT_TRUE(base::TouchFile(tracker->GetFullDBFilePath(kOrigin1, kDB1), | 
|  | now, now)); | 
|  | EXPECT_TRUE(base::TouchFile(tracker->GetFullDBFilePath(kOrigin2, kDB2), | 
|  | now, now)); | 
|  | base::Time three_days_ago = now - base::TimeDelta::FromDays(3); | 
|  | EXPECT_TRUE(base::TouchFile(tracker->GetFullDBFilePath(kOrigin2, kDB3), | 
|  | three_days_ago, three_days_ago)); | 
|  |  | 
|  | // Delete databases modified since yesterday. db2 is whitelisted. | 
|  | base::Time yesterday = base::Time::Now(); | 
|  | yesterday -= base::TimeDelta::FromDays(1); | 
|  | result = tracker->DeleteDataModifiedSince( | 
|  | yesterday, callback.callback()); | 
|  | EXPECT_EQ(net::ERR_IO_PENDING, result); | 
|  | ASSERT_FALSE(callback.have_result()); | 
|  | EXPECT_TRUE(observer.DidReceiveNewNotification()); | 
|  | tracker->DatabaseClosed(kOrigin1, kDB1); | 
|  | tracker->DatabaseClosed(kOrigin2, kDB2); | 
|  | result = callback.GetResult(result); | 
|  | EXPECT_EQ(net::OK, result); | 
|  | EXPECT_FALSE(base::PathExists( | 
|  | tracker->DatabaseDirectory().AppendASCII(kOrigin1))); | 
|  | EXPECT_TRUE( | 
|  | base::PathExists(tracker->GetFullDBFilePath(kOrigin2, kDB2))); | 
|  | EXPECT_TRUE( | 
|  | base::PathExists(tracker->GetFullDBFilePath(kOrigin2, kDB3))); | 
|  |  | 
|  | tracker->DatabaseClosed(kOrigin2, kDB3); | 
|  | tracker->RemoveObserver(&observer); | 
|  | } | 
|  |  | 
|  | static void TestDatabaseTracker(bool incognito_mode) { | 
|  | // Initialize the tracker database. | 
|  | base::ScopedTempDir temp_dir; | 
|  | ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); | 
|  | scoped_refptr<MockSpecialStoragePolicy> special_storage_policy = | 
|  | new MockSpecialStoragePolicy; | 
|  | special_storage_policy->AddProtected(GURL(kOrigin2Url)); | 
|  | scoped_refptr<DatabaseTracker> tracker( | 
|  | new DatabaseTracker(temp_dir.path(), | 
|  | incognito_mode, | 
|  | special_storage_policy.get(), | 
|  | NULL, | 
|  | NULL)); | 
|  |  | 
|  | // Add two observers. | 
|  | TestObserver observer1; | 
|  | TestObserver observer2; | 
|  | tracker->AddObserver(&observer1); | 
|  | tracker->AddObserver(&observer2); | 
|  |  | 
|  | // Open three new databases. | 
|  | int64 database_size = 0; | 
|  | const std::string kOrigin1 = | 
|  | storage::GetIdentifierFromOrigin(GURL(kOrigin1Url)); | 
|  | const std::string kOrigin2 = | 
|  | storage::GetIdentifierFromOrigin(GURL(kOrigin2Url)); | 
|  | const base::string16 kDB1 = ASCIIToUTF16("db1"); | 
|  | const base::string16 kDB2 = ASCIIToUTF16("db2"); | 
|  | const base::string16 kDB3 = ASCIIToUTF16("db3"); | 
|  | const base::string16 kDescription = ASCIIToUTF16("database_description"); | 
|  |  | 
|  | // Get the info for kOrigin1 and kOrigin2 | 
|  | DatabaseTracker::CachedOriginInfo* origin1_info = | 
|  | tracker->GetCachedOriginInfo(kOrigin1); | 
|  | DatabaseTracker::CachedOriginInfo* origin2_info = | 
|  | tracker->GetCachedOriginInfo(kOrigin1); | 
|  | EXPECT_TRUE(origin1_info); | 
|  | EXPECT_TRUE(origin2_info); | 
|  |  | 
|  |  | 
|  | tracker->DatabaseOpened(kOrigin1, kDB1, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(0, database_size); | 
|  | tracker->DatabaseOpened(kOrigin2, kDB2, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(0, database_size); | 
|  | tracker->DatabaseOpened(kOrigin1, kDB3, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(0, database_size); | 
|  |  | 
|  | // Write some data to each file and check that the listeners are | 
|  | // called with the appropriate values. | 
|  | EXPECT_TRUE(base::CreateDirectory( | 
|  | tracker->DatabaseDirectory().Append(base::FilePath::FromUTF16Unsafe( | 
|  | tracker->GetOriginDirectory(kOrigin1))))); | 
|  | EXPECT_TRUE(base::CreateDirectory( | 
|  | tracker->DatabaseDirectory().Append(base::FilePath::FromUTF16Unsafe( | 
|  | tracker->GetOriginDirectory(kOrigin2))))); | 
|  | EXPECT_EQ(1, base::WriteFile( | 
|  | tracker->GetFullDBFilePath(kOrigin1, kDB1), "a", 1)); | 
|  | EXPECT_EQ(2, base::WriteFile( | 
|  | tracker->GetFullDBFilePath(kOrigin2, kDB2), "aa", 2)); | 
|  | EXPECT_EQ(4, base::WriteFile( | 
|  | tracker->GetFullDBFilePath(kOrigin1, kDB3), "aaaa", 4)); | 
|  | tracker->DatabaseModified(kOrigin1, kDB1); | 
|  | CheckNotificationReceived(&observer1, kOrigin1, kDB1, 1); | 
|  | CheckNotificationReceived(&observer2, kOrigin1, kDB1, 1); | 
|  | tracker->DatabaseModified(kOrigin2, kDB2); | 
|  | CheckNotificationReceived(&observer1, kOrigin2, kDB2, 2); | 
|  | CheckNotificationReceived(&observer2, kOrigin2, kDB2, 2); | 
|  | tracker->DatabaseModified(kOrigin1, kDB3); | 
|  | CheckNotificationReceived(&observer1, kOrigin1, kDB3, 4); | 
|  | CheckNotificationReceived(&observer2, kOrigin1, kDB3, 4); | 
|  |  | 
|  | // Close all databases | 
|  | tracker->DatabaseClosed(kOrigin1, kDB1); | 
|  | tracker->DatabaseClosed(kOrigin2, kDB2); | 
|  | tracker->DatabaseClosed(kOrigin1, kDB3); | 
|  |  | 
|  | // Open an existing database and check the reported size | 
|  | tracker->DatabaseOpened(kOrigin1, kDB1, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(1, database_size); | 
|  | tracker->DatabaseClosed(kOrigin1, kDB1); | 
|  |  | 
|  | // Remove an observer; this should clear all caches. | 
|  | tracker->RemoveObserver(&observer2); | 
|  |  | 
|  | // Close the tracker database and clear all caches. | 
|  | // Then make sure that DatabaseOpened() still returns the correct result. | 
|  | tracker->CloseTrackerDatabaseAndClearCaches(); | 
|  | tracker->DatabaseOpened(kOrigin1, kDB1, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(1, database_size); | 
|  | tracker->DatabaseClosed(kOrigin1, kDB1); | 
|  |  | 
|  | // Remove all observers. | 
|  | tracker->RemoveObserver(&observer1); | 
|  |  | 
|  | // Trying to delete a database in use should fail | 
|  | tracker->DatabaseOpened(kOrigin1, kDB3, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_FALSE(tracker->DeleteClosedDatabase(kOrigin1, kDB3)); | 
|  | origin1_info = tracker->GetCachedOriginInfo(kOrigin1); | 
|  | EXPECT_TRUE(origin1_info); | 
|  | EXPECT_EQ(4, origin1_info->GetDatabaseSize(kDB3)); | 
|  | tracker->DatabaseClosed(kOrigin1, kDB3); | 
|  |  | 
|  | // Delete a database and make sure the space used by that origin is updated | 
|  | EXPECT_TRUE(tracker->DeleteClosedDatabase(kOrigin1, kDB3)); | 
|  | origin1_info = tracker->GetCachedOriginInfo(kOrigin1); | 
|  | EXPECT_TRUE(origin1_info); | 
|  | EXPECT_EQ(1, origin1_info->GetDatabaseSize(kDB1)); | 
|  | EXPECT_EQ(0, origin1_info->GetDatabaseSize(kDB3)); | 
|  |  | 
|  | // Get all data for all origins | 
|  | std::vector<OriginInfo> origins_info; | 
|  | EXPECT_TRUE(tracker->GetAllOriginsInfo(&origins_info)); | 
|  | EXPECT_EQ(size_t(2), origins_info.size()); | 
|  | EXPECT_EQ(kOrigin1, origins_info[0].GetOriginIdentifier()); | 
|  | EXPECT_EQ(1, origins_info[0].TotalSize()); | 
|  | EXPECT_EQ(1, origins_info[0].GetDatabaseSize(kDB1)); | 
|  | EXPECT_EQ(0, origins_info[0].GetDatabaseSize(kDB3)); | 
|  |  | 
|  | EXPECT_EQ(kOrigin2, origins_info[1].GetOriginIdentifier()); | 
|  | EXPECT_EQ(2, origins_info[1].TotalSize()); | 
|  |  | 
|  | // Trying to delete an origin with databases in use should fail | 
|  | tracker->DatabaseOpened(kOrigin1, kDB1, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_FALSE(tracker->DeleteOrigin(kOrigin1, false)); | 
|  | origin1_info = tracker->GetCachedOriginInfo(kOrigin1); | 
|  | EXPECT_TRUE(origin1_info); | 
|  | EXPECT_EQ(1, origin1_info->GetDatabaseSize(kDB1)); | 
|  | tracker->DatabaseClosed(kOrigin1, kDB1); | 
|  |  | 
|  | // Delete an origin that doesn't have any database in use | 
|  | EXPECT_TRUE(tracker->DeleteOrigin(kOrigin1, false)); | 
|  | origins_info.clear(); | 
|  | EXPECT_TRUE(tracker->GetAllOriginsInfo(&origins_info)); | 
|  | EXPECT_EQ(size_t(1), origins_info.size()); | 
|  | EXPECT_EQ(kOrigin2, origins_info[0].GetOriginIdentifier()); | 
|  |  | 
|  | origin1_info = tracker->GetCachedOriginInfo(kOrigin1); | 
|  | EXPECT_TRUE(origin1_info); | 
|  | EXPECT_EQ(0, origin1_info->TotalSize()); | 
|  | } | 
|  |  | 
|  | static void DatabaseTrackerQuotaIntegration() { | 
|  | const GURL kOrigin(kOrigin1Url); | 
|  | const std::string kOriginId = storage::GetIdentifierFromOrigin(kOrigin); | 
|  | const base::string16 kName = ASCIIToUTF16("name"); | 
|  | const base::string16 kDescription = ASCIIToUTF16("description"); | 
|  |  | 
|  | base::ScopedTempDir temp_dir; | 
|  | ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); | 
|  |  | 
|  | // Initialize the tracker with a QuotaManagerProxy | 
|  | scoped_refptr<TestQuotaManagerProxy> test_quota_proxy( | 
|  | new TestQuotaManagerProxy); | 
|  | scoped_refptr<DatabaseTracker> tracker( | 
|  | new DatabaseTracker(temp_dir.path(), | 
|  | false /* incognito */, | 
|  | NULL, | 
|  | test_quota_proxy.get(), | 
|  | NULL)); | 
|  | EXPECT_TRUE(test_quota_proxy->registered_client_); | 
|  |  | 
|  | // Create a database and modify it a couple of times, close it, | 
|  | // then delete it. Observe the tracker notifies accordingly. | 
|  |  | 
|  | int64 database_size = 0; | 
|  | tracker->DatabaseOpened(kOriginId, kName, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_TRUE(test_quota_proxy->WasAccessNotified(kOrigin)); | 
|  | test_quota_proxy->reset(); | 
|  |  | 
|  | base::FilePath db_file(tracker->GetFullDBFilePath(kOriginId, kName)); | 
|  | EXPECT_TRUE(base::CreateDirectory(db_file.DirName())); | 
|  | EXPECT_TRUE(EnsureFileOfSize(db_file, 10)); | 
|  | tracker->DatabaseModified(kOriginId, kName); | 
|  | EXPECT_TRUE(test_quota_proxy->WasModificationNotified(kOrigin, 10)); | 
|  | test_quota_proxy->reset(); | 
|  |  | 
|  | EXPECT_TRUE(EnsureFileOfSize(db_file, 100)); | 
|  | tracker->DatabaseModified(kOriginId, kName); | 
|  | EXPECT_TRUE(test_quota_proxy->WasModificationNotified(kOrigin, 90)); | 
|  | test_quota_proxy->reset(); | 
|  |  | 
|  | tracker->DatabaseClosed(kOriginId, kName); | 
|  | EXPECT_TRUE(test_quota_proxy->WasAccessNotified(kOrigin)); | 
|  | EXPECT_EQ(net::OK, tracker->DeleteDatabase( | 
|  | kOriginId, kName, net::CompletionCallback())); | 
|  | EXPECT_TRUE(test_quota_proxy->WasModificationNotified(kOrigin, -100)); | 
|  | test_quota_proxy->reset(); | 
|  |  | 
|  | // Create a database and modify it, try to delete it while open, | 
|  | // then close it (at which time deletion will actually occur). | 
|  | // Observe the tracker notifies accordingly. | 
|  |  | 
|  | tracker->DatabaseOpened(kOriginId, kName, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_TRUE(test_quota_proxy->WasAccessNotified(kOrigin)); | 
|  | test_quota_proxy->reset(); | 
|  |  | 
|  | db_file = tracker->GetFullDBFilePath(kOriginId, kName); | 
|  | EXPECT_TRUE(base::CreateDirectory(db_file.DirName())); | 
|  | EXPECT_TRUE(EnsureFileOfSize(db_file, 100)); | 
|  | tracker->DatabaseModified(kOriginId, kName); | 
|  | EXPECT_TRUE(test_quota_proxy->WasModificationNotified(kOrigin, 100)); | 
|  | test_quota_proxy->reset(); | 
|  |  | 
|  | EXPECT_EQ(net::ERR_IO_PENDING, | 
|  | tracker->DeleteDatabase(kOriginId, kName, | 
|  | net::CompletionCallback())); | 
|  | EXPECT_FALSE(test_quota_proxy->WasModificationNotified(kOrigin, -100)); | 
|  |  | 
|  | tracker->DatabaseClosed(kOriginId, kName); | 
|  | EXPECT_TRUE(test_quota_proxy->WasAccessNotified(kOrigin)); | 
|  | EXPECT_TRUE(test_quota_proxy->WasModificationNotified(kOrigin, -100)); | 
|  | test_quota_proxy->reset(); | 
|  |  | 
|  | // Create a database and up the file size without telling | 
|  | // the tracker about the modification, than simulate a | 
|  | // a renderer crash. | 
|  | // Observe the tracker notifies accordingly. | 
|  |  | 
|  | tracker->DatabaseOpened(kOriginId, kName, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_TRUE(test_quota_proxy->WasAccessNotified(kOrigin)); | 
|  | test_quota_proxy->reset(); | 
|  | db_file = tracker->GetFullDBFilePath(kOriginId, kName); | 
|  | EXPECT_TRUE(base::CreateDirectory(db_file.DirName())); | 
|  | EXPECT_TRUE(EnsureFileOfSize(db_file, 100)); | 
|  | DatabaseConnections crashed_renderer_connections; | 
|  | crashed_renderer_connections.AddConnection(kOriginId, kName); | 
|  | EXPECT_FALSE(test_quota_proxy->WasModificationNotified(kOrigin, 100)); | 
|  | tracker->CloseDatabases(crashed_renderer_connections); | 
|  | EXPECT_TRUE(test_quota_proxy->WasModificationNotified(kOrigin, 100)); | 
|  |  | 
|  | // Cleanup. | 
|  | crashed_renderer_connections.RemoveAllConnections(); | 
|  | test_quota_proxy->SimulateQuotaManagerDestroyed(); | 
|  | } | 
|  |  | 
|  | static void DatabaseTrackerClearSessionOnlyDatabasesOnExit() { | 
|  | int64 database_size = 0; | 
|  | const std::string kOrigin1 = | 
|  | storage::GetIdentifierFromOrigin(GURL(kOrigin1Url)); | 
|  | const std::string kOrigin2 = | 
|  | storage::GetIdentifierFromOrigin(GURL(kOrigin2Url)); | 
|  | const base::string16 kDB1 = ASCIIToUTF16("db1"); | 
|  | const base::string16 kDB2 = ASCIIToUTF16("db2"); | 
|  | const base::string16 kDescription = ASCIIToUTF16("database_description"); | 
|  |  | 
|  | // Initialize the tracker database. | 
|  | base::MessageLoop message_loop; | 
|  | base::ScopedTempDir temp_dir; | 
|  | ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); | 
|  | base::FilePath origin1_db_dir; | 
|  | base::FilePath origin2_db_dir; | 
|  | { | 
|  | scoped_refptr<MockSpecialStoragePolicy> special_storage_policy = | 
|  | new MockSpecialStoragePolicy; | 
|  | special_storage_policy->AddSessionOnly(GURL(kOrigin2Url)); | 
|  | scoped_refptr<DatabaseTracker> tracker( | 
|  | new DatabaseTracker(temp_dir.path(), | 
|  | false, | 
|  | special_storage_policy.get(), | 
|  | NULL, | 
|  | base::MessageLoopProxy::current().get())); | 
|  |  | 
|  | // Open two new databases. | 
|  | tracker->DatabaseOpened(kOrigin1, kDB1, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(0, database_size); | 
|  | tracker->DatabaseOpened(kOrigin2, kDB2, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(0, database_size); | 
|  |  | 
|  | // Write some data to each file. | 
|  | base::FilePath db_file; | 
|  | db_file = tracker->GetFullDBFilePath(kOrigin1, kDB1); | 
|  | EXPECT_TRUE(base::CreateDirectory(db_file.DirName())); | 
|  | EXPECT_TRUE(EnsureFileOfSize(db_file, 1)); | 
|  |  | 
|  | db_file = tracker->GetFullDBFilePath(kOrigin2, kDB2); | 
|  | EXPECT_TRUE(base::CreateDirectory(db_file.DirName())); | 
|  | EXPECT_TRUE(EnsureFileOfSize(db_file, 2)); | 
|  |  | 
|  | // Store the origin database directories as long as they still exist. | 
|  | origin1_db_dir = tracker->GetFullDBFilePath(kOrigin1, kDB1).DirName(); | 
|  | origin2_db_dir = tracker->GetFullDBFilePath(kOrigin2, kDB2).DirName(); | 
|  |  | 
|  | tracker->DatabaseModified(kOrigin1, kDB1); | 
|  | tracker->DatabaseModified(kOrigin2, kDB2); | 
|  |  | 
|  | // Close all databases. | 
|  | tracker->DatabaseClosed(kOrigin1, kDB1); | 
|  | tracker->DatabaseClosed(kOrigin2, kDB2); | 
|  |  | 
|  | tracker->Shutdown(); | 
|  | } | 
|  |  | 
|  | // At this point, the database tracker should be gone. Create a new one. | 
|  | scoped_refptr<DatabaseTracker> tracker( | 
|  | new DatabaseTracker(temp_dir.path(), false, NULL, NULL, NULL)); | 
|  |  | 
|  | // Get all data for all origins. | 
|  | std::vector<OriginInfo> origins_info; | 
|  | EXPECT_TRUE(tracker->GetAllOriginsInfo(&origins_info)); | 
|  | // kOrigin1 was not session-only, so it survived. kOrigin2 was session-only | 
|  | // and it got deleted. | 
|  | EXPECT_EQ(size_t(1), origins_info.size()); | 
|  | EXPECT_EQ(kOrigin1, origins_info[0].GetOriginIdentifier()); | 
|  | EXPECT_TRUE( | 
|  | base::PathExists(tracker->GetFullDBFilePath(kOrigin1, kDB1))); | 
|  | EXPECT_EQ(base::FilePath(), tracker->GetFullDBFilePath(kOrigin2, kDB2)); | 
|  |  | 
|  | // The origin directory of kOrigin1 remains, but the origin directory of | 
|  | // kOrigin2 is deleted. | 
|  | EXPECT_TRUE(base::PathExists(origin1_db_dir)); | 
|  | EXPECT_FALSE(base::PathExists(origin2_db_dir)); | 
|  | } | 
|  |  | 
|  | static void DatabaseTrackerSetForceKeepSessionState() { | 
|  | int64 database_size = 0; | 
|  | const std::string kOrigin1 = | 
|  | storage::GetIdentifierFromOrigin(GURL(kOrigin1Url)); | 
|  | const std::string kOrigin2 = | 
|  | storage::GetIdentifierFromOrigin(GURL(kOrigin2Url)); | 
|  | const base::string16 kDB1 = ASCIIToUTF16("db1"); | 
|  | const base::string16 kDB2 = ASCIIToUTF16("db2"); | 
|  | const base::string16 kDescription = ASCIIToUTF16("database_description"); | 
|  |  | 
|  | // Initialize the tracker database. | 
|  | base::MessageLoop message_loop; | 
|  | base::ScopedTempDir temp_dir; | 
|  | ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); | 
|  | base::FilePath origin1_db_dir; | 
|  | base::FilePath origin2_db_dir; | 
|  | { | 
|  | scoped_refptr<MockSpecialStoragePolicy> special_storage_policy = | 
|  | new MockSpecialStoragePolicy; | 
|  | special_storage_policy->AddSessionOnly(GURL(kOrigin2Url)); | 
|  | scoped_refptr<DatabaseTracker> tracker( | 
|  | new DatabaseTracker(temp_dir.path(), | 
|  | false, | 
|  | special_storage_policy.get(), | 
|  | NULL, | 
|  | base::MessageLoopProxy::current().get())); | 
|  | tracker->SetForceKeepSessionState(); | 
|  |  | 
|  | // Open two new databases. | 
|  | tracker->DatabaseOpened(kOrigin1, kDB1, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(0, database_size); | 
|  | tracker->DatabaseOpened(kOrigin2, kDB2, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(0, database_size); | 
|  |  | 
|  | // Write some data to each file. | 
|  | base::FilePath db_file; | 
|  | db_file = tracker->GetFullDBFilePath(kOrigin1, kDB1); | 
|  | EXPECT_TRUE(base::CreateDirectory(db_file.DirName())); | 
|  | EXPECT_TRUE(EnsureFileOfSize(db_file, 1)); | 
|  |  | 
|  | db_file = tracker->GetFullDBFilePath(kOrigin2, kDB2); | 
|  | EXPECT_TRUE(base::CreateDirectory(db_file.DirName())); | 
|  | EXPECT_TRUE(EnsureFileOfSize(db_file, 2)); | 
|  |  | 
|  | // Store the origin database directories as long as they still exist. | 
|  | origin1_db_dir = tracker->GetFullDBFilePath(kOrigin1, kDB1).DirName(); | 
|  | origin2_db_dir = tracker->GetFullDBFilePath(kOrigin2, kDB2).DirName(); | 
|  |  | 
|  | tracker->DatabaseModified(kOrigin1, kDB1); | 
|  | tracker->DatabaseModified(kOrigin2, kDB2); | 
|  |  | 
|  | // Close all databases. | 
|  | tracker->DatabaseClosed(kOrigin1, kDB1); | 
|  | tracker->DatabaseClosed(kOrigin2, kDB2); | 
|  |  | 
|  | tracker->Shutdown(); | 
|  | } | 
|  |  | 
|  | // At this point, the database tracker should be gone. Create a new one. | 
|  | scoped_refptr<DatabaseTracker> tracker( | 
|  | new DatabaseTracker(temp_dir.path(), false, NULL, NULL, NULL)); | 
|  |  | 
|  | // Get all data for all origins. | 
|  | std::vector<OriginInfo> origins_info; | 
|  | EXPECT_TRUE(tracker->GetAllOriginsInfo(&origins_info)); | 
|  | // No origins were deleted. | 
|  | EXPECT_EQ(size_t(2), origins_info.size()); | 
|  | EXPECT_TRUE( | 
|  | base::PathExists(tracker->GetFullDBFilePath(kOrigin1, kDB1))); | 
|  | EXPECT_TRUE( | 
|  | base::PathExists(tracker->GetFullDBFilePath(kOrigin2, kDB2))); | 
|  |  | 
|  | EXPECT_TRUE(base::PathExists(origin1_db_dir)); | 
|  | EXPECT_TRUE(base::PathExists(origin2_db_dir)); | 
|  | } | 
|  |  | 
|  | static void EmptyDatabaseNameIsValid() { | 
|  | const GURL kOrigin(kOrigin1Url); | 
|  | const std::string kOriginId = storage::GetIdentifierFromOrigin(kOrigin); | 
|  | const base::string16 kEmptyName; | 
|  | const base::string16 kDescription(ASCIIToUTF16("description")); | 
|  | const base::string16 kChangedDescription( | 
|  | ASCIIToUTF16("changed_description")); | 
|  |  | 
|  | // Initialize a tracker database, no need to put it on disk. | 
|  | const bool kUseInMemoryTrackerDatabase = true; | 
|  | base::ScopedTempDir temp_dir; | 
|  | ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); | 
|  | scoped_refptr<DatabaseTracker> tracker( | 
|  | new DatabaseTracker(temp_dir.path(), kUseInMemoryTrackerDatabase, | 
|  | NULL, NULL, NULL)); | 
|  |  | 
|  | // Starts off with no databases. | 
|  | std::vector<OriginInfo> infos; | 
|  | EXPECT_TRUE(tracker->GetAllOriginsInfo(&infos)); | 
|  | EXPECT_TRUE(infos.empty()); | 
|  |  | 
|  | // Create a db with an empty name. | 
|  | int64 database_size = -1; | 
|  | tracker->DatabaseOpened(kOriginId, kEmptyName, kDescription, 0, | 
|  | &database_size); | 
|  | EXPECT_EQ(0, database_size); | 
|  | tracker->DatabaseModified(kOriginId, kEmptyName); | 
|  | EXPECT_TRUE(tracker->GetAllOriginsInfo(&infos)); | 
|  | EXPECT_EQ(1u, infos.size()); | 
|  | EXPECT_EQ(kDescription, infos[0].GetDatabaseDescription(kEmptyName)); | 
|  | EXPECT_FALSE(tracker->GetFullDBFilePath(kOriginId, kEmptyName).empty()); | 
|  | tracker->DatabaseOpened(kOriginId, kEmptyName, kChangedDescription, 0, | 
|  | &database_size); | 
|  | infos.clear(); | 
|  | EXPECT_TRUE(tracker->GetAllOriginsInfo(&infos)); | 
|  | EXPECT_EQ(1u, infos.size()); | 
|  | EXPECT_EQ(kChangedDescription, infos[0].GetDatabaseDescription(kEmptyName)); | 
|  | tracker->DatabaseClosed(kOriginId, kEmptyName); | 
|  | tracker->DatabaseClosed(kOriginId, kEmptyName); | 
|  |  | 
|  | // Deleting it should return to the initial state. | 
|  | EXPECT_EQ(net::OK, tracker->DeleteDatabase(kOriginId, kEmptyName, | 
|  | net::CompletionCallback())); | 
|  | infos.clear(); | 
|  | EXPECT_TRUE(tracker->GetAllOriginsInfo(&infos)); | 
|  | EXPECT_TRUE(infos.empty()); | 
|  | } | 
|  |  | 
|  | static void HandleSqliteError() { | 
|  | const GURL kOrigin(kOrigin1Url); | 
|  | const std::string kOriginId = storage::GetIdentifierFromOrigin(kOrigin); | 
|  | const base::string16 kName(ASCIIToUTF16("name")); | 
|  | const base::string16 kDescription(ASCIIToUTF16("description")); | 
|  |  | 
|  | // Initialize a tracker database, no need to put it on disk. | 
|  | const bool kUseInMemoryTrackerDatabase = true; | 
|  | base::ScopedTempDir temp_dir; | 
|  | ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); | 
|  | scoped_refptr<DatabaseTracker> tracker( | 
|  | new DatabaseTracker(temp_dir.path(), kUseInMemoryTrackerDatabase, | 
|  | NULL, NULL, NULL)); | 
|  |  | 
|  | // Setup to observe OnScheduledForDelete notifications. | 
|  | TestObserver observer(false, true); | 
|  | tracker->AddObserver(&observer); | 
|  |  | 
|  | // Verify does no harm when there is no such database. | 
|  | tracker->HandleSqliteError(kOriginId, kName, SQLITE_CORRUPT); | 
|  | EXPECT_FALSE(tracker->IsDatabaseScheduledForDeletion(kOriginId, kName)); | 
|  | EXPECT_FALSE(observer.DidReceiveNewNotification()); | 
|  |  | 
|  | // -------------------------------------------------------- | 
|  | // Create a record of a database in the tracker db and create | 
|  | // a spoof_db_file on disk in the expected location. | 
|  | int64 database_size = 0; | 
|  | tracker->DatabaseOpened(kOriginId, kName, kDescription, 0, | 
|  | &database_size); | 
|  | base::FilePath spoof_db_file = tracker->GetFullDBFilePath(kOriginId, kName); | 
|  | EXPECT_FALSE(tracker->GetFullDBFilePath(kOriginId, kName).empty()); | 
|  | EXPECT_TRUE(base::CreateDirectory(spoof_db_file.DirName())); | 
|  | EXPECT_TRUE(EnsureFileOfSize(spoof_db_file, 1)); | 
|  |  | 
|  | // Verify does no harm with a non-error is reported. | 
|  | tracker->HandleSqliteError(kOriginId, kName, SQLITE_OK); | 
|  | EXPECT_FALSE(tracker->IsDatabaseScheduledForDeletion(kOriginId, kName)); | 
|  | EXPECT_FALSE(observer.DidReceiveNewNotification()); | 
|  |  | 
|  | // Verify that with a connection open, the db is scheduled for deletion, | 
|  | // but that the file still exists. | 
|  | tracker->HandleSqliteError(kOriginId, kName, SQLITE_CORRUPT); | 
|  | EXPECT_TRUE(tracker->IsDatabaseScheduledForDeletion(kOriginId, kName)); | 
|  | EXPECT_TRUE(observer.DidReceiveNewNotification()); | 
|  | EXPECT_TRUE(base::PathExists(spoof_db_file)); | 
|  |  | 
|  | // Verify that once closed, the file is deleted and the record in the | 
|  | // tracker db is removed. | 
|  | tracker->DatabaseClosed(kOriginId, kName); | 
|  | EXPECT_FALSE(base::PathExists(spoof_db_file)); | 
|  | EXPECT_TRUE(tracker->GetFullDBFilePath(kOriginId, kName).empty()); | 
|  |  | 
|  | // -------------------------------------------------------- | 
|  | // Create another record of a database in the tracker db and create | 
|  | // a spoof_db_file on disk in the expected location. | 
|  | tracker->DatabaseOpened(kOriginId, kName, kDescription, 0, | 
|  | &database_size); | 
|  | base::FilePath spoof_db_file2 = tracker->GetFullDBFilePath(kOriginId, | 
|  | kName); | 
|  | EXPECT_FALSE(tracker->GetFullDBFilePath(kOriginId, kName).empty()); | 
|  | EXPECT_NE(spoof_db_file, spoof_db_file2); | 
|  | EXPECT_TRUE(base::CreateDirectory(spoof_db_file2.DirName())); | 
|  | EXPECT_TRUE(EnsureFileOfSize(spoof_db_file2, 1)); | 
|  |  | 
|  | // Verify that with no connection open, the db is deleted immediately. | 
|  | tracker->DatabaseClosed(kOriginId, kName); | 
|  | tracker->HandleSqliteError(kOriginId, kName, SQLITE_CORRUPT); | 
|  | EXPECT_FALSE(tracker->IsDatabaseScheduledForDeletion(kOriginId, kName)); | 
|  | EXPECT_FALSE(observer.DidReceiveNewNotification()); | 
|  | EXPECT_TRUE(tracker->GetFullDBFilePath(kOriginId, kName).empty()); | 
|  | EXPECT_FALSE(base::PathExists(spoof_db_file2)); | 
|  |  | 
|  | tracker->RemoveObserver(&observer); | 
|  | } | 
|  | }; | 
|  |  | 
|  | TEST(DatabaseTrackerTest, DeleteOpenDatabase) { | 
|  | DatabaseTracker_TestHelper_Test::TestDeleteOpenDatabase(false); | 
|  | } | 
|  |  | 
|  | TEST(DatabaseTrackerTest, DeleteOpenDatabaseIncognitoMode) { | 
|  | DatabaseTracker_TestHelper_Test::TestDeleteOpenDatabase(true); | 
|  | } | 
|  |  | 
|  | TEST(DatabaseTrackerTest, DatabaseTracker) { | 
|  | DatabaseTracker_TestHelper_Test::TestDatabaseTracker(false); | 
|  | } | 
|  |  | 
|  | TEST(DatabaseTrackerTest, DatabaseTrackerIncognitoMode) { | 
|  | DatabaseTracker_TestHelper_Test::TestDatabaseTracker(true); | 
|  | } | 
|  |  | 
|  | TEST(DatabaseTrackerTest, DatabaseTrackerQuotaIntegration) { | 
|  | // There is no difference in behavior between incognito and not. | 
|  | DatabaseTracker_TestHelper_Test::DatabaseTrackerQuotaIntegration(); | 
|  | } | 
|  |  | 
|  | TEST(DatabaseTrackerTest, DatabaseTrackerClearSessionOnlyDatabasesOnExit) { | 
|  | // Only works for regular mode. | 
|  | DatabaseTracker_TestHelper_Test:: | 
|  | DatabaseTrackerClearSessionOnlyDatabasesOnExit(); | 
|  | } | 
|  |  | 
|  | TEST(DatabaseTrackerTest, DatabaseTrackerSetForceKeepSessionState) { | 
|  | // Only works for regular mode. | 
|  | DatabaseTracker_TestHelper_Test::DatabaseTrackerSetForceKeepSessionState(); | 
|  | } | 
|  |  | 
|  | TEST(DatabaseTrackerTest, EmptyDatabaseNameIsValid) { | 
|  | DatabaseTracker_TestHelper_Test::EmptyDatabaseNameIsValid(); | 
|  | } | 
|  |  | 
|  | TEST(DatabaseTrackerTest, HandleSqliteError) { | 
|  | DatabaseTracker_TestHelper_Test::HandleSqliteError(); | 
|  | } | 
|  |  | 
|  | }  // namespace content |