blob: ea3036472eddf3ad7ab8dd970014a45465d8bcf1 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/ash/os_feedback/chrome_os_feedback_delegate.h"
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/shell.h"
#include "ash/webui/os_feedback_ui/backend/histogram_util.h"
#include "ash/webui/os_feedback_ui/mojom/os_feedback_ui.mojom.h"
#include "ash/webui/system_apps/public/system_web_app_type.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/memory/ref_counted_memory.h"
#include "base/strings/strcat.h"
#include "base/strings/string_view_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ash/browser_delegate/browser_controller.h"
#include "chrome/browser/ash/browser_delegate/browser_delegate.h"
#include "chrome/browser/ash/multidevice_setup/multidevice_setup_client_factory.h"
#include "chrome/browser/ash/os_feedback/os_feedback_screenshot_manager.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/feedback/feedback_uploader_chrome.h"
#include "chrome/browser/feedback/feedback_uploader_factory_chrome.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/webui/ash/diagnostics_dialog/diagnostics_dialog.h"
#include "chrome/browser/ui/webui/ash/os_feedback_dialog/os_feedback_dialog.h"
#include "chrome/common/webui_url_constants.h"
#include "chromeos/ash/services/multidevice_setup/public/cpp/multidevice_setup_client.h"
#include "components/feedback/content/content_tracing_manager.h"
#include "components/feedback/feedback_common.h"
#include "components/feedback/feedback_data.h"
#include "components/feedback/feedback_report.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "content/public/browser/browser_context.h"
#include "extensions/browser/api/feedback_private/feedback_private_api.h"
#include "extensions/browser/api/feedback_private/feedback_service.h"
#include "mojo/public/cpp/base/big_buffer.h"
#include "mojo/public/mojom/base/safe_base_name.mojom.h"
#include "net/base/network_change_notifier.h"
#include "ui/aura/window.h"
#include "ui/snapshot/snapshot.h"
#include "ui/web_dialogs/web_dialog_delegate.h"
#include "url/gurl.h"
namespace ash {
namespace {
using ::ash::os_feedback_ui::mojom::AttachedFilePtr;
using ::ash::os_feedback_ui::mojom::SendReportStatus;
using extensions::FeedbackParams;
using extensions::FeedbackPrivateAPI;
feedback::FeedbackUploader* GetFeedbackUploaderForContext(
content::BrowserContext* context) {
return feedback::FeedbackUploaderFactoryChrome::GetForBrowserContext(context);
}
scoped_refptr<base::RefCountedMemory> GetScreenshotData() {
auto* screenshot_manager = OsFeedbackScreenshotManager::GetIfExists();
if (screenshot_manager) {
return screenshot_manager->GetScreenshotData();
}
return nullptr;
}
constexpr std::size_t MAX_ATTACHED_FILE_SIZE_BYTES = 10 * 1024 * 1024;
bool ShouldAddAttachment(const AttachedFilePtr& attached_file) {
if (!(attached_file && attached_file->file_data.data())) {
// Does not have data.
return false;
}
if (attached_file->file_name.path().empty()) {
// The file name is empty.
return false;
}
if (attached_file->file_data.size() > MAX_ATTACHED_FILE_SIZE_BYTES) {
LOG(WARNING) << "Can't upload file larger than 10 MB. File size: "
<< attached_file->file_data.size();
return false;
}
return true;
}
// Find the native window of feedback SWA or dialog.
gfx::NativeWindow FindFeedbackWindow(Profile* profile) {
BrowserDelegate* feedback_browser = ash::FindSystemWebAppBrowser(
profile, ash::SystemWebAppType::OS_FEEDBACK, ash::BrowserType::kApp);
if (feedback_browser) {
return feedback_browser->GetNativeWindow();
}
return OsFeedbackDialog::FindDialogWindow();
}
// Key-value pair to be added to FeedbackData when user grants consent to Google
// to follow-up on feedback report. See (go/feedback-user-consent-faq) for more
// information.
// Consent key matches cross-platform key.
constexpr char kFeedbackUserConsentKey[] = "feedbackUserCtlConsent";
// Consent value matches JavaScript: `String(true)`.
constexpr char kFeedbackUserConsentGrantedValue[] = "true";
// Consent value matches JavaScript: `String(false)`.
constexpr char kFeedbackUserConsentDeniedValue[] = "false";
constexpr char kExtraDiagnosticsKey[] = "EXTRA_DIAGNOSTICS";
constexpr char kLinkCrossDeviceDogfoodFeedbackWithBluetoothLogs[] =
"linkCrossDeviceDogfoodFeedbackWithBluetoothLogs";
constexpr char kLinkCrossDeviceDogfoodFeedbackWithoutBluetoothLogs[] =
"linkCrossDeviceDogfoodFeedbackWithoutBluetoothLogs";
} // namespace
ChromeOsFeedbackDelegate::ChromeOsFeedbackDelegate(content::WebUI* web_ui)
: ChromeOsFeedbackDelegate(Profile::FromWebUI(web_ui)) {}
ChromeOsFeedbackDelegate::ChromeOsFeedbackDelegate(Profile* profile)
: ChromeOsFeedbackDelegate(profile,
FeedbackPrivateAPI::GetFactoryInstance()
->Get(profile)
->GetService()) {}
ChromeOsFeedbackDelegate::ChromeOsFeedbackDelegate(
Profile* profile,
scoped_refptr<extensions::FeedbackService> feedback_service)
: profile_(profile), feedback_service_(feedback_service) {
ash::BrowserDelegate* browser =
ash::BrowserController::GetInstance()->GetLastUsedBrowser();
if (!browser) {
return;
}
auto* web_contents = (browser->GetType() == BrowserType::kDevTools)
? browser->GetInspectedWebContents()
: browser->GetActiveWebContents();
// Save the last active page url before opening the feedback tool.
page_url_ = web_contents ? web_contents->GetLastCommittedURL() : GURL();
}
// Static.
bool ChromeOsFeedbackDelegate::IsWifiDebugLogsAllowed(
const PrefService* prefs) {
if (prefs == nullptr) {
return false;
}
const base::Value::List& allowed_list =
prefs->GetList(prefs::kUserFeedbackWithLowLevelDebugDataAllowed);
for (const auto& item : allowed_list) {
if (item == "all" || item == "wifi") {
return true;
}
}
return false;
}
ChromeOsFeedbackDelegate ChromeOsFeedbackDelegate::CreateForTesting(
Profile* profile) {
return ChromeOsFeedbackDelegate(profile);
}
ChromeOsFeedbackDelegate ChromeOsFeedbackDelegate::CreateForTesting(
Profile* profile,
scoped_refptr<extensions::FeedbackService> feedback_service) {
return ChromeOsFeedbackDelegate(profile, feedback_service);
}
ChromeOsFeedbackDelegate::~ChromeOsFeedbackDelegate() {
auto* screenshot_manager = OsFeedbackScreenshotManager::GetIfExists();
if (screenshot_manager) {
screenshot_manager->DeleteScreenshotData();
}
}
std::string ChromeOsFeedbackDelegate::GetApplicationLocale() {
return g_browser_process->GetApplicationLocale();
}
std::optional<GURL> ChromeOsFeedbackDelegate::GetLastActivePageUrl() {
// GetLastActivePageUrl will be called when the UI is about to be displayed.
PreloadSystemLogs();
return page_url_;
}
std::optional<std::string> ChromeOsFeedbackDelegate::GetSignedInUserEmail()
const {
auto* identity_manager = IdentityManagerFactory::GetForProfile(profile_);
if (!identity_manager)
return std::nullopt;
// Browser sync consent is not required to use feedback.
return identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
.email;
}
std::optional<std::string>
ChromeOsFeedbackDelegate::GetLinkedPhoneMacAddress() {
CHECK(features::IsLinkCrossDeviceDogfoodFeedbackEnabled());
auto* multidevice_setup_client =
ash::multidevice_setup::MultiDeviceSetupClientFactory::GetForProfile(
profile_);
if (!multidevice_setup_client) {
return std::nullopt;
}
std::optional<multidevice::RemoteDeviceRef> remote_device_ref =
multidevice_setup_client->GetHostStatus().second;
if (!remote_device_ref.has_value()) {
return std::nullopt;
}
return remote_device_ref.value().bluetooth_public_address();
}
bool ChromeOsFeedbackDelegate::IsWifiDebugLogsAllowed() const {
return IsWifiDebugLogsAllowed(profile_->GetPrefs());
}
int ChromeOsFeedbackDelegate::GetPerformanceTraceId() {
if (ContentTracingManager* manager = ContentTracingManager::Get()) {
return manager->RequestTrace();
} else {
return 0;
}
}
void ChromeOsFeedbackDelegate::GetScreenshotPng(
GetScreenshotPngCallback callback) {
scoped_refptr<base::RefCountedMemory> png_data = GetScreenshotData();
if (png_data && png_data.get()) {
std::vector<uint8_t> data(png_data->data(),
png_data->data() + png_data->size());
std::move(callback).Run(data);
} else {
std::vector<uint8_t> empty_data;
std::move(callback).Run(empty_data);
}
}
void ChromeOsFeedbackDelegate::SendReport(
os_feedback_ui::mojom::ReportPtr report,
SendReportCallback callback) {
// Populate feedback_params
FeedbackParams feedback_params;
feedback_params.form_submit_time = base::TimeTicks::Now();
feedback_params.load_system_info = report->include_system_logs_and_histograms;
feedback_params.send_histograms = report->include_system_logs_and_histograms;
feedback_params.send_bluetooth_logs = report->send_bluetooth_logs;
feedback_params.send_wifi_debug_logs =
report->send_wifi_debug_logs && IsWifiDebugLogsAllowed();
feedback_params.send_tab_titles = report->include_screenshot;
feedback_params.send_autofill_metadata = report->include_autofill_metadata;
feedback_params.is_internal_email =
report->feedback_context->is_internal_account;
base::WeakPtr<feedback::FeedbackUploader> uploader =
GetFeedbackUploaderForContext(profile_)->AsWeakPtr();
scoped_refptr<::feedback::FeedbackData> feedback_data =
base::MakeRefCounted<feedback::FeedbackData>(
std::move(uploader), ContentTracingManager::Get());
feedback_data->set_description(base::UTF16ToUTF8(report->description));
const auto& feedback_context = report->feedback_context;
if (feedback_context->email.has_value()) {
feedback_data->set_user_email(feedback_context->email.value());
}
if (feedback_context->page_url.has_value()) {
feedback_data->set_page_url(feedback_context->page_url.value().spec());
}
if (feedback_context->extra_diagnostics.has_value() &&
!feedback_context->extra_diagnostics.value().empty()) {
feedback_data->AddLog(kExtraDiagnosticsKey,
feedback_context->extra_diagnostics.value());
}
feedback_data->set_trace_id(report->feedback_context->trace_id);
feedback_data->set_from_assistant(feedback_context->from_assistant);
feedback_data->set_assistant_debug_info_allowed(
feedback_context->assistant_debug_info_allowed);
if (feedback_context->category_tag.has_value()) {
feedback_data->set_category_tag(feedback_context->category_tag.value());
}
if (feedback_params.send_autofill_metadata &&
feedback_context->autofill_metadata.has_value()) {
feedback_data->set_autofill_metadata(*feedback_context->autofill_metadata);
}
scoped_refptr<base::RefCountedMemory> png_data = GetScreenshotData();
if (report->include_screenshot && png_data) {
feedback_data->set_image(std::string(base::as_string_view(*png_data)));
}
// Append consent value to report. For cross platform implementations see:
// extensions/browser/api/feedback_private/feedback_private_api.cc
if (report->contact_user_consent_granted) {
feedback_data->AddLog(kFeedbackUserConsentKey,
kFeedbackUserConsentGrantedValue);
ash::os_feedback_ui::metrics::EmitFeedbackAppCanContactUser(
os_feedback_ui::metrics::FeedbackAppContactUserConsentType::kYes);
} else {
feedback_data->AddLog(kFeedbackUserConsentKey,
kFeedbackUserConsentDeniedValue);
feedback_context->email.has_value()
? ash::os_feedback_ui::metrics::EmitFeedbackAppCanContactUser(
os_feedback_ui::metrics::FeedbackAppContactUserConsentType::kNo)
: ash::os_feedback_ui::metrics::EmitFeedbackAppCanContactUser(
os_feedback_ui::metrics::FeedbackAppContactUserConsentType::
kNoEmail);
}
const AttachedFilePtr& attached_file = report->attached_file;
if (ShouldAddAttachment(attached_file)) {
feedback_data->set_attached_filename(
attached_file->file_name.path().AsUTF8Unsafe());
const std::string file_data(
reinterpret_cast<const char*>(attached_file->file_data.data()),
attached_file->file_data.size());
// Compress attached file and add to |feedback_data|. The operation is done
// by posting a task to thread pool. The |feedback_data| will manage waiting
// for all tasks to complete.
feedback_data->AttachAndCompressFileData(std::move(file_data));
// Records whether the file is included when the feedback report is
// submitted.
ash::os_feedback_ui::metrics::EmitFeedbackAppIncludedFile(true);
} else {
ash::os_feedback_ui::metrics::EmitFeedbackAppIncludedFile(false);
}
// Handle Feedback Metrics
// Records whether the screenshot is included when the feedback report is
// submitted.
ash::os_feedback_ui::metrics::EmitFeedbackAppIncludedScreenshot(
report->include_screenshot);
// Records whether the email is included when the feedback report is
// submitted.
ash::os_feedback_ui::metrics::EmitFeedbackAppIncludedEmail(
feedback_context->email.has_value());
// Records whether the page url is included when the feedback report is
// submitted.
ash::os_feedback_ui::metrics::EmitFeedbackAppIncludedUrl(
feedback_context->page_url.has_value());
// Records whether the system and information is included when the feedback
// report is submitted.
ash::os_feedback_ui::metrics::EmitFeedbackAppIncludedSystemInfo(
report->include_system_logs_and_histograms);
// Records the length of description in the textbox when the feedback
// report is submitted.
ash::os_feedback_ui::metrics::EmitFeedbackAppDescriptionLength(
report->description.length());
// If system logs are included, get them from preloaded response.
if (feedback_params.load_system_info && system_logs_response_ &&
system_logs_response_->size() > 0) {
for (auto& itr : *system_logs_response_) {
if (FeedbackCommon::IncludeInSystemLogs(
itr.first, feedback_params.is_internal_email))
feedback_data->AddLog(std::move(itr.first), std::move(itr.second));
}
// Set to false so they won't be loaded again in feedback service.
feedback_params.load_system_info = false;
}
feedback_service_->RedactThenSendFeedback(
feedback_params, feedback_data,
base::BindOnce(&ChromeOsFeedbackDelegate::OnSendFeedbackDone,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
// Only get and set the mac address if all the following are true:
// 1. The flag is enabled,
// 2. It is an internal account,
// 3. Category tag has a value, and
// 4. The value of the category tag is
// kLinkCrossDeviceDogfoodFeedbackWithBluetoothLogs or
// kLinkCrossDeviceDogfoodFeedbackWithoutBluetoothLogs.
bool is_linked_cross_device_feedback_report =
feedback_context->category_tag.has_value() &&
(feedback_context->category_tag.value() ==
kLinkCrossDeviceDogfoodFeedbackWithBluetoothLogs ||
feedback_context->category_tag.value() ==
kLinkCrossDeviceDogfoodFeedbackWithoutBluetoothLogs);
if (features::IsLinkCrossDeviceDogfoodFeedbackEnabled() &&
feedback_context->is_internal_account &&
feedback_context->category_tag.has_value() &&
is_linked_cross_device_feedback_report) {
feedback_data->set_mac_address(GetLinkedPhoneMacAddress());
}
}
void ChromeOsFeedbackDelegate::OnSendFeedbackDone(SendReportCallback callback,
bool status) {
// When status is true, it means the report will be sent shortly.
const SendReportStatus send_status =
status ? SendReportStatus::kSuccess : SendReportStatus::kDelayed;
std::move(callback).Run(send_status);
}
// An active feedback app can be either a SWA (for logged in users) or a dialog
// (for users not logged in).
// - Open the diagnostics app as SWA when feedback SWA exists.
// - Otherwise, open it as a dialog.
void ChromeOsFeedbackDelegate::OpenDiagnosticsApp() {
if (ash::FindSystemWebAppBrowser(profile_, ash::SystemWebAppType::OS_FEEDBACK,
ash::BrowserType::kApp)) {
ash::LaunchSystemWebAppAsync(profile_, ash::SystemWebAppType::DIAGNOSTICS);
return;
}
gfx::NativeWindow window = OsFeedbackDialog::FindDialogWindow();
CHECK(window);
ash::DiagnosticsDialog::ShowDialog(
ash::DiagnosticsDialog::DiagnosticsPage::kDefault, window);
}
void ChromeOsFeedbackDelegate::OpenExploreApp() {
ash::LaunchSystemWebAppAsync(profile_, ash::SystemWebAppType::HELP);
}
void ChromeOsFeedbackDelegate::OpenMetricsDialog() {
OpenWebDialog(GURL(chrome::kChromeUIHistogramsURL), /*args=*/"");
}
void ChromeOsFeedbackDelegate::OpenSystemInfoDialog() {
GURL systemInfoUrl = GURL(
base::StrCat({chrome::kChromeUIFeedbackURL, "html/system_info.html"}));
OpenWebDialog(systemInfoUrl, /*args=*/"");
}
void ChromeOsFeedbackDelegate::OpenAutofillMetadataDialog(
const std::string& autofill_metadata) {
GURL autofillInfoUrl = GURL(base::StrCat(
{chrome::kChromeUIFeedbackURL, "html/autofill_metadata_info.html"}));
OpenWebDialog(autofillInfoUrl, autofill_metadata);
}
bool ChromeOsFeedbackDelegate::IsChildAccount() {
return profile_->IsChild();
}
void ChromeOsFeedbackDelegate::OpenWebDialog(GURL url,
const std::string& args) {
gfx::NativeWindow window = FindFeedbackWindow(profile_);
CHECK(window);
views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window);
auto delegate = std::make_unique<ui::WebDialogDelegate>();
delegate->set_can_close(true);
delegate->set_dialog_args(args);
delegate->set_dialog_content_url(url);
delegate->set_dialog_size(gfx::Size(640, 400));
delegate->set_can_maximize(true);
delegate->set_can_minimize(true);
delegate->set_can_resize(true);
delegate->set_show_dialog_title(true);
// The delegate is self-owning once the dialog is shown.
chrome::ShowWebDialog(widget->GetNativeView(), profile_, delegate.release());
}
void ChromeOsFeedbackDelegate::PreloadSystemLogs() {
base::TimeTicks fetch_start_time = base::TimeTicks::Now();
feedback_service_->GetFeedbackPrivateDelegate()->FetchSystemInformation(
profile_,
base::BindOnce(&ChromeOsFeedbackDelegate::PreloadSystemLogsDone,
weak_ptr_factory_.GetWeakPtr(), fetch_start_time));
}
void ChromeOsFeedbackDelegate::PreloadSystemLogsDone(
base::TimeTicks fetch_start_time,
std::unique_ptr<system_logs::SystemLogsResponse> response) {
base::UmaHistogramMediumTimes("Feedback.Duration.FetchSystemInformation",
base::TimeTicks::Now() - fetch_start_time);
system_logs_response_ = std::move(response);
}
} // namespace ash