chromium/chrome/browser/media/android/cdm/per_device_provisioning_permission.cc

// Copyright 2019 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/media/android/cdm/per_device_provisioning_permission.h"

#include <utility>

#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/time/time.h"
#include "chrome/browser/android/android_theme_resources.h"
#include "components/permissions/permission_request.h"
#include "components/permissions/permission_request_manager.h"
#include "components/permissions/request_type.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "media/base/android/media_drm_bridge.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/origin.h"

namespace {

// Only keep track of the last response for a short period of time.
constexpr base::TimeDelta kLastRequestDelta = base::Minutes(15);

// Keep track of the last response. This is only kept in memory, so once Chrome
// quits it is forgotten.
class LastResponse {
 public:
  // If |origin| matches the previously saved |origin_| and this request is
  // before |expiry_time|, return true indicating that the previous value should
  // be used, and update |allowed| with the previous response. If the origin
  // doesn't match or the previous response was too long ago, return false.
  bool Matches(const url::Origin& origin, bool* allowed) {
    if (!origin_.IsSameOriginWith(origin) || base::Time::Now() > expiry_time_)
      return false;

    *allowed = allowed_;
    return true;
  }

  // Updates this object with the latest |origin| and |response|.
  void Update(const url::Origin& origin, bool response) {
    origin_ = origin;
    expiry_time_ = base::Time::Now() + kLastRequestDelta;
    allowed_ = response;
  }

 private:
  url::Origin origin_;
  base::Time expiry_time_;
  bool allowed_ = false;
};

// Returns an object containing the last response. We only keep track of one
// response (the latest). This is done for simplicity, as it is unlikely that
// there will different origins getting to this path at the same time. Requests
// could be from different |render_frame_host| objects, but this matches what
// normal permission requests do when the decision is persisted in user's
// profile.
LastResponse& GetLastResponse() {
  static base::NoDestructor<LastResponse> s_last_response;
  return *s_last_response;
}

// A permissions::PermissionRequest to allow MediaDrmBridge to use per-device
// provisioning.
class PerDeviceProvisioningPermissionRequest final
    : public permissions::PermissionRequest {
 public:
  PerDeviceProvisioningPermissionRequest(
      const url::Origin& origin,
      base::OnceCallback<void(bool)> callback)
      : PermissionRequest(
            origin.GetURL(),
            permissions::RequestType::kProtectedMediaIdentifier,
            /*has_gesture=*/false,
            base::BindRepeating(
                &PerDeviceProvisioningPermissionRequest::PermissionDecided,
                base::Unretained(this)),
            base::BindOnce(
                &PerDeviceProvisioningPermissionRequest::DeleteRequest,
                base::Unretained(this))),
        origin_(origin),
        callback_(std::move(callback)) {}

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

  void PermissionDecided(ContentSetting result,
                         bool is_one_time,
                         bool is_final_decision) {
    DCHECK(!is_one_time);
    DCHECK(!is_final_decision);
    const bool granted = result == ContentSetting::CONTENT_SETTING_ALLOW;
    UpdateLastResponse(granted);
    std::move(callback_).Run(granted);
  }

  void DeleteRequest() {
    // The |callback_| may not have run if the prompt was ignored, e.g. the tab
    // was closed while the prompt was displayed. Don't save this result as the
    // last response since it wasn't really a user action.
    if (callback_)
      std::move(callback_).Run(false);

    delete this;
  }

 private:
  // Can only be self-destructed. See DeleteRequest().
  ~PerDeviceProvisioningPermissionRequest() override = default;

  void UpdateLastResponse(bool allowed) {
    GetLastResponse().Update(origin_, allowed);
  }

  const url::Origin origin_;
  base::OnceCallback<void(bool)> callback_;
};

}  // namespace

void RequestPerDeviceProvisioningPermission(
    content::RenderFrameHost* render_frame_host,
    base::OnceCallback<void(bool)> callback) {
  DVLOG(1) << __func__;
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(render_frame_host);
  DCHECK(callback);

  // Return the previous response if it was for the same origin.
  bool last_response = false;
  if (GetLastResponse().Matches(render_frame_host->GetLastCommittedOrigin(),
                                &last_response)) {
    DVLOG(1) << "Using previous response: " << last_response;
    std::move(callback).Run(last_response);
    return;
  }

  auto* web_contents =
      content::WebContents::FromRenderFrameHost(render_frame_host);
  DCHECK(web_contents) << "WebContents not available.";

  auto* permission_request_manager =
      permissions::PermissionRequestManager::FromWebContents(web_contents);
  if (!permission_request_manager) {
    std::move(callback).Run(false);
    return;
  }

  // The created PerDeviceProvisioningPermissionRequest deletes itself once
  // complete. See PerDeviceProvisioningPermissionRequest::DeleteRequest().
  permission_request_manager->AddRequest(
      render_frame_host,
      new PerDeviceProvisioningPermissionRequest(
          render_frame_host->GetLastCommittedOrigin(), std::move(callback)));
}