// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/feedback/feedback_uploader.h"

#include <string>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "components/feedback/features.h"
#include "components/feedback/feedback_report.h"
#include "components/feedback/feedback_switches.h"
#include "components/variations/net/variations_http_headers.h"
#include "net/base/load_flags.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace feedback {

namespace {

constexpr char kReportSendingResultHistogramName[] =
    "Feedback.ReportSending.Result";
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class FeedbackReportSendingResult {
  kSuccessAtFirstTry = 0,  // The report was uploaded successfully without retry
  kSuccessAfterRetry = 1,  // The report was uploaded successfully after retry
  kDropped = 2,            // The report is corrupt or invalid and was dropped
  kMaxValue = kDropped,
};

constexpr base::FilePath::CharType kFeedbackReportPath[] =
    FILE_PATH_LITERAL("Feedback Reports");

#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
constexpr char kFeedbackPostUrl[] =
    "https://www.google.com/tools/feedback/chrome/__submit";
#else
constexpr char kFeedbackPostUrl[] = "";
#endif

constexpr char kProtoBufMimeType[] = "application/x-protobuf";

constexpr int kHttpPostSuccessNoContent = 204;
constexpr int kHttpPostFailNoConnection = -1;
constexpr int kHttpPostFailClientError = 400;
constexpr int kHttpPostFailServerError = 500;

// The minimum time to wait before uploading reports are retried. Exponential
// backoff delay is applied on successive failures.
// This value can be overriden by tests by calling
// FeedbackUploader::SetMinimumRetryDelayForTesting().
base::TimeDelta g_minimum_retry_delay = base::Minutes(60);

// If a new report is queued to be dispatched immediately while another is being
// dispatched, this is the time to wait for the on-going dispatching to finish.
base::TimeDelta g_dispatching_wait_delay = base::Seconds(4);

GURL GetFeedbackPostGURL() {
  const base::CommandLine& command_line =
      *base::CommandLine::ForCurrentProcess();
  return GURL(command_line.HasSwitch(switches::kFeedbackServer)
                  ? command_line.GetSwitchValueASCII(switches::kFeedbackServer)
                  : kFeedbackPostUrl);
}

// Creates a new SingleThreadTaskRunner that is used to run feedback blocking
// background work.
scoped_refptr<base::SingleThreadTaskRunner> CreateUploaderTaskRunner() {
  // Uses a BLOCK_SHUTDOWN file task runner to prevent losing reports or
  // corrupting report's files.
  return base::ThreadPool::CreateSingleThreadTaskRunner(
      {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
       base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
}

}  // namespace

FeedbackUploader::FeedbackUploader(
    bool is_off_the_record,
    const base::FilePath& state_path,
    SharedURLLoaderFactoryGetter shared_url_loader_factory_getter)
    : FeedbackUploader(is_off_the_record,
                       state_path,
                       std::move(shared_url_loader_factory_getter),
                       nullptr) {}

FeedbackUploader::FeedbackUploader(
    bool is_off_the_record,
    const base::FilePath& state_path,
    scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory)
    : FeedbackUploader(is_off_the_record,
                       state_path,
                       SharedURLLoaderFactoryGetter(),
                       shared_url_loader_factory) {}

FeedbackUploader::~FeedbackUploader() = default;

// static
void FeedbackUploader::SetMinimumRetryDelayForTesting(base::TimeDelta delay) {
  g_minimum_retry_delay = delay;
}

void FeedbackUploader::QueueReport(std::unique_ptr<std::string> data,
                                   bool has_email,
                                   int product_id) {
  reports_queue_.emplace(base::MakeRefCounted<FeedbackReport>(
      feedback_reports_path_, base::Time::Now(), std::move(data), task_runner_,
      has_email, product_id));
  UpdateUploadTimer();
}

void FeedbackUploader::RequeueReport(scoped_refptr<FeedbackReport> report) {
  DCHECK_EQ(task_runner_, report->reports_task_runner());
  report->set_upload_at(base::Time::Now());
  reports_queue_.emplace(std::move(report));
  UpdateUploadTimer();
}

void FeedbackUploader::StartDispatchingReport() {
  DispatchReport();
}

void FeedbackUploader::OnReportUploadSuccess() {
  if (retry_delay_ == g_minimum_retry_delay) {
    UMA_HISTOGRAM_ENUMERATION(kReportSendingResultHistogramName,
                              FeedbackReportSendingResult::kSuccessAtFirstTry);
  } else {
    UMA_HISTOGRAM_ENUMERATION(kReportSendingResultHistogramName,
                              FeedbackReportSendingResult::kSuccessAfterRetry);
  }
  retry_delay_ = g_minimum_retry_delay;
  is_dispatching_ = false;
  // Explicitly release the successfully dispatched report.
  report_being_dispatched_->DeleteReportOnDisk();
  report_being_dispatched_ = nullptr;
  UpdateUploadTimer();
}

void FeedbackUploader::OnReportUploadFailure(bool should_retry) {
  if (should_retry) {
    // Implement a backoff delay by doubling the retry delay on each failure.
    retry_delay_ *= 2;
    report_being_dispatched_->set_upload_at(retry_delay_ + base::Time::Now());
    reports_queue_.emplace(report_being_dispatched_);
  } else {
    // The report won't be retried, hence explicitly delete its file on disk.
    report_being_dispatched_->DeleteReportOnDisk();
    UMA_HISTOGRAM_ENUMERATION(kReportSendingResultHistogramName,
                              FeedbackReportSendingResult::kDropped);
  }

  // The report dispatching failed, and should either be retried or not. In all
  // cases, we need to release |report_being_dispatched_|. If it was up for
  // retry, then it has already been re-enqueued and will be kept alive.
  // Otherwise we're done with it and it should destruct.
  report_being_dispatched_ = nullptr;
  is_dispatching_ = false;
  UpdateUploadTimer();
}

bool FeedbackUploader::ReportsUploadTimeComparator::operator()(
    const scoped_refptr<FeedbackReport>& a,
    const scoped_refptr<FeedbackReport>& b) const {
  return a->upload_at() > b->upload_at();
}

FeedbackUploader::FeedbackUploader(
    bool is_off_the_record,
    const base::FilePath& state_path,
    SharedURLLoaderFactoryGetter url_loader_factory_getter,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
    : url_loader_factory_getter_(std::move(url_loader_factory_getter)),
      url_loader_factory_(url_loader_factory),
      feedback_reports_path_(state_path.Append(kFeedbackReportPath)),
      task_runner_(CreateUploaderTaskRunner()),
      feedback_post_url_(GetFeedbackPostGURL()),
      retry_delay_(g_minimum_retry_delay),
      is_off_the_record_(is_off_the_record) {
  DCHECK(!!url_loader_factory_getter_ != !!url_loader_factory_);
}

void FeedbackUploader::AppendExtraHeadersToUploadRequest(
    network::ResourceRequest* resource_request) {}

void FeedbackUploader::DispatchReport() {
  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("chrome_feedback_report_app", R"(
        semantics {
          sender: "Chrome Feedback Report App"
          description:
            "Users can press Alt+Shift+i to report a bug or a feedback in "
            "general. Along with the free-form text they entered, system logs "
            "that helps in diagnosis of the issue are sent to Google. This "
            "service uploads the report to Google Feedback server."
          trigger:
            "When user chooses to send a feedback to Google."
          data:
            "The free-form text that user has entered and useful debugging "
            "logs (UI logs, Chrome logs, kernel logs, auto update engine logs, "
            "ARC++ logs, etc.). The logs are redacted to remove any "
            "user-private data. The user can view the system information "
            "before sending, and choose to send the feedback report without "
            "system information and the logs (unchecking 'Send system "
            "information' prevents sending logs as well), the screenshot, or "
            "even his/her email address."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
              email: "cros-device-enablement@google.com"
            }
          }
          user_data {
            type: ARBITRARY_DATA
            type: EMAIL
            type: IMAGE
            type: USER_CONTENT
          }
          last_reviewed: "2023-08-14"
        }
        policy {
          cookies_allowed: NO
          setting:
            "This feature cannot be disabled by settings and is only activated "
            "by direct user request."
          chrome_policy {
            UserFeedbackAllowed {
              UserFeedbackAllowed: false
            }
          }
        })");
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = feedback_post_url_;
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  resource_request->method = "POST";

  // Tell feedback server about the variation state of this install.
  if (report_being_dispatched_->should_include_variations()) {
    variations::AppendVariationsHeaderUnknownSignedIn(
        feedback_post_url_,
        is_off_the_record_ ? variations::InIncognito::kYes
                           : variations::InIncognito::kNo,
        resource_request.get());
  }

  if (report_being_dispatched_->has_email()) {
    AppendExtraHeadersToUploadRequest(resource_request.get());
  }

  std::unique_ptr<network::SimpleURLLoader> simple_url_loader =
      network::SimpleURLLoader::Create(std::move(resource_request),
                                       traffic_annotation);
  network::SimpleURLLoader* simple_url_loader_ptr = simple_url_loader.get();
  simple_url_loader->AttachStringForUpload(report_being_dispatched_->data(),
                                           kProtoBufMimeType);
  auto it = uploads_in_progress_.insert(uploads_in_progress_.begin(),
                                        std::move(simple_url_loader));

  if (!url_loader_factory_) {
    // Lazily create the URLLoaderFactory.
    url_loader_factory_ = std::move(url_loader_factory_getter_).Run();
    DCHECK(url_loader_factory_);
  }

  simple_url_loader_ptr->DownloadHeadersOnly(
      url_loader_factory_.get(),
      base::BindOnce(&FeedbackUploader::OnDispatchComplete,
                     base::Unretained(this), std::move(it)));
}

void FeedbackUploader::OnDispatchComplete(
    UrlLoaderList::iterator it,
    scoped_refptr<net::HttpResponseHeaders> headers) {
  std::stringstream error_stream;
  network::SimpleURLLoader* simple_url_loader = it->get();
  int response_code = kHttpPostFailNoConnection;
  if (headers) {
    response_code = headers->response_code();
  }
  if (response_code == kHttpPostSuccessNoContent) {
    error_stream << "Success";
    OnReportUploadSuccess();
  } else {
    bool should_retry = true;
    // Process the error for debug output
    if (response_code == kHttpPostFailNoConnection) {
      error_stream << "No connection to server.";
    } else if ((response_code >= kHttpPostFailClientError) &&
               (response_code < kHttpPostFailServerError)) {
      // Client errors mean that the server failed to parse the proto that was
      // sent, or that some requirements weren't met by the server side
      // validation, and hence we should NOT retry sending this corrupt report
      // and give up.
      should_retry = false;

      error_stream << "Client error: HTTP response code " << response_code;
    } else if (response_code >= kHttpPostFailServerError) {
      error_stream << "Server error: HTTP response code " << response_code;
    } else {
      error_stream << "Unknown error: HTTP response code " << response_code;
    }

    OnReportUploadFailure(should_retry);
  }

  LOG(WARNING) << "FEEDBACK: Submission to feedback server ("
               << simple_url_loader->GetFinalURL()
               << ") status: " << error_stream.str();
  uploads_in_progress_.erase(it);
}

void FeedbackUploader::UpdateUploadTimer() {
  if (reports_queue_.empty())
    return;

  scoped_refptr<FeedbackReport> report = reports_queue_.top();

  // Don't send reports in Tast tests so that they don't spam Listnr.
  if (feedback::features::IsSkipSendingFeedbackReportInTastTestsEnabled()) {
    report->DeleteReportOnDisk();
    reports_queue_.pop();
    return;
  }

  const base::Time now = base::Time::Now();
  if (report->upload_at() <= now && !is_dispatching_) {
    reports_queue_.pop();
    is_dispatching_ = true;
    report_being_dispatched_ = report;
    StartDispatchingReport();
  } else {
    // Stop the old timer and start an updated one.
    const base::TimeDelta delay = (is_dispatching_ || now > report->upload_at())
                                      ? g_dispatching_wait_delay
                                      : report->upload_at() - now;
    upload_timer_.Stop();
    upload_timer_.Start(FROM_HERE, delay, this,
                        &FeedbackUploader::UpdateUploadTimer);
  }
}

GURL FeedbackUploader::SetFeedbackGURLForTesting(GURL url) {
  GURL previous_url = feedback_post_url_;
  feedback_post_url_ = std::move(url);
  return previous_url;
}

}  // namespace feedback
