chromium/chrome/browser/ash/policy/remote_commands/device_command_screenshot_job.cc

// Copyright 2015 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/ash/policy/remote_commands/device_command_screenshot_job.h"

#include <fstream>
#include <optional>
#include <utility>

#include "ash/shell.h"
#include "base/barrier_callback.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/syslog_logging.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "net/http/http_request_headers.h"

namespace policy {

namespace {

using ResultCode = DeviceCommandScreenshotJob::ResultCode;

// String constant identifying the result field in the result payload.
const char* const kResultFieldName = "result";

// Template string constant for populating the name field.
const char* const kNameFieldTemplate = "Screen %zu";

// Template string constant for populating the name field.
const char* const kFilenameFieldTemplate = "screen%zu.png";

// String constant identifying the header field which stores the command id.
const char* const kCommandIdHeaderName = "Command-ID";

// String constant signalling that the segment contains a png image.
const char* const kContentTypeImagePng = "image/png";

// String constant identifying the header field which stores the file type.
const char* const kFileTypeHeaderName = "File-Type";

// String constant signalling that the data segment contains screenshots.
const char* const kFileTypeScreenshotFile = "screenshot_file";

// Helper method to hide the |screen_index| and `std::make_pair` from the
// |DeviceCommandScreenshotJob::Delegate|.
void CallCollectAndUpload(
    base::OnceCallback<void(ScreenshotData)> collect_and_upload,
    size_t screen_index,
    scoped_refptr<base::RefCountedMemory> png_data) {
  std::move(collect_and_upload).Run(std::make_pair(screen_index, png_data));
}

std::string CreatePayload(ResultCode result_code) {
  base::Value::Dict root_dict;
  if (result_code != ResultCode::SUCCESS) {
    root_dict.Set(kResultFieldName, result_code);
  }

  return base::WriteJson(root_dict).value();
}

ResultCode ToResultCode(UploadJob::ErrorCode error_code) {
  switch (error_code) {
    case UploadJob::AUTHENTICATION_ERROR:
      return ResultCode::FAILURE_AUTHENTICATION;
    case UploadJob::NETWORK_ERROR:
    case UploadJob::SERVER_ERROR:
      return ResultCode::FAILURE_SERVER;
  }
}

}  // namespace

// String constant identifying the upload url field in the command payload.
constexpr char DeviceCommandScreenshotJob::kUploadUrlFieldName[] =
    "fileUploadUrl";

DeviceCommandScreenshotJob::DeviceCommandScreenshotJob(
    std::unique_ptr<Delegate> screenshot_delegate)
    : screenshot_delegate_(std::move(screenshot_delegate)) {
  DCHECK(screenshot_delegate_);
}

DeviceCommandScreenshotJob::~DeviceCommandScreenshotJob() = default;

enterprise_management::RemoteCommand_Type DeviceCommandScreenshotJob::GetType()
    const {
  return enterprise_management::RemoteCommand_Type_DEVICE_SCREENSHOT;
}

void DeviceCommandScreenshotJob::OnSuccess() {
  SYSLOG(INFO) << "Upload successful.";
  ReportResult(ResultType::kSuccess, ResultCode::SUCCESS);
}

void DeviceCommandScreenshotJob::OnFailure(UploadJob::ErrorCode error_code) {
  SYSLOG(ERROR) << "Upload failure: " << error_code;
  ReportResult(ResultType::kFailure, ToResultCode(error_code));
}

bool DeviceCommandScreenshotJob::ParseCommandPayload(
    const std::string& command_payload) {
  std::optional<base::Value> root(base::JSONReader::Read(command_payload));
  if (!root || !root->is_dict()) {
    return false;
  }
  const std::string* upload_url =
      root->GetDict().FindString(kUploadUrlFieldName);
  if (!upload_url) {
    return false;
  }
  upload_url_ = GURL(*upload_url);
  return true;
}

void DeviceCommandScreenshotJob::OnScreenshotsReady(
    scoped_refptr<base::TaskRunner> task_runner,
    std::vector<ScreenshotData> upload_data) {
  // TODO(https://crbug.com/1297571) Do we really need to re-post here?
  // Can we add guarantees to
  // `DeviceCommandScreenshotJob::Delegate::TakeSnapshot`?
  task_runner->PostTask(
      FROM_HERE,
      base::BindOnce(&DeviceCommandScreenshotJob::StartScreenshotUpload,
                     weak_ptr_factory_.GetWeakPtr(), std::move(upload_data)));
}

void DeviceCommandScreenshotJob::StartScreenshotUpload(
    std::vector<ScreenshotData> upload_data) {
  std::sort(begin(upload_data), end(upload_data),
            [](const auto& l, const auto& r) { return l.first < r.first; });

  for (const auto& screenshot_entry : upload_data) {
    if (!screenshot_entry.second) {
      LOG(WARNING) << "not uploading empty screenshot at index "
                   << screenshot_entry.first;
      continue;
    }
    std::map<std::string, std::string> header_fields;
    header_fields.insert(
        std::make_pair(kFileTypeHeaderName, kFileTypeScreenshotFile));
    header_fields.insert(std::make_pair(net::HttpRequestHeaders::kContentType,
                                        kContentTypeImagePng));
    header_fields.insert(std::make_pair(kCommandIdHeaderName,
                                        base::NumberToString(unique_id())));
    std::unique_ptr<std::string> data = std::make_unique<std::string>(
        (const char*)screenshot_entry.second->front(),
        screenshot_entry.second->size());
    upload_job_->AddDataSegment(
        base::StringPrintf(kNameFieldTemplate, screenshot_entry.first),
        base::StringPrintf(kFilenameFieldTemplate, screenshot_entry.first),
        header_fields, std::move(data));
  }
  upload_job_->Start();
}

void DeviceCommandScreenshotJob::ReportResult(ResultType result_type,
                                              ResultCode result_code) {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(result_callback_), result_type,
                                CreatePayload(result_code)));
}

