// Copyright 2018 The Chromium OS 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 "crash-reporter/crash_sender_util.h"

#include <inttypes.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <algorithm>
#include <string>
#include <utility>
#include <vector>

#include <base/files/file_enumerator.h>
#include <base/files/file_util.h>
#include <base/guid.h>
#include <base/json/json_writer.h>
#include <base/rand_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/threading/platform_thread.h>
#include <base/time/time.h>
#include <brillo/flag_helper.h>
#include <brillo/http/http_proxy.h>
#include <brillo/http/http_transport.h>
#include <brillo/http/http_utils.h>
#include <brillo/variant_dictionary.h>
#include <chromeos/dbus/service_constants.h>

#include "crash-reporter/crash_sender.pb.h"
#include "crash-reporter/crash_sender_paths.h"
#include "crash-reporter/paths.h"
#include "crash-reporter/util.h"

namespace util {

bool g_force_is_mock = false;
bool g_force_is_mock_successful = false;

namespace {

// URL to send official build crash reports to.
constexpr char kReportUploadProdUrl[] = "https://clients2.google.com/cr/report";
// URL to send test/dev build crash reports to.
constexpr char kReportUploadStagingUrl[] =
    "https://clients2.google.com/cr/staging_report";
constexpr char kUndefined[] = "undefined";
constexpr char kChromeOsProduct[] = "ChromeOS";
constexpr char kUploadVarPrefix[] = "upload_var_";
constexpr char kUploadTextPrefix[] = "upload_text_";
constexpr char kUploadFilePrefix[] = "upload_file_";
constexpr char kOsTimestamp[] = "os_millis";
constexpr char kAlreadyUploadedExt[] = ".alreadyuploaded";
constexpr char kProcessingExt[] = ".processing";

// Keys used in uploads.log file. (All timestamps are measured in seconds.)
constexpr char kJsonLogKeyUploadId[] = "upload_id";
constexpr char kJsonLogKeyUploadTime[] = "upload_time";
constexpr char kJsonLogKeyLocalId[] = "local_id";
constexpr char kJsonLogKeyCaptureTime[] = "capture_time";
constexpr char kJsonLogKeyState[] = "state";
constexpr char kJsonLogKeySource[] = "source";

// Keys used in CrashDetails::metadata.
constexpr char kMetadataKeyCaptureTimeMillis[] = "upload_var_reportTimeMillis";
constexpr char kMetadataKeySource[] = "exec_name";

// Values used for kJsonLogKeySource.
constexpr char kMetadataValueRedacted[] = "REDACTED";

// Length of the client ID. This is a standard GUID which has the dashes
// removed.
constexpr size_t kClientIdLength = 32U;

// Buffer size for reading a meta file into memory, in bytes.
constexpr size_t kMaxMetaFileSize = 1024 * 1024;

// Must match testModeSuccessful in the tast-test chrome_crash_loop.go.
constexpr char kTestModeSuccessful[] =
    "Test Mode: Logging success and exiting instead of actually uploading";

// UMA metrics to track crash removal attempts and failures.
constexpr char kUMAFailedCrashRemoval[] = "Crash.Sender.FailedCrashRemoval";
constexpr char kUMAAttemptedCrashRemoval[] =
    "Crash.Sender.AttemptedCrashRemoval";

// UMA enum to track reasons crash_sender attempts to delete a crash.
constexpr char kCrashSenderRemoveHistName[] =
    "Platform.CrOS.CrashSenderRemoveReason";

// Returns true if the given report kind is known.
// TODO(satorux): Move collector constants to a common file.
bool IsKnownKind(const std::string& kind) {
  return (kind == "minidump" || kind == "kcrash" || kind == "log" ||
          kind == "devcore" || kind == "eccrash" || kind == "bertdump");
}

// Returns true if the given key is valid for crash metadata.
bool IsValidKey(const std::string& key) {
  if (key.empty())
    return false;

  for (const char c : key) {
    if (!(base::IsAsciiAlpha(c) || base::IsAsciiDigit(c) || c == '_' ||
          c == '-' || c == '.')) {
      return false;
    }
  }

  return true;
}

// Converts metadata into CrashInfo.
void MetadataToCrashInfo(const brillo::KeyValueStore& metadata,
                         CrashInfo* info) {
  info->payload_file = GetBaseNameFromMetadata(metadata, "payload");
  info->payload_kind = GetKindFromPayloadPath(info->payload_file);
}

// This class assists us in recovering from crashes while processing crashes.
// When it is constructed, it attempts to create a ".processing" file for the
// given metadata file, and when it is destructed it removes it.
// If crash_sender crashes, or otherwise exits without running the destructor,
// the .processing file will still exist. ChooseAction uses the existence of
// this file to determine that the crash may be malformed and avoid processing
// it again.
class ScopedProcessingFile {
 public:
  explicit ScopedProcessingFile(const base::FilePath& meta_file)
      : processing_file_(meta_file.ReplaceExtension(kProcessingExt)) {
    base::File f(processing_file_,
                 base::File::FLAG_CREATE | base::File::FLAG_WRITE);
    if (!f.IsValid()) {
      LOG(ERROR) << "Failed to mark crash as being processed";
    }
  }

  // Disallow copy and assign (and implicitly, move).
  ScopedProcessingFile(const ScopedProcessingFile& other) = delete;
  ScopedProcessingFile& operator=(const ScopedProcessingFile& other) = delete;

  ~ScopedProcessingFile() {
    if (!base::DeleteFile(processing_file_, /*recursive=*/false)) {
      LOG(ERROR) << "Failed to remove .processing file. Crash will be deleted.";
    }
  }

 private:
  const base::FilePath processing_file_;
};

}  // namespace

void ParseCommandLine(int argc,
                      const char* const* argv,
                      CommandLineFlags* flags) {
  DEFINE_int32(max_spread_time, kMaxSpreadTimeInSeconds,
               "Max time in secs to sleep before sending (0 to send now)");
  DEFINE_string(crash_directory, "",
                "If set, upload only crashes in this directory.");
  const std::string ignore_rate_limits_description = base::StringPrintf(
      "Ignore normal limit of %d crash uploads per day", kMaxCrashRate);
  DEFINE_bool(ignore_rate_limits, false,
              ignore_rate_limits_description.c_str());
  const std::string ignore_hold_off_time_description = base::StringPrintf(
      "Assume all crash reports are completely written to disk. Do not "
      "wait %" PRId64 " seconds after meta file is written to start sending.",
      kMaxHoldOffTime.InSeconds());
  DEFINE_bool(ignore_hold_off_time, false,
              ignore_hold_off_time_description.c_str());
  DEFINE_bool(dev, false,
              "Send crash reports regardless of image/build type "
              "and upload them to the staging server instead.");
  DEFINE_bool(ignore_pause_file, false,
              "Ignore the existence of the pause file and run anyways");
  DEFINE_bool(test_mode, false,
              "Do not upload crashes; instead, log a special message if the "
              "crash is valid. Used by tast test ChromeCrashLoop.");
  DEFINE_bool(delete_crashes, true,
              "Instead of removing crashes after uploading them, create a new "
              "file indicating that they have been uploaded. Used by "
              "integration testing frameworks to provide data to the crash "
              "server while also capturing crashes in a framework-specific "
              "way.");
  DEFINE_bool(ignore_test_image, false,
              "Upload crashes to the crash server even if running on a test "
              "image, but do NOT ignore official image check.");
  DEFINE_bool(upload_old_reports, false,
              "If set, ignore the timestamp check and upload older reports.");
  brillo::FlagHelper::Init(argc, argv, "Chromium OS Crash Sender");
  if (FLAGS_max_spread_time < 0) {
    LOG(ERROR) << "Invalid value for max spread time: "
               << FLAGS_max_spread_time;
    exit(EXIT_FAILURE);
  }
  flags->max_spread_time = base::TimeDelta::FromSeconds(FLAGS_max_spread_time);
  flags->crash_directory = FLAGS_crash_directory;
  flags->ignore_rate_limits = FLAGS_ignore_rate_limits;
  flags->ignore_hold_off_time = FLAGS_ignore_hold_off_time;
  flags->allow_dev_sending = FLAGS_dev;
  flags->ignore_pause_file = FLAGS_ignore_pause_file;
  flags->test_mode = FLAGS_test_mode;
  flags->delete_crashes = FLAGS_delete_crashes;
  flags->ignore_test_image = FLAGS_ignore_test_image;
  flags->upload_old_reports = FLAGS_upload_old_reports;
  if (flags->test_mode) {
    // The pause file is intended to pause the cronjob crash_sender during
    // tests, not the crash_sender invoked by the test code.
    flags->ignore_pause_file = true;
  }
}

void RecordCrashDone() {
  if (IsMock()) {
    // For testing purposes, emit a message to log so that we
    // know when the test has received all the messages from this run.
    // The string is referenced in
    // third_party/autotest/files/client/cros/crash/crash_test.py and
    // platform/tast-tests/src/chromiumos/tast/local/crash/sender.go
    LOG(INFO) << "crash_sender done. (mock)";
  }
}

bool IsMock() {
  if (g_force_is_mock) {
    return true;
  }
  return base::PathExists(
      paths::GetAt(paths::kSystemRunStateDirectory, paths::kMockCrashSending));
}

bool IsMockSuccessful() {
  if (g_force_is_mock_successful) {
    return true;
  }
  int64_t file_size;
  return base::GetFileSize(paths::GetAt(paths::kSystemRunStateDirectory,
                                        paths::kMockCrashSending),
                           &file_size) &&
         !file_size;
}

bool DoesPauseFileExist() {
  return base::PathExists(paths::Get(paths::kPauseCrashSending));
}

std::string GetImageType() {
  if (util::IsTestImage())
    return "test";
  else if (util::IsDeveloperImage())
    return "dev";
  else if (IsMock() && !IsMockSuccessful())
    return "mock-fail";
  else
    return "";
}

base::FilePath GetBasePartOfCrashFile(const base::FilePath& file_name) {
  std::vector<std::string> components;
  file_name.GetComponents(&components);

  std::vector<std::string> parts = base::SplitString(
      components.back(), ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  if (parts.size() < 4) {
    LOG(ERROR) << "Unexpected file name format: " << file_name.value();
    return file_name;
  }

  parts.resize(4);
  const std::string base_name = base::JoinString(parts, ".");

  if (components.size() == 1)
    return base::FilePath(base_name);
  return file_name.DirName().Append(base_name);
}

void RemoveOrphanedCrashFiles(const base::FilePath& crash_dir) {
  base::FileEnumerator iter(crash_dir, true /* recursive */,
                            base::FileEnumerator::FILES, "*");
  for (base::FilePath file = iter.Next(); !file.empty(); file = iter.Next()) {
    // Get the meta data file path.
    const base::FilePath meta_file =
        base::FilePath(GetBasePartOfCrashFile(file).value() + ".meta");

    // Check how old the file is.
    base::File::Info info;
    if (!base::GetFileInfo(file, &info)) {
      PLOG(WARNING) << "Failed to get file info: " << file.value();
      continue;
    }
    base::TimeDelta delta = base::Time::Now() - info.last_modified;

    if (!base::PathExists(meta_file) && delta.InHours() >= 24) {
      LOG(INFO) << "Removing old orphaned file: " << file.value();
      if (!base::DeleteFile(file, false /* recursive */))
        PLOG(WARNING) << "Failed to remove " << file.value();
    }
  }
}

void SortReports(std::vector<MetaFile>* reports) {
  std::sort(reports->begin(), reports->end(),
            [](const MetaFile& m1, const MetaFile& m2) {
              // Send older reports first to avoid starvation if there is a
              // constant stream of crashes (that is, if thing A is producing
              // crash reports constantly, and thing B produces one crash
              // report, make sure thing B's crash report gets sent eventually.)
              return m1.second.last_modified < m2.second.last_modified;
            });
}

std::vector<base::FilePath> GetMetaFiles(const base::FilePath& crash_dir) {
  std::vector<base::FilePath> meta_files;
  if (!base::DirectoryExists(crash_dir)) {
    // Directory not existing is not an error.
    return meta_files;
  }

  base::FileEnumerator iter(crash_dir, false /* recursive */,
                            base::FileEnumerator::FILES, "*.meta");
  std::vector<std::pair<base::Time, base::FilePath>> time_meta_pairs;
  for (base::FilePath file = iter.Next(); !file.empty(); file = iter.Next()) {
    base::File::Info info;
    if (!base::GetFileInfo(file, &info)) {
      PLOG(WARNING) << "Failed to get file info: " << file.value();
      continue;
    }
    time_meta_pairs.push_back(std::make_pair(info.last_modified, file));
  }
  std::sort(time_meta_pairs.begin(), time_meta_pairs.end());

  for (const auto& pair : time_meta_pairs)
    meta_files.push_back(pair.second);
  return meta_files;
}

base::FilePath GetBaseNameFromMetadata(const brillo::KeyValueStore& metadata,
                                       const std::string& key) {
  std::string value;
  if (!metadata.GetString(key, &value))
    return base::FilePath();

  return base::FilePath(value).BaseName();
}

std::string GetKindFromPayloadPath(const base::FilePath& payload_path) {
  std::vector<std::string> parts =
      base::SplitString(payload_path.BaseName().value(), ".",
                        base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  // Suppress "gz".
  if (parts.size() >= 2 && parts.back() == "gz")
    parts.pop_back();

  if (parts.size() <= 1)
    return "";

  std::string extension = parts.back();
  if (extension == "dmp")
    return "minidump";

  return extension;
}

bool ParseMetadata(const std::string& raw_metadata,
                   brillo::KeyValueStore* metadata) {
  metadata->Clear();
  if (!metadata->LoadFromString(raw_metadata))
    return false;

  for (const auto& key : metadata->GetKeys()) {
    if (!IsValidKey(key))
      return false;
  }

  return true;
}

bool IsCompleteMetadata(const brillo::KeyValueStore& metadata) {
  // *.meta files always end with done=1 so we can tell if they are complete.
  std::string value;
  if (!metadata.GetString("done", &value))
    return false;
  return value == "1";
}

bool IsAlreadyUploaded(const base::FilePath& meta_file) {
  return base::PathExists(meta_file.ReplaceExtension(kAlreadyUploadedExt));
}

bool IsTimestampNewEnough(const base::FilePath& timestamp_file) {
  const base::Time threshold =
      base::Time::Now() - base::TimeDelta::FromHours(24);

  base::File::Info info;
  if (!base::GetFileInfo(timestamp_file, &info)) {
    PLOG(ERROR) << "Failed to get file info: " << timestamp_file.value();
    return false;
  }

  return threshold < info.last_modified;
}

bool IsBelowRate(const base::FilePath& timestamps_dir,
                 int max_crash_rate,
                 int max_crash_bytes) {
  // If we can't get a size for one of our uploads, use this as a size. It's
  // an overestimate, but it ensures that when a user upgrades from a previous
  // version of the code to this version, we don't send a huge batch of reports
  // because the previous version didn't write out sizes.
  const int kGuesstimateBytes = util::kDefaultMaxUploadBytes;

  // Count the number of timestamp files, that were written in the past 24
  // hours. Remove files that are older. Each file that exists should contain
  // a SendRecord protobuf giving the number of bytes used for that send; add
  // them up.
  int current_rate = 0;
  int current_bytes = 0;
  base::FileEnumerator iter(timestamps_dir, false /* recursive */,
                            base::FileEnumerator::FILES, "*");
  for (base::FilePath file = iter.Next(); !file.empty(); file = iter.Next()) {
    if (IsTimestampNewEnough(file)) {
      ++current_rate;
      std::string serialized;
      if (!base::ReadFileToString(file, &serialized)) {
        PLOG(WARNING) << "Unable to read timestamp file at " << file.value();
        // Keep going without reading the file; what else can we do? If we
        // really get a file with bad permissions, we don't want to stop ever
        // sending crashes from this computer, so we shouldn't return false.
        // But do add something to current_bytes to avoid uploading an unlimited
        // number of reports if this happens to all our files.
        current_bytes += kGuesstimateBytes;
        continue;
      }

      crash::SendRecord previous_send;
      if (!previous_send.ParseFromString(serialized)) {
        LOG(WARNING) << "Could not parse " << file.value();
        current_bytes += kGuesstimateBytes;
        continue;
      }

      if (previous_send.size() <= 0) {
        // Zero is not a realistic size for the upload, so don't believe it.
        // Probably from a previous version of the code that didn't write out
        // the sizes. proto3 will read an empty file as "all fields are zero".
        LOG(WARNING) << "Previous upload size was " << previous_send.size()
                     << "; ignoring and guessing " << kGuesstimateBytes;
        current_bytes += kGuesstimateBytes;
        continue;
      }
      current_bytes += previous_send.size();
    } else {
      if (!base::DeleteFile(file, false /* recursive */))
        PLOG(WARNING) << "Failed to remove old report " << file.value();
    }
  }
  LOG(INFO) << "Current send rate: " << current_rate << " sends and "
            << current_bytes << " bytes/24hrs";

  // We allow either condition independently; see comments around
  // kMaxCrashBytes. Therefore, we use || instead of the more common &&.
  return current_rate < max_crash_rate || current_bytes < max_crash_bytes;
}

void RecordSendAttempt(const base::FilePath& timestamps_dir, int bytes) {
  if (!base::CreateDirectory(timestamps_dir)) {
    PLOG(ERROR) << "Failed to create a timestamps directory: "
                << timestamps_dir.value();
    return;
  }

  base::FilePath temp_file_path;
  base::ScopedFILE temp_file(
#if BASE_VER < 780000
      base::CreateAndOpenTemporaryFileInDir
#else
      base::CreateAndOpenTemporaryStreamInDir
#endif
      (timestamps_dir, &temp_file_path));
  if (temp_file == nullptr) {
    PLOG(ERROR) << "Failed to create a file in " << timestamps_dir.value();
  } else {
    crash::SendRecord record;
    record.set_size(bytes);
    std::string serialized;
    record.SerializeToString(&serialized);
    fwrite(serialized.c_str(), 1, serialized.size(), temp_file.get());
  }
}

bool GetSleepTime(const base::FilePath& meta_file,
                  const base::TimeDelta& max_spread_time,
                  const base::TimeDelta& hold_off_time,
                  base::TimeDelta* sleep_time) {
  base::File::Info info;
  if (!base::GetFileInfo(meta_file, &info)) {
    PLOG(ERROR) << "Failed to get file info: " << meta_file.value();
    return false;
  }

  // The meta file should be written *after* all to-be-uploaded files that it
  // references.  Nevertheless, as a safeguard, a hold-off time after writing
  // the meta file is ensured.  Also, sending of crash reports is spread out
  // randomly by up to |max_spread_time|. Thus, for the sleep call the greater
  // of the two delays is used. Use max() to ensure that holdoff_time is not
  // negative.
  const base::TimeDelta hold_off_time_remaining =
      std::max(info.last_modified + hold_off_time - base::Time::Now(),
               base::TimeDelta());

  const int seconds = (max_spread_time.InSeconds() <= 0
                           ? 0
                           : base::RandInt(0, max_spread_time.InSeconds()));
  const base::TimeDelta spread_time = base::TimeDelta::FromSeconds(seconds);

  *sleep_time = std::max(spread_time, hold_off_time_remaining);

  return true;
}

std::string GetClientId() {
  std::string client_id;
  base::FilePath client_id_dir = paths::Get(paths::kCrashSenderStateDirectory);
  if (!base::CreateDirectory(client_id_dir)) {
    PLOG(ERROR) << "Failed to create directory: " << client_id_dir.value();
    return "";
  }
  base::FilePath client_id_file = client_id_dir.Append(paths::kClientId);
  if (base::PathExists(client_id_file)) {
    if (!base::ReadFileToString(client_id_file, &client_id)) {
      PLOG(ERROR) << "Error reading client ID file: " << client_id_file.value();
    } else if (client_id.length() != kClientIdLength) {
      // Don't log what this is, otherwise we may need to scrub it.
      LOG(ERROR) << "Client ID has wrong format, regenerate it";
    } else {
      return client_id;
    }
  }
  client_id = base::GenerateGUID();
  // Strip out the dashes, we don't want those.
  base::RemoveChars(client_id, "-", &client_id);

  if (base::WriteFile(client_id_file, client_id.c_str(), client_id.length()) !=
      client_id.length()) {
    PLOG(ERROR) << "Error writing out client ID to file: "
                << client_id_file.value();
  }

  return client_id;
}

Sender::Sender(std::unique_ptr<MetricsLibraryInterface> metrics_lib,
               std::unique_ptr<base::Clock> clock,
               const Sender::Options& options)
    : metrics_lib_(std::move(metrics_lib)),
      session_manager_proxy_(options.session_manager_proxy),
      shill_proxy_(options.shill_proxy),
      form_data_boundary_(options.form_data_boundary),
      always_write_uploads_log_(options.always_write_uploads_log),
      max_crash_rate_(options.max_crash_rate),
      max_crash_bytes_(options.max_crash_bytes),
      max_spread_time_(options.max_spread_time),
      hold_off_time_(options.hold_off_time),
      sleep_function_(options.sleep_function),
      allow_dev_sending_(options.allow_dev_sending),
      test_mode_(options.test_mode),
      delete_crashes_(options.delete_crashes),
      upload_old_reports_(options.upload_old_reports),
      clock_(std::move(clock)) {}

bool Sender::Init() {
  if (!scoped_temp_dir_.CreateUniqueTempDir()) {
    PLOG(ERROR) << "Failed to create a temporary directory";
    return false;
  }

  return true;
}

base::File Sender::AcquireLockFileOrDie() {
  base::FilePath lock_file_path = paths::Get(paths::kCrashSenderLockFile);
  base::File lock_file(lock_file_path, base::File::FLAG_OPEN_ALWAYS |
                                           base::File::FLAG_READ |
                                           base::File::FLAG_WRITE);
  if (!lock_file.IsValid()) {
    LOG(FATAL) << "Error opening " << lock_file_path.value() << ": "
               << base::File::ErrorToString(lock_file.error_details());
  }

  base::TimeDelta wait_for_lock_file = base::TimeDelta::FromMinutes(5);

  if (IsCrashTestInProgress()) {
    // When running crash.SenderLock test, don't wait a full 5 minutes before
    // completing the test.
    wait_for_lock_file = base::TimeDelta::FromSeconds(1);
  }

  base::Time stop_time = clock_->Now() + wait_for_lock_file;
  while (clock_->Now() < stop_time) {
    if (lock_file.Lock() == base::File::FILE_OK) {
      return lock_file;
    }
    const base::TimeDelta kSleepTime = base::TimeDelta::FromSeconds(1);
    if (sleep_function_.is_null()) {
      base::PlatformThread::Sleep(kSleepTime);
    } else {
      sleep_function_.Run(kSleepTime);
    }
  }

  // Last try. Exit if this one doesn't succeed.
  auto result = lock_file.Lock();
  if (result != base::File::FILE_OK) {
    // Note: If another process is holding the lock, this will just say
    // something unhelpful like "FILE_ERROR_FAILED"; File::Lock doesn't have a
    // separate return code corresponding to EWOULDBLOCK.
    LOG(ERROR) << "Failed to acquire a lock: "
               << base::File::ErrorToString(result);
    RecordCrashDone();
    exit(EXIT_FAILURE);
  }

  return lock_file;
}

bool Sender::HasCrashUploadingConsent() {
  if (util::HasMockConsent()) {
    return true;
  }

  return metrics_lib_->AreMetricsEnabled();
}

bool Sender::IsSafeDeviceCoredump(const CrashInfo& info) {
  std::string value;
  if (!info.metadata.GetString("exec_name", &value))
    return false;
  return value == "devcoredump_msm";
}

Sender::Action Sender::ChooseAction(const base::FilePath& meta_file,
                                    std::string* reason,
                                    CrashInfo* info) {
  if (!IsMock() && !IsOfficialImage() && !allow_dev_sending_ && !test_mode_) {
    *reason = "Not an official OS version";
    SendCrashRemoveReasonToUMA(kNotOfficialImage);
    return kRemove;
  }

  // HasCrashUploadingConsent() returns false in guest mode, thus IsGuestMode()
  // should be checked first (otherwise, all crash files are deleted in guest
  // mode).
  //
  // Note that this check is slightly racey, but should be rare enough for us
  // not to care:
  //
  // - crash_sender checks IsGuestMode() and it returns false
  // - User logs in to guest mode
  // - crash_sender checks HasCrashUploadingConsent() and it's now false
  // - Reports are deleted
  if (metrics_lib_->IsGuestMode()) {
    *reason = "Crash sending delayed due to guest mode";
    return kIgnore;
  }
  if (!HasCrashUploadingConsent()) {
    *reason = "Crash reporting is disabled";
    // Note that this will probably not actually be sent (since there's no
    // consent). Record it for completion and in case the user later enables
    // metrics consent.
    SendCrashRemoveReasonToUMA(kNoMetricsConsent);
    return kRemove;
  }

  if (base::PathExists(meta_file.ReplaceExtension(kProcessingExt))) {
    *reason = ".processing file already exists for: " + meta_file.value();
    SendCrashRemoveReasonToUMA(kProcessingFileExists);
    return kRemove;
  }

  ScopedProcessingFile f(meta_file);

  if (IsMock()) {
    CHECK(!crash_during_testing_) << "crashing as requested";
  }

  std::string raw_metadata;
  if (!base::ReadFileToStringWithMaxSize(meta_file, &raw_metadata,
                                         kMaxMetaFileSize)) {
    if (raw_metadata.empty()) {
      *reason = "Metadata file is inaccessible: " + meta_file.value();
      return kIgnore;
    }

    *reason = "Metadata file is unusually large: " + meta_file.value();
    SendCrashRemoveReasonToUMA(kLargeMetaFile);
    return kRemove;
  }

  if (!ParseMetadata(raw_metadata, &info->metadata)) {
    *reason = "Corrupted metadata: " + raw_metadata;
    SendCrashRemoveReasonToUMA(kUnparseableMetaFile);
    return kRemove;
  }

  MetadataToCrashInfo(info->metadata, info);

  if (info->payload_file.empty()) {
    *reason = "Payload is not found in the meta data: " + raw_metadata;
    SendCrashRemoveReasonToUMA(kPayloadUnspecified);
    return kRemove;
  }

  // Check for absolute path, or Append will CHECK-fail.
  if (info->payload_file.IsAbsolute()) {
    *reason =
        "Corrupt meta: payload path is absolute: " + info->payload_file.value();
    SendCrashRemoveReasonToUMA(kPayloadAbsolute);
    return kRemove;
  }

  // Make it an absolute path.
  info->payload_file = meta_file.DirName().Append(info->payload_file);

  if (!base::PathExists(info->payload_file)) {
    *reason = "Missing payload: " + info->payload_file.value();
    SendCrashRemoveReasonToUMA(kPayloadNonexistent);
    return kRemove;
  }

  if (!IsKnownKind(info->payload_kind)) {
    *reason = "Unknown kind: " + info->payload_kind;
    SendCrashRemoveReasonToUMA(kPayloadKindUnknown);
    return kRemove;
  }

  // If we have an OS timestamp in the metadata and it's too old to upload and
  // upload_old_reports flag is not set then remove the report. We wouldn't have
  // gotten here if the current OS version is too old, so this is an old report
  // from before an OS update.
  std::string os_timestamp_str;
  int64_t os_millis;
  if (!allow_dev_sending_ && !test_mode_ && !upload_old_reports_ &&
      info->metadata.GetString(kOsTimestamp, &os_timestamp_str) &&
      base::StringToInt64(os_timestamp_str, &os_millis) &&
      util::IsOsTimestampTooOldForUploads(
          base::Time::UnixEpoch() +
              base::TimeDelta::FromMilliseconds(os_millis),
          clock_.get())) {
    *reason = "Old OS version";
    SendCrashRemoveReasonToUMA(kOSVersionTooOld);
    return kRemove;
  }

  base::File::Info file_info;
  if (!base::GetFileInfo(meta_file, &file_info)) {
    // Should not happen since it succeeded to read the file.
    *reason = "Failed to get file info";
    return kIgnore;
  }

  info->last_modified = file_info.last_modified;
  if (!IsCompleteMetadata(info->metadata)) {
    const base::TimeDelta delta = clock_->Now() - file_info.last_modified;
    if (delta.InHours() >= 24) {
      *reason = "Removing old incomplete metadata";
      SendCrashRemoveReasonToUMA(kOldIncompleteMeta);
      return kRemove;
    } else {
      *reason = "Recent incomplete metadata";
      return kIgnore;
    }
  }

  if (IsAlreadyUploaded(meta_file)) {
    *reason = "Not uploading already-uploaded crash";
    return kIgnore;
  }

  if (info->payload_kind == "devcore" && !IsDeviceCoredumpUploadAllowed() &&
      !IsSafeDeviceCoredump(*info)) {
    *reason = "Device coredump upload not allowed";
    return kIgnore;
  }

  return kSend;
}

void Sender::RemoveAndPickCrashFiles(const base::FilePath& crash_dir,
                                     std::vector<MetaFile>* to_send) {
  std::vector<base::FilePath> meta_files = GetMetaFiles(crash_dir);

  for (const auto& meta_file : meta_files) {
    LOG(INFO) << "Checking metadata: " << meta_file.value();

    std::string reason;
    CrashInfo info;
    switch (ChooseAction(meta_file, &reason, &info)) {
      case kRemove:
        LOG(INFO) << "Removing: " << reason;
        RemoveReportFiles(meta_file, delete_crashes_);
        break;
      case kIgnore:
        LOG(INFO) << "Ignoring: " << reason;
        break;
      case kSend:
        to_send->push_back(std::make_pair(meta_file, std::move(info)));
        break;
      default:
        NOTREACHED();
    }
  }
}

void Sender::SendCrashes(const std::vector<MetaFile>& crash_meta_files,
                         base::TimeDelta* total_sleep_time) {
  if (crash_meta_files.empty())
    return;

  std::string client_id = GetClientId();

  if (total_sleep_time) {
    *total_sleep_time = base::TimeDelta();
  }

  base::File lock(AcquireLockFileOrDie());
  for (const auto& pair : crash_meta_files) {
    const base::FilePath& meta_file = pair.first;
    const CrashInfo& info = pair.second;
    LOG(INFO) << "Evaluating crash report: " << meta_file.value();

    base::TimeDelta sleep_time;
    if (!GetSleepTime(meta_file, max_spread_time_, hold_off_time_,
                      &sleep_time)) {
      LOG(WARNING) << "Failed to compute sleep time for " << meta_file.value();
      continue;
    }

    LOG(INFO) << "Scheduled to send in " << sleep_time.InSeconds() << "s";
    lock.Close();  // Don't hold lock during sleep.
    if (!IsMock()) {
      base::PlatformThread::Sleep(sleep_time);
    } else if (!sleep_function_.is_null()) {
      sleep_function_.Run(sleep_time);
    }
    if (total_sleep_time) {
      *total_sleep_time += sleep_time;
    }
    lock = AcquireLockFileOrDie();

    {
      // Mark the crash as being processed so that if we crash, we don't try to
      // send the crash again.
      // This is in a scope so that RemoveReportFiles doesn't try to remove
      // the .processing file (causing a LOG(ERROR) in the ScopedProcessingFile
      // destructor).
      ScopedProcessingFile processing(meta_file);

      // This should be checked inside of the loop, since the device can disable
      // metrics while sending crash reports with an interval up to
      // max_spread_time_ between sends. We only need to check if metrics are
      // enabled and not guest mode because in guest mode, it always indicates
      // that metrics are disabled.
      if (!HasCrashUploadingConsent()) {
        LOG(INFO) << "Metrics disabled or guest mode entered, delaying crash "
                  << "sending";
        return;
      }

      // User-specific crash reports become inaccessible if the user signs out
      // while sleeping, thus we need to check if the metadata is still
      // accessible.
      if (!base::PathExists(meta_file)) {
        LOG(INFO) << "Metadata is no longer accessible: " << meta_file.value();
        continue;
      }

      const base::FilePath timestamps_dir =
          paths::Get(paths::kTimestampsDirectory);
      if (!IsBelowRate(timestamps_dir, max_crash_rate_, max_crash_bytes_)) {
        LOG(WARNING) << "Cannot send more crashes. Sending "
                     << meta_file.value()
                     << " would exceed the max daily rate of "
                     << max_crash_rate_ << " crashes and " << max_crash_bytes_
                     << " bytes";
        return;
      }

      // If we are offline, then don't try to send any crashes.
      if (!IsMock() && !IsNetworkOnline()) {
        LOG(INFO) << "Stopping crash sending; network is offline";
        return;
      }

      const CrashDetails details = {
          .meta_file = meta_file,
          .payload_file = info.payload_file,
          .payload_kind = info.payload_kind,
          .client_id = client_id,
          .metadata = info.metadata,
      };
      if (!RequestToSendCrash(details)) {
        LOG(WARNING) << "Failed to send " << meta_file.value()
                     << ", not removing; will retry later";
        continue;
      }
    }
    LOG(INFO) << "Successfully sent crash " << meta_file.value()
              << " and removing.";
    SendCrashRemoveReasonToUMA(kFinishedUploading);
    RemoveReportFiles(meta_file, delete_crashes_);
  }
}

std::vector<base::FilePath> Sender::GetUserCrashDirectories() {
  // Set up the session manager proxy if it's not given from the options.
  if (!session_manager_proxy_) {
    EnsureDBusIsReady();
    session_manager_proxy_.reset(
        new org::chromium::SessionManagerInterfaceProxy(bus_));
  }

  std::vector<base::FilePath> directories;
  util::GetUserCrashDirectories(session_manager_proxy_.get(), &directories);
  util::GetDaemonStoreCrashDirectories(session_manager_proxy_.get(),
                                       &directories);

  return directories;
}

std::unique_ptr<brillo::http::FormData> Sender::CreateCrashFormData(
    const CrashDetails& details, std::string* product_name_out) {
  std::unique_ptr<brillo::http::FormData> form_data =
      std::make_unique<brillo::http::FormData>(form_data_boundary_);

  std::string exec_name;
  if (!details.metadata.GetString("exec_name", &exec_name))
    exec_name = kUndefined;
  form_data->AddTextField("exec_name", exec_name);

  std::string board;
  if (!details.metadata.GetString("board", &board) &&
      !GetCachedKeyValueDefault(base::FilePath(paths::kLsbRelease),
                                "CHROMEOS_RELEASE_BOARD", &board)) {
    board = kUndefined;
  }
  form_data->AddTextField("board", board);

  std::string hwclass = util::GetHardwareClass();
  form_data->AddTextField("hwclass", hwclass);

  // When uploading Chrome reports we need to report the right product and
  // version. If the meta file does not specify it we try to examine os-release
  // content. If not available there product gets assigned default product name
  // and version is derived from CHROMEOS_RELEASE_VERSION in /etc/lsb-release.
  std::string product;
  if (!details.metadata.GetString("upload_var_prod", &product)) {
    product =
        GetOsReleaseValue({"GOOGLE_CRASH_ID", "ID"}).value_or(kChromeOsProduct);
  }
  form_data->AddTextField("prod", product);

  std::string version;
  if (!details.metadata.GetString("upload_var_ver", &version)) {
    if (!details.metadata.GetString("ver", &version)) {
      version = GetOsReleaseValue(
                    {"GOOGLE_CRASH_VERSION_ID", "BUILD_ID", "VERSION_ID"})
                    .value_or(kUndefined);
    }
  }
  form_data->AddTextField("ver", version);

  std::string sig;
  if (details.metadata.GetString("sig", &sig)) {
    form_data->AddTextField("sig", sig);
    form_data->AddTextField("sig2", sig);
  }
  base::FilePath payload_file = details.payload_file;
  if (!payload_file.IsAbsolute()) {
    payload_file = details.meta_file.DirName().Append(payload_file);
  }
  brillo::ErrorPtr file_error;
  if (!form_data->AddFileField("upload_file_" + details.payload_kind,
                               payload_file, {}, &file_error)) {
    LOG(ERROR) << "Failed adding payload file as attachment: "
               << file_error->GetMessage();
    return nullptr;
  }

  for (const auto& key : details.metadata.GetKeys()) {
    if (!base::StartsWith(key, "upload_", base::CompareCase::SENSITIVE) ||
        key == "upload_var_prod" || key == "upload_var_ver" ||
        key == "upload_var_guid") {
      continue;
    }
    std::string value;
    details.metadata.GetString(key, &value);
    bool is_upload_var =
        base::StartsWith(key, kUploadVarPrefix, base::CompareCase::SENSITIVE);
    bool is_upload_text =
        base::StartsWith(key, kUploadTextPrefix, base::CompareCase::SENSITIVE);
    bool is_upload_file =
        base::StartsWith(key, kUploadFilePrefix, base::CompareCase::SENSITIVE);
    if (is_upload_var) {
      form_data->AddTextField(key.substr(sizeof(kUploadVarPrefix) - 1), value);
    } else if (is_upload_text || is_upload_file) {
      base::FilePath value_file(value);
      // Relative paths are relative to the meta data file.
      if (!value_file.IsAbsolute()) {
        value_file = details.meta_file.DirName().Append(value_file);
      }
      if (is_upload_text) {
        std::string value_content;
        if (base::ReadFileToString(value_file, &value_content)) {
          form_data->AddTextField(key.substr(sizeof(kUploadTextPrefix) - 1),
                                  value_content);
        } else {
          LOG(ERROR) << "Failed attaching file contents from "
                     << value_file.value();
        }
      } else {  // not is_upload_text so must be is_upload_file
        brillo::ErrorPtr error;
        if (base::PathExists(value_file) &&
            !form_data->AddFileField(key.substr(sizeof(kUploadFilePrefix) - 1),
                                     value_file, {}, &error)) {
          LOG(ERROR) << "Failed attaching file " << value_file.value()
                     << " of: " << error->GetMessage();
        }
      }
    }
  }

  std::string image_type = GetImageType();
  if (!image_type.empty())
    form_data->AddTextField("image_type", image_type);

  std::string boot_mode = util::GetBootModeString();
  if (!boot_mode.empty())
    form_data->AddTextField("boot_mode", boot_mode);

  std::string error_type;
  if (details.metadata.GetString("error_type", &error_type))
    form_data->AddTextField("error_type", error_type);

  LOG(INFO) << "Sending crash:";
  if (product != kChromeOsProduct)
    LOG(INFO) << "  Sending crash report on behalf of " << product;
  LOG(INFO) << "  Metadata: " << details.meta_file.value() << " ("
            << details.payload_kind << ")";
  LOG(INFO) << "  Payload: " << details.payload_file.value();
  LOG(INFO) << "  Version: " << version;
  if (!image_type.empty())
    LOG(INFO) << "  Image type: " << image_type;
  if (!boot_mode.empty())
    LOG(INFO) << "  Boot mode: " << boot_mode;
  if (IsMock()) {
    LOG(INFO) << "  Product: " << product;
    LOG(INFO) << "  URL: " << kReportUploadProdUrl;
    LOG(INFO) << "  Board: " << board;
    LOG(INFO) << "  HWClass: " << hwclass;
    if (!sig.empty())
      LOG(INFO) << "  sig: " << sig;
  }

  LOG(INFO) << "  Exec name: " << exec_name;
  if (!error_type.empty())
    LOG(INFO) << "  Error type: " << error_type;

  form_data->AddTextField("guid", details.client_id);

  if (product_name_out)
    *product_name_out = product;

  return form_data;
}

void Sender::RemoveReportFiles(const base::FilePath& meta_file,
                               bool delete_crashes) {
  if (meta_file.Extension() != ".meta") {
    LOG(ERROR) << "Not a meta file: " << meta_file.value();
    return;
  }
  SendCrashRemoveReasonToUMA(kTotalRemoval);

  const std::string pattern =
      meta_file.BaseName().RemoveExtension().value() + ".*";

  bool add_uploaded_file = !delete_crashes;
  if (delete_crashes) {
    if (!metrics_lib_->SendCrosEventToUMA(kUMAAttemptedCrashRemoval)) {
      LOG(WARNING) << "Failed to record crash removal attempt in UMA";
    }
    base::FileEnumerator iter(meta_file.DirName(), false /* recursive */,
                              base::FileEnumerator::FILES, pattern);
    for (base::FilePath file = iter.Next(); !file.empty(); file = iter.Next()) {
      if (!base::DeleteFile(file, false /* recursive */)) {
        PLOG(WARNING) << "Failed to remove " << file.value();
        // We may have failed to remove the file due to incorrect selinux config
        // on the directory. However, we may still be able to add files to it,
        // so mark the crash as uploaded to prevent uploading it again.
        // See https://crbug.com/1060019.
        if (file.Extension() == ".meta") {
          if (!metrics_lib_->SendCrosEventToUMA(kUMAFailedCrashRemoval)) {
            LOG(WARNING) << "Further, couldn't record UMA event for failure";
          }
          add_uploaded_file = true;
        }
      }
    }
  }
  if (add_uploaded_file) {
    base::File f(meta_file.ReplaceExtension(kAlreadyUploadedExt),
                 base::File::FLAG_CREATE | base::File::FLAG_WRITE);
    if (!f.IsValid()) {
      LOG(ERROR) << "Failed to mark crash as uploaded";
    }
  }
}

void Sender::SendCrashRemoveReasonToUMA(CrashRemoveReason reason) {
  metrics_lib_->SendEnumToUMA(kCrashSenderRemoveHistName, reason,
                              kSendReasonCount);
}

std::unique_ptr<base::Value> Sender::CreateJsonEntity(
    const std::string& report_id,
    const std::string& product_name,
    const CrashDetails& details) {
  auto root_dict = std::make_unique<base::Value>(base::Value::Type::DICTIONARY);

  int64_t timestamp = (base::Time::Now() - base::Time::UnixEpoch()).InSeconds();
  root_dict->SetKey(kJsonLogKeyUploadTime,
                    base::Value(std::to_string(timestamp)));

  root_dict->SetKey(kJsonLogKeyUploadId, base::Value(report_id));
  root_dict->SetKey(kJsonLogKeyLocalId, base::Value(product_name));

  // The |capture_timestamp| should be converted from milliseconds to seconds.
  std::string capture_timestamp;
  int64_t capture_timestamp_millis;
  if (details.metadata.GetString(kMetadataKeyCaptureTimeMillis,
                                 &capture_timestamp) &&
      base::StringToInt64(capture_timestamp, &capture_timestamp_millis)) {
    root_dict->SetKey(
        kJsonLogKeyCaptureTime,
        base::Value(std::to_string(capture_timestamp_millis / 1000)));
  }

  // The state value is always same as
  // UploadList::UploadInfo::State::Uploaded.
  root_dict->SetKey(kJsonLogKeyState, base::Value(3));

  std::string source;
  if (details.metadata.GetString(kMetadataKeySource, &source)) {
    // Hide the real source to avoid privacy concern if it is not a system
    // crash.
    if (!paths::Get(paths::kSystemCrashDirectory).IsParent(details.meta_file))
      source = kMetadataValueRedacted;
    root_dict->SetKey(kJsonLogKeySource, base::Value(source));
  }

  return root_dict;
}

bool Sender::RequestToSendCrash(const CrashDetails& details) {
  std::string product_name;
  std::unique_ptr<brillo::http::FormData> form_data =
      CreateCrashFormData(details, &product_name);
  if (!form_data) {
    return false;
  }

  if (test_mode_) {
    LOG(WARNING) << kTestModeSuccessful;
    return true;
  }

  std::string report_id;

  auto stream_data = form_data->ExtractDataStream();
  uint64_t uncompressed_size = stream_data->GetSize();
  // Compress the data before sending it to the server. We compress the entire
  // request body and then specify the Content-Encoding as gzip to achieve this.
  std::vector<unsigned char> compressed_form_data =
      util::GzipStream(std::move(stream_data));

  // Record the send attempt even if it fails. We may still have used up network
  // bandwidth even if we lose the connection at the end.
  const base::FilePath timestamps_dir = paths::Get(paths::kTimestampsDirectory);
  int size = static_cast<int>(compressed_form_data.size());
  if (size == 0) {
    // Compression failed; we'll end up using the uncompressed stream below.
    size = static_cast<int>(uncompressed_size);
  }
  RecordSendAttempt(timestamps_dir, size);

  if (!IsMock()) {
    // Determine the proxy server if it's not given from the options.
    if (proxy_servers_.empty()) {
      EnsureDBusIsReady();
      brillo::http::GetChromeProxyServers(bus_, kReportUploadProdUrl,
                                          &proxy_servers_);
    }

    std::shared_ptr<brillo::http::Transport> transport;
    if (proxy_servers_.empty() || proxy_servers_[0] == "direct://") {
      transport = brillo::http::Transport::CreateDefault();
    } else {
      transport =
          brillo::http::Transport::CreateDefaultWithProxy(proxy_servers_[0]);
    }

    brillo::ErrorPtr upload_error;
    std::unique_ptr<brillo::http::Response> response;
    if (!compressed_form_data.empty()) {
      response = brillo::http::PostBinaryAndBlock(
          allow_dev_sending_ ? kReportUploadStagingUrl : kReportUploadProdUrl,
          compressed_form_data.data(), compressed_form_data.size(),
          form_data->GetContentType(),
          {{brillo::http::request_header::kContentEncoding, "gzip"}}, transport,
          &upload_error);
    } else {
      LOG(ERROR) << "Failed compressing crash data for upload, perform the "
                 << "upload uncompressed";
      // This really should never happen, but it's probably better to try to
      // send this uncompressed even though it requires regenerating all the
      // data since extracting the data stream from the FormData is a
      // potentially destructive operation.
      form_data = CreateCrashFormData(details, &product_name);
      if (!form_data) {
        return false;
      }
      response = brillo::http::PostFormDataAndBlock(
          allow_dev_sending_ ? kReportUploadStagingUrl : kReportUploadProdUrl,
          std::move(form_data), {} /* headers */, transport, &upload_error);
    }

    if (!response) {
      LOG(ERROR) << "Crash sending failed with error: "
                 << upload_error->GetMessage();
      return false;
    }
    if (!response->IsSuccessful()) {
      LOG(ERROR) << "Crash sending failed with HTTP "
                 << response->GetStatusCode() << ": "
                 << response->GetStatusText();
      return false;
    }

    report_id = response->ExtractDataAsString();
  } else {
    if (!IsMockSuccessful()) {
      LOG(INFO) << "Mocking unsuccessful send";
      return false;
    }
    CHECK(!crash_during_testing_) << "crashing as requested";
    LOG(INFO) << "Mocking successful send";

    if (!always_write_uploads_log_)
      return true;

    if (!details.metadata.GetString("fake_report_id", &report_id))
      report_id = kUndefined;
  }

  if (product_name == "Chrome_ChromeOS")
    product_name = "Chrome";
  if (!util::IsOfficialImage()) {
    base::ReplaceSubstringsAfterOffset(&product_name, 0, "Chrome", "Chromium");
  }
  std::string silent;
  details.metadata.GetString("silent", &silent);
  if (always_write_uploads_log_ || (!USE_CHROMELESS_TTY && silent != "true")) {
    base::FilePath upload_logs_path(paths::Get(paths::kChromeCrashLog));

    // Open the file before we check the normalized path or it will fail if the
    // path doesn't exist.
    base::File upload_logs_file(upload_logs_path, base::File::FLAG_OPEN_ALWAYS |
                                                      base::File::FLAG_APPEND);

    base::FilePath normalized_path;
    if (base::NormalizeFilePath(upload_logs_path, &normalized_path) &&
        upload_logs_path == normalized_path) {
      std::unique_ptr<base::Value> json_entity =
          CreateJsonEntity(report_id, product_name, details);
      std::string upload_log_entry;
      if (!base::JSONWriter::Write(*json_entity, &upload_log_entry)) {
        LOG(WARNING) << "Cannot construct a valid uploads.log entry in JSON "
                        "format, so skip the update.";
        return true;
      }

      upload_log_entry += "\n";
      if (!upload_logs_file.IsValid() ||
          upload_logs_file.WriteAtCurrentPos(upload_log_entry.c_str(),
                                             upload_log_entry.size()) !=
              upload_log_entry.size()) {
        PLOG(ERROR) << "Error writing to Chrome uploads.log file";
      }
    } else {
      LOG(ERROR) << "Did not write to Chrome uploads.log file because the "
                 << "normalized path didn't match the target path, target: "
                 << upload_logs_path.value()
                 << " normalized: " << normalized_path.value();
    }
  }
  LOG(INFO) << "Crash report receipt ID " << report_id;
  return true;
}

void Sender::EnsureDBusIsReady() {
  if (!bus_) {
    dbus::Bus::Options options;
    options.bus_type = dbus::Bus::SYSTEM;
    bus_ = new dbus::Bus(options);
    CHECK(bus_->Connect());
  }
}

base::Optional<std::string> Sender::GetOsReleaseValue(
    const std::vector<std::string>& keys) {
  if (!os_release_reader_) {
    os_release_reader_ = std::make_unique<brillo::OsReleaseReader>();
    os_release_reader_->Load();
  }
  std::string value;
  for (const auto& key : keys) {
    if (os_release_reader_->GetString(key, &value))
      return base::Optional<std::string>(value);
  }
  return base::Optional<std::string>();
}

bool Sender::IsNetworkOnline() {
  if (!shill_proxy_) {
    EnsureDBusIsReady();
    shill_proxy_ =
        std::make_unique<org::chromium::flimflam::ManagerProxy>(bus_);
  }
  brillo::VariantDictionary dict;
  brillo::ErrorPtr err;
  if (!shill_proxy_->GetProperties(&dict, &err)) {
    // If we don't know, then just assume we are connected.
    LOG(WARNING) << "Failed making D-Bus call for network state; attempting "
                 << "upload anyways";
    return true;
  }
  const std::string state = brillo::GetVariantValueOrDefault<std::string>(
      dict, shill::kConnectionStateProperty);
  if (state.empty()) {
    // If we didn't get a valid value back, then assume we are connected.
    LOG(WARNING) << "Received empty ConnectionState property from shill; "
                 << "attempting upload anyways";
    return true;
  }
  // Possible values for this are defined in platform2/shill/service.cc, but the
  // only one that means we have an Internet connection is "online". All of the
  // other values represent some other reduced (or no) level of connectivity or
  // the process of establishing a connection.
  return base::EqualsCaseInsensitiveASCII(state, "online");
}

}  // namespace util
