// Copyright (c) 2013 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 "components/net_log/net_export_file_writer.h"

#include <memory>
#include <set>
#include <utility>

#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/sequenced_task_runner.h"
#include "base/task_runner_util.h"
#include "base/task_scheduler/post_task.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/net_log/chrome_net_log.h"

namespace net_log {

namespace {

// Path of logs relative to default temporary directory given by
// base::GetTempDir(). Must be kept in sync with
// chrome/android/java/res/xml/file_paths.xml. Only used if not saving log file
// to a custom path.
const base::FilePath::CharType kLogRelativePath[] =
    FILE_PATH_LITERAL("net-export/chrome-net-export-log.json");

// Contains file-related initialization tasks for NetExportFileWriter.
NetExportFileWriter::DefaultLogPathResults SetUpDefaultLogPath(
    const NetExportFileWriter::DirectoryGetter& default_log_base_dir_getter) {
  NetExportFileWriter::DefaultLogPathResults results;
  results.default_log_path_success = false;
  results.log_exists = false;

  base::FilePath default_base_dir;
  if (!default_log_base_dir_getter.Run(&default_base_dir))
    return results;

  results.default_log_path = default_base_dir.Append(kLogRelativePath);
  if (!base::CreateDirectoryAndGetError(results.default_log_path.DirName(),
                                        nullptr))
    return results;

  results.log_exists = base::PathExists(results.default_log_path);
  results.default_log_path_success = true;
  return results;
}

// If running on a POSIX OS, this will attempt to set all the permission flags
// of the file at |path| to 1. Will return |path| on success and the empty path
// on failure.
base::FilePath GetPathWithAllPermissions(const base::FilePath& path) {
  if (!base::PathExists(path))
    return base::FilePath();
#if defined(OS_POSIX)
  return base::SetPosixFilePermissions(path, base::FILE_PERMISSION_MASK)
             ? path
             : base::FilePath();
#else
  return path;
#endif
}

scoped_refptr<base::SequencedTaskRunner> CreateFileTaskRunner() {
  // The tasks posted to this sequenced task runner do synchronous File I/O for
  // checking paths and setting permissions on files.
  //
  // These operations can be skipped on shutdown since FileNetLogObserver's API
  // doesn't require things to have completed until notified of completion.
  return base::CreateSequencedTaskRunnerWithTraits(
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});
}

}  // namespace

NetExportFileWriter::NetExportFileWriter()
    : state_(STATE_UNINITIALIZED),
      log_exists_(false),
      log_capture_mode_known_(false),
      log_capture_mode_(net::NetLogCaptureMode::Default()),
      default_log_base_dir_getter_(base::Bind(&base::GetTempDir)),
      weak_ptr_factory_(this) {}

NetExportFileWriter::~NetExportFileWriter() {
  if (net_log_exporter_) {
    net_log_exporter_->Stop(base::Value(base::Value::Type::DICTIONARY),
                            base::DoNothing());
  }
}

void NetExportFileWriter::AddObserver(StateObserver* observer) {
  DCHECK(thread_checker_.CalledOnValidThread());
  state_observer_list_.AddObserver(observer);
}

void NetExportFileWriter::RemoveObserver(StateObserver* observer) {
  DCHECK(thread_checker_.CalledOnValidThread());
  state_observer_list_.RemoveObserver(observer);
}

void NetExportFileWriter::Initialize() {
  DCHECK(thread_checker_.CalledOnValidThread());

  file_task_runner_ = CreateFileTaskRunner();

  if (state_ != STATE_UNINITIALIZED)
    return;

  state_ = STATE_INITIALIZING;

  NotifyStateObserversAsync();

  base::PostTaskAndReplyWithResult(
      file_task_runner_.get(), FROM_HERE,
      base::Bind(&SetUpDefaultLogPath, default_log_base_dir_getter_),
      base::Bind(&NetExportFileWriter::SetStateAfterSetUpDefaultLogPath,
                 weak_ptr_factory_.GetWeakPtr()));
}

void NetExportFileWriter::StartNetLog(
    const base::FilePath& log_path,
    net::NetLogCaptureMode capture_mode,
    uint64_t max_file_size,
    const base::CommandLine::StringType& command_line_string,
    const std::string& channel_string,
    network::mojom::NetworkContext* network_context) {
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK(file_task_runner_);

  if (state_ != STATE_NOT_LOGGING)
    return;

  if (!log_path.empty())
    log_path_ = log_path;

  DCHECK(!log_path_.empty());

  state_ = STATE_STARTING_LOG;

  NotifyStateObserversAsync();

  network_context->CreateNetLogExporter(mojo::MakeRequest(&net_log_exporter_));
  base::Value custom_constants = base::Value::FromUniquePtrValue(
      ChromeNetLog::GetPlatformConstants(command_line_string, channel_string));

  base::PostTaskAndReplyWithResult(
      file_task_runner_.get(), FROM_HERE,
      base::BindOnce(&NetExportFileWriter::CreateOutputFile, log_path_),
      base::BindOnce(&NetExportFileWriter::StartNetLogAfterCreateFile,
                     weak_ptr_factory_.GetWeakPtr(), capture_mode,
                     max_file_size, std::move(custom_constants)));
}

void NetExportFileWriter::StartNetLogAfterCreateFile(
    net::NetLogCaptureMode capture_mode,
    uint64_t max_file_size,
    base::Value custom_constants,
    base::File output_file) {
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK_EQ(STATE_STARTING_LOG, state_);

  // TODO(morlovich): Communicate file open trouble better
  // (https://crbug.com/838977)
  if (!output_file.IsValid()) {
    ResetExporterThenSetStateNotLogging();
    return;
  }

  network::mojom::NetLogExporter_CaptureMode rpc_capture_mode =
      network::mojom::NetLogExporter::CaptureMode::DEFAULT;
  if (capture_mode.include_socket_bytes()) {
    rpc_capture_mode =
        network::mojom::NetLogExporter::CaptureMode::INCLUDE_SOCKET_BYTES;
  } else if (capture_mode.include_cookies_and_credentials()) {
    rpc_capture_mode = network::mojom::NetLogExporter::CaptureMode::
        INCLUDE_COOKIES_AND_CREDENTIALS;
  }

  // base::Unretained(this) is safe here since |net_log_exporter_| is owned by
  // |this| and is a mojo InterfacePtr, which guarantees callback cancellation
  // upon its destruction.
  net_log_exporter_->Start(
      std::move(output_file), std::move(custom_constants), rpc_capture_mode,
      max_file_size,
      base::BindOnce(&NetExportFileWriter::OnStartResult,
                     base::Unretained(this), capture_mode));
}

void NetExportFileWriter::OnStartResult(net::NetLogCaptureMode capture_mode,
                                        int result) {
  if (result == net::OK) {
    state_ = STATE_LOGGING;
    log_exists_ = true;
    log_capture_mode_known_ = true;
    log_capture_mode_ = capture_mode;

    NotifyStateObservers();
  } else {
    ResetExporterThenSetStateNotLogging();
  }
}

void NetExportFileWriter::StopNetLog(
    std::unique_ptr<base::DictionaryValue> polled_data) {
  DCHECK(thread_checker_.CalledOnValidThread());

  if (state_ != STATE_LOGGING)
    return;

  state_ = STATE_STOPPING_LOG;

  NotifyStateObserversAsync();

  base::Value polled_data_value(base::Value::Type::DICTIONARY);
  if (polled_data)
    polled_data_value = base::Value::FromUniquePtrValue(std::move(polled_data));
  // base::Unretained(this) is safe here since |net_log_exporter_| is owned by
  // |this| and is a mojo InterfacePtr, which guarantees callback cancellation
  // upon its destruction.
  net_log_exporter_->Stop(std::move(polled_data_value),
                          base::BindOnce(&NetExportFileWriter::OnStopResult,
                                         base::Unretained(this)));
}

void NetExportFileWriter::OnStopResult(int result) {
  ResetExporterThenSetStateNotLogging();
}

std::unique_ptr<base::DictionaryValue> NetExportFileWriter::GetState() const {
  DCHECK(thread_checker_.CalledOnValidThread());

  auto dict = std::make_unique<base::DictionaryValue>();

  dict->SetString("file", log_path_.LossyDisplayName());

  base::StringPiece state_string;
  switch (state_) {
    case STATE_UNINITIALIZED:
      state_string = "UNINITIALIZED";
      break;
    case STATE_INITIALIZING:
      state_string = "INITIALIZING";
      break;
    case STATE_NOT_LOGGING:
      state_string = "NOT_LOGGING";
      break;
    case STATE_STARTING_LOG:
      state_string = "STARTING_LOG";
      break;
    case STATE_LOGGING:
      state_string = "LOGGING";
      break;
    case STATE_STOPPING_LOG:
      state_string = "STOPPING_LOG";
      break;
  }
  dict->SetString("state", state_string);

  dict->SetBoolean("logExists", log_exists_);
  dict->SetBoolean("logCaptureModeKnown", log_capture_mode_known_);
  dict->SetString("captureMode", CaptureModeToString(log_capture_mode_));

  return dict;
}

void NetExportFileWriter::GetFilePathToCompletedLog(
    const FilePathCallback& path_callback) const {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (!(log_exists_ && state_ == STATE_NOT_LOGGING)) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::Bind(path_callback, base::FilePath()));
    return;
  }

  DCHECK(file_task_runner_);
  DCHECK(!log_path_.empty());

  base::PostTaskAndReplyWithResult(
      file_task_runner_.get(), FROM_HERE,
      base::Bind(&GetPathWithAllPermissions, log_path_), path_callback);
}

std::string NetExportFileWriter::CaptureModeToString(
    net::NetLogCaptureMode capture_mode) {
  if (capture_mode == net::NetLogCaptureMode::Default())
    return "STRIP_PRIVATE_DATA";
  if (capture_mode == net::NetLogCaptureMode::IncludeCookiesAndCredentials())
    return "NORMAL";
  if (capture_mode == net::NetLogCaptureMode::IncludeSocketBytes())
    return "LOG_BYTES";
  NOTREACHED();
  return "STRIP_PRIVATE_DATA";
}

net::NetLogCaptureMode NetExportFileWriter::CaptureModeFromString(
    const std::string& capture_mode_string) {
  if (capture_mode_string == "STRIP_PRIVATE_DATA")
    return net::NetLogCaptureMode::Default();
  if (capture_mode_string == "NORMAL")
    return net::NetLogCaptureMode::IncludeCookiesAndCredentials();
  if (capture_mode_string == "LOG_BYTES")
    return net::NetLogCaptureMode::IncludeSocketBytes();
  NOTREACHED();
  return net::NetLogCaptureMode::Default();
}

void NetExportFileWriter::SetDefaultLogBaseDirectoryGetterForTest(
    const DirectoryGetter& getter) {
  default_log_base_dir_getter_ = getter;
}

void NetExportFileWriter::NotifyStateObservers() {
  DCHECK(thread_checker_.CalledOnValidThread());
  std::unique_ptr<base::DictionaryValue> state = GetState();
  for (StateObserver& observer : state_observer_list_) {
    observer.OnNewState(*state);
  }
}

void NetExportFileWriter::NotifyStateObserversAsync() {
  DCHECK(thread_checker_.CalledOnValidThread());
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::Bind(&NetExportFileWriter::NotifyStateObservers,
                            weak_ptr_factory_.GetWeakPtr()));
}

void NetExportFileWriter::SetStateAfterSetUpDefaultLogPath(
    const DefaultLogPathResults& set_up_default_log_path_results) {
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK_EQ(STATE_INITIALIZING, state_);

  if (set_up_default_log_path_results.default_log_path_success) {
    state_ = STATE_NOT_LOGGING;
    log_path_ = set_up_default_log_path_results.default_log_path;
    log_exists_ = set_up_default_log_path_results.log_exists;
    DCHECK(!log_capture_mode_known_);
  } else {
    state_ = STATE_UNINITIALIZED;
  }
  NotifyStateObservers();
}

void NetExportFileWriter::ResetExporterThenSetStateNotLogging() {
  DCHECK(thread_checker_.CalledOnValidThread());
  net_log_exporter_.reset();
  state_ = STATE_NOT_LOGGING;

  NotifyStateObservers();
}

base::File NetExportFileWriter::CreateOutputFile(base::FilePath path) {
  return base::File(path,
                    base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
}

}  // namespace net_log