void DeviceCommandScreenshotJob::RunImpl(CallbackWithResult result_callback) {
  result_callback_ = std::move(result_callback);

  SYSLOG(INFO) << "Executing screenshot command.";

  // Fail if the delegate says screenshots are not allowed in this session.
  if (!screenshot_delegate_->IsScreenshotAllowed()) {
    SYSLOG(ERROR) << "Screenshots are not allowed.";
    ReportResult(ResultType::kFailure, ResultCode::FAILURE_USER_INPUT);
    return;
  }

  aura::Window::Windows root_windows = ash::Shell::GetAllRootWindows();

  // Immediately fail if the upload url is invalid.
  if (!upload_url_.is_valid()) {
    SYSLOG(ERROR) << upload_url_ << " is not a valid URL.";
    ReportResult(ResultType::kFailure, ResultCode::FAILURE_INVALID_URL);
    return;
  }

  // Immediately fail if there are no attached screens.
  if (root_windows.size() == 0) {
    SYSLOG(ERROR) << "No attached screens.";
    ReportResult(ResultType::kFailure,
                 ResultCode::FAILURE_SCREENSHOT_ACQUISITION);
    return;
  }

  upload_job_ = screenshot_delegate_->CreateUploadJob(upload_url_, this);
  DCHECK(upload_job_);

  // Post tasks to the sequenced worker pool for taking screenshots on each
  // attached screen.
  auto collect_and_upload = base::BarrierCallback<ScreenshotData>(
      root_windows.size(),
      base::BindOnce(&DeviceCommandScreenshotJob::OnScreenshotsReady,
                     weak_ptr_factory_.GetWeakPtr(),
                     base::SingleThreadTaskRunner::GetCurrentDefault()));
  for (size_t screen_index = 0; screen_index < root_windows.size();
       ++screen_index) {
    aura::Window* root_window = root_windows[screen_index];
    gfx::Rect rect = root_window->bounds();
    screenshot_delegate_->TakeSnapshot(
        root_window, rect,
        base::BindOnce(CallCollectAndUpload, collect_and_upload, screen_index));
  }
}

void DeviceCommandScreenshotJob::TerminateImpl() {
  weak_ptr_factory_.InvalidateWeakPtrs();
}

}  // namespace policy