/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsBaseClipboard.h" #include "ContentAnalysis.h" #include "mozilla/Components.h" #include "mozilla/contentanalysis/ContentAnalysisIPCTypes.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/CanonicalBrowsingContext.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/PromiseNativeHandler.h" #include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/dom/WindowContext.h" #include "mozilla/ErrorResult.h" #include "mozilla/MoveOnlyFunction.h" #include "mozilla/RefPtr.h" #include "mozilla/Services.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_widget.h" #include "nsContentUtils.h" #include "nsFocusManager.h" #include "nsIClipboardOwner.h" #include "nsIPromptService.h" #include "nsISupportsPrimitives.h" #include "nsError.h" #include "nsXPCOM.h" using mozilla::GenericPromise; using mozilla::LogLevel; using mozilla::UniquePtr; using mozilla::dom::BrowsingContext; using mozilla::dom::CanonicalBrowsingContext; using mozilla::dom::ClipboardCapabilities; using mozilla::dom::Document; static const int32_t kGetAvailableFlavorsRetryCount = 5; namespace { struct ClipboardGetRequest { ClipboardGetRequest(const nsTArray& aFlavorList, nsIAsyncClipboardGetCallback* aCallback) : mFlavorList(aFlavorList.Clone()), mCallback(aCallback) {} const nsTArray mFlavorList; const nsCOMPtr mCallback; }; class UserConfirmationRequest final : public mozilla::dom::PromiseNativeHandler { public: NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(UserConfirmationRequest) UserConfirmationRequest(int32_t aClipboardType, Document* aRequestingChromeDocument, nsIPrincipal* aRequestingPrincipal, nsBaseClipboard* aClipboard, mozilla::dom::WindowContext* aRequestingWindowContext) : mClipboardType(aClipboardType), mRequestingChromeDocument(aRequestingChromeDocument), mRequestingPrincipal(aRequestingPrincipal), mClipboard(aClipboard), mRequestingWindowContext(aRequestingWindowContext) { MOZ_ASSERT( mClipboard->nsIClipboard::IsClipboardTypeSupported(aClipboardType)); } void ResolvedCallback(JSContext* aCx, JS::Handle aValue, mozilla::ErrorResult& aRv) override; void RejectedCallback(JSContext* aCx, JS::Handle aValue, mozilla::ErrorResult& aRv) override; bool IsEqual(int32_t aClipboardType, Document* aRequestingChromeDocument, nsIPrincipal* aRequestingPrincipal, mozilla::dom::WindowContext* aRequestingWindowContext) const { if (!(ClipboardType() == aClipboardType && RequestingChromeDocument() == aRequestingChromeDocument && RequestingPrincipal()->Equals(aRequestingPrincipal) && (mRequestingWindowContext && aRequestingWindowContext))) { return false; } // Only check requesting window contexts if content analysis is active nsCOMPtr contentAnalysis = mozilla::components::nsIContentAnalysis::Service(); if (!contentAnalysis) { return false; } bool contentAnalysisIsActive; nsresult rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive); if (MOZ_LIKELY(NS_FAILED(rv) || !contentAnalysisIsActive)) { return true; } return mRequestingWindowContext->Id() == aRequestingWindowContext->Id(); } int32_t ClipboardType() const { return mClipboardType; } Document* RequestingChromeDocument() const { return mRequestingChromeDocument; } nsIPrincipal* RequestingPrincipal() const { return mRequestingPrincipal; } void AddClipboardGetRequest(const nsTArray& aFlavorList, nsIAsyncClipboardGetCallback* aCallback) { MOZ_ASSERT(!aFlavorList.IsEmpty()); MOZ_ASSERT(aCallback); mPendingClipboardGetRequests.AppendElement( mozilla::MakeUnique(aFlavorList, aCallback)); } void RejectPendingClipboardGetRequests(nsresult aError) { MOZ_ASSERT(NS_FAILED(aError)); auto requests = std::move(mPendingClipboardGetRequests); for (const auto& request : requests) { MOZ_ASSERT(request); MOZ_ASSERT(request->mCallback); request->mCallback->OnError(aError); } } void ProcessPendingClipboardGetRequests() { auto requests = std::move(mPendingClipboardGetRequests); for (const auto& request : requests) { MOZ_ASSERT(request); MOZ_ASSERT(!request->mFlavorList.IsEmpty()); MOZ_ASSERT(request->mCallback); mClipboard->AsyncGetDataInternal(request->mFlavorList, mClipboardType, mRequestingWindowContext, request->mCallback); } } nsTArray>& GetPendingClipboardGetRequests() { return mPendingClipboardGetRequests; } private: ~UserConfirmationRequest() = default; const int32_t mClipboardType; RefPtr mRequestingChromeDocument; const nsCOMPtr mRequestingPrincipal; const RefPtr mClipboard; const RefPtr mRequestingWindowContext; // Track the pending read requests that wait for user confirmation. nsTArray> mPendingClipboardGetRequests; }; NS_IMPL_CYCLE_COLLECTION(UserConfirmationRequest, mRequestingChromeDocument) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UserConfirmationRequest) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(UserConfirmationRequest) NS_IMPL_CYCLE_COLLECTING_RELEASE(UserConfirmationRequest) static mozilla::StaticRefPtr sUserConfirmationRequest; void UserConfirmationRequest::ResolvedCallback(JSContext* aCx, JS::Handle aValue, mozilla::ErrorResult& aRv) { MOZ_DIAGNOSTIC_ASSERT(sUserConfirmationRequest == this); sUserConfirmationRequest = nullptr; JS::Rooted detailObj(aCx, &aValue.toObject()); nsCOMPtr propBag; nsresult rv = mozilla::dom::UnwrapArg( aCx, detailObj, getter_AddRefs(propBag)); if (NS_FAILED(rv)) { RejectPendingClipboardGetRequests(rv); return; } bool result = false; rv = propBag->GetPropertyAsBool(u"ok"_ns, &result); if (NS_FAILED(rv)) { RejectPendingClipboardGetRequests(rv); return; } if (!result) { RejectPendingClipboardGetRequests(NS_ERROR_DOM_NOT_ALLOWED_ERR); return; } ProcessPendingClipboardGetRequests(); } void UserConfirmationRequest::RejectedCallback(JSContext* aCx, JS::Handle aValue, mozilla::ErrorResult& aRv) { MOZ_DIAGNOSTIC_ASSERT(sUserConfirmationRequest == this); sUserConfirmationRequest = nullptr; RejectPendingClipboardGetRequests(NS_ERROR_FAILURE); } } // namespace NS_IMPL_ISUPPORTS(nsBaseClipboard::AsyncSetClipboardData, nsIAsyncSetClipboardData) nsBaseClipboard::AsyncSetClipboardData::AsyncSetClipboardData( int32_t aClipboardType, nsBaseClipboard* aClipboard, nsIAsyncClipboardRequestCallback* aCallback) : mClipboardType(aClipboardType), mClipboard(aClipboard), mCallback(aCallback) { MOZ_ASSERT(mClipboard); MOZ_ASSERT( mClipboard->nsIClipboard::IsClipboardTypeSupported(mClipboardType)); } NS_IMETHODIMP nsBaseClipboard::AsyncSetClipboardData::SetData(nsITransferable* aTransferable, nsIClipboardOwner* aOwner) { MOZ_CLIPBOARD_LOG("AsyncSetClipboardData::SetData (%p): clipboard=%d", this, mClipboardType); if (!IsValid()) { return NS_ERROR_FAILURE; } if (MOZ_CLIPBOARD_LOG_ENABLED()) { nsTArray flavors; if (NS_SUCCEEDED(aTransferable->FlavorsTransferableCanImport(flavors))) { for (const auto& flavor : flavors) { MOZ_CLIPBOARD_LOG(" MIME %s", flavor.get()); } } } MOZ_ASSERT(mClipboard); MOZ_ASSERT( mClipboard->nsIClipboard::IsClipboardTypeSupported(mClipboardType)); MOZ_DIAGNOSTIC_ASSERT(mClipboard->mPendingWriteRequests[mClipboardType] == this); RefPtr request = std::move(mClipboard->mPendingWriteRequests[mClipboardType]); nsresult rv = mClipboard->SetData(aTransferable, aOwner, mClipboardType); MaybeNotifyCallback(rv); return rv; } NS_IMETHODIMP nsBaseClipboard::AsyncSetClipboardData::Abort(nsresult aReason) { // Note: This may be called during destructor, so it should not attempt to // take a reference to mClipboard. if (!IsValid() || !NS_FAILED(aReason)) { return NS_ERROR_FAILURE; } MaybeNotifyCallback(aReason); return NS_OK; } void nsBaseClipboard::AsyncSetClipboardData::MaybeNotifyCallback( nsresult aResult) { // Note: This may be called during destructor, so it should not attempt to // take a reference to mClipboard. MOZ_ASSERT(IsValid()); if (nsCOMPtr callback = mCallback.forget()) { callback->OnComplete(aResult); } // Once the callback is notified, setData should not be allowed, so invalidate // this request. mClipboard = nullptr; } void nsBaseClipboard::RejectPendingAsyncSetDataRequestIfAny( int32_t aClipboardType) { MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType)); auto& request = mPendingWriteRequests[aClipboardType]; if (request) { request->Abort(NS_ERROR_ABORT); request = nullptr; } } NS_IMETHODIMP nsBaseClipboard::AsyncSetData( int32_t aWhichClipboard, nsIAsyncClipboardRequestCallback* aCallback, nsIAsyncSetClipboardData** _retval) { MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard); *_retval = nullptr; if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) { MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__, aWhichClipboard); return NS_ERROR_DOM_NOT_SUPPORTED_ERR; } // Reject existing pending AsyncSetData request if any. RejectPendingAsyncSetDataRequestIfAny(aWhichClipboard); // Create a new AsyncSetClipboardData. RefPtr request = mozilla::MakeRefPtr(aWhichClipboard, this, aCallback); mPendingWriteRequests[aWhichClipboard] = request; request.forget(_retval); return NS_OK; } namespace { class SafeContentAnalysisResultCallback final : public nsIContentAnalysisCallback { public: explicit SafeContentAnalysisResultCallback( std::function&&)> aResolver) : mResolver(std::move(aResolver)) {} void Callback(RefPtr&& aResult) { MOZ_ASSERT(mResolver, "Called SafeContentAnalysisResultCallback twice!"); if (auto resolver = std::move(mResolver)) { resolver(std::move(aResult)); } } NS_IMETHODIMP ContentResult(nsIContentAnalysisResponse* aResponse) override { using namespace mozilla::contentanalysis; RefPtr result = ContentAnalysisResult::FromContentAnalysisResponse(aResponse); Callback(result); return NS_OK; } NS_IMETHODIMP Error(nsresult aError) override { using namespace mozilla::contentanalysis; Callback(ContentAnalysisResult::FromNoResult( NoContentAnalysisResult::ERROR_OTHER)); return NS_OK; } NS_DECL_THREADSAFE_ISUPPORTS private: // Private destructor to force this to be allocated in a RefPtr, which is // necessary for safe usage. ~SafeContentAnalysisResultCallback() { MOZ_ASSERT(!mResolver, "SafeContentAnalysisResultCallback never called!"); } mozilla::MoveOnlyFunction&&)> mResolver; }; NS_IMPL_ISUPPORTS(SafeContentAnalysisResultCallback, nsIContentAnalysisCallback); } // namespace // Returning: // - true means a content analysis request was fired // - false means there is no text data in the transferable // - NoContentAnalysisResult means there was an error static mozilla::Result CheckClipboardContentAnalysisAsText( uint64_t aInnerWindowId, SafeContentAnalysisResultCallback* aResolver, nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis, nsITransferable* aTextTrans) { using namespace mozilla::contentanalysis; nsCOMPtr transferData; if (NS_FAILED(aTextTrans->GetTransferData(kTextMime, getter_AddRefs(transferData)))) { return false; } nsCOMPtr textData = do_QueryInterface(transferData); if (MOZ_UNLIKELY(!textData)) { return false; } nsString text; if (NS_FAILED(textData->GetData(text))) { return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER); } RefPtr window = mozilla::dom::WindowGlobalParent::GetByInnerWindowId(aInnerWindowId); if (!window) { // The window has gone away in the meantime return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER); } nsCOMPtr contentAnalysisRequest = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, std::move(text), false, EmptyCString(), aDocumentURI, nsIContentAnalysisRequest::OperationType::eClipboard, window); nsresult rv = aContentAnalysis->AnalyzeContentRequestCallback( contentAnalysisRequest, /* aAutoAcknowledge */ true, aResolver); if (NS_FAILED(rv)) { return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER); } return true; } // Returning: // - true means a content analysis request was fired // - false means there is no file data in the transferable // - NoContentAnalysisResult means there was an error static mozilla::Result CheckClipboardContentAnalysisAsFile( uint64_t aInnerWindowId, SafeContentAnalysisResultCallback* aResolver, nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis, nsITransferable* aFileTrans) { using namespace mozilla::contentanalysis; nsCOMPtr transferData; nsresult rv = aFileTrans->GetTransferData(kFileMime, getter_AddRefs(transferData)); nsString filePath; if (NS_SUCCEEDED(rv)) { if (nsCOMPtr file = do_QueryInterface(transferData)) { rv = file->GetPath(filePath); } else { MOZ_ASSERT_UNREACHABLE("clipboard data had kFileMime but no nsIFile!"); return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER); } } if (NS_FAILED(rv) || filePath.IsEmpty()) { return false; } RefPtr window = mozilla::dom::WindowGlobalParent::GetByInnerWindowId(aInnerWindowId); if (!window) { // The window has gone away in the meantime return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER); } // Let the content analysis code calculate the digest nsCOMPtr contentAnalysisRequest = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, std::move(filePath), true, EmptyCString(), aDocumentURI, nsIContentAnalysisRequest::OperationType::eCustomDisplayString, window); rv = aContentAnalysis->AnalyzeContentRequestCallback( contentAnalysisRequest, /* aAutoAcknowledge */ true, aResolver); if (NS_FAILED(rv)) { return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER); } return true; } static void CheckClipboardContentAnalysis( mozilla::dom::WindowGlobalParent* aWindow, nsITransferable* aTransferable, SafeContentAnalysisResultCallback* aResolver) { using namespace mozilla::contentanalysis; // Content analysis is only needed if an outside webpage has access to // the data. So, skip content analysis if there is: // - no associated window (for example, scripted clipboard read by system // code) // - the window is a chrome docshell // - the window is being rendered in the parent process (for example, // about:support and the like) if (!aWindow || aWindow->GetBrowsingContext()->IsChrome() || aWindow->IsInProcess()) { aResolver->Callback(ContentAnalysisResult::FromNoResult( NoContentAnalysisResult::CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS)); return; } nsCOMPtr contentAnalysis = mozilla::components::nsIContentAnalysis::Service(); if (!contentAnalysis) { aResolver->Callback(ContentAnalysisResult::FromNoResult( NoContentAnalysisResult::ERROR_OTHER)); return; } bool contentAnalysisIsActive; nsresult rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive); if (MOZ_LIKELY(NS_FAILED(rv) || !contentAnalysisIsActive)) { aResolver->Callback(ContentAnalysisResult::FromNoResult( NoContentAnalysisResult::CONTENT_ANALYSIS_NOT_ACTIVE)); return; } nsCOMPtr currentURI = aWindow->Canonical()->GetDocumentURI(); uint64_t innerWindowId = aWindow->InnerWindowId(); nsTArray flavors; rv = aTransferable->FlavorsTransferableCanExport(flavors); if (NS_WARN_IF(NS_FAILED(rv))) { aResolver->Callback(ContentAnalysisResult::FromNoResult( NoContentAnalysisResult::ERROR_OTHER)); return; } bool keepChecking = true; if (flavors.Contains(kFileMime)) { auto fileResult = CheckClipboardContentAnalysisAsFile( innerWindowId, aResolver, currentURI, contentAnalysis, aTransferable); if (fileResult.isErr()) { aResolver->Callback( ContentAnalysisResult::FromNoResult(fileResult.unwrapErr())); return; } keepChecking = !fileResult.unwrap(); } if (keepChecking) { // Failed to get the clipboard data as a file, so try as text auto textResult = CheckClipboardContentAnalysisAsText( innerWindowId, aResolver, currentURI, contentAnalysis, aTransferable); if (textResult.isErr()) { aResolver->Callback( ContentAnalysisResult::FromNoResult(textResult.unwrapErr())); return; } if (!textResult.unwrap()) { // Couldn't get file or text data from this aResolver->Callback(ContentAnalysisResult::FromNoResult( NoContentAnalysisResult::ERROR_COULD_NOT_GET_DATA)); return; } } } static bool CheckClipboardContentAnalysisSync( mozilla::dom::WindowGlobalParent* aWindow, const nsCOMPtr& trans) { bool requestDone = false; RefPtr result; auto callback = mozilla::MakeRefPtr( [&requestDone, &result](RefPtr&& aResult) { result = std::move(aResult); requestDone = true; }); CheckClipboardContentAnalysis(aWindow, trans, callback); mozilla::SpinEventLoopUntil("CheckClipboardContentAnalysisSync"_ns, [&requestDone]() -> bool { return requestDone; }); return result->GetShouldAllowContent(); } nsBaseClipboard::nsBaseClipboard(const ClipboardCapabilities& aClipboardCaps) : mClipboardCaps(aClipboardCaps) { using mozilla::MakeUnique; // Initialize clipboard cache. mCaches[kGlobalClipboard] = MakeUnique(); if (mClipboardCaps.supportsSelectionClipboard()) { mCaches[kSelectionClipboard] = MakeUnique(); } if (mClipboardCaps.supportsFindClipboard()) { mCaches[kFindClipboard] = MakeUnique(); } if (mClipboardCaps.supportsSelectionCache()) { mCaches[kSelectionCache] = MakeUnique(); } } nsBaseClipboard::~nsBaseClipboard() { for (auto& request : mPendingWriteRequests) { if (request) { request->Abort(NS_ERROR_ABORT); request = nullptr; } } } NS_IMPL_ISUPPORTS(nsBaseClipboard, nsIClipboard) /** * Sets the transferable object * */ NS_IMETHODIMP nsBaseClipboard::SetData(nsITransferable* aTransferable, nsIClipboardOwner* aOwner, int32_t aWhichClipboard) { NS_ASSERTION(aTransferable, "clipboard given a null transferable"); MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard); if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) { MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__, aWhichClipboard); return NS_ERROR_FAILURE; } if (MOZ_CLIPBOARD_LOG_ENABLED()) { nsTArray flavors; if (NS_SUCCEEDED(aTransferable->FlavorsTransferableCanImport(flavors))) { for (const auto& flavor : flavors) { MOZ_CLIPBOARD_LOG(" MIME %s", flavor.get()); } } } const auto& clipboardCache = mCaches[aWhichClipboard]; MOZ_ASSERT(clipboardCache); if (aTransferable == clipboardCache->GetTransferable() && aOwner == clipboardCache->GetClipboardOwner()) { MOZ_CLIPBOARD_LOG("%s: skipping update.", __FUNCTION__); return NS_OK; } clipboardCache->Clear(); nsresult rv = NS_ERROR_FAILURE; if (aTransferable) { mIgnoreEmptyNotification = true; // Reject existing pending asyncSetData request if any. RejectPendingAsyncSetDataRequestIfAny(aWhichClipboard); rv = SetNativeClipboardData(aTransferable, aWhichClipboard); mIgnoreEmptyNotification = false; } if (NS_FAILED(rv)) { MOZ_CLIPBOARD_LOG("%s: setting native clipboard data failed.", __FUNCTION__); return rv; } auto result = GetNativeClipboardSequenceNumber(aWhichClipboard); if (result.isErr()) { MOZ_CLIPBOARD_LOG("%s: getting native clipboard change count failed.", __FUNCTION__); return result.unwrapErr(); } clipboardCache->Update(aTransferable, aOwner, result.unwrap()); return NS_OK; } nsresult nsBaseClipboard::GetDataFromClipboardCache( nsITransferable* aTransferable, int32_t aClipboardType) { MOZ_ASSERT(aTransferable); MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType)); MOZ_ASSERT(mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()); const auto* clipboardCache = GetClipboardCacheIfValid(aClipboardType); if (!clipboardCache) { return NS_ERROR_FAILURE; } return clipboardCache->GetData(aTransferable); } /** * Gets the transferable object from system clipboard. */ NS_IMETHODIMP nsBaseClipboard::GetData( nsITransferable* aTransferable, int32_t aWhichClipboard, mozilla::dom::WindowContext* aWindowContext) { MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard); if (!aTransferable) { NS_ASSERTION(false, "clipboard given a null transferable"); return NS_ERROR_FAILURE; } if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) { MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__, aWhichClipboard); return NS_ERROR_FAILURE; } if (mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) { // If we were the last ones to put something on the native clipboard, then // just use the cached transferable. Otherwise clear it because it isn't // relevant any more. if (NS_SUCCEEDED( GetDataFromClipboardCache(aTransferable, aWhichClipboard))) { // maybe try to fill in more types? Is there a point? if (!CheckClipboardContentAnalysisSync(aWindowContext->Canonical(), aTransferable)) { aTransferable->ClearAllData(); return NS_ERROR_CONTENT_BLOCKED; } return NS_OK; } // at this point we can't satisfy the request from cache data so let's look // for things other people put on the system clipboard } nsresult rv = GetNativeClipboardData(aTransferable, aWhichClipboard); if (NS_FAILED(rv)) { return rv; } if (!CheckClipboardContentAnalysisSync(aWindowContext->Canonical(), aTransferable)) { aTransferable->ClearAllData(); return NS_ERROR_CONTENT_BLOCKED; } return NS_OK; } void nsBaseClipboard::MaybeRetryGetAvailableFlavors( const nsTArray& aFlavorList, int32_t aWhichClipboard, nsIAsyncClipboardGetCallback* aCallback, int32_t aRetryCount, mozilla::dom::WindowContext* aRequestingWindowContext) { // Note we have to get the clipboard sequence number first before the actual // read. This is to use it to verify the clipboard data is still the one we // try to read, instead of the later state. auto sequenceNumberOrError = GetNativeClipboardSequenceNumber(aWhichClipboard); if (sequenceNumberOrError.isErr()) { MOZ_CLIPBOARD_LOG("%s: unable to get sequence number for clipboard %d.", __FUNCTION__, aWhichClipboard); aCallback->OnError(sequenceNumberOrError.unwrapErr()); return; } int32_t sequenceNumber = sequenceNumberOrError.unwrap(); AsyncHasNativeClipboardDataMatchingFlavors( aFlavorList, aWhichClipboard, [self = RefPtr{this}, callback = nsCOMPtr{aCallback}, aWhichClipboard, aRetryCount, flavorList = aFlavorList.Clone(), sequenceNumber, requestingWindowContext = RefPtr{aRequestingWindowContext}](auto aFlavorsOrError) { if (aFlavorsOrError.isErr()) { MOZ_CLIPBOARD_LOG( "%s: unable to get available flavors for clipboard %d.", __FUNCTION__, aWhichClipboard); callback->OnError(aFlavorsOrError.unwrapErr()); return; } auto sequenceNumberOrError = self->GetNativeClipboardSequenceNumber(aWhichClipboard); if (sequenceNumberOrError.isErr()) { MOZ_CLIPBOARD_LOG( "%s: unable to get sequence number for clipboard %d.", __FUNCTION__, aWhichClipboard); callback->OnError(sequenceNumberOrError.unwrapErr()); return; } if (sequenceNumber == sequenceNumberOrError.unwrap()) { auto asyncGetClipboardData = mozilla::MakeRefPtr( aWhichClipboard, sequenceNumber, std::move(aFlavorsOrError.unwrap()), false, self, requestingWindowContext); callback->OnSuccess(asyncGetClipboardData); return; } if (aRetryCount > 0) { MOZ_CLIPBOARD_LOG( "%s: clipboard=%d, ignore the data due to the sequence number " "doesn't match, retry (%d) ..", __FUNCTION__, aWhichClipboard, aRetryCount); self->MaybeRetryGetAvailableFlavors(flavorList, aWhichClipboard, callback, aRetryCount - 1, requestingWindowContext); return; } MOZ_DIAGNOSTIC_ASSERT(false, "How can this happen?!?"); callback->OnError(NS_ERROR_FAILURE); }); } NS_IMETHODIMP nsBaseClipboard::AsyncGetData( const nsTArray& aFlavorList, int32_t aWhichClipboard, mozilla::dom::WindowContext* aRequestingWindowContext, nsIPrincipal* aRequestingPrincipal, nsIAsyncClipboardGetCallback* aCallback) { MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard); if (!aCallback || !aRequestingPrincipal || aFlavorList.IsEmpty()) { return NS_ERROR_INVALID_ARG; } if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) { MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__, aWhichClipboard); return NS_ERROR_FAILURE; } // We want to disable security check for automated tests that have the pref // set to true, or extension that have clipboard read permission. if (mozilla::StaticPrefs:: dom_events_testing_asyncClipboard_DoNotUseDirectly() || nsContentUtils::PrincipalHasPermission(*aRequestingPrincipal, nsGkAtoms::clipboardRead)) { AsyncGetDataInternal(aFlavorList, aWhichClipboard, aRequestingWindowContext, aCallback); return NS_OK; } // If cache data is valid, we are the last ones to put something on the native // clipboard, then check if the data is from the same-origin page, if (auto* clipboardCache = GetClipboardCacheIfValid(aWhichClipboard)) { nsCOMPtr trans = clipboardCache->GetTransferable(); MOZ_ASSERT(trans); if (nsCOMPtr principal = trans->GetRequestingPrincipal()) { if (aRequestingPrincipal->Subsumes(principal)) { MOZ_CLIPBOARD_LOG("%s: native clipboard data is from same-origin page.", __FUNCTION__); AsyncGetDataInternal(aFlavorList, aWhichClipboard, aRequestingWindowContext, aCallback); return NS_OK; } } } // TODO: enable showing the "Paste" button in this case; see bug 1773681. if (aRequestingPrincipal->GetIsAddonOrExpandedAddonPrincipal()) { MOZ_CLIPBOARD_LOG("%s: Addon without read permission.", __FUNCTION__); return aCallback->OnError(NS_ERROR_FAILURE); } RequestUserConfirmation(aWhichClipboard, aFlavorList, aRequestingWindowContext, aRequestingPrincipal, aCallback); return NS_OK; } already_AddRefed nsBaseClipboard::MaybeCreateGetRequestFromClipboardCache( const nsTArray& aFlavorList, int32_t aClipboardType, mozilla::dom::WindowContext* aRequestingWindowContext) { MOZ_DIAGNOSTIC_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType)); if (!mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) { return nullptr; } // If we were the last ones to put something on the native clipboard, then // just use the cached transferable. Otherwise clear it because it isn't // relevant any more. ClipboardCache* clipboardCache = GetClipboardCacheIfValid(aClipboardType); if (!clipboardCache) { return nullptr; } nsITransferable* cachedTransferable = clipboardCache->GetTransferable(); MOZ_ASSERT(cachedTransferable); nsTArray transferableFlavors; if (NS_FAILED(cachedTransferable->FlavorsTransferableCanExport( transferableFlavors))) { return nullptr; } nsTArray results; for (const auto& flavor : aFlavorList) { for (const auto& transferableFlavor : transferableFlavors) { // XXX We need special check for image as we always put the // image as "native" on the clipboard. if (transferableFlavor.Equals(flavor) || (transferableFlavor.Equals(kNativeImageMime) && nsContentUtils::IsFlavorImage(flavor))) { MOZ_CLIPBOARD_LOG(" has %s", flavor.get()); results.AppendElement(flavor); } } } // XXX Do we need to check system clipboard for the flavors that cannot // be found in cache? return mozilla::MakeAndAddRef( aClipboardType, clipboardCache->GetSequenceNumber(), std::move(results), true /* aFromCache */, this, aRequestingWindowContext); } void nsBaseClipboard::AsyncGetDataInternal( const nsTArray& aFlavorList, int32_t aClipboardType, mozilla::dom::WindowContext* aRequestingWindowContext, nsIAsyncClipboardGetCallback* aCallback) { MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType)); if (nsCOMPtr asyncGetClipboardData = MaybeCreateGetRequestFromClipboardCache(aFlavorList, aClipboardType, aRequestingWindowContext)) { aCallback->OnSuccess(asyncGetClipboardData); return; } // At this point we can't satisfy the request from cache data so let's // look for things other people put on the system clipboard. MaybeRetryGetAvailableFlavors(aFlavorList, aClipboardType, aCallback, kGetAvailableFlavorsRetryCount, aRequestingWindowContext); } NS_IMETHODIMP nsBaseClipboard::GetDataSnapshotSync( const nsTArray& aFlavorList, int32_t aWhichClipboard, mozilla::dom::WindowContext* aRequestingWindowContext, nsIAsyncGetClipboardData** _retval) { MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard); *_retval = nullptr; if (aFlavorList.IsEmpty()) { return NS_ERROR_INVALID_ARG; } if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) { MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__, aWhichClipboard); return NS_ERROR_FAILURE; } if (nsCOMPtr asyncGetClipboardData = MaybeCreateGetRequestFromClipboardCache(aFlavorList, aWhichClipboard, aRequestingWindowContext)) { asyncGetClipboardData.forget(_retval); return NS_OK; } auto sequenceNumberOrError = GetNativeClipboardSequenceNumber(aWhichClipboard); if (sequenceNumberOrError.isErr()) { MOZ_CLIPBOARD_LOG("%s: unable to get sequence number for clipboard %d.", __FUNCTION__, aWhichClipboard); return sequenceNumberOrError.unwrapErr(); } nsTArray results; for (const auto& flavor : aFlavorList) { auto resultOrError = HasNativeClipboardDataMatchingFlavors( AutoTArray{flavor}, aWhichClipboard); if (resultOrError.isOk() && resultOrError.unwrap()) { results.AppendElement(flavor); } } *_retval = mozilla::MakeAndAddRef( aWhichClipboard, sequenceNumberOrError.unwrap(), std::move(results), false /* aFromCache */, this, aRequestingWindowContext) .take(); return NS_OK; } NS_IMETHODIMP nsBaseClipboard::EmptyClipboard(int32_t aWhichClipboard) { MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard); if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) { MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__, aWhichClipboard); return NS_ERROR_FAILURE; } EmptyNativeClipboardData(aWhichClipboard); const auto& clipboardCache = mCaches[aWhichClipboard]; MOZ_ASSERT(clipboardCache); if (mIgnoreEmptyNotification) { MOZ_DIAGNOSTIC_ASSERT(!clipboardCache->GetTransferable() && !clipboardCache->GetClipboardOwner() && clipboardCache->GetSequenceNumber() == -1, "How did we have data in clipboard cache here?"); return NS_OK; } clipboardCache->Clear(); return NS_OK; } mozilla::Result, nsresult> nsBaseClipboard::GetFlavorsFromClipboardCache(int32_t aClipboardType) { MOZ_ASSERT(mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()); MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType)); const auto* clipboardCache = GetClipboardCacheIfValid(aClipboardType); if (!clipboardCache) { return mozilla::Err(NS_ERROR_FAILURE); } nsITransferable* cachedTransferable = clipboardCache->GetTransferable(); MOZ_ASSERT(cachedTransferable); nsTArray flavors; nsresult rv = cachedTransferable->FlavorsTransferableCanExport(flavors); if (NS_FAILED(rv)) { return mozilla::Err(rv); } if (MOZ_CLIPBOARD_LOG_ENABLED()) { MOZ_CLIPBOARD_LOG(" Cached transferable types (nums %zu)\n", flavors.Length()); for (const auto& flavor : flavors) { MOZ_CLIPBOARD_LOG(" MIME %s", flavor.get()); } } return std::move(flavors); } NS_IMETHODIMP nsBaseClipboard::HasDataMatchingFlavors(const nsTArray& aFlavorList, int32_t aWhichClipboard, bool* aOutResult) { MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard); if (MOZ_CLIPBOARD_LOG_ENABLED()) { MOZ_CLIPBOARD_LOG(" Asking for content clipboard=%i:\n", aWhichClipboard); for (const auto& flavor : aFlavorList) { MOZ_CLIPBOARD_LOG(" MIME %s", flavor.get()); } } *aOutResult = false; if (mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) { // First, check if we have valid data in our cached transferable. auto flavorsOrError = GetFlavorsFromClipboardCache(aWhichClipboard); if (flavorsOrError.isOk()) { for (const auto& transferableFlavor : flavorsOrError.unwrap()) { for (const auto& flavor : aFlavorList) { if (transferableFlavor.Equals(flavor)) { MOZ_CLIPBOARD_LOG(" has %s", flavor.get()); *aOutResult = true; return NS_OK; } } } } } auto resultOrError = HasNativeClipboardDataMatchingFlavors(aFlavorList, aWhichClipboard); if (resultOrError.isErr()) { MOZ_CLIPBOARD_LOG( "%s: checking native clipboard data matching flavors falied.", __FUNCTION__); return resultOrError.unwrapErr(); } *aOutResult = resultOrError.unwrap(); return NS_OK; } NS_IMETHODIMP nsBaseClipboard::IsClipboardTypeSupported(int32_t aWhichClipboard, bool* aRetval) { NS_ENSURE_ARG_POINTER(aRetval); switch (aWhichClipboard) { case kGlobalClipboard: // We always support the global clipboard. *aRetval = true; return NS_OK; case kSelectionClipboard: *aRetval = mClipboardCaps.supportsSelectionClipboard(); return NS_OK; case kFindClipboard: *aRetval = mClipboardCaps.supportsFindClipboard(); return NS_OK; case kSelectionCache: *aRetval = mClipboardCaps.supportsSelectionCache(); return NS_OK; default: *aRetval = false; return NS_OK; } } void nsBaseClipboard::AsyncHasNativeClipboardDataMatchingFlavors( const nsTArray& aFlavorList, int32_t aWhichClipboard, HasMatchingFlavorsCallback&& aCallback) { MOZ_DIAGNOSTIC_ASSERT( nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); MOZ_CLIPBOARD_LOG( "nsBaseClipboard::AsyncHasNativeClipboardDataMatchingFlavors: " "clipboard=%d", aWhichClipboard); nsTArray results; for (const auto& flavor : aFlavorList) { auto resultOrError = HasNativeClipboardDataMatchingFlavors( AutoTArray{flavor}, aWhichClipboard); if (resultOrError.isOk() && resultOrError.unwrap()) { results.AppendElement(flavor); } } aCallback(std::move(results)); } void nsBaseClipboard::AsyncGetNativeClipboardData( nsITransferable* aTransferable, int32_t aWhichClipboard, GetDataCallback&& aCallback) { aCallback(GetNativeClipboardData(aTransferable, aWhichClipboard)); } void nsBaseClipboard::ClearClipboardCache(int32_t aClipboardType) { MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType)); const mozilla::UniquePtr& cache = mCaches[aClipboardType]; MOZ_ASSERT(cache); cache->Clear(); } void nsBaseClipboard::RequestUserConfirmation( int32_t aClipboardType, const nsTArray& aFlavorList, mozilla::dom::WindowContext* aWindowContext, nsIPrincipal* aRequestingPrincipal, nsIAsyncClipboardGetCallback* aCallback) { MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType)); MOZ_ASSERT(aCallback); if (!aWindowContext) { aCallback->OnError(NS_ERROR_FAILURE); return; } CanonicalBrowsingContext* cbc = CanonicalBrowsingContext::Cast(aWindowContext->GetBrowsingContext()); MOZ_ASSERT( cbc->IsContent(), "Should not require user confirmation when access from chrome window"); RefPtr chromeTop = cbc->TopCrossChromeBoundary(); Document* chromeDoc = chromeTop ? chromeTop->GetDocument() : nullptr; if (!chromeDoc || !chromeDoc->HasFocus(mozilla::IgnoreErrors())) { MOZ_CLIPBOARD_LOG("%s: reject due to not in the focused window", __FUNCTION__); aCallback->OnError(NS_ERROR_FAILURE); return; } mozilla::dom::Element* activeElementInChromeDoc = chromeDoc->GetActiveElement(); if (activeElementInChromeDoc != cbc->Top()->GetEmbedderElement()) { // Reject if the request is not from web content that is in the focused tab. MOZ_CLIPBOARD_LOG("%s: reject due to not in the focused tab", __FUNCTION__); aCallback->OnError(NS_ERROR_FAILURE); return; } // If there is a pending user confirmation request, check if we could reuse // it. If not, reject the request. if (sUserConfirmationRequest) { if (sUserConfirmationRequest->IsEqual( aClipboardType, chromeDoc, aRequestingPrincipal, aWindowContext)) { sUserConfirmationRequest->AddClipboardGetRequest(aFlavorList, aCallback); return; } aCallback->OnError(NS_ERROR_DOM_NOT_ALLOWED_ERR); return; } nsresult rv = NS_ERROR_FAILURE; nsCOMPtr promptService = do_GetService("@mozilla.org/prompter;1", &rv); if (NS_FAILED(rv)) { aCallback->OnError(NS_ERROR_DOM_NOT_ALLOWED_ERR); return; } RefPtr promise; if (NS_FAILED(promptService->ConfirmUserPaste(aWindowContext->Canonical(), getter_AddRefs(promise)))) { aCallback->OnError(NS_ERROR_DOM_NOT_ALLOWED_ERR); return; } sUserConfirmationRequest = new UserConfirmationRequest( aClipboardType, chromeDoc, aRequestingPrincipal, this, aWindowContext); sUserConfirmationRequest->AddClipboardGetRequest(aFlavorList, aCallback); promise->AppendNativeHandler(sUserConfirmationRequest); } NS_IMPL_ISUPPORTS(nsBaseClipboard::AsyncGetClipboardData, nsIAsyncGetClipboardData) nsBaseClipboard::AsyncGetClipboardData::AsyncGetClipboardData( int32_t aClipboardType, int32_t aSequenceNumber, nsTArray&& aFlavors, bool aFromCache, nsBaseClipboard* aClipboard, mozilla::dom::WindowContext* aRequestingWindowContext) : mClipboardType(aClipboardType), mSequenceNumber(aSequenceNumber), mFlavors(std::move(aFlavors)), mFromCache(aFromCache), mClipboard(aClipboard), mRequestingWindowContext(aRequestingWindowContext) { MOZ_ASSERT(mClipboard); MOZ_ASSERT( mClipboard->nsIClipboard::IsClipboardTypeSupported(mClipboardType)); } NS_IMETHODIMP nsBaseClipboard::AsyncGetClipboardData::GetValid( bool* aOutResult) { *aOutResult = IsValid(); return NS_OK; } NS_IMETHODIMP nsBaseClipboard::AsyncGetClipboardData::GetFlavorList( nsTArray& aFlavors) { aFlavors.AppendElements(mFlavors); return NS_OK; } NS_IMETHODIMP nsBaseClipboard::AsyncGetClipboardData::GetData( nsITransferable* aTransferable, nsIAsyncClipboardRequestCallback* aCallback) { MOZ_CLIPBOARD_LOG("AsyncGetClipboardData::GetData: %p", this); if (!aTransferable || !aCallback) { return NS_ERROR_INVALID_ARG; } nsTArray flavors; nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors); if (NS_FAILED(rv)) { return rv; } // If the requested flavor is not in the list, throw an error. for (const auto& flavor : flavors) { if (!mFlavors.Contains(flavor)) { return NS_ERROR_FAILURE; } } if (!IsValid()) { aCallback->OnComplete(NS_ERROR_FAILURE); return NS_OK; } MOZ_ASSERT(mClipboard); auto contentAnalysisCallback = mozilla::MakeRefPtr( [transferable = nsCOMPtr{aTransferable}, callback = nsCOMPtr{aCallback}]( RefPtr&& aResult) { if (aResult->GetShouldAllowContent()) { callback->OnComplete(NS_OK); } else { transferable->ClearAllData(); callback->OnComplete(NS_ERROR_CONTENT_BLOCKED); } }); if (mFromCache) { const auto* clipboardCache = mClipboard->GetClipboardCacheIfValid(mClipboardType); // `IsValid()` above ensures we should get a valid cache and matched // sequence number here. MOZ_DIAGNOSTIC_ASSERT(clipboardCache); MOZ_DIAGNOSTIC_ASSERT(clipboardCache->GetSequenceNumber() == mSequenceNumber); if (NS_SUCCEEDED(clipboardCache->GetData(aTransferable))) { CheckClipboardContentAnalysis(mRequestingWindowContext ? mRequestingWindowContext->Canonical() : nullptr, aTransferable, contentAnalysisCallback); return NS_OK; } // At this point we can't satisfy the request from cache data so let's look // for things other people put on the system clipboard. } // Since this is an async operation, we need to check if the data is still // valid after we get the result. mClipboard->AsyncGetNativeClipboardData( aTransferable, mClipboardType, [callback = nsCOMPtr{aCallback}, self = RefPtr{this}, transferable = nsCOMPtr{aTransferable}, contentAnalysisCallback = std::move(contentAnalysisCallback)](nsresult aResult) mutable { if (NS_FAILED(aResult)) { callback->OnComplete(aResult); return; } // `IsValid()` checks the clipboard sequence number to ensure the data // we are requesting is still valid. if (!self->IsValid()) { callback->OnComplete(NS_ERROR_FAILURE); return; } CheckClipboardContentAnalysis( self->mRequestingWindowContext ? self->mRequestingWindowContext->Canonical() : nullptr, transferable, contentAnalysisCallback); }); return NS_OK; } bool nsBaseClipboard::AsyncGetClipboardData::IsValid() { if (!mClipboard) { return false; } // If the data should from cache, check if cache is still valid or the // sequence numbers are matched. if (mFromCache) { const auto* clipboardCache = mClipboard->GetClipboardCacheIfValid(mClipboardType); if (!clipboardCache) { mClipboard = nullptr; return false; } return mSequenceNumber == clipboardCache->GetSequenceNumber(); } auto resultOrError = mClipboard->GetNativeClipboardSequenceNumber(mClipboardType); if (resultOrError.isErr()) { mClipboard = nullptr; return false; } if (mSequenceNumber != resultOrError.unwrap()) { mClipboard = nullptr; return false; } return true; } nsBaseClipboard::ClipboardCache* nsBaseClipboard::GetClipboardCacheIfValid( int32_t aClipboardType) { MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType)); const mozilla::UniquePtr& cache = mCaches[aClipboardType]; MOZ_ASSERT(cache); if (!cache->GetTransferable()) { MOZ_ASSERT(cache->GetSequenceNumber() == -1); return nullptr; } auto changeCountOrError = GetNativeClipboardSequenceNumber(aClipboardType); if (changeCountOrError.isErr()) { return nullptr; } if (changeCountOrError.unwrap() != cache->GetSequenceNumber()) { // Clipboard cache is invalid, clear it. cache->Clear(); return nullptr; } return cache.get(); } void nsBaseClipboard::ClipboardCache::Clear() { if (mClipboardOwner) { mClipboardOwner->LosingOwnership(mTransferable); mClipboardOwner = nullptr; } mTransferable = nullptr; mSequenceNumber = -1; } nsresult nsBaseClipboard::ClipboardCache::GetData( nsITransferable* aTransferable) const { MOZ_ASSERT(aTransferable); MOZ_ASSERT(mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()); // get flavor list that includes all acceptable flavors (including ones // obtained through conversion) nsTArray flavors; if (NS_FAILED(aTransferable->FlavorsTransferableCanImport(flavors))) { return NS_ERROR_FAILURE; } MOZ_ASSERT(mTransferable); for (const auto& flavor : flavors) { nsCOMPtr dataSupports; // XXX Maybe we need special check for image as we always put the image as // "native" on the clipboard. if (NS_SUCCEEDED(mTransferable->GetTransferData( flavor.get(), getter_AddRefs(dataSupports)))) { MOZ_CLIPBOARD_LOG("%s: getting %s from cache.", __FUNCTION__, flavor.get()); aTransferable->SetTransferData(flavor.get(), dataSupports); // XXX we only read the first available type from native clipboard, so // make cache behave the same. return NS_OK; } } return NS_ERROR_FAILURE; }