From 3ab1acfd47b8af1d595b8c36a269282ca815d9f4 Mon Sep 17 00:00:00 2001 From: Greg Stoll Date: Fri, 8 Dec 2023 17:48:31 +0000 Subject: [PATCH] Bug 1811076: Part 6 - Show a cancelable "slow agent" dialog on slow content analysis r=nika,Gijs,rkraesig,fluent-reviewers,bolsson This adds a modal dialog to some content analysis use cases that blocks input while CA is running. The dialog can be canceled, which cancels the CA (if possible) and the operation that required it. Differential Revision: https://phabricator.services.mozilla.com/D189578 --- .../content/ContentAnalysis.sys.mjs | 16 +- netwerk/base/nsIPrompt.idl | 2 + .../contentanalysis/ContentAnalysis.cpp | 396 +++++++++++++----- .../contentanalysis/ContentAnalysis.h | 46 +- .../contentanalysis/nsIContentAnalysis.idl | 11 +- .../prompts/content/commonDialog.css | 9 + .../prompts/content/commonDialog.js | 1 + .../prompts/content/commonDialog.xhtml | 10 + .../prompts/src/CommonDialog.sys.mjs | 4 + .../components/prompts/src/Prompter.sys.mjs | 4 + .../windowwatcher/nsIPromptService.idl | 5 + .../en-US/toolkit/global/commonDialog.ftl | 4 + 12 files changed, 397 insertions(+), 111 deletions(-) diff --git a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs index 85cfb7b861561..b56766ac77fa0 100644 --- a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs +++ b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs @@ -16,6 +16,13 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gContentAnalysis", + "@mozilla.org/contentanalysis;1", + Ci.nsIContentAnalysis +); + ChromeUtils.defineESModuleGetters(lazy, { clearTimeout: "resource://gre/modules/Timer.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", @@ -387,7 +394,10 @@ export const ContentAnalysis = { }, _shouldShowBlockingNotification(aOperation) { - return false; + return !( + aOperation == Ci.nsIContentAnalysisRequest.FILE_DOWNLOADED || + aOperation == Ci.nsIContentAnalysisRequest.PRINT + ); }, // This function also transforms the nameOrL10NId so we won't have to @@ -486,7 +496,8 @@ export const ContentAnalysis = { content: this._getResourceNameFromNameOrL10NId(aResourceNameOrL10NId), }), Ci.nsIPromptService.BUTTON_POS_0 * - Ci.nsIPromptService.BUTTON_TITLE_CANCEL, + Ci.nsIPromptService.BUTTON_TITLE_CANCEL + + Ci.nsIPromptService.SHOW_SPINNER, null, null, null, @@ -504,6 +515,7 @@ export const ContentAnalysis = { // This is also be called if the tab/window is closed while a request is in progress, // in which case we need to cancel the request. if (this.requestTokenToRequestInfo.delete(aRequestToken)) { + lazy.gContentAnalysis.cancelContentAnalysisRequest(aRequestToken); let dlpBusyView = this.dlpBusyViewsByTopBrowsingContext.getEntry(aBrowsingContext); if (dlpBusyView) { diff --git a/netwerk/base/nsIPrompt.idl b/netwerk/base/nsIPrompt.idl index 9ceaa6b89244d..6fbed3355d9a6 100644 --- a/netwerk/base/nsIPrompt.idl +++ b/netwerk/base/nsIPrompt.idl @@ -56,6 +56,8 @@ interface nsIPrompt : nsISupports /* used for security dialogs, buttons are initially disabled */ const unsigned long BUTTON_DELAY_ENABLE = 1 << 26; + const unsigned long SHOW_SPINNER = 1 << 27; + const unsigned long STD_OK_CANCEL_BUTTONS = (BUTTON_TITLE_OK * BUTTON_POS_0) + (BUTTON_TITLE_CANCEL * BUTTON_POS_1); const unsigned long STD_YES_NO_BUTTONS = (BUTTON_TITLE_YES * BUTTON_POS_0) + diff --git a/toolkit/components/contentanalysis/ContentAnalysis.cpp b/toolkit/components/contentanalysis/ContentAnalysis.cpp index ec54d6b6ba94e..35770c048cad3 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.cpp +++ b/toolkit/components/contentanalysis/ContentAnalysis.cpp @@ -9,6 +9,7 @@ #include "content_analysis/sdk/analysis_client.h" #include "base/process_util.h" +#include "mozilla/Components.h" #include "mozilla/dom/Promise.h" #include "mozilla/Logging.h" #include "mozilla/ScopeExit.h" @@ -32,6 +33,19 @@ # include #endif // XP_WIN +namespace mozilla::contentanalysis { + +LazyLogModule gContentAnalysisLog("contentanalysis"); +#define LOGD(...) \ + MOZ_LOG(mozilla::contentanalysis::gContentAnalysisLog, \ + mozilla::LogLevel::Debug, (__VA_ARGS__)) + +#define LOGE(...) \ + MOZ_LOG(mozilla::contentanalysis::gContentAnalysisLog, \ + mozilla::LogLevel::Error, (__VA_ARGS__)) + +} // namespace mozilla::contentanalysis + namespace { const char* kIsDLPEnabledPref = "browser.contentanalysis.enabled"; @@ -72,9 +86,6 @@ static nsresult GetFileDisplayName(const nsString& aFilePath, namespace mozilla::contentanalysis { -LazyLogModule gContentAnalysisLog("contentanalysis"); -#define LOGD(...) MOZ_LOG(gContentAnalysisLog, LogLevel::Debug, (__VA_ARGS__)) - NS_IMETHODIMP ContentAnalysisRequest::GetAnalysisType(uint32_t* aAnalysisType) { *aAnalysisType = mAnalysisType; @@ -144,25 +155,19 @@ ContentAnalysisRequest::GetWindowGlobalParent( return NS_OK; } -/* static */ -StaticDataMutex> - ContentAnalysis::sCaClient("ContentAnalysisClient"); +nsresult ContentAnalysis::CreateContentAnalysisClient(nsCString&& aPipePathName, + bool aIsPerUser) { + MOZ_ASSERT(!NS_IsMainThread()); + // This method should only be called once + MOZ_ASSERT(!mCaClientPromise->IsResolved()); -nsresult ContentAnalysis::EnsureContentAnalysisClient() { - auto caClientRef = sCaClient.Lock(); - auto& caClient = caClientRef.ref(); - if (caClient) { - return NS_OK; - } - - nsAutoCString pipePathName; - Preferences::GetCString(kPipePathNamePref, pipePathName); - caClient.reset( - content_analysis::sdk::Client::Create( - {pipePathName.Data(), Preferences::GetBool(kIsPerUserPref)}) + std::shared_ptr client( + content_analysis::sdk::Client::Create({aPipePathName.Data(), aIsPerUser}) .release()); - LOGD("Content analysis is %s", caClient ? "connected" : "not available"); - return caClient ? NS_OK : NS_ERROR_NOT_AVAILABLE; + LOGD("Content analysis is %s", client ? "connected" : "not available"); + mCaClientPromise->Resolve(client, __func__); + + return NS_OK; } ContentAnalysisRequest::ContentAnalysisRequest( @@ -562,26 +567,61 @@ NS_IMPL_CLASSINFO(ContentAnalysisResponse, nullptr, 0, {0}); NS_IMPL_ISUPPORTS_CI(ContentAnalysisResponse, nsIContentAnalysisResponse); NS_IMPL_ISUPPORTS(ContentAnalysisCallback, nsIContentAnalysisCallback); NS_IMPL_ISUPPORTS(ContentAnalysisResult, nsIContentAnalysisResult); -NS_IMPL_ISUPPORTS(ContentAnalysis, nsIContentAnalysis); +NS_IMPL_ISUPPORTS(ContentAnalysis, nsIContentAnalysis, ContentAnalysis); + +ContentAnalysis::ContentAnalysis() + : mCaClientPromise( + new ClientPromise::Private("ContentAnalysis::ContentAnalysis")), + mClientCreationAttempted(false), + mCallbackMap("ContentAnalysis::mCallbackMap") {} ContentAnalysis::~ContentAnalysis() { - auto caClientRef = sCaClient.Lock(); - auto& caClient = caClientRef.ref(); - caClient = nullptr; + // Accessing mClientCreationAttempted so need to be on the main thread + MOZ_ASSERT(NS_IsMainThread()); + if (!mClientCreationAttempted) { + // Reject the promise to avoid assertions when it gets destroyed + mCaClientPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } } NS_IMETHODIMP ContentAnalysis::GetIsActive(bool* aIsActive) { *aIsActive = false; + // Need to be on the main thread to read prefs + MOZ_ASSERT(NS_IsMainThread()); // gAllowContentAnalysis is only set in the parent process MOZ_ASSERT(XRE_IsParentProcess()); if (!gAllowContentAnalysis || !Preferences::GetBool(kIsDLPEnabledPref)) { + LOGD("Local DLP Content Analysis is not active"); return NS_OK; } - - nsresult rv = EnsureContentAnalysisClient(); - *aIsActive = NS_SUCCEEDED(rv); - LOGD("Local DLP Content Analysis is %sactive", *aIsActive ? "" : "not "); + *aIsActive = true; + LOGD("Local DLP Content Analysis is active"); + // mClientCreationAttempted is only accessed on the main thread, + // so no need for synchronization here. + if (!mClientCreationAttempted) { + mClientCreationAttempted = true; + LOGD("Dispatching background task to create Content Analysis client"); + + nsCString pipePathName; + nsresult rv = Preferences::GetCString(kPipePathNamePref, pipePathName); + if (NS_WARN_IF(NS_FAILED(rv))) { + mCaClientPromise->Reject(rv, __func__); + return rv; + } + bool isPerUser = Preferences::GetBool(kIsPerUserPref); + rv = NS_DispatchBackgroundTask(NS_NewCancelableRunnableFunction( + "ContentAnalysis::CreateContentAnalysisClient", + [owner = RefPtr{this}, pipePathName = std::move(pipePathName), + isPerUser]() mutable { + owner->CreateContentAnalysisClient(std::move(pipePathName), + isPerUser); + })); + if (NS_WARN_IF(NS_FAILED(rv))) { + mCaClientPromise->Reject(rv, __func__); + return rv; + } + } return NS_OK; } @@ -596,6 +636,41 @@ ContentAnalysis::GetMightBeActive(bool* aMightBeActive) { return NS_OK; } +nsresult ContentAnalysis::CancelWithError(nsCString aRequestToken, + nsresult aResult) { + return NS_DispatchToMainThread(NS_NewCancelableRunnableFunction( + "ContentAnalysis::RunAnalyzeRequestTask::HandleResponse", + [aResult, aRequestToken = std::move(aRequestToken)] { + RefPtr owner = GetContentAnalysisFromService(); + if (!owner) { + // May be shutting down + return; + } + nsMainThreadPtrHandle callbackHolder; + { + auto lock = owner->mCallbackMap.Lock(); + auto callbackData = lock->Extract(aRequestToken); + if (callbackData.isSome()) { + callbackHolder = callbackData->TakeCallbackHolder(); + } + } + if (callbackHolder) { + callbackHolder->Error(aResult); + } + })); +} + +RefPtr ContentAnalysis::GetContentAnalysisFromService() { + RefPtr contentAnalysisService = + mozilla::components::nsIContentAnalysis::Service(); + if (!contentAnalysisService) { + // May be shutting down + return nullptr; + } + + return contentAnalysisService; +} + nsresult ContentAnalysis::RunAnalyzeRequestTask( RefPtr aRequest, RefPtr aCallback) { @@ -603,15 +678,11 @@ nsresult ContentAnalysis::RunAnalyzeRequestTask( auto callbackCopy = aCallback; auto se = MakeScopeExit([&] { if (!NS_SUCCEEDED(rv)) { - LOGD("RunAnalyzeRequestTask failed"); + LOGE("RunAnalyzeRequestTask failed"); callbackCopy->Error(rv); } }); - // The Client object from the SDK must be kept live as long as there are - // active transactions. - RefPtr owner = this; - content_analysis::sdk::ContentAnalysisRequest pbRequest; rv = ConvertToProtobuf(aRequest, &pbRequest); NS_ENSURE_SUCCESS(rv, rv); @@ -620,69 +691,142 @@ nsresult ContentAnalysis::RunAnalyzeRequestTask( LogRequest(&pbRequest); nsCString requestToken; + nsMainThreadPtrHandle callbackHolderCopy( + new nsMainThreadPtrHolder( + "content analysis callback", aCallback)); + CallbackData callbackData(std::move(callbackHolderCopy)); rv = aRequest->GetRequestToken(requestToken); NS_ENSURE_SUCCESS(rv, rv); + { + auto lock = mCallbackMap.Lock(); + lock->InsertOrUpdate(requestToken, std::move(callbackData)); + } - // The content analysis connection is synchronous so run in the background. - rv = NS_DispatchBackgroundTask( - NS_NewRunnableFunction( - "RunAnalyzeRequestTask", - [pbRequest = std::move(pbRequest), aCallback = std::move(aCallback), - requestToken = std::move(requestToken), owner] { - nsresult rv = NS_ERROR_FAILURE; - content_analysis::sdk::ContentAnalysisResponse pbResponse; - - auto resolveOnMainThread = MakeScopeExit([&] { - NS_DispatchToMainThread(NS_NewRunnableFunction( - "ResolveOnMainThread", - [rv, owner, aCallback = std::move(aCallback), - pbResponse = std::move(pbResponse), requestToken]() mutable { - if (NS_SUCCEEDED(rv)) { + LOGD("Issuing ContentAnalysisRequest for token %s", requestToken.get()); + LogRequest(&pbRequest); + + mCaClientPromise->Then( + GetCurrentSerialEventTarget(), __func__, + [requestToken, pbRequest = std::move(pbRequest)]( + std::shared_ptr client) { + // The content analysis call is synchronous so run in the background. + NS_DispatchBackgroundTask( + NS_NewCancelableRunnableFunction( + __func__, + [requestToken, pbRequest = std::move(pbRequest), client] { + RefPtr owner = + GetContentAnalysisFromService(); + if (!owner) { + // May be shutting down + return; + } + + if (!client) { + owner->CancelWithError(std::move(requestToken), + NS_ERROR_NOT_AVAILABLE); + return; + } + { + auto callbackMap = owner->mCallbackMap.Lock(); + if (!callbackMap->Contains(requestToken)) { LOGD( - "Content analysis resolving response promise for " - "token %s", + "RunAnalyzeRequestTask token %s has already been " + "cancelled - not issuing request", requestToken.get()); - RefPtr response = - ContentAnalysisResponse::FromProtobuf( - std::move(pbResponse)); - if (response) { + return; + } + } + + // Run request, then dispatch back to main thread to resolve + // aCallback + content_analysis::sdk::ContentAnalysisResponse pbResponse; + int err = client->Send(pbRequest, &pbResponse); + if (err != 0) { + LOGE("RunAnalyzeRequestTask client transaction failed"); + owner->CancelWithError(std::move(requestToken), + NS_ERROR_FAILURE); + return; + } + LOGD("Content analysis client transaction succeeded"); + LogResponse(&pbResponse); + NS_DispatchToMainThread(NS_NewCancelableRunnableFunction( + "ContentAnalysis::RunAnalyzeRequestTask::HandleResponse", + [pbResponse = std::move(pbResponse)]() mutable { + RefPtr owner = + GetContentAnalysisFromService(); + if (!owner) { + // May be shutting down + return; + } + + RefPtr response = + ContentAnalysisResponse::FromProtobuf( + std::move(pbResponse)); + if (!response) { + LOGE("Content analysis got invalid response!"); + return; + } + nsCString responseRequestToken; + nsresult requestRv = + response->GetRequestToken(responseRequestToken); + if (NS_FAILED(requestRv)) { + LOGE( + "Content analysis couldn't get request token " + "from response!"); + return; + } + + Maybe maybeCallbackData; + { + auto callbackMap = owner->mCallbackMap.Lock(); + maybeCallbackData = + callbackMap->Extract(responseRequestToken); + } + if (maybeCallbackData.isNothing()) { + LOGD( + "Content analysis did not find callback for " + "token %s", + responseRequestToken.get()); + return; + } response->SetOwner(owner); + if (maybeCallbackData->Canceled()) { + // request has already been cancelled, so there's + // nothing to do + LOGD( + "Content analysis got response but ignoring " + "because it was already cancelled for token %s", + responseRequestToken.get()); + return; + } + + LOGD( + "Content analysis resolving response promise for " + "token %s", + responseRequestToken.get()); nsCOMPtr obsServ = mozilla::services::GetObserverService(); + obsServ->NotifyObservers(response, "dlp-response", nullptr); - aCallback->ContentResult(response); - } else { - aCallback->Error(NS_ERROR_FAILURE); - } - } else { - aCallback->Error(rv); - } - })); - }); - - auto caClientRef = sCaClient.Lock(); - auto& caClient = caClientRef.ref(); - if (!caClient) { - LOGD("RunAnalyzeRequestTask failed to get client"); - rv = NS_ERROR_NOT_AVAILABLE; - return; - } - - // Run request, then dispatch back to main thread to resolve - // aPromise - int err = caClient->Send(pbRequest, &pbResponse); - if (err != 0) { - LOGD("RunAnalyzeRequestTask client transaction failed"); - rv = NS_ERROR_FAILURE; - return; - } - - LOGD("Content analysis client transaction succeeded"); - LogResponse(&pbResponse); - rv = NS_OK; - }), - NS_DISPATCH_EVENT_MAY_BLOCK); + + nsMainThreadPtrHandle + callbackHolder = + maybeCallbackData->TakeCallbackHolder(); + callbackHolder->ContentResult(response); + })); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + }, + [requestToken](nsresult rv) { + LOGD("RunAnalyzeRequestTask failed to get client"); + RefPtr owner = GetContentAnalysisFromService(); + if (!owner) { + // May be shutting down + return; + } + owner->CancelWithError(std::move(requestToken), rv); + }); return rv; } @@ -722,6 +866,37 @@ ContentAnalysis::AnalyzeContentRequestCallback( return rv; } +NS_IMETHODIMP +ContentAnalysis::CancelContentAnalysisRequest(const nsACString& aRequestToken) { + NS_DispatchToMainThread(NS_NewCancelableRunnableFunction( + "CancelContentAnalysisRequest", + [requestToken = nsCString{aRequestToken}]() { + RefPtr self = GetContentAnalysisFromService(); + if (!self) { + // May be shutting down + return; + } + + auto callbackMap = self->mCallbackMap.Lock(); + auto entry = callbackMap->Lookup(requestToken); + LOGD("Content analysis cancelling request %s", requestToken.get()); + if (entry) { + entry->SetCanceled(); + RefPtr cancelResponse = + ContentAnalysisResponse::FromAction( + nsIContentAnalysisResponse::CANCELED, requestToken); + cancelResponse->SetOwner(self); + nsMainThreadPtrHandle callbackHolder = + entry->TakeCallbackHolder(); + callbackHolder->ContentResult(cancelResponse.get()); + } else { + LOGD("Content analysis request not found when trying to cancel %s", + requestToken.get()); + } + })); + return NS_OK; +} + NS_IMETHODIMP ContentAnalysisResponse::Acknowledge( nsIContentAnalysisAcknowledgement* aAcknowledgement) { @@ -746,25 +921,37 @@ nsresult ContentAnalysis::RunAcknowledgeTask( LOGD("Issuing ContentAnalysisAcknowledgement"); LogAcknowledgement(&pbAck); - // The Client object from the SDK must be kept live as long as there are - // active transactions. - RefPtr owner = this; - // The content analysis connection is synchronous so run in the background. LOGD("RunAcknowledgeTask dispatching acknowledge task"); - return NS_DispatchBackgroundTask(NS_NewRunnableFunction( - "RunAcknowledgeTask", [owner, pbAck = std::move(pbAck)] { - auto caClientRef = sCaClient.Lock(); - auto& caClient = caClientRef.ref(); - if (!caClient) { - LOGD("RunAcknowledgeTask failed to get the client"); - return; - } - - DebugOnly err = caClient->Acknowledge(pbAck); - MOZ_ASSERT(err == 0); - LOGD("RunAcknowledgeTask sent transaction acknowledgement"); - })); + mCaClientPromise->Then( + GetCurrentSerialEventTarget(), __func__, + [pbAck = std::move(pbAck)]( + std::shared_ptr client) { + NS_DispatchBackgroundTask( + NS_NewCancelableRunnableFunction( + __func__, + [pbAck = std::move(pbAck), client] { + RefPtr owner = + GetContentAnalysisFromService(); + if (!owner) { + // May be shutting down + return; + } + if (!client) { + return; + } + + int err = client->Acknowledge(pbAck); + MOZ_ASSERT(err == 0); + LOGD( + "RunAcknowledgeTask sent transaction acknowledgement, " + "err=%d", + err); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + }, + [](nsresult rv) { LOGD("RunAcknowledgeTask failed to get the client"); }); + return rv; } NS_IMETHODIMP ContentAnalysisCallback::ContentResult( @@ -789,4 +976,7 @@ NS_IMETHODIMP ContentAnalysisCallback::Error(nsresult aError) { ContentAnalysisCallback::ContentAnalysisCallback(RefPtr aPromise) : mPromise(Some(new nsMainThreadPtrHolder( "content analysis promise", aPromise))) {} + +#undef LOGD +#undef LOGE } // namespace mozilla::contentanalysis diff --git a/toolkit/components/contentanalysis/ContentAnalysis.h b/toolkit/components/contentanalysis/ContentAnalysis.h index 12f61a150ef94..b6c57305e823d 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.h +++ b/toolkit/components/contentanalysis/ContentAnalysis.h @@ -8,11 +8,12 @@ #include "mozilla/DataMutex.h" #include "mozilla/dom/WindowGlobalParent.h" -#include "mozilla/Mutex.h" #include "nsIContentAnalysis.h" #include "nsProxyRelease.h" #include "nsString.h" +#include "nsTHashMap.h" +#include #include namespace content_analysis::sdk { @@ -73,30 +74,65 @@ class ContentAnalysisRequest final : public nsIContentAnalysisRequest { RefPtr mWindowGlobalParent; }; +#define CONTENTANALYSIS_IID \ + { \ + 0xa37bed74, 0x4b50, 0x443a, { \ + 0xbf, 0x58, 0xf4, 0xeb, 0xbd, 0x30, 0x67, 0xb4 \ + } \ + } + class ContentAnalysisResponse; class ContentAnalysis final : public nsIContentAnalysis { public: + NS_DECLARE_STATIC_IID_ACCESSOR(CONTENTANALYSIS_IID) NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSICONTENTANALYSIS - ContentAnalysis() = default; + ContentAnalysis(); private: ~ContentAnalysis(); // Remove unneeded copy constructor/assignment ContentAnalysis(const ContentAnalysis&) = delete; ContentAnalysis& operator=(ContentAnalysis&) = delete; - nsresult EnsureContentAnalysisClient(); + nsresult CreateContentAnalysisClient(nsCString&& aPipePathName, + bool aIsPerUser); nsresult RunAnalyzeRequestTask(RefPtr aRequest, RefPtr aCallback); nsresult RunAcknowledgeTask( nsIContentAnalysisAcknowledgement* aAcknowledgement, const nsACString& aRequestToken); - - static StaticDataMutex> sCaClient; + nsresult CancelWithError(nsCString aRequestToken, nsresult aResult); + static RefPtr GetContentAnalysisFromService(); + + using ClientPromise = + MozPromise, nsresult, + false>; + RefPtr mCaClientPromise; + // Only accessed from the main thread + bool mClientCreationAttempted; + + class CallbackData { + public: + explicit CallbackData( + nsMainThreadPtrHandle&& aCallbackHolder) + : mCallbackHolder(aCallbackHolder) {} + + nsMainThreadPtrHandle TakeCallbackHolder() { + return std::move(mCallbackHolder); + } + void SetCanceled() { mCallbackHolder = nullptr; } + bool Canceled() const { return !mCallbackHolder; } + + private: + nsMainThreadPtrHandle mCallbackHolder; + }; + DataMutex> mCallbackMap; friend class ContentAnalysisResponse; }; +NS_DEFINE_STATIC_IID_ACCESSOR(ContentAnalysis, CONTENTANALYSIS_IID) + class ContentAnalysisResponse final : public nsIContentAnalysisResponse { public: NS_DECL_ISUPPORTS diff --git a/toolkit/components/contentanalysis/nsIContentAnalysis.idl b/toolkit/components/contentanalysis/nsIContentAnalysis.idl index 9bd74258b76df..1484586f4a7c9 100644 --- a/toolkit/components/contentanalysis/nsIContentAnalysis.idl +++ b/toolkit/components/contentanalysis/nsIContentAnalysis.idl @@ -46,6 +46,7 @@ interface nsIContentAnalysisResponse : nsISupports // Values that do not appear in analysis.proto const unsigned long ALLOW = 1000; + const unsigned long CANCELED = 1001; [infallible] readonly attribute unsigned long action; [infallible] readonly attribute boolean shouldAllowContent; @@ -160,7 +161,8 @@ interface nsIContentAnalysisCallback : nsISupports interface nsIContentAnalysis : nsISupports { /** - * True if content analysis should be consulted. + * True if content analysis should be consulted. Must only be accessed from + * the parent process's main thread. */ readonly attribute bool isActive; @@ -194,4 +196,11 @@ interface nsIContentAnalysis : nsISupports * from C++ since it takes a callback instead of returning a Promise. */ void analyzeContentRequestCallback(in nsIContentAnalysisRequest aCar, in nsIContentAnalysisCallback callback); + + /** + * Cancels the request that is in progress. This may not actually cancel the request + * with the analysis server, but it means that Gecko will immediately act like the request + * was denied. + */ + void cancelContentAnalysisRequest(in ACString aRequestToken); }; diff --git a/toolkit/components/prompts/content/commonDialog.css b/toolkit/components/prompts/content/commonDialog.css index 3521af13c6ed7..ac01353aae42e 100644 --- a/toolkit/components/prompts/content/commonDialog.css +++ b/toolkit/components/prompts/content/commonDialog.css @@ -58,6 +58,15 @@ dialog[insecureauth] { flex: 1; } +#spinnerContainer { + align-items: center; +} + +#spinnerContainer > img { + width: 16px; + height: 16px; +} + #loginLabel, #password1Label { text-align: start; } diff --git a/toolkit/components/prompts/content/commonDialog.js b/toolkit/components/prompts/content/commonDialog.js index e2edd35679bd7..11b3e820177fc 100644 --- a/toolkit/components/prompts/content/commonDialog.js +++ b/toolkit/components/prompts/content/commonDialog.js @@ -83,6 +83,7 @@ function commonDialogOnLoad() { infoBody: document.getElementById("infoBody"), infoTitle: document.getElementById("infoTitle"), infoIcon: document.getElementById("infoIcon"), + spinnerContainer: document.getElementById("spinnerContainer"), checkbox: document.getElementById("checkbox"), checkboxContainer: document.getElementById("checkboxContainer"), button3: dialog.getButton("extra2"), diff --git a/toolkit/components/prompts/content/commonDialog.xhtml b/toolkit/components/prompts/content/commonDialog.xhtml index 83cc37d9edb79..def3b93956ea9 100644 --- a/toolkit/components/prompts/content/commonDialog.xhtml +++ b/toolkit/components/prompts/content/commonDialog.xhtml @@ -83,6 +83,16 @@ /> +