// 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 "components/feedback/feedback_common.h"

#include <utility>

#include "base/memory/ptr_util.h"
#include "base/strings/string_util.h"
#include "components/feedback/feedback_report.h"
#include "components/feedback/feedback_util.h"
#include "components/feedback/proto/common.pb.h"
#include "components/feedback/proto/dom.pb.h"
#include "components/feedback/proto/extension.pb.h"
#include "components/feedback/proto/math.pb.h"

namespace {

#if defined(OS_CHROMEOS)
constexpr int kChromeOSProductId = 208;
#else
constexpr int kChromeBrowserProductId = 237;
#endif

constexpr char kMultilineIndicatorString[] = "<multiline>\n";
constexpr char kMultilineStartString[] = "---------- START ----------\n";
constexpr char kMultilineEndString[] = "---------- END ----------\n\n";

// The below thresholds were chosen arbitrarily to conveniently show small data
// as part of the report itself without having to look into the system_logs.zip
// file.
constexpr size_t kFeedbackMaxLength = 1024;
constexpr size_t kFeedbackMaxLineCount = 10;

constexpr base::FilePath::CharType kLogsFilename[] =
    FILE_PATH_LITERAL("system_logs.txt");
constexpr char kLogsAttachmentName[] = "system_logs.zip";

constexpr char kZipExt[] = ".zip";

constexpr char kPngMimeType[] = "image/png";
constexpr char kArbitraryMimeType[] = "application/octet-stream";

// Determine if the given feedback value is small enough to not need to
// be compressed.
bool BelowCompressionThreshold(const std::string& content) {
  if (content.length() > kFeedbackMaxLength)
    return false;
  const size_t line_count = std::count(content.begin(), content.end(), '\n');
  if (line_count > kFeedbackMaxLineCount)
    return false;
  return true;
}

// Converts the system logs into a string that we can compress and send
// with the report.
// TODO(dcheng): This should probably just take advantage of string's move
// constructor.
std::unique_ptr<std::string> LogsToString(
    const FeedbackCommon::SystemLogsMap& sys_info) {
  std::unique_ptr<std::string> syslogs_string(new std::string);
  for (const auto& iter : sys_info) {
    std::string key = iter.first;
    std::string value = iter.second;

    base::TrimString(key, "\n ", &key);
    base::TrimString(value, "\n ", &value);

    // We must avoid adding the crash IDs to the system_logs.txt file for
    // privacy reasons. They should just be part of the product specific data.
    if (key == feedback::FeedbackReport::kCrashReportIdsKey)
      continue;

    if (value.find("\n") != std::string::npos) {
      syslogs_string->append(key + "=" + kMultilineIndicatorString +
                             kMultilineStartString + value + "\n" +
                             kMultilineEndString);
    } else {
      syslogs_string->append(key + "=" + value + "\n");
    }
  }
  return syslogs_string;
}

void AddFeedbackData(userfeedback::ExtensionSubmit* feedback_data,
                     const std::string& key,
                     const std::string& value) {
  // Don't bother with empty keys or values.
  if (key.empty() || value.empty())
    return;
  // Create log_value object and add it to the web_data object.
  userfeedback::ProductSpecificData log_value;
  log_value.set_key(key);
  log_value.set_value(value);
  userfeedback::WebData* web_data = feedback_data->mutable_web_data();
  *(web_data->add_product_specific_data()) = log_value;
}

// Adds data as an attachment to feedback_data if the data is non-empty.
void AddAttachment(userfeedback::ExtensionSubmit* feedback_data,
                   const char* name,
                   const std::string& data) {
  if (data.empty())
    return;

  userfeedback::ProductSpecificBinaryData* attachment =
      feedback_data->add_product_specific_binary_data();
  attachment->set_mime_type(kArbitraryMimeType);
  attachment->set_name(name);
  attachment->set_data(data);
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// FeedbackCommon::AttachedFile::
////////////////////////////////////////////////////////////////////////////////

FeedbackCommon::AttachedFile::AttachedFile(const std::string& filename,
                                           std::unique_ptr<std::string> data)
    : name(filename), data(std::move(data)) {}

FeedbackCommon::AttachedFile::~AttachedFile() {}

////////////////////////////////////////////////////////////////////////////////
// FeedbackCommon::
////////////////////////////////////////////////////////////////////////////////

FeedbackCommon::FeedbackCommon() : product_id_(-1) {}

void FeedbackCommon::AddFile(const std::string& filename,
                             std::unique_ptr<std::string> data) {
  base::AutoLock lock(attachments_lock_);
  attachments_.emplace_back(new AttachedFile(filename, std::move(data)));
}

void FeedbackCommon::AddLog(const std::string& name, const std::string& value) {
  if (!logs_)
    logs_ = base::WrapUnique(new SystemLogsMap);
  (*logs_)[name] = value;
}

void FeedbackCommon::AddLogs(std::unique_ptr<SystemLogsMap> logs) {
  if (logs_)
    logs_->insert(logs->begin(), logs->end());
  else
    logs_ = std::move(logs);
}

void FeedbackCommon::PrepareReport(
    userfeedback::ExtensionSubmit* feedback_data) const {
  // Unused field, needs to be 0 though.
  feedback_data->set_type_id(0);

  // Set whether we're reporting from ChromeOS or Chrome on another platform.
  userfeedback::ChromeData chrome_data;
#if defined(OS_CHROMEOS)
  const userfeedback::ChromeData_ChromePlatform chrome_platform =
      userfeedback::ChromeData_ChromePlatform_CHROME_OS;
  const int default_product_id = kChromeOSProductId;
  userfeedback::ChromeOsData chrome_os_data;
  chrome_os_data.set_category(
      userfeedback::ChromeOsData_ChromeOsCategory_OTHER);
  *(chrome_data.mutable_chrome_os_data()) = chrome_os_data;
#else
  const userfeedback::ChromeData_ChromePlatform chrome_platform =
      userfeedback::ChromeData_ChromePlatform_CHROME_BROWSER;
  const int default_product_id = kChromeBrowserProductId;
  userfeedback::ChromeBrowserData chrome_browser_data;
  chrome_browser_data.set_category(
      userfeedback::ChromeBrowserData_ChromeBrowserCategory_OTHER);
  *(chrome_data.mutable_chrome_browser_data()) = chrome_browser_data;
#endif  // defined(OS_CHROMEOS)
  chrome_data.set_chrome_platform(chrome_platform);
  *(feedback_data->mutable_chrome_data()) = chrome_data;

  feedback_data->set_product_id(HasProductId() ? product_id_
                                               : default_product_id);

  userfeedback::CommonData* common_data = feedback_data->mutable_common_data();
  // We're not using gaia ids, we're using the e-mail field instead.
  common_data->set_gaia_id(0);
  common_data->set_user_email(user_email());
  common_data->set_description(description());
  common_data->set_source_description_language(locale());

  userfeedback::WebData* web_data = feedback_data->mutable_web_data();
  web_data->set_url(page_url());
  web_data->mutable_navigator()->set_user_agent(user_agent());

  AddFilesAndLogsToReport(feedback_data);

  if (image() && image()->size()) {
    userfeedback::PostedScreenshot screenshot;
    screenshot.set_mime_type(kPngMimeType);

    // Set that we 'have' dimensions of the screenshot. These dimensions are
    // ignored by the server but are a 'required' field in the protobuf.
    userfeedback::Dimensions dimensions;
    dimensions.set_width(0.0);
    dimensions.set_height(0.0);

    *(screenshot.mutable_dimensions()) = dimensions;
    screenshot.set_binary_content(*image());

    *(feedback_data->mutable_screenshot()) = screenshot;
  }

  if (category_tag().size())
    feedback_data->set_bucket(category_tag());
}

FeedbackCommon::~FeedbackCommon() {}

void FeedbackCommon::CompressFile(
    const base::FilePath& filename,
    const std::string& zipname,
    std::unique_ptr<std::string> data_to_be_compressed) {
  std::unique_ptr<std::string> compressed_data(new std::string());
  if (feedback_util::ZipString(filename, *data_to_be_compressed,
                               compressed_data.get())) {
    std::string attachment_file_name = zipname;
    if (attachment_file_name.empty()) {
      // We need to use the UTF8Unsafe methods here to accommodate Windows,
      // which uses wide strings to store file paths.
      attachment_file_name = filename.BaseName().AsUTF8Unsafe().append(kZipExt);
    }

    AddFile(attachment_file_name, std::move(compressed_data));
  }
}

void FeedbackCommon::CompressLogs() {
  if (!logs_)
    return;
  std::unique_ptr<std::string> logs = LogsToString(*logs_);
  if (!logs->empty()) {
    CompressFile(base::FilePath(kLogsFilename), kLogsAttachmentName,
                 std::move(logs));
  }
}

void FeedbackCommon::AddFilesAndLogsToReport(
    userfeedback::ExtensionSubmit* feedback_data) const {
  for (size_t i = 0; i < attachments(); ++i) {
    const AttachedFile* file = attachment(i);
    AddAttachment(feedback_data, file->name.c_str(), *file->data);
  }

  if (!logs_)
    return;

  for (const auto& iter : *logs_) {
    if (BelowCompressionThreshold(iter.second)) {
      // Small enough logs should end up in the report data itself. However,
      // they're still added as part of the system_logs.zip file.
      AddFeedbackData(feedback_data, iter.first, iter.second);
    }
  }
}
