// Copyright (c) 2012 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 "content/browser/dom_storage/dom_storage_context_impl.h"

#include <stddef.h>
#include <stdlib.h>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/guid.h"
#include "base/location.h"
#include "base/time/time.h"
#include "content/browser/dom_storage/dom_storage_area.h"
#include "content/browser/dom_storage/dom_storage_database.h"
#include "content/browser/dom_storage/dom_storage_namespace.h"
#include "content/browser/dom_storage/dom_storage_task_runner.h"
#include "content/browser/dom_storage/session_storage_database.h"
#include "content/common/dom_storage/dom_storage_types.h"
#include "content/public/browser/dom_storage_context.h"
#include "content/public/browser/local_storage_usage_info.h"
#include "content/public/browser/session_storage_usage_info.h"
#include "storage/browser/quota/special_storage_policy.h"

namespace content {

static const int kSessionStoraceScavengingSeconds = 60;

// Offset the session storage namespace ids generated by different contexts
// to help identify when an id from one is mistakenly used in another.
static int g_session_id_offset_sequence = 1;

DOMStorageContextImpl::DOMStorageContextImpl(
    const base::FilePath& localstorage_directory,
    const base::FilePath& sessionstorage_directory,
    storage::SpecialStoragePolicy* special_storage_policy,
    DOMStorageTaskRunner* task_runner)
    : localstorage_directory_(localstorage_directory),
      sessionstorage_directory_(sessionstorage_directory),
      task_runner_(task_runner),
      session_id_offset_(abs((g_session_id_offset_sequence++ % 10)) * 1000),
      is_shutdown_(false),
      force_keep_session_state_(false),
      special_storage_policy_(special_storage_policy),
      scavenging_started_(false) {
  // AtomicSequenceNum starts at 0 but we want to start session
  // namespace ids at one since zero is reserved for the
  // kLocalStorageNamespaceId.
  session_id_sequence_.GetNext();
}

DOMStorageContextImpl::~DOMStorageContextImpl() {
  if (session_storage_database_.get()) {
    // SessionStorageDatabase shouldn't be deleted right away: deleting it will
    // potentially involve waiting in leveldb::DBImpl::~DBImpl, and waiting
    // shouldn't happen on this thread.
    SessionStorageDatabase* to_release = session_storage_database_.get();
    to_release->AddRef();
    session_storage_database_ = NULL;
    task_runner_->PostShutdownBlockingTask(
        FROM_HERE,
        DOMStorageTaskRunner::COMMIT_SEQUENCE,
        base::Bind(&SessionStorageDatabase::Release,
                   base::Unretained(to_release)));
  }
}

DOMStorageNamespace* DOMStorageContextImpl::GetStorageNamespace(
    int64_t namespace_id) {
  if (is_shutdown_)
    return NULL;
  StorageNamespaceMap::iterator found = namespaces_.find(namespace_id);
  if (found == namespaces_.end()) {
    if (namespace_id == kLocalStorageNamespaceId) {
      if (!localstorage_directory_.empty()) {
        if (!base::CreateDirectory(localstorage_directory_)) {
          LOG(ERROR) << "Failed to create 'Local Storage' directory,"
                        " falling back to in-memory only.";
          localstorage_directory_ = base::FilePath();
        }
      }
      DOMStorageNamespace* local =
          new DOMStorageNamespace(localstorage_directory_, task_runner_.get());
      namespaces_[kLocalStorageNamespaceId] = local;
      return local;
    }
    return NULL;
  }
  return found->second.get();
}

void DOMStorageContextImpl::GetLocalStorageUsage(
    std::vector<LocalStorageUsageInfo>* infos,
    bool include_file_info) {
  if (localstorage_directory_.empty())
    return;
  base::FileEnumerator enumerator(localstorage_directory_, false,
                                  base::FileEnumerator::FILES);
  for (base::FilePath path = enumerator.Next(); !path.empty();
       path = enumerator.Next()) {
    if (path.MatchesExtension(DOMStorageArea::kDatabaseFileExtension)) {
      LocalStorageUsageInfo info;
      info.origin = DOMStorageArea::OriginFromDatabaseFileName(path);
      if (include_file_info) {
        base::FileEnumerator::FileInfo find_info = enumerator.GetInfo();
        info.data_size = find_info.GetSize();
        info.last_modified = find_info.GetLastModifiedTime();
      }
      infos->push_back(info);
    }
  }
}

void DOMStorageContextImpl::GetSessionStorageUsage(
    std::vector<SessionStorageUsageInfo>* infos) {
  if (!session_storage_database_.get())
    return;
  std::map<std::string, std::vector<GURL> > namespaces_and_origins;
  session_storage_database_->ReadNamespacesAndOrigins(
      &namespaces_and_origins);
  for (std::map<std::string, std::vector<GURL> >::const_iterator it =
           namespaces_and_origins.begin();
       it != namespaces_and_origins.end(); ++it) {
    for (std::vector<GURL>::const_iterator origin_it = it->second.begin();
         origin_it != it->second.end(); ++origin_it) {
      SessionStorageUsageInfo info;
      info.persistent_namespace_id = it->first;
      info.origin = *origin_it;
      infos->push_back(info);
    }
  }
}

void DOMStorageContextImpl::DeleteLocalStorage(const GURL& origin) {
  DCHECK(!is_shutdown_);
  DOMStorageNamespace* local = GetStorageNamespace(kLocalStorageNamespaceId);
  local->DeleteLocalStorageOrigin(origin);
  // Synthesize a 'cleared' event if the area is open so CachedAreas in
  // renderers get emptied out too.
  DOMStorageArea* area = local->GetOpenStorageArea(origin);
  if (area)
    NotifyAreaCleared(area, origin);
}

void DOMStorageContextImpl::DeleteSessionStorage(
    const SessionStorageUsageInfo& usage_info) {
  DCHECK(!is_shutdown_);
  DOMStorageNamespace* dom_storage_namespace = NULL;
  std::map<std::string, int64_t>::const_iterator it =
      persistent_namespace_id_to_namespace_id_.find(
          usage_info.persistent_namespace_id);
  if (it != persistent_namespace_id_to_namespace_id_.end()) {
    dom_storage_namespace = GetStorageNamespace(it->second);
  } else {
    int64_t namespace_id = AllocateSessionId();
    CreateSessionNamespace(namespace_id, usage_info.persistent_namespace_id);
    dom_storage_namespace = GetStorageNamespace(namespace_id);
  }
  dom_storage_namespace->DeleteSessionStorageOrigin(usage_info.origin);
  // Synthesize a 'cleared' event if the area is open so CachedAreas in
  // renderers get emptied out too.
  DOMStorageArea* area =
      dom_storage_namespace->GetOpenStorageArea(usage_info.origin);
  if (area)
    NotifyAreaCleared(area, usage_info.origin);
}

void DOMStorageContextImpl::Flush() {
  for (auto& entry : namespaces_)
    entry.second->Flush();
}

void DOMStorageContextImpl::Shutdown() {
  is_shutdown_ = true;
  StorageNamespaceMap::const_iterator it = namespaces_.begin();
  for (; it != namespaces_.end(); ++it)
    it->second->Shutdown();

  if (localstorage_directory_.empty() && !session_storage_database_.get())
    return;

  // Respect the content policy settings about what to
  // keep and what to discard.
  if (force_keep_session_state_)
    return;  // Keep everything.

  bool has_session_only_origins =
      special_storage_policy_.get() &&
      special_storage_policy_->HasSessionOnlyOrigins();

  if (has_session_only_origins) {
    // We may have to delete something. We continue on the
    // commit sequence after area shutdown tasks have cycled
    // thru that sequence (and closed their database files).
    bool success = task_runner_->PostShutdownBlockingTask(
        FROM_HERE,
        DOMStorageTaskRunner::COMMIT_SEQUENCE,
        base::Bind(&DOMStorageContextImpl::ClearSessionOnlyOrigins, this));
    DCHECK(success);
  }
}

void DOMStorageContextImpl::AddEventObserver(EventObserver* observer) {
  event_observers_.AddObserver(observer);
}

void DOMStorageContextImpl::RemoveEventObserver(EventObserver* observer) {
  event_observers_.RemoveObserver(observer);
}

void DOMStorageContextImpl::NotifyItemSet(
    const DOMStorageArea* area,
    const base::string16& key,
    const base::string16& new_value,
    const base::NullableString16& old_value,
    const GURL& page_url) {
  FOR_EACH_OBSERVER(
      EventObserver, event_observers_,
      OnDOMStorageItemSet(area, key, new_value, old_value, page_url));
}

void DOMStorageContextImpl::NotifyItemRemoved(
    const DOMStorageArea* area,
    const base::string16& key,
    const base::string16& old_value,
    const GURL& page_url) {
  FOR_EACH_OBSERVER(
      EventObserver, event_observers_,
      OnDOMStorageItemRemoved(area, key, old_value, page_url));
}

void DOMStorageContextImpl::NotifyAreaCleared(
    const DOMStorageArea* area,
    const GURL& page_url) {
  FOR_EACH_OBSERVER(
      EventObserver, event_observers_,
      OnDOMStorageAreaCleared(area, page_url));
}

int64_t DOMStorageContextImpl::AllocateSessionId() {
  return session_id_sequence_.GetNext() + session_id_offset_;
}

std::string DOMStorageContextImpl::AllocatePersistentSessionId() {
  std::string guid = base::GenerateGUID();
  std::replace(guid.begin(), guid.end(), '-', '_');
  return guid;
}

void DOMStorageContextImpl::CreateSessionNamespace(
    int64_t namespace_id,
    const std::string& persistent_namespace_id) {
  if (is_shutdown_)
    return;
  DCHECK(namespace_id != kLocalStorageNamespaceId);
  DCHECK(namespaces_.find(namespace_id) == namespaces_.end());
  namespaces_[namespace_id] = new DOMStorageNamespace(
      namespace_id, persistent_namespace_id, session_storage_database_.get(),
      task_runner_.get());
  persistent_namespace_id_to_namespace_id_[persistent_namespace_id] =
      namespace_id;
}

void DOMStorageContextImpl::DeleteSessionNamespace(int64_t namespace_id,
                                                   bool should_persist_data) {
  DCHECK_NE(kLocalStorageNamespaceId, namespace_id);
  StorageNamespaceMap::const_iterator it = namespaces_.find(namespace_id);
  if (it == namespaces_.end())
    return;
  std::string persistent_namespace_id =  it->second->persistent_namespace_id();
  if (session_storage_database_.get()) {
    if (!should_persist_data) {
      task_runner_->PostShutdownBlockingTask(
          FROM_HERE,
          DOMStorageTaskRunner::COMMIT_SEQUENCE,
          base::Bind(
              base::IgnoreResult(&SessionStorageDatabase::DeleteNamespace),
              session_storage_database_,
              persistent_namespace_id));
    } else {
      // Ensure that the data gets committed before we shut down.
      it->second->Shutdown();
      if (!scavenging_started_) {
        // Protect the persistent namespace ID from scavenging.
        protected_persistent_session_ids_.insert(persistent_namespace_id);
      }
    }
  }
  persistent_namespace_id_to_namespace_id_.erase(persistent_namespace_id);
  namespaces_.erase(namespace_id);
}

void DOMStorageContextImpl::CloneSessionNamespace(
    int64_t existing_id,
    int64_t new_id,
    const std::string& new_persistent_id) {
  if (is_shutdown_)
    return;
  DCHECK_NE(kLocalStorageNamespaceId, existing_id);
  DCHECK_NE(kLocalStorageNamespaceId, new_id);
  StorageNamespaceMap::iterator found = namespaces_.find(existing_id);
  if (found != namespaces_.end())
    namespaces_[new_id] = found->second->Clone(new_id, new_persistent_id);
  else
    CreateSessionNamespace(new_id, new_persistent_id);
}

void DOMStorageContextImpl::ClearSessionOnlyOrigins() {
  if (!localstorage_directory_.empty()) {
    std::vector<LocalStorageUsageInfo> infos;
    const bool kDontIncludeFileInfo = false;
    GetLocalStorageUsage(&infos, kDontIncludeFileInfo);
    for (size_t i = 0; i < infos.size(); ++i) {
      const GURL& origin = infos[i].origin;
      if (special_storage_policy_->IsStorageProtected(origin))
        continue;
      if (!special_storage_policy_->IsStorageSessionOnly(origin))
        continue;

      base::FilePath database_file_path = localstorage_directory_.Append(
          DOMStorageArea::DatabaseFileNameFromOrigin(origin));
      sql::Connection::Delete(database_file_path);
    }
  }
  if (session_storage_database_.get()) {
    std::vector<SessionStorageUsageInfo> infos;
    GetSessionStorageUsage(&infos);
    for (size_t i = 0; i < infos.size(); ++i) {
      const GURL& origin = infos[i].origin;
      if (special_storage_policy_->IsStorageProtected(origin))
        continue;
      if (!special_storage_policy_->IsStorageSessionOnly(origin))
        continue;
      session_storage_database_->DeleteArea(infos[i].persistent_namespace_id,
                                            origin);
    }
  }
}

void DOMStorageContextImpl::SetSaveSessionStorageOnDisk() {
  DCHECK(namespaces_.empty());
  if (!sessionstorage_directory_.empty()) {
    session_storage_database_ = new SessionStorageDatabase(
        sessionstorage_directory_);
  }
}

void DOMStorageContextImpl::StartScavengingUnusedSessionStorage() {
  if (session_storage_database_.get()) {
    task_runner_->PostDelayedTask(
        FROM_HERE, base::Bind(&DOMStorageContextImpl::FindUnusedNamespaces,
                              this),
        base::TimeDelta::FromSeconds(kSessionStoraceScavengingSeconds));
  }
}

void DOMStorageContextImpl::FindUnusedNamespaces() {
  DCHECK(session_storage_database_.get());
  if (scavenging_started_)
    return;
  scavenging_started_ = true;
  std::set<std::string> namespace_ids_in_use;
  for (StorageNamespaceMap::const_iterator it = namespaces_.begin();
       it != namespaces_.end(); ++it)
    namespace_ids_in_use.insert(it->second->persistent_namespace_id());
  std::set<std::string> protected_persistent_session_ids;
  protected_persistent_session_ids.swap(protected_persistent_session_ids_);
  task_runner_->PostShutdownBlockingTask(
      FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE,
      base::Bind(
          &DOMStorageContextImpl::FindUnusedNamespacesInCommitSequence,
          this, namespace_ids_in_use, protected_persistent_session_ids));
}

void DOMStorageContextImpl::FindUnusedNamespacesInCommitSequence(
    const std::set<std::string>& namespace_ids_in_use,
    const std::set<std::string>& protected_persistent_session_ids) {
  DCHECK(session_storage_database_.get());
  // Delete all namespaces which don't have an associated DOMStorageNamespace
  // alive.
  std::map<std::string, std::vector<GURL> > namespaces_and_origins;
  session_storage_database_->ReadNamespacesAndOrigins(&namespaces_and_origins);
  for (std::map<std::string, std::vector<GURL> >::const_iterator it =
           namespaces_and_origins.begin();
       it != namespaces_and_origins.end(); ++it) {
    if (namespace_ids_in_use.find(it->first) == namespace_ids_in_use.end() &&
        protected_persistent_session_ids.find(it->first) ==
        protected_persistent_session_ids.end()) {
      deletable_persistent_namespace_ids_.push_back(it->first);
    }
  }
  if (!deletable_persistent_namespace_ids_.empty()) {
    task_runner_->PostDelayedTask(
        FROM_HERE, base::Bind(
            &DOMStorageContextImpl::DeleteNextUnusedNamespace,
            this),
        base::TimeDelta::FromSeconds(kSessionStoraceScavengingSeconds));
  }
}

void DOMStorageContextImpl::DeleteNextUnusedNamespace() {
  if (is_shutdown_)
    return;
  task_runner_->PostShutdownBlockingTask(
        FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE,
        base::Bind(
            &DOMStorageContextImpl::DeleteNextUnusedNamespaceInCommitSequence,
            this));
}

void DOMStorageContextImpl::DeleteNextUnusedNamespaceInCommitSequence() {
  if (deletable_persistent_namespace_ids_.empty())
    return;
  const std::string& persistent_id = deletable_persistent_namespace_ids_.back();
  session_storage_database_->DeleteNamespace(persistent_id);
  deletable_persistent_namespace_ids_.pop_back();
  if (!deletable_persistent_namespace_ids_.empty()) {
    task_runner_->PostDelayedTask(
        FROM_HERE, base::Bind(
            &DOMStorageContextImpl::DeleteNextUnusedNamespace,
            this),
        base::TimeDelta::FromSeconds(kSessionStoraceScavengingSeconds));
  }
}

}  // namespace content
