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

#include "chrome/browser/ui/webui/media/webrtc_logs_ui.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/time_formatting.h"
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/media/webrtc/webrtc_event_log_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/media_resources.h"
#include "components/prefs/pref_service.h"
#include "components/upload_list/text_log_upload_list.h"
#include "components/upload_list/upload_list.h"
#include "components/version_info/version_info.h"
#include "components/webrtc_logging/browser/text_log_list.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/browser/web_ui_message_handler.h"
#include "ui/webui/webui_util.h"

using content::WebContents;
using content::WebUIMessageHandler;

namespace {

void CreateAndAddWebRtcLogsUIHTMLSource(Profile* profile) {
  content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd(
      profile, chrome::kChromeUIWebRtcLogsHost);

  static constexpr webui::LocalizedString kStrings[] = {
      {"webrtcLogsTitle", IDS_WEBRTC_LOGS_TITLE},
      {"webrtcTextLogCountFormat",
       IDS_WEBRTC_TEXT_LOGS_LOG_COUNT_BANNER_FORMAT},
      {"webrtcEventLogCountFormat",
       IDS_WEBRTC_EVENT_LOGS_LOG_COUNT_BANNER_FORMAT},
      {"webrtcLogHeaderFormat", IDS_WEBRTC_LOGS_LOG_HEADER_FORMAT},
      {"webrtcLogLocalFileLabelFormat",
       IDS_WEBRTC_LOGS_LOG_LOCAL_FILE_LABEL_FORMAT},
      {"noLocalLogFileMessage", IDS_WEBRTC_LOGS_NO_LOCAL_LOG_FILE_MESSAGE},
      {"webrtcLogUploadTimeFormat", IDS_WEBRTC_LOGS_LOG_UPLOAD_TIME_FORMAT},
      {"webrtcLogFailedUploadTimeFormat",
       IDS_WEBRTC_LOGS_LOG_FAILED_UPLOAD_TIME_FORMAT},
      {"webrtcLogReportIdFormat", IDS_WEBRTC_LOGS_LOG_REPORT_ID_FORMAT},
      {"webrtcEventLogLocalLogIdFormat",
       IDS_WEBRTC_LOGS_EVENT_LOG_LOCAL_LOG_ID},
      {"bugLinkText", IDS_WEBRTC_LOGS_BUG_LINK_LABEL},
      {"webrtcLogNotUploadedMessage", IDS_WEBRTC_LOGS_LOG_NOT_UPLOADED_MESSAGE},
      {"webrtcLogPendingMessage", IDS_WEBRTC_LOGS_LOG_PENDING_MESSAGE},
      {"webrtcLogActivelyUploadedMessage",
       IDS_WEBRTC_LOGS_LOG_ACTIVELY_UPLOADED_MESSAGE},
      {"noTextLogsMessage", IDS_WEBRTC_LOGS_NO_TEXT_LOGS_MESSAGE},
      {"noEventLogsMessage", IDS_WEBRTC_LOGS_NO_EVENT_LOGS_MESSAGE},
  };
  source->AddLocalizedStrings(kStrings);

  source->UseStringsJs();

  source->AddResourcePath("webrtc_logs.css", IDR_MEDIA_WEBRTC_LOGS_CSS);
  source->AddResourcePath("webrtc_logs.js", IDR_MEDIA_WEBRTC_LOGS_JS);
  source->SetDefaultResource(IDR_MEDIA_WEBRTC_LOGS_HTML);
}

////////////////////////////////////////////////////////////////////////////////
//
// WebRtcLogsDOMHandler
//
////////////////////////////////////////////////////////////////////////////////

// The handler for Javascript messages for the chrome://webrtc-logs/ page.
class WebRtcLogsDOMHandler final : public WebUIMessageHandler {
 public:
  explicit WebRtcLogsDOMHandler(Profile* profile);

  WebRtcLogsDOMHandler(const WebRtcLogsDOMHandler&) = delete;
  WebRtcLogsDOMHandler& operator=(const WebRtcLogsDOMHandler&) = delete;

  ~WebRtcLogsDOMHandler() override;

  // WebUIMessageHandler implementation.
  void OnJavascriptDisallowed() override;
  void RegisterMessages() override;

 private:
  using WebRtcEventLogManager = webrtc_event_logging::WebRtcEventLogManager;

  // Asynchronously fetches the list of upload WebRTC logs. Called from JS.
  void HandleRequestWebRtcLogs(const base::ListValue& args);

  // Asynchronously load WebRTC text logs.
  void LoadAllWebRtcTextLogs(std::string callback_id);

  // Callback for when WebRTC text logs have been asynchronously loaded.
  void OnAllWebRtcTextLogsLoaded(std::string callback_id);

  // Asynchronously load WebRTC event logs.
  void LoadWebRtcEventLogs(const std::string& callback_id);

  // Callback for when WebRTC event logs have been asynchronously loaded.
  void OnWebRtcEventLogsLoaded(
      const std::string& callback_id,
      const std::vector<UploadList::UploadInfo>& event_logs);

  // Update the chrome://webrtc-logs/ page.
  void UpdateUI(const std::string& callback_id);

  // Update the text/event logs part of the forementioned page.
  base::ListValue UpdateUIWithTextLogs() const;
  base::ListValue UpdateUIWithEventLogs() const;

  // Convert a history entry about a captured WebRTC event log into a
  // Value of the type expected by updateWebRtcLogsList().
  base::Value EventLogUploadInfoToValue(
      const UploadList::UploadInfo& info) const;

  // Helpers for `EventLogUploadInfoToValue()`.
  base::Value FromPendingLog(const UploadList::UploadInfo& info) const;
  base::Value FromActivelyUploadedLog(const UploadList::UploadInfo& info) const;
  base::Value FromNotUploadedLog(const UploadList::UploadInfo& info) const;
  base::Value FromUploadUnsuccessfulLog(
      const UploadList::UploadInfo& info) const;
  base::Value FromUploadSuccessfulLog(const UploadList::UploadInfo& info) const;

  bool SanityCheckOnUploadInfo(const UploadList::UploadInfo& info) const;

  void LoadWebRtcTextLog(size_t index, std::string callback_id);

  // The directories where the (text/event) logs are stored.
  const std::vector<base::FilePath> text_log_dirs_;
  const base::FilePath event_log_dir_;

  // Identifies to WebRtcEventLogManager the profile with which |this| is
  // associated. Technically, we should be able to just keep Profile*,
  // but avoiding it makes less lifetime assumptions.
  // Note that the profile->GetOriginalProfile() is used, to make sure that
  // for incognito profiles, the parent profile's event logs are shown,
  // as is the behavior for text logs.
  const WebRtcEventLogManager::BrowserContextId original_browser_context_id_;

  // Loads, parses and stores the list of uploaded text WebRTC logs.
  std::vector<scoped_refptr<UploadList>> text_log_upload_lists_;

  // List of WebRTC logs captured and possibly uploaded to Crash.
  std::vector<UploadList::UploadInfo> event_logs_;

  // Factory for creating weak references to instances of this class.
  base::WeakPtrFactory<WebRtcLogsDOMHandler> weak_ptr_factory_{this};
};

WebRtcLogsDOMHandler::WebRtcLogsDOMHandler(Profile* profile)
    : text_log_dirs_(
          webrtc_logging::TextLogList::
              GetWebRtcLogDirectoriesForBrowserContextPath(profile->GetPath())),
      event_log_dir_(
          WebRtcEventLogManager::GetRemoteBoundWebRtcEventLogsDir(profile)),
      original_browser_context_id_(webrtc_event_logging::GetBrowserContextId(
          profile->GetOriginalProfile())) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  for (const auto& log_dir : text_log_dirs_) {
    text_log_upload_lists_.push_back(new TextLogUploadList(
        webrtc_logging::TextLogList::GetWebRtcLogListFileForDirectory(
            log_dir)));
  }
}

WebRtcLogsDOMHandler::~WebRtcLogsDOMHandler() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  for (auto& upload_list : text_log_upload_lists_) {
    upload_list->CancelLoadCallback();
  }
}

void WebRtcLogsDOMHandler::RegisterMessages() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  web_ui()->RegisterMessageCallback(
      "requestWebRtcLogsList",
      base::BindRepeating(&WebRtcLogsDOMHandler::HandleRequestWebRtcLogs,
                          base::Unretained(this)));
}

void WebRtcLogsDOMHandler::HandleRequestWebRtcLogs(
    const base::ListValue& args) {
  std::string callback_id = args[0].GetString();
  AllowJavascript();
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  LoadAllWebRtcTextLogs(std::move(callback_id));
}

void WebRtcLogsDOMHandler::OnJavascriptDisallowed() {
  weak_ptr_factory_.InvalidateWeakPtrs();
}

void WebRtcLogsDOMHandler::LoadAllWebRtcTextLogs(std::string callback_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK_GE(text_log_upload_lists_.size(), 1u);
  LoadWebRtcTextLog(0, std::move(callback_id));
}

void WebRtcLogsDOMHandler::LoadWebRtcTextLog(size_t index,
                                             std::string callback_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  text_log_upload_lists_[index]->Load(
      index < text_log_upload_lists_.size() - 1
          ? base::BindOnce(&WebRtcLogsDOMHandler::LoadWebRtcTextLog,
                           weak_ptr_factory_.GetWeakPtr(), index + 1,
                           std::move(callback_id))
          : base::BindOnce(&WebRtcLogsDOMHandler::OnAllWebRtcTextLogsLoaded,
                           weak_ptr_factory_.GetWeakPtr(),
                           std::move(callback_id)));
}

void WebRtcLogsDOMHandler::OnAllWebRtcTextLogsLoaded(std::string callback_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  LoadWebRtcEventLogs(callback_id);  // Text logs loaded; on to the event logs.
}

void WebRtcLogsDOMHandler::LoadWebRtcEventLogs(const std::string& callback_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  WebRtcEventLogManager* manager = WebRtcEventLogManager::GetInstance();
  if (manager) {
    manager->GetHistory(
        original_browser_context_id_,
        base::BindOnce(&WebRtcLogsDOMHandler::OnWebRtcEventLogsLoaded,
                       weak_ptr_factory_.GetWeakPtr(), callback_id));
  } else {
    OnWebRtcEventLogsLoaded(callback_id, std::vector<UploadList::UploadInfo>());
  }
}

void WebRtcLogsDOMHandler::OnWebRtcEventLogsLoaded(
    const std::string& callback_id,
    const std::vector<UploadList::UploadInfo>& event_logs) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  event_logs_ = event_logs;

  // All log histories loaded asynchronously; time to display.
  UpdateUI(callback_id);
}

void WebRtcLogsDOMHandler::UpdateUI(const std::string& callback_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  base::DictValue result;
  result.Set("textLogs", UpdateUIWithTextLogs());
  result.Set("eventLogs", UpdateUIWithEventLogs());
  result.Set("version", version_info::GetVersionNumber());
  ResolveJavascriptCallback(base::Value(callback_id), result);
}

base::ListValue WebRtcLogsDOMHandler::UpdateUIWithTextLogs() const {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  base::ListValue result;
  for (size_t i = 0; i < text_log_upload_lists_.size(); ++i) {
    const std::vector<const UploadList::UploadInfo*> uploads =
        text_log_upload_lists_[i]->GetUploads(50);

    for (const auto* upload : uploads) {
      base::DictValue upload_value;
      upload_value.Set("id", upload->upload_id);

      std::u16string value_w;
      if (!upload->upload_time.is_null()) {
        value_w = base::TimeFormatFriendlyDateAndTime(upload->upload_time);
      }
      upload_value.Set("upload_time", value_w);

      std::string value;
      if (!upload->local_id.empty()) {
        value = text_log_dirs_[i]
                    .AppendASCII(upload->local_id)
                    .AddExtension(FILE_PATH_LITERAL(".gz"))
                    .AsUTF8Unsafe();
      }
      upload_value.Set("local_file", value);

      // In october 2015, capture time was added to the log list, previously the
      // local ID was used as capture time. The local ID has however changed so
      // that it might not be a time. We fall back on the local ID if it
      // translates to a time within reasonable bounds, otherwise we fall back
      // on the upload time.
      // TODO(grunell): Use |capture_time| only.
      if (!upload->capture_time.is_null()) {
        value_w = base::TimeFormatFriendlyDateAndTime(upload->capture_time);
      } else {
        // Fall back on local ID as time. We need to check that it's within
        // reasonable bounds, since the ID may not represent time. Check between
        // 2012 when the feature was introduced and now.
        double seconds_since_epoch;
        if (base::StringToDouble(upload->local_id, &seconds_since_epoch)) {
          base::Time capture_time =
              base::Time::FromSecondsSinceUnixEpoch(seconds_since_epoch);
          static constexpr base::Time::Exploded kLowerLimit = {
              .year = 2012, .month = 1, .day_of_month = 1};
          base::Time out_time;
          bool conversion_success =
              base::Time::FromUTCExploded(kLowerLimit, &out_time);
          DCHECK(conversion_success);
          if (capture_time > out_time && capture_time < base::Time::Now()) {
            value_w = base::TimeFormatFriendlyDateAndTime(capture_time);
          }
        }
      }
      // If we haven't set |value_w| above, we fall back on the upload time,
      // which was already in the variable. In case it's empty set the string to
      // inform that the time is unknown.
      if (value_w.empty()) {
        value_w = std::u16string(u"(unknown time)");
      }
      upload_value.Set("capture_time", value_w);

      result.Append(std::move(upload_value));
    }
  }
  return result;
}

base::ListValue WebRtcLogsDOMHandler::UpdateUIWithEventLogs() const {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  base::ListValue result;
  for (const auto& log : event_logs_) {
    result.Append(EventLogUploadInfoToValue(log));
  }
  return result;
}

base::Value WebRtcLogsDOMHandler::EventLogUploadInfoToValue(
    const UploadList::UploadInfo& info) const {
  switch (info.state) {
    case UploadList::UploadInfo::State::Pending:
      // TODO(crbug.com/40545136): Display actively-written logs differently
      // than fully captured pending logs.
      return info.upload_time.is_null() ? FromPendingLog(info)
                                        : FromActivelyUploadedLog(info);
    case UploadList::UploadInfo::State::NotUploaded:
      return info.upload_time.is_null() ? FromNotUploadedLog(info)
                                        : FromUploadUnsuccessfulLog(info);
    case UploadList::UploadInfo::State::Uploaded:
      return FromUploadSuccessfulLog(info);
    case UploadList::UploadInfo::State::Pending_UserRequested:
      NOTREACHED();
  }

  LOG(ERROR) << "Unrecognized state (" << static_cast<int>(info.state) << ").";
  return base::Value();
}

base::Value WebRtcLogsDOMHandler::FromPendingLog(
    const UploadList::UploadInfo& info) const {
  DCHECK_EQ(info.state, UploadList::UploadInfo::State::Pending);
  DCHECK(info.upload_time.is_null());

  if (!SanityCheckOnUploadInfo(info)) {
    return base::Value();
  }

  base::DictValue log;
  log.Set("state", "pending");
  log.Set("capture_time",
          base::TimeFormatFriendlyDateAndTime(info.capture_time));
  log.Set("local_file",
          event_log_dir_.AppendASCII(info.local_id).AsUTF8Unsafe());
  return base::Value(std::move(log));
}

base::Value WebRtcLogsDOMHandler::FromActivelyUploadedLog(
    const UploadList::UploadInfo& info) const {
  DCHECK_EQ(info.state, UploadList::UploadInfo::State::Pending);
  DCHECK(!info.upload_time.is_null());

  if (!SanityCheckOnUploadInfo(info)) {
    return base::Value();
  }

  base::DictValue log;
  log.Set("state", "actively_uploaded");
  log.Set("capture_time",
          base::TimeFormatFriendlyDateAndTime(info.capture_time));
  log.Set("local_file",
          event_log_dir_.AppendASCII(info.local_id).AsUTF8Unsafe());
  return base::Value(std::move(log));
}

base::Value WebRtcLogsDOMHandler::FromNotUploadedLog(
    const UploadList::UploadInfo& info) const {
  DCHECK_EQ(info.state, UploadList::UploadInfo::State::NotUploaded);
  DCHECK(info.upload_time.is_null());

  if (!SanityCheckOnUploadInfo(info)) {
    return base::Value();
  }

  base::DictValue log;
  log.Set("state", "not_uploaded");
  log.Set("capture_time",
          base::TimeFormatFriendlyDateAndTime(info.capture_time));
  log.Set("local_id", info.local_id);
  if (info.local_id.find("_local") != std::string::npos) {
    log.Set("local_file",
            event_log_dir_.AppendASCII(info.local_id).AsUTF8Unsafe());
  }
  return base::Value(std::move(log));
}

base::Value WebRtcLogsDOMHandler::FromUploadUnsuccessfulLog(
    const UploadList::UploadInfo& info) const {
  DCHECK_EQ(info.state, UploadList::UploadInfo::State::NotUploaded);
  DCHECK(!info.upload_time.is_null());

  if (!SanityCheckOnUploadInfo(info)) {
    return base::Value();
  }

  if (!info.upload_id.empty()) {
    LOG(ERROR) << "Unexpected upload ID.";
    return base::Value();
  }

  base::DictValue log;
  log.Set("state", "upload_unsuccessful");
  log.Set("capture_time",
          base::TimeFormatFriendlyDateAndTime(info.capture_time));
  log.Set("local_id", info.local_id);
  log.Set("upload_time", base::TimeFormatFriendlyDateAndTime(info.upload_time));
  return base::Value(std::move(log));
}

base::Value WebRtcLogsDOMHandler::FromUploadSuccessfulLog(
    const UploadList::UploadInfo& info) const {
  DCHECK_EQ(info.state, UploadList::UploadInfo::State::Uploaded);
  DCHECK(!info.upload_time.is_null());

  if (!SanityCheckOnUploadInfo(info)) {
    return base::Value();
  }

  if (info.upload_id.empty()) {
    LOG(ERROR) << "Unknown upload ID.";
    return base::Value();
  }

  base::DictValue log;
  log.Set("state", "upload_successful");
  log.Set("capture_time",
          base::TimeFormatFriendlyDateAndTime(info.capture_time));
  log.Set("local_id", info.local_id);
  log.Set("upload_id", info.upload_id);
  log.Set("upload_time", base::TimeFormatFriendlyDateAndTime(info.upload_time));
  return base::Value(std::move(log));
}

bool WebRtcLogsDOMHandler::SanityCheckOnUploadInfo(
    const UploadList::UploadInfo& info) const {
  if (info.capture_time.is_null()) {
    LOG(ERROR) << "Unknown capture time.";
    return false;
  }

  if (info.local_id.empty()) {
    LOG(ERROR) << "Unknown local ID.";
    return false;
  }

  return true;
}

}  // namespace

///////////////////////////////////////////////////////////////////////////////
//
// WebRtcLogsUI
//
///////////////////////////////////////////////////////////////////////////////

WebRtcLogsUI::WebRtcLogsUI(content::WebUI* web_ui) : WebUIController(web_ui) {
  Profile* profile = Profile::FromWebUI(web_ui);
  web_ui->AddMessageHandler(std::make_unique<WebRtcLogsDOMHandler>(profile));

  // Set up the chrome://webrtc-logs/ source.
  CreateAndAddWebRtcLogsUIHTMLSource(profile);
}
