chromium/chrome/renderer/wallet/boarding_pass_extractor.cc

// 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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/renderer/wallet/boarding_pass_extractor.h"

#include "base/metrics/histogram_macros.h"
#include "chrome/common/chrome_isolated_world_ids.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/public/platform/scheduler/web_agent_group_scheduler.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_script_source.h"
#include "v8/include/v8-isolate.h"

namespace wallet {

namespace {
const std::string GetDefaultExtractionScript() {
  std::string script = R"###(
    // Check if the str contains 'boarding pass'.
    function containBoardingPass(str) {
        return str.toLowerCase().replace(/\s+/g, '').includes('boardingpass');
    }

    // Scale up the base64 encoded image to be detectable by BarcodeDetector.
    function scaleUpIfNeeded(img) {
        if (img.src.startsWith('data:image/')
            && (img.width < 500 || image.height < 500)) {
            let canvas = new OffscreenCanvas(500, 500);
            let ctx = canvas.getContext('2d');
            ctx.ImageSmoothingEnabled = false;
            ctx.drawImage(img, 0, 0, 500, 500);
            return canvas;
        }
        return img;
    }

    // Check if the rawValue is a valid BCBP string.
    function isValidBoardingPass(rawValue) {
        return rawValue.length > 60 && /^M[1-9]/.test(rawValue);
    }

    // Detect and decode boarding passes on current web page.
    async function detectBoardingPass() {
        var results = [];
        // Check if BarcodeDetector is supported by current browser.
        if (!'BarcodeDetector' in window) {
            return [];
        }

        // Check if current URL or page title contains 'boarding pass'.
        if (!containBoardingPass(window.location.href)
            && !containBoardingPass(document.title)) {
            return [];
        }

        var detector;
        try {
            detector = new BarcodeDetector();
        } catch (error) {
            return [];
        }
        for (let i = 0; i < document.images.length; ++i) {
            try {
                const barcodes = await detector.detect(
                    scaleUpIfNeeded(document.images[i]));
                for (const barcode of barcodes) {
                    // Check if it's a valid boarding pass
                    if (barcode.rawValue
                        && isValidBoardingPass(barcode.rawValue)) {
                        results.push(barcode.rawValue);
                    }
                }
            } catch (error) {
            }
        }
        return results;
    }

    detected_results_promise = detectBoardingPass();
  )###";
  return script;
}

std::vector<std::string> ConvertResultsToStrings(base::Value& value) {
  std::vector<std::string> results;

  if (!value.is_list()) {
    return results;
  }

  for (const auto& item : value.GetList()) {
    if (!item.is_string()) {
      continue;
    }
    results.push_back(item.GetString());
  }
  return results;
}
}  // namespace

BoardingPassExtractor::BoardingPassExtractor(
    content::RenderFrame* render_frame,
    service_manager::BinderRegistry* registry)
    : content::RenderFrameObserver(render_frame) {
  // Being a RenderFrameObserver, this object is scoped to the RenderFrame.
  // Unretained is safe here because `registry` is also scoped to the
  // RenderFrame.
  registry->AddInterface(base::BindRepeating(
      &BoardingPassExtractor::BindReceiver, base::Unretained(this)));

  mojo::Remote<ukm::mojom::UkmRecorderFactory> factory;
  content::RenderThread::Get()->BindHostReceiver(
      factory.BindNewPipeAndPassReceiver());
  ukm_recorder_ = ukm::MojoUkmRecorder::Create(*factory);
}

BoardingPassExtractor::~BoardingPassExtractor() = default;

void BoardingPassExtractor::BindReceiver(
    mojo::PendingReceiver<mojom::BoardingPassExtractor> receiver) {
  receiver_.reset();
  receiver_.Bind(std::move(receiver));
}

void BoardingPassExtractor::OnDestruct() {
  delete this;
}

void BoardingPassExtractor::ExtractBoardingPass(
    ExtractBoardingPassCallback callback) {
  ExtractBoardingPassWithScript(GetDefaultExtractionScript(),
                                std::move(callback));
}

void BoardingPassExtractor::ExtractBoardingPassWithScript(
    const std::string& script,
    ExtractBoardingPassCallback callback) {
  blink::WebLocalFrame* main_frame = render_frame()->GetWebFrame();
  v8::HandleScope handle_scope(main_frame->GetAgentGroupScheduler()->Isolate());
  blink::WebScriptSource source =
      blink::WebScriptSource(blink::WebString::FromUTF8(script));

  main_frame->RequestExecuteScript(
      ISOLATED_WORLD_ID_CHROME_INTERNAL, base::span_from_ref(source),
      blink::mojom::UserActivationOption::kDoNotActivate,
      blink::mojom::EvaluationTiming::kAsynchronous,
      blink::mojom::LoadEventBlockingOption::kDoNotBlock,
      base::BindOnce(&BoardingPassExtractor::OnBoardingPassExtracted,
                     base::Unretained(this), std::move(callback)),
      blink::BackForwardCacheAware::kAllow,
      blink::mojom::WantResultOption::kWantResult,
      blink::mojom::PromiseResultOption::kAwait);
}

void BoardingPassExtractor::OnBoardingPassExtracted(
    ExtractBoardingPassCallback callback,
    std::optional<base::Value> results,
    base::TimeTicks start_time) {
  std::vector<std::string> boarding_passes;
  if (results.has_value()) {
    boarding_passes = ConvertResultsToStrings(*results);
  }

  ukm::builders::Wallet_BoardingPassDetect(
      render_frame()->GetWebFrame()->GetDocument().GetUkmSourceId())
      .SetDetected(!boarding_passes.empty())
      .Record(ukm_recorder_.get());

  UMA_HISTOGRAM_BOOLEAN("Android.Wallet.BoardingPass.Detected",
                        !boarding_passes.empty());

  std::move(callback).Run(std::move(boarding_passes));
}

}  // namespace wallet