// Copyright 2023 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/enterprise/data_protection/print_utils.h"

#include <cstring>
#include <optional>
#include <utility>

#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "chrome/browser/profiles/profile.h"
#include "components/device_event_log/device_event_log.h"
#include "components/enterprise/buildflags/buildflags.h"
#include "content/public/browser/web_contents.h"
#include "printing/printing_features.h"

namespace enterprise_data_protection {

namespace {

bool ShouldScan(PrintScanningContext context) {
  switch (context) {
    // For "normal" prints, the scanning can happen immediately after the user
    // clicks "Print" in the print preview dialog as the preview document is
    // representative of what they are printing.
    case PrintScanningContext::kNormalPrintAfterPreview:
      return true;
    case PrintScanningContext::kNormalPrintBeforePrintDocument:
      return false;

    // For "system dialog" prints, the scanning waits until the user picks
    // settings from the system dialog, but starts applying enterprise-logic
    // logic at the `kBeforeSystemDialog` context for that to happen.
    //
    // Scanning also happens right before the document is printed through an
    // existing print job when that is triggered after the print preview
    // dialog.
    case PrintScanningContext::kBeforeSystemDialog:
      return true;
    case PrintScanningContext::kSystemPrintAfterPreview:
      return false;
    case PrintScanningContext::kSystemPrintBeforePrintDocument:
      return true;

#if BUILDFLAG(IS_MAC)
    // For the "Open PDF in Preview" option on Mac, scan right as it happens
    // from the print preview dialog.
    case PrintScanningContext::kOpenPdfInPreview:
      return true;
#endif  // BUILDFLAG(IS_MAC)
  }
}

}  // namespace

void PrintIfAllowedByPolicy(scoped_refptr<base::RefCountedMemory> print_data,
                            content::WebContents* initiator,
                            std::string printer_name,
                            PrintScanningContext context,
                            base::OnceCallback<void(bool)> on_verdict,
                            base::OnceClosure hide_preview) {
  // In some cases like the web contents closing or the render process crashing,
  // it's possible for `initiator` to be null. In that case, printing can simply
  // be aborted.
  if (!initiator) {
    std::move(on_verdict).Run(/*allowed=*/false);
    return;
  }

  // This needs to be done to avoid having an embedded page compare against
  // policies and report its URL. This is especially important when Chrome's PDF
  // reader is used, as a cryptic extension URL will be sent for
  // scanning/reporting in that case.
  // TODO(b/289243948): Add browser test coverage for web contents received and
  // passed to the delegate.
  content::WebContents* web_contents = initiator->GetOutermostWebContents();

  auto scanning_data = GetPrintAnalysisData(web_contents, context);

  if (!scanning_data) {
    std::move(on_verdict).Run(/*allowed=*/true);
    return;
  }

  // Populate print metadata.
  scanning_data->printer_name = std::move(printer_name);

  // Hide the preview dialog so it doesn't cover the content analysis dialog
  // showing the status of the scanning. Since that scanning dialog only appears
  // when the policy is set to be blocking, don't run `hide_preview` for other
  // policy configurations to avoid races between the dialog being hidden vs
  // destroyed.
  if (scanning_data->settings.block_until_verdict ==
      enterprise_connectors::BlockUntilVerdict::kBlock) {
    std::move(hide_preview).Run();
  }

  PrintIfAllowedByPolicy(print_data, web_contents, std::move(*scanning_data),
                         std::move(on_verdict));
}

void PrintIfAllowedByPolicy(
    scoped_refptr<base::RefCountedMemory> print_data,
    content::WebContents* initiator,
    enterprise_connectors::ContentAnalysisDelegate::Data scanning_data,
    base::OnceCallback<void(bool)> on_verdict) {
  // The preview document bytes are copied so that the content analysis code
  // can arbitrarily use them without having to handle ownership issues with
  // other printing code.
  base::MappedReadOnlyRegion region =
      base::ReadOnlySharedMemoryRegion::Create(print_data->size());
  if (!region.IsValid()) {
    // Allow printing if the scan can't happen due to memory failure.
    PRINTER_LOG(ERROR) << "Printed without analysis due to memory failure";
    std::move(on_verdict).Run(/*allowed=*/true);
    return;
  }
  region.mapping.GetMemoryAsSpan<uint8_t>().copy_prefix_from(*print_data);
  scanning_data.page = std::move(region.region);

  auto on_scan_result = base::BindOnce(
      [](base::OnceCallback<void(bool should_proceed)> callback,
         const enterprise_connectors::ContentAnalysisDelegate::Data& data,
         enterprise_connectors::ContentAnalysisDelegate::Result& result) {
        std::move(callback).Run(result.page_result);
      },
      std::move(on_verdict));

  // This needs to be done to avoid having an embedded page compare against
  // policies and report its URL. This is especially important when Chrome's PDF
  // reader is used, as a cryptic extension URL will be sent for
  // scanning/reporting in that case.
  // TODO(b/289243948): Add browser test coverage for web contents received and
  // passed to the delegate.
  content::WebContents* web_contents = initiator->GetOutermostWebContents();

  enterprise_connectors::ContentAnalysisDelegate::CreateForWebContents(
      web_contents, std::move(scanning_data), std::move(on_scan_result),
      enterprise_connectors::DeepScanAccessPoint::PRINT);
}

std::optional<enterprise_connectors::ContentAnalysisDelegate::Data>
GetPrintAnalysisData(content::WebContents* web_contents,
                     PrintScanningContext context) {
  enterprise_connectors::ContentAnalysisDelegate::Data scanning_data;

  bool enabled = enterprise_connectors::ContentAnalysisDelegate::IsEnabled(
      Profile::FromBrowserContext(web_contents->GetBrowserContext()),
      web_contents->GetOutermostWebContents()->GetLastCommittedURL(),
      &scanning_data, enterprise_connectors::AnalysisConnector::PRINT);

  if (enabled && ShouldScan(context)) {
    switch (context) {
#if BUILDFLAG(IS_MAC)
      case PrintScanningContext::kOpenPdfInPreview:
#endif  // BUILDFLAG(IS_MAC)
      case PrintScanningContext::kNormalPrintAfterPreview:
      case PrintScanningContext::kNormalPrintBeforePrintDocument:
        scanning_data.reason =
            enterprise_connectors::ContentAnalysisRequest::PRINT_PREVIEW_PRINT;
        break;

      case PrintScanningContext::kBeforeSystemDialog:
      case PrintScanningContext::kSystemPrintAfterPreview:
      case PrintScanningContext::kSystemPrintBeforePrintDocument:
        scanning_data.reason =
            enterprise_connectors::ContentAnalysisRequest::SYSTEM_DIALOG_PRINT;
        break;
    }

    return scanning_data;
  }

  return std::nullopt;
}

}  // namespace enterprise_data_protection
