| // 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. |
| |
| #include "content/browser/media/media_license_manager.h" |
| |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/flat_map.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/sequence_checker.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "components/services/storage/public/cpp/buckets/bucket_locator.h" |
| #include "components/services/storage/public/cpp/buckets/constants.h" |
| #include "components/services/storage/public/cpp/constants.h" |
| #include "content/browser/media/media_license_database.h" |
| #include "content/browser/media/media_license_storage_host.h" |
| #include "media/cdm/cdm_type.h" |
| #include "sql/database.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "third_party/blink/public/mojom/quota/quota_types.mojom-shared.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Creates a task runner suitable for running SQLite database operations. |
| scoped_refptr<base::SequencedTaskRunner> CreateDatabaseTaskRunner() { |
| // We use a SequencedTaskRunner so that there is a global ordering to a |
| // storage key's directory operations. |
| return base::ThreadPool::CreateSequencedTaskRunner({ |
| // Needed for file I/O. |
| base::MayBlock(), |
| |
| // Reasonable compromise, given that a few database operations are |
| // blocking, while most operations are not. We should be able to do better |
| // when we get scheduling APIs on the Web Platform. |
| base::TaskPriority::USER_VISIBLE, |
| |
| // Needed to allow for clearing site data on shutdown. |
| base::TaskShutdownBehavior::BLOCK_SHUTDOWN, |
| }); |
| } |
| |
| } // namespace |
| |
| MediaLicenseManager::CdmFileId::CdmFileId(const std::string& name, |
| const media::CdmType& cdm_type) |
| : name(name), cdm_type(cdm_type) {} |
| MediaLicenseManager::CdmFileId::CdmFileId(const CdmFileId&) = default; |
| MediaLicenseManager::CdmFileId::~CdmFileId() = default; |
| |
| MediaLicenseManager::CdmFileIdAndContents::CdmFileIdAndContents( |
| const CdmFileId& file, |
| std::vector<uint8_t> data) |
| : file(file), data(std::move(data)) {} |
| MediaLicenseManager::CdmFileIdAndContents::CdmFileIdAndContents( |
| const CdmFileIdAndContents&) = default; |
| MediaLicenseManager::CdmFileIdAndContents::~CdmFileIdAndContents() = default; |
| |
| MediaLicenseManager::MediaLicenseManager( |
| bool in_memory, |
| scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy, |
| scoped_refptr<storage::QuotaManagerProxy> quota_manager_proxy) |
| : db_runner_(CreateDatabaseTaskRunner()), |
| in_memory_(in_memory), |
| special_storage_policy_(std::move(special_storage_policy)), |
| quota_manager_proxy_(std::move(quota_manager_proxy)), |
| // Using a raw pointer is safe since `quota_client_` is owned by |
| // this instance. |
| quota_client_(this), |
| quota_client_receiver_("a_client_) { |
| if (quota_manager_proxy_) { |
| // Quota client assumes all backends have registered. |
| quota_manager_proxy_->RegisterClient( |
| quota_client_receiver_.BindNewPipeAndPassRemote(), |
| storage::QuotaClientType::kMediaLicense, |
| {blink::mojom::StorageType::kTemporary}); |
| } |
| } |
| |
| MediaLicenseManager::~MediaLicenseManager() = default; |
| |
| void MediaLicenseManager::OpenCdmStorage( |
| const BindingContext& binding_context, |
| mojo::PendingReceiver<media::mojom::CdmStorage> receiver) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| const auto& storage_key = binding_context.storage_key; |
| auto it_hosts = hosts_.find(storage_key); |
| if (it_hosts != hosts_.end()) { |
| // A storage host for this storage key already exists. |
| it_hosts->second->BindReceiver(binding_context, std::move(receiver)); |
| return; |
| } |
| |
| auto& receiver_list = pending_receivers_[storage_key]; |
| receiver_list.emplace_back(binding_context, std::move(receiver)); |
| if (receiver_list.size() > 1) { |
| // If a pending receiver for this storage key already existed, there is |
| // an in-flight `UpdateOrCreateBucket()` call for this storage key. |
| return; |
| } |
| |
| // Get the default bucket for `storage_key`. |
| quota_manager_proxy()->UpdateOrCreateBucket( |
| storage::BucketInitParams::ForDefaultBucket(storage_key), |
| base::SequencedTaskRunner::GetCurrentDefault(), |
| base::BindOnce(&MediaLicenseManager::DidGetBucket, |
| weak_factory_.GetWeakPtr(), storage_key)); |
| } |
| |
| void MediaLicenseManager::DidGetBucket( |
| const blink::StorageKey& storage_key, |
| storage::QuotaErrorOr<storage::BucketInfo> result) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| auto it = pending_receivers_.find(storage_key); |
| DCHECK(it != pending_receivers_.end()); |
| |
| auto receivers_list = std::move(it->second); |
| pending_receivers_.erase(it); |
| DCHECK_GT(receivers_list.size(), 0u); |
| |
| storage::BucketLocator bucket_locator; |
| if (result.ok()) { |
| bucket_locator = result->ToBucketLocator(); |
| } else { |
| // Use the null locator, but update the `storage_key` field so |
| // `storage_host` can be identified when it is to be removed from `hosts_`. |
| // We could consider falling back to using an in-memory database in this |
| // case, but failing here seems easier to reason about from a website |
| // author's point of view. |
| MediaLicenseStorageHost::ReportDatabaseOpenError( |
| MediaLicenseStorageHost::MediaLicenseStorageHostOpenError:: |
| kBucketLocatorError); |
| DCHECK(bucket_locator.id.is_null()); |
| bucket_locator.storage_key = storage_key; |
| } |
| |
| // All receivers associated with `storage_key` will be bound to the same host. |
| auto storage_host = |
| std::make_unique<MediaLicenseStorageHost>(this, bucket_locator); |
| |
| for (auto& context_and_receiver : receivers_list) { |
| storage_host->BindReceiver(context_and_receiver.first, |
| std::move(context_and_receiver.second)); |
| } |
| |
| hosts_.emplace(storage_key, std::move(storage_host)); |
| } |
| |
| void MediaLicenseManager::DeleteBucketData( |
| const storage::BucketLocator& bucket, |
| storage::mojom::QuotaClient::DeleteBucketDataCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| auto it_hosts = hosts_.find(bucket.storage_key); |
| if (it_hosts != hosts_.end()) { |
| // Let the host gracefully handle data deletion. |
| it_hosts->second->DeleteBucketData( |
| base::BindOnce(&MediaLicenseManager::DidDeleteBucketData, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| return; |
| } |
| |
| // If we have an in-memory profile, any data for the storage key would have |
| // lived in the associated MediaLicenseStorageHost. |
| if (in_memory()) { |
| std::move(callback).Run(blink::mojom::QuotaStatusCode::kOk); |
| return; |
| } |
| |
| // Otherwise delete database file. |
| auto path = GetDatabasePath(bucket); |
| db_runner()->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&sql::Database::Delete, path), |
| base::BindOnce(&MediaLicenseManager::DidDeleteBucketData, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void MediaLicenseManager::DidDeleteBucketData( |
| storage::mojom::QuotaClient::DeleteBucketDataCallback callback, |
| bool success) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| std::move(callback).Run(success ? blink::mojom::QuotaStatusCode::kOk |
| : blink::mojom::QuotaStatusCode::kUnknown); |
| } |
| |
| base::FilePath MediaLicenseManager::GetDatabasePath( |
| const storage::BucketLocator& bucket_locator) { |
| if (in_memory()) |
| return base::FilePath(); |
| |
| auto media_license_dir = quota_manager_proxy()->GetClientBucketPath( |
| bucket_locator, storage::QuotaClientType::kMediaLicense); |
| return media_license_dir.Append(storage::kMediaLicenseDatabaseFileName); |
| } |
| |
| void MediaLicenseManager::OnHostReceiverDisconnect( |
| MediaLicenseStorageHost* host, |
| base::PassKey<MediaLicenseStorageHost> pass_key) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(host); |
| |
| if (in_memory()) { |
| // Don't delete `host` for an in-memory profile, since the data is not safe |
| // to delete yet. For example, a site may be re-visited within the same |
| // incognito session. `host` will be destroyed when `this` is destroyed. |
| return; |
| } |
| |
| DCHECK_GT(hosts_.count(host->storage_key()), 0ul); |
| DCHECK_EQ(hosts_[host->storage_key()].get(), host); |
| |
| if (!host->has_empty_receiver_set()) |
| return; |
| |
| size_t count_removed = hosts_.erase(host->storage_key()); |
| DCHECK_EQ(count_removed, 1u); |
| } |
| |
| } // namespace content |