diff options
Diffstat (limited to 'toolkit/components/contentanalysis')
24 files changed, 2138 insertions, 263 deletions
diff --git a/toolkit/components/contentanalysis/ContentAnalysis.cpp b/toolkit/components/contentanalysis/ContentAnalysis.cpp index 2977072984..4f4c53a324 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.cpp +++ b/toolkit/components/contentanalysis/ContentAnalysis.cpp @@ -17,9 +17,11 @@ #include "mozilla/Logging.h" #include "mozilla/ScopeExit.h" #include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" #include "mozilla/StaticMutex.h" #include "mozilla/StaticPrefs_browser.h" #include "nsAppRunner.h" +#include "nsBaseClipboard.h" #include "nsComponentManagerUtils.h" #include "nsIClassInfoImpl.h" #include "nsIFile.h" @@ -28,6 +30,8 @@ #include "nsIOutputStream.h" #include "nsIPrintSettings.h" #include "nsIStorageStream.h" +#include "nsISupportsPrimitives.h" +#include "nsITransferable.h" #include "ScopedNSSTypes.h" #include "xpcpublic.h" @@ -61,7 +65,6 @@ namespace { const char* kIsDLPEnabledPref = "browser.contentanalysis.enabled"; const char* kIsPerUserPref = "browser.contentanalysis.is_per_user"; const char* kPipePathNamePref = "browser.contentanalysis.pipe_path_name"; -const char* kDefaultAllowPref = "browser.contentanalysis.default_allow"; const char* kClientSignature = "browser.contentanalysis.client_signature"; const char* kAllowUrlPref = "browser.contentanalysis.allow_url_regex_list"; const char* kDenyUrlPref = "browser.contentanalysis.deny_url_regex_list"; @@ -793,6 +796,17 @@ static bool ShouldAllowAction( aResponseCode == nsIContentAnalysisResponse::Action::eReportOnly || aResponseCode == nsIContentAnalysisResponse::Action::eWarn; } + +static DefaultResult GetDefaultResultFromPref() { + uint32_t value = StaticPrefs::browser_contentanalysis_default_result(); + if (value > static_cast<uint32_t>(DefaultResult::eLastValue)) { + LOGE( + "Invalid value for browser.contentanalysis.default_result pref " + "value"); + return DefaultResult::eBlock; + } + return static_cast<DefaultResult>(value); +} } // namespace NS_IMETHODIMP ContentAnalysisResponse::GetShouldAllowContent( @@ -805,7 +819,7 @@ NS_IMETHODIMP ContentAnalysisResult::GetShouldAllowContent( bool* aShouldAllowContent) { if (mValue.is<NoContentAnalysisResult>()) { NoContentAnalysisResult result = mValue.as<NoContentAnalysisResult>(); - if (Preferences::GetBool(kDefaultAllowPref)) { + if (GetDefaultResultFromPref() == DefaultResult::eAllow) { *aShouldAllowContent = result != NoContentAnalysisResult::DENY_DUE_TO_CANCELED; } else { @@ -816,6 +830,7 @@ NS_IMETHODIMP ContentAnalysisResult::GetShouldAllowContent( ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE || result == NoContentAnalysisResult:: ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS || + result == NoContentAnalysisResult::ALLOW_DUE_TO_SAME_TAB_SOURCE || result == NoContentAnalysisResult::ALLOW_DUE_TO_COULD_NOT_GET_DATA; } } else { @@ -1062,7 +1077,7 @@ nsresult ContentAnalysis::CancelWithError(nsCString aRequestToken, nsresult aResult) { return NS_DispatchToMainThread(NS_NewCancelableRunnableFunction( "ContentAnalysis::CancelWithError", - [aResult, aRequestToken = std::move(aRequestToken)] { + [aResult, aRequestToken = std::move(aRequestToken)]() mutable { RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService(); if (!owner) { // May be shutting down @@ -1071,12 +1086,24 @@ nsresult ContentAnalysis::CancelWithError(nsCString aRequestToken, owner->SetLastResult(aResult); nsCOMPtr<nsIObserverService> obsServ = mozilla::services::GetObserverService(); - bool allow = Preferences::GetBool(kDefaultAllowPref); + DefaultResult defaultResponse = GetDefaultResultFromPref(); + nsIContentAnalysisResponse::Action action; + switch (defaultResponse) { + case DefaultResult::eAllow: + action = nsIContentAnalysisResponse::Action::eAllow; + break; + case DefaultResult::eWarn: + action = nsIContentAnalysisResponse::Action::eWarn; + break; + case DefaultResult::eBlock: + action = nsIContentAnalysisResponse::Action::eCanceled; + break; + default: + MOZ_ASSERT(false); + action = nsIContentAnalysisResponse::Action::eCanceled; + } RefPtr<ContentAnalysisResponse> response = - ContentAnalysisResponse::FromAction( - allow ? nsIContentAnalysisResponse::Action::eAllow - : nsIContentAnalysisResponse::Action::eCanceled, - aRequestToken); + ContentAnalysisResponse::FromAction(action, aRequestToken); response->SetOwner(owner); nsIContentAnalysisResponse::CancelError cancelError; switch (aResult) { @@ -1092,20 +1119,29 @@ nsresult ContentAnalysis::CancelWithError(nsCString aRequestToken, break; } response->SetCancelError(cancelError); - obsServ->NotifyObservers(response, "dlp-response", nullptr); - nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolder; + Maybe<CallbackData> maybeCallbackData; { auto lock = owner->mCallbackMap.Lock(); - auto callbackData = lock->Extract(aRequestToken); - if (callbackData.isSome()) { - callbackHolder = callbackData->TakeCallbackHolder(); + maybeCallbackData = lock->Extract(aRequestToken); + if (maybeCallbackData.isNothing()) { + LOGD("Content analysis did not find callback for token %s", + aRequestToken.get()); + return; } } + if (action == nsIContentAnalysisResponse::Action::eWarn) { + owner->SendWarnResponse(std::move(aRequestToken), + std::move(*maybeCallbackData), response); + return; + } + nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolder = + maybeCallbackData->TakeCallbackHolder(); + obsServ->NotifyObservers(response, "dlp-response", nullptr); if (callbackHolder) { - if (allow) { - callbackHolder->ContentResult(response); - } else { + if (action == nsIContentAnalysisResponse::Action::eCanceled) { callbackHolder->Error(aResult); + } else { + callbackHolder->ContentResult(response); } } })); @@ -1184,6 +1220,22 @@ nsresult ContentAnalysis::RunAnalyzeRequestTask( ConvertToProtobuf(aRequest, GetUserActionId(), aRequestCount, &pbRequest); NS_ENSURE_SUCCESS(rv, rv); LogRequest(&pbRequest); + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + // Avoid serializing the string here if no one is observing this message + if (obsServ->HasObservers("dlp-request-sent-raw")) { + std::string requestString = pbRequest.SerializeAsString(); + nsTArray<char16_t> requestArray; + requestArray.SetLength(requestString.size() + 1); + for (size_t i = 0; i < requestString.size(); ++i) { + // Since NotifyObservers() expects a null-terminated string, + // make sure none of these values are 0. + requestArray[i] = requestString[i] + 0xFF00; + } + requestArray[requestString.size()] = 0; + obsServ->NotifyObservers(this, "dlp-request-sent-raw", + requestArray.Elements()); + } mCaClientPromise->Then( GetCurrentSerialEventTarget(), __func__, @@ -1289,6 +1341,20 @@ void ContentAnalysis::DoAnalyzeRequest( })); } +void ContentAnalysis::SendWarnResponse( + nsCString&& aResponseRequestToken, CallbackData aCallbackData, + RefPtr<ContentAnalysisResponse>& aResponse) { + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + { + auto warnResponseDataMap = mWarnResponseDataMap.Lock(); + warnResponseDataMap->InsertOrUpdate( + aResponseRequestToken, + WarnResponseData(std::move(aCallbackData), aResponse)); + } + obsServ->NotifyObservers(aResponse, "dlp-response", nullptr); +} + void ContentAnalysis::IssueResponse(RefPtr<ContentAnalysisResponse>& response) { MOZ_ASSERT(NS_IsMainThread()); nsCString responseRequestToken; @@ -1336,13 +1402,8 @@ void ContentAnalysis::IssueResponse(RefPtr<ContentAnalysisResponse>& response) { nsCOMPtr<nsIObserverService> obsServ = mozilla::services::GetObserverService(); if (action == nsIContentAnalysisResponse::Action::eWarn) { - { - auto warnResponseDataMap = mWarnResponseDataMap.Lock(); - warnResponseDataMap->InsertOrUpdate( - responseRequestToken, - WarnResponseData(std::move(*maybeCallbackData), response)); - } - obsServ->NotifyObservers(response, "dlp-response", nullptr); + SendWarnResponse(std::move(responseRequestToken), + std::move(*maybeCallbackData), response); return; } @@ -1696,6 +1757,233 @@ ContentAnalysis::PrintToPDFToDetermineIfPrintAllowed( } #endif +NS_IMPL_ISUPPORTS(ContentAnalysis::SafeContentAnalysisResultCallback, + nsIContentAnalysisCallback); + +// - true means a content analysis request was fired +// - false means there is no text data in the transferable +// - NoContentAnalysisResult means there was an error +using ClipboardContentAnalysisResult = + mozilla::Result<bool, mozilla::contentanalysis::NoContentAnalysisResult>; + +NS_IMETHODIMP ContentAnalysis::SafeContentAnalysisResultCallback::ContentResult( + nsIContentAnalysisResponse* aResponse) { + RefPtr<ContentAnalysisResult> result = + ContentAnalysisResult::FromContentAnalysisResponse(aResponse); + Callback(result); + return NS_OK; +} + +NS_IMETHODIMP ContentAnalysis::SafeContentAnalysisResultCallback::Error( + nsresult aError) { + Callback(ContentAnalysisResult::FromNoResult( + NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR)); + return NS_OK; +} + +ClipboardContentAnalysisResult CheckClipboardContentAnalysisAsText( + uint64_t aInnerWindowId, + ContentAnalysis::SafeContentAnalysisResultCallback* aResolver, + nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis, + nsITransferable* aTextTrans, const char* aFlavor) { + nsCOMPtr<nsISupports> transferData; + if (NS_FAILED( + aTextTrans->GetTransferData(aFlavor, getter_AddRefs(transferData)))) { + return false; + } + nsCOMPtr<nsISupportsString> textData = do_QueryInterface(transferData); + if (MOZ_UNLIKELY(!textData)) { + return false; + } + nsString text; + if (NS_FAILED(textData->GetData(text))) { + return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR); + } + if (text.IsEmpty()) { + // Content Analysis doesn't expect to analyze an empty string. + // Just approve it. + return true; + } + RefPtr<mozilla::dom::WindowGlobalParent> window = + mozilla::dom::WindowGlobalParent::GetByInnerWindowId(aInnerWindowId); + if (!window) { + // The window has gone away in the meantime + return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR); + } + nsCOMPtr<nsIContentAnalysisRequest> 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::DENY_DUE_TO_OTHER_ERROR); + } + return true; +} + +ClipboardContentAnalysisResult CheckClipboardContentAnalysisAsFile( + uint64_t aInnerWindowId, + ContentAnalysis::SafeContentAnalysisResultCallback* aResolver, + nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis, + nsITransferable* aFileTrans) { + nsCOMPtr<nsISupports> transferData; + nsresult rv = + aFileTrans->GetTransferData(kFileMime, getter_AddRefs(transferData)); + nsString filePath; + if (NS_SUCCEEDED(rv)) { + if (nsCOMPtr<nsIFile> file = do_QueryInterface(transferData)) { + rv = file->GetPath(filePath); + } else { + MOZ_ASSERT_UNREACHABLE("clipboard data had kFileMime but no nsIFile!"); + return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR); + } + } + if (NS_FAILED(rv) || filePath.IsEmpty()) { + return false; + } + RefPtr<mozilla::dom::WindowGlobalParent> window = + mozilla::dom::WindowGlobalParent::GetByInnerWindowId(aInnerWindowId); + if (!window) { + // The window has gone away in the meantime + return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR); + } + // Let the content analysis code calculate the digest + nsCOMPtr<nsIContentAnalysisRequest> 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::DENY_DUE_TO_OTHER_ERROR); + } + return true; +} + +void ContentAnalysis::CheckClipboardContentAnalysis( + nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow, + nsITransferable* aTransferable, int32_t aClipboardType, + 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:: + ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS)); + return; + } + nsCOMPtr<nsIContentAnalysis> contentAnalysis = + mozilla::components::nsIContentAnalysis::Service(); + if (!contentAnalysis) { + aResolver->Callback(ContentAnalysisResult::FromNoResult( + NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR)); + return; + } + + bool contentAnalysisIsActive; + nsresult rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive); + if (MOZ_LIKELY(NS_FAILED(rv) || !contentAnalysisIsActive)) { + aResolver->Callback(ContentAnalysisResult::FromNoResult( + NoContentAnalysisResult::ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE)); + return; + } + + uint64_t innerWindowId = aWindow->InnerWindowId(); + if (mozilla::StaticPrefs:: + browser_contentanalysis_bypass_for_same_tab_operations()) { + mozilla::Maybe<uint64_t> cacheInnerWindowId = + aClipboard->GetClipboardCacheInnerWindowId(aClipboardType); + if (cacheInnerWindowId.isSome() && *cacheInnerWindowId == innerWindowId) { + // If the same page copied this data to the clipboard (and the above + // preference is set) we can skip content analysis and immediately allow + // this. + aResolver->Callback(ContentAnalysisResult::FromNoResult( + NoContentAnalysisResult::ALLOW_DUE_TO_SAME_TAB_SOURCE)); + return; + } + } + + nsCOMPtr<nsIURI> currentURI = aWindow->Canonical()->GetDocumentURI(); + nsTArray<nsCString> flavors; + rv = aTransferable->FlavorsTransferableCanExport(flavors); + if (NS_WARN_IF(NS_FAILED(rv))) { + aResolver->Callback(ContentAnalysisResult::FromNoResult( + NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR)); + 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, + kTextMime); + if (textResult.isErr()) { + aResolver->Callback( + ContentAnalysisResult::FromNoResult(textResult.unwrapErr())); + return; + } + keepChecking = !textResult.unwrap(); + } + if (keepChecking) { + // Failed to get the clipboard data as a file or text, so try as html + auto htmlResult = CheckClipboardContentAnalysisAsText( + innerWindowId, aResolver, currentURI, contentAnalysis, aTransferable, + kHTMLMime); + if (htmlResult.isErr()) { + aResolver->Callback( + ContentAnalysisResult::FromNoResult(htmlResult.unwrapErr())); + return; + } + if (!htmlResult.unwrap()) { + // Couldn't get file or text or html data from this + aResolver->Callback(ContentAnalysisResult::FromNoResult( + NoContentAnalysisResult::ALLOW_DUE_TO_COULD_NOT_GET_DATA)); + return; + } + } +} + +bool ContentAnalysis::CheckClipboardContentAnalysisSync( + nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow, + const nsCOMPtr<nsITransferable>& trans, int32_t aClipboardType) { + bool requestDone = false; + RefPtr<nsIContentAnalysisResult> result; + auto callback = mozilla::MakeRefPtr<SafeContentAnalysisResultCallback>( + [&requestDone, &result](RefPtr<nsIContentAnalysisResult>&& aResult) { + result = std::move(aResult); + requestDone = true; + }); + CheckClipboardContentAnalysis(aClipboard, aWindow, trans, aClipboardType, + callback); + mozilla::SpinEventLoopUntil("CheckClipboardContentAnalysisSync"_ns, + [&requestDone]() -> bool { return requestDone; }); + return result->GetShouldAllowContent(); +} + NS_IMETHODIMP ContentAnalysisResponse::Acknowledge( nsIContentAnalysisAcknowledgement* aAcknowledgement) { diff --git a/toolkit/components/contentanalysis/ContentAnalysis.h b/toolkit/components/contentanalysis/ContentAnalysis.h index f2545624fd..2d8a1891b7 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.h +++ b/toolkit/components/contentanalysis/ContentAnalysis.h @@ -7,6 +7,7 @@ #define mozilla_contentanalysis_h #include "mozilla/DataMutex.h" +#include "mozilla/MoveOnlyFunction.h" #include "mozilla/MozPromise.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/MaybeDiscarded.h" @@ -24,6 +25,7 @@ # include <windows.h> #endif // XP_WIN +class nsBaseClipboard; class nsIPrincipal; class nsIPrintSettings; class ContentAnalysisTest; @@ -42,6 +44,13 @@ class ContentAnalysisResponse; namespace mozilla::contentanalysis { +enum class DefaultResult : uint8_t { + eBlock = 0, + eWarn = 1, + eAllow = 2, + eLastValue = 2 +}; + class ContentAnalysisDiagnosticInfo final : public nsIContentAnalysisDiagnosticInfo { public: @@ -149,6 +158,7 @@ class ContentAnalysis final : public nsIContentAnalysis { nsCString GetUserActionId(); void SetLastResult(nsresult aLastResult) { mLastResult = aLastResult; } +#if defined(XP_WIN) struct PrintAllowedResult final { bool mAllowed; dom::MaybeDiscarded<dom::BrowsingContext> @@ -175,13 +185,42 @@ class ContentAnalysis final : public nsIContentAnalysis { }; using PrintAllowedPromise = MozPromise<PrintAllowedResult, PrintAllowedError, true>; -#if defined(XP_WIN) MOZ_CAN_RUN_SCRIPT static RefPtr<PrintAllowedPromise> PrintToPDFToDetermineIfPrintAllowed( dom::CanonicalBrowsingContext* aBrowsingContext, nsIPrintSettings* aPrintSettings); #endif // defined(XP_WIN) + class SafeContentAnalysisResultCallback final + : public nsIContentAnalysisCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICONTENTANALYSISCALLBACK + explicit SafeContentAnalysisResultCallback( + std::function<void(RefPtr<nsIContentAnalysisResult>&&)> aResolver) + : mResolver(std::move(aResolver)) {} + void Callback(RefPtr<nsIContentAnalysisResult>&& aResult) { + MOZ_ASSERT(mResolver, "Called SafeContentAnalysisResultCallback twice!"); + if (auto resolver = std::move(mResolver)) { + resolver(std::move(aResult)); + } + } + + private: + ~SafeContentAnalysisResultCallback() { + MOZ_ASSERT(!mResolver, "SafeContentAnalysisResultCallback never called!"); + } + mozilla::MoveOnlyFunction<void(RefPtr<nsIContentAnalysisResult>&&)> + mResolver; + }; + static bool CheckClipboardContentAnalysisSync( + nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow, + const nsCOMPtr<nsITransferable>& trans, int32_t aClipboardType); + static void CheckClipboardContentAnalysis( + nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow, + nsITransferable* aTransferable, int32_t aClipboardType, + SafeContentAnalysisResultCallback* aResolver); + private: ~ContentAnalysis(); // Remove unneeded copy constructor/assignment @@ -210,7 +249,6 @@ class ContentAnalysis final : public nsIContentAnalysis { const std::shared_ptr<content_analysis::sdk::Client>& aClient); void IssueResponse(RefPtr<ContentAnalysisResponse>& response); bool LastRequestSucceeded(); - // Did the URL filter completely handle the request or do we need to check // with the agent. enum UrlFilterResult { eCheck, eDeny, eAllow }; @@ -259,6 +297,9 @@ class ContentAnalysis final : public nsIContentAnalysis { RefPtr<ContentAnalysisResponse> mResponse; }; DataMutex<nsTHashMap<nsCString, WarnResponseData>> mWarnResponseDataMap; + void SendWarnResponse(nsCString&& aResponseRequestToken, + CallbackData aCallbackData, + RefPtr<ContentAnalysisResponse>& aResponse); std::vector<std::regex> mAllowUrlList; std::vector<std::regex> mDenyUrlList; diff --git a/toolkit/components/contentanalysis/ContentAnalysisIPCTypes.h b/toolkit/components/contentanalysis/ContentAnalysisIPCTypes.h index a554036257..48e650d778 100644 --- a/toolkit/components/contentanalysis/ContentAnalysisIPCTypes.h +++ b/toolkit/components/contentanalysis/ContentAnalysisIPCTypes.h @@ -19,6 +19,7 @@ namespace contentanalysis { enum class NoContentAnalysisResult : uint8_t { ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE, ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS, + ALLOW_DUE_TO_SAME_TAB_SOURCE, ALLOW_DUE_TO_COULD_NOT_GET_DATA, DENY_DUE_TO_CANCELED, DENY_DUE_TO_INVALID_JSON_RESPONSE, diff --git a/toolkit/components/contentanalysis/tests/browser/browser.toml b/toolkit/components/contentanalysis/tests/browser/browser.toml index bdbf350593..7a9a3533d1 100644 --- a/toolkit/components/contentanalysis/tests/browser/browser.toml +++ b/toolkit/components/contentanalysis/tests/browser/browser.toml @@ -4,6 +4,33 @@ support-files = [ "head.js", ] +["browser_clipboard_content_analysis.js"] + +["browser_clipboard_paste_file_content_analysis.js"] +support-files = [ + "clipboard_paste_file.html", +] + +["browser_clipboard_paste_inputandtextarea_content_analysis.js"] +support-files = [ + "clipboard_paste_inputandtextarea.html", +] + +["browser_clipboard_paste_noformatting_content_analysis.js"] +support-files = [ + "clipboard_paste_noformatting.html", +] + +["browser_clipboard_paste_prompt_content_analysis.js"] +support-files = [ + "clipboard_paste_prompt.html", +] + +["browser_clipboard_read_async_content_analysis.js"] +support-files = [ + "clipboard_read_async.html", +] + ["browser_content_analysis_policies.js"] ["browser_print_changing_page_content_analysis.js"] diff --git a/toolkit/components/contentanalysis/tests/browser/browser_clipboard_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_content_analysis.js new file mode 100644 index 0000000000..875dbaa6e3 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_content_analysis.js @@ -0,0 +1,363 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test is used to check copy and paste in editable areas to ensure that non-text +// types (html and images) are copied to and pasted from the clipboard properly. + +var testPage = + "<body style='margin: 0'>" + + " <img id='img' tabindex='1' src='http://example.org/browser/browser/base/content/test/general/moz.png'>" + + " <div id='main' contenteditable='true'>Test <b>Bold</b> After Text</div>" + + "</body>"; + +let mockCA = makeMockContentAnalysis(); + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); +}); + +async function testClipboardWithContentAnalysis(allowPaste) { + mockCA.setupForTest(allowPaste); + let tab = BrowserTestUtils.addTab(gBrowser); + let browser = gBrowser.getBrowserForTab(tab); + + gBrowser.selectedTab = tab; + + await promiseTabLoadEvent(tab, "data:text/html," + escape(testPage)); + await SimpleTest.promiseFocus(browser); + + function sendKey(key, code) { + return BrowserTestUtils.synthesizeKey( + key, + { code, accelKey: true }, + browser + ); + } + + // On windows, HTML clipboard includes extra data. + // The values are from widget/windows/nsDataObj.cpp. + const htmlPrefix = navigator.platform.includes("Win") + ? "<html><body>\n<!--StartFragment-->" + : ""; + const htmlPostfix = navigator.platform.includes("Win") + ? "<!--EndFragment-->\n</body>\n</html>" + : ""; + + await SpecialPowers.spawn(browser, [], () => { + var doc = content.document; + var main = doc.getElementById("main"); + main.focus(); + + // Select an area of the text. + let selection = doc.getSelection(); + selection.modify("move", "left", "line"); + selection.modify("move", "right", "character"); + selection.modify("move", "right", "character"); + selection.modify("move", "right", "character"); + selection.modify("extend", "right", "word"); + selection.modify("extend", "right", "word"); + }); + + // The data is empty as the selection was copied during the event default phase. + let copyEventPromise = BrowserTestUtils.waitForContentEvent( + browser, + "copy", + false, + event => { + return event.clipboardData.mozItemCount == 0; + } + ); + await SpecialPowers.spawn(browser, [], () => {}); + await sendKey("c"); + await copyEventPromise; + + let pastePromise = SpecialPowers.spawn( + browser, + [htmlPrefix, htmlPostfix, allowPaste], + (htmlPrefixChild, htmlPostfixChild, allowPaste) => { + let selection = content.document.getSelection(); + selection.modify("move", "right", "line"); + + return new Promise((resolve, _reject) => { + content.addEventListener( + "paste", + event => { + let clipboardData = event.clipboardData; + Assert.equal( + clipboardData.mozItemCount, + 1, + "One item on clipboard" + ); + Assert.equal( + clipboardData.types.length, + 2, + "Two types on clipboard" + ); + Assert.equal( + clipboardData.types[0], + "text/html", + "text/html on clipboard" + ); + Assert.equal( + clipboardData.types[1], + "text/plain", + "text/plain on clipboard" + ); + Assert.equal( + clipboardData.getData("text/html"), + allowPaste + ? htmlPrefixChild + "t <b>Bold</b>" + htmlPostfixChild + : "", + "text/html value" + ); + Assert.equal( + clipboardData.getData("text/plain"), + allowPaste ? "t Bold" : "", + "text/plain value" + ); + resolve(); + }, + { capture: true, once: true } + ); + }); + } + ); + + await SpecialPowers.spawn(browser, [], () => {}); + + await sendKey("v"); + await pastePromise; + // 2 calls because there are two formats on the clipboard + is(mockCA.calls.length, 2, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest( + mockCA.calls[0], + htmlPrefix + "t <b>Bold</b>" + htmlPostfix + ); + assertContentAnalysisRequest(mockCA.calls[1], "t Bold"); + mockCA.clearCalls(); + + let copyPromise = SpecialPowers.spawn(browser, [], () => { + var main = content.document.getElementById("main"); + + Assert.equal( + main.innerHTML, + "Test <b>Bold</b> After Textt <b>Bold</b>", + "Copy and paste html" + ); + + let selection = content.document.getSelection(); + selection.modify("extend", "left", "word"); + selection.modify("extend", "left", "word"); + selection.modify("extend", "left", "character"); + + return new Promise((resolve, _reject) => { + content.addEventListener( + "cut", + event => { + event.clipboardData.setData("text/plain", "Some text"); + event.clipboardData.setData("text/html", "<i>Italic</i> "); + selection.deleteFromDocument(); + event.preventDefault(); + resolve(); + }, + { capture: true, once: true } + ); + }); + }); + + await SpecialPowers.spawn(browser, [], () => {}); + + await sendKey("x"); + await copyPromise; + + pastePromise = SpecialPowers.spawn( + browser, + [htmlPrefix, htmlPostfix, allowPaste], + (htmlPrefixChild, htmlPostfixChild, allowPaste) => { + let selection = content.document.getSelection(); + selection.modify("move", "left", "line"); + + return new Promise((resolve, _reject) => { + content.addEventListener( + "paste", + event => { + let clipboardData = event.clipboardData; + Assert.equal( + clipboardData.mozItemCount, + 1, + "One item on clipboard 2" + ); + Assert.equal( + clipboardData.types.length, + 2, + "Two types on clipboard 2" + ); + Assert.equal( + clipboardData.types[0], + "text/html", + "text/html on clipboard 2" + ); + Assert.equal( + clipboardData.types[1], + "text/plain", + "text/plain on clipboard 2" + ); + Assert.equal( + clipboardData.getData("text/html"), + allowPaste + ? htmlPrefixChild + "<i>Italic</i> " + htmlPostfixChild + : "", + "text/html value 2" + ); + Assert.equal( + clipboardData.getData("text/plain"), + allowPaste ? "Some text" : "", + "text/plain value 2" + ); + resolve(); + }, + { capture: true, once: true } + ); + }); + } + ); + + await SpecialPowers.spawn(browser, [], () => {}); + + await sendKey("v"); + await pastePromise; + // 2 calls because there are two formats on the clipboard + is(mockCA.calls.length, 2, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest( + mockCA.calls[0], + htmlPrefix + "<i>Italic</i> " + htmlPostfix + ); + assertContentAnalysisRequest(mockCA.calls[1], "Some text"); + mockCA.clearCalls(); + + await SpecialPowers.spawn(browser, [], () => { + var main = content.document.getElementById("main"); + Assert.equal( + main.innerHTML, + "<i>Italic</i> Test <b>Bold</b> After<b></b>", + "Copy and paste html 2" + ); + }); + + // Next, check that the Copy Image command works. + + // The context menu needs to be opened to properly initialize for the copy + // image command to run. + let contextMenu = document.getElementById("contentAreaContextMenu"); + let contextMenuShown = promisePopupShown(contextMenu); + BrowserTestUtils.synthesizeMouseAtCenter( + "#img", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + await contextMenuShown; + + document.getElementById("context-copyimage-contents").doCommand(); + + contextMenu.hidePopup(); + await promisePopupHidden(contextMenu); + + // Focus the content again + await SimpleTest.promiseFocus(browser); + + pastePromise = SpecialPowers.spawn( + browser, + [htmlPrefix, htmlPostfix, allowPaste], + (htmlPrefixChild, htmlPostfixChild, allowPaste) => { + var doc = content.document; + var main = doc.getElementById("main"); + main.focus(); + + return new Promise((resolve, reject) => { + content.addEventListener( + "paste", + event => { + let clipboardData = event.clipboardData; + + // DataTransfer doesn't support the image types yet, so only text/html + // will be present. + let clipboardText = clipboardData.getData("text/html"); + if (allowPaste) { + if ( + clipboardText !== + htmlPrefixChild + + '<img id="img" tabindex="1" src="http://example.org/browser/browser/base/content/test/general/moz.png">' + + htmlPostfixChild + ) { + reject( + "Clipboard Data did not contain an image, was " + + clipboardText + ); + } + } else if (clipboardText !== "") { + reject("Clipboard Data should be empty, was " + clipboardText); + } + resolve(); + }, + { capture: true, once: true } + ); + }); + } + ); + + await SpecialPowers.spawn(browser, [], () => {}); + await sendKey("v"); + await pastePromise; + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest( + mockCA.calls[0], + htmlPrefix + + '<img id="img" tabindex="1" src="http://example.org/browser/browser/base/content/test/general/moz.png">' + + htmlPostfix + ); + mockCA.clearCalls(); + + // The new content should now include an image. + await SpecialPowers.spawn(browser, [], () => { + var main = content.document.getElementById("main"); + Assert.equal( + main.innerHTML, + '<i>Italic</i> <img id="img" tabindex="1" ' + + 'src="http://example.org/browser/browser/base/content/test/general/moz.png">' + + "Test <b>Bold</b> After<b></b>", + "Paste after copy image" + ); + }); + + gBrowser.removeCurrentTab(); +} + +function assertContentAnalysisRequest(request, expectedText) { + is( + request.url.spec, + "data:text/html," + escape(testPage), + "request has correct URL" + ); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.eBulkDataEntry, + "request has correct analysisType" + ); + is( + request.operationTypeForDisplay, + Ci.nsIContentAnalysisRequest.eClipboard, + "request has correct operationTypeForDisplay" + ); + is(request.filePath, "", "request filePath should be empty"); + is(request.textContent, expectedText, "request textContent should match"); + is(request.printDataHandle, 0, "request printDataHandle should not be 0"); + is(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} +add_task(async function testClipboardWithContentAnalysisAllow() { + await testClipboardWithContentAnalysis(true); +}); + +add_task(async function testClipboardWithContentAnalysisBlock() { + await testClipboardWithContentAnalysis(false); +}); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_file_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_file_content_analysis.js new file mode 100644 index 0000000000..8441c4d7fd --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_file_content_analysis.js @@ -0,0 +1,202 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that (real) files can be pasted into chrome/content. +// Pasting files should also hide all other data from content. + +function setClipboard(path) { + const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(path); + + const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + trans.addDataFlavor("application/x-moz-file"); + trans.setTransferData("application/x-moz-file", file); + + trans.addDataFlavor("text/plain"); + const str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + str.data = "Alternate"; + trans.setTransferData("text/plain", str); + + // Write to clipboard. + Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard); +} + +let mockCA = makeMockContentAnalysis(); + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); +}); + +const PAGE_URL = + "https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_file.html"; + +function assertContentAnalysisRequest( + request, + expectedDisplayType, + expectedFilePath, + expectedText +) { + is(request.url.spec, PAGE_URL, "request has correct URL"); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.eBulkDataEntry, + "request has correct analysisType" + ); + is( + request.operationTypeForDisplay, + expectedDisplayType, + "request has correct operationTypeForDisplay" + ); + is(request.filePath, expectedFilePath, "request filePath should match"); + is(request.textContent, expectedText, "request textContent should match"); + is(request.printDataHandle, 0, "request printDataHandle should not be 0"); + is(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} +function assertContentAnalysisRequestFile(request, expectedFilePath) { + assertContentAnalysisRequest( + request, + Ci.nsIContentAnalysisRequest.eCustomDisplayString, + expectedFilePath, + "" + ); +} +function assertContentAnalysisRequestText(request, expectedText) { + assertContentAnalysisRequest( + request, + Ci.nsIContentAnalysisRequest.eClipboard, + "", + expectedText + ); +} + +async function testClipboardPasteFileWithContentAnalysis(allowPaste) { + mockCA.setupForTest(allowPaste); + await SpecialPowers.pushPrefEnv({ + set: [["dom.events.dataTransfer.mozFile.enabled", true]], + }); + + // Create a temporary file that will be pasted. + const file = await IOUtils.createUniqueFile( + PathUtils.tempDir, + "test-file.txt", + 0o600 + ); + const FILE_TEXT = "Hello World!"; + await IOUtils.writeUTF8(file, FILE_TEXT); + + // Put the data directly onto the native clipboard to make sure + // it isn't handled internally in Gecko somehow. + setClipboard(file); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL); + let browser = tab.linkedBrowser; + + let resultPromise = SpecialPowers.spawn(browser, [allowPaste], allowPaste => { + return new Promise(resolve => { + content.document.addEventListener("testresult", event => { + resolve(event.detail.result); + }); + content.document.getElementById("pasteAllowed").checked = allowPaste; + }); + }); + + // Focus <input> in content + await SpecialPowers.spawn(browser, [], async function () { + content.document.getElementById("input").focus(); + }); + + // Paste file into <input> in content + await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser); + + let result = await resultPromise; + is( + result, + allowPaste ? PathUtils.filename(file) : "", + "Correctly pasted file in content" + ); + is(mockCA.calls.length, 2, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequestFile(mockCA.calls[0], file); + assertContentAnalysisRequestText(mockCA.calls[1], "Alternate"); + mockCA.clearCalls(); + + // The following part of the test is done in-process (note the use of document here instead of + // content.document) so none of this should go through content analysis. + var input = document.createElement("input"); + document.documentElement.appendChild(input); + input.focus(); + + await new Promise((resolve, _reject) => { + input.addEventListener( + "paste", + function (event) { + let dt = event.clipboardData; + is(dt.types.length, 3, "number of types"); + ok(dt.types.includes("text/plain"), "text/plain exists in types"); + ok( + dt.types.includes("application/x-moz-file"), + "application/x-moz-file exists in types" + ); + is(dt.types[2], "Files", "Last type should be 'Files'"); + ok( + dt.mozTypesAt(0).contains("text/plain"), + "text/plain exists in mozTypesAt" + ); + is( + dt.getData("text/plain"), + "Alternate", + "text/plain returned in getData" + ); + is( + dt.mozGetDataAt("text/plain", 0), + "Alternate", + "text/plain returned in mozGetDataAt" + ); + + ok( + dt.mozTypesAt(0).contains("application/x-moz-file"), + "application/x-moz-file exists in mozTypesAt" + ); + let mozFile = dt.mozGetDataAt("application/x-moz-file", 0); + + ok( + mozFile instanceof Ci.nsIFile, + "application/x-moz-file returned nsIFile with mozGetDataAt" + ); + + is( + mozFile.leafName, + PathUtils.filename(file), + "nsIFile has correct leafName" + ); + + is(mozFile.fileSize, FILE_TEXT.length, "nsIFile has correct length"); + + resolve(); + }, + { capture: true, once: true } + ); + + EventUtils.synthesizeKey("v", { accelKey: true }); + }); + is(mockCA.calls.length, 0, "Correct number of calls to Content Analysis"); + + input.remove(); + + BrowserTestUtils.removeTab(tab); + + await IOUtils.remove(file); +} + +add_task(async function testClipboardPasteFileWithContentAnalysisAllow() { + await testClipboardPasteFileWithContentAnalysis(true); +}); + +add_task(async function testClipboardPasteFileWithContentAnalysisBlock() { + await testClipboardPasteFileWithContentAnalysis(false); +}); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_inputandtextarea_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_inputandtextarea_content_analysis.js new file mode 100644 index 0000000000..6fe37c3368 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_inputandtextarea_content_analysis.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let mockCA = makeMockContentAnalysis(); + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); +}); + +const PAGE_URL = + "https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_inputandtextarea.html"; +const CLIPBOARD_TEXT_STRING = "Just some text"; +async function testClipboardPaste(allowPaste) { + mockCA.setupForTest(allowPaste); + + setClipboardData(); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL); + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [allowPaste], async allowPaste => { + content.document.getElementById("pasteAllowed").checked = allowPaste; + }); + await testPasteWithElementId("testInput", browser, allowPaste); + await testPasteWithElementId("testTextArea", browser, allowPaste); + + BrowserTestUtils.removeTab(tab); +} + +function setClipboardData() { + const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + trans.addDataFlavor("text/plain"); + const str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + str.data = CLIPBOARD_TEXT_STRING; + trans.setTransferData("text/plain", str); + + // Write to clipboard. + Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard); +} + +async function testPasteWithElementId(elementId, browser, allowPaste) { + let resultPromise = SpecialPowers.spawn(browser, [], () => { + return new Promise(resolve => { + content.document.addEventListener( + "testresult", + event => { + resolve(event.detail.result); + }, + { once: true } + ); + }); + }); + + // Paste into content + await SpecialPowers.spawn(browser, [elementId], async elementId => { + content.document.getElementById(elementId).focus(); + }); + await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser); + let result = await resultPromise; + is(result, undefined, "Got unexpected result from page"); + + // Because we call event.clipboardData.getData in the test, this causes another call to + // content analysis. + is(mockCA.calls.length, 2, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0], CLIPBOARD_TEXT_STRING); + assertContentAnalysisRequest(mockCA.calls[1], CLIPBOARD_TEXT_STRING); + mockCA.clearCalls(); + let value = await getElementValue(browser, elementId); + is( + value, + allowPaste ? CLIPBOARD_TEXT_STRING : "", + "element has correct value" + ); +} + +function assertContentAnalysisRequest(request, expectedText) { + is(request.url.spec, PAGE_URL, "request has correct URL"); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.eBulkDataEntry, + "request has correct analysisType" + ); + is( + request.operationTypeForDisplay, + Ci.nsIContentAnalysisRequest.eClipboard, + "request has correct operationTypeForDisplay" + ); + is(request.filePath, "", "request filePath should match"); + is(request.textContent, expectedText, "request textContent should match"); + is(request.printDataHandle, 0, "request printDataHandle should not be 0"); + is(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} + +async function getElementValue(browser, elementId) { + return await SpecialPowers.spawn(browser, [elementId], async elementId => { + return content.document.getElementById(elementId).value; + }); +} + +add_task(async function testClipboardPasteWithContentAnalysisAllow() { + await testClipboardPaste(true); +}); + +add_task(async function testClipboardPasteWithContentAnalysisBlock() { + await testClipboardPaste(false); +}); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_noformatting_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_noformatting_content_analysis.js new file mode 100644 index 0000000000..0eedcddd17 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_noformatting_content_analysis.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let mockCA = makeMockContentAnalysis(); + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); +}); + +const PAGE_URL = + "https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_noformatting.html"; +async function testClipboardPasteNoFormatting(allowPaste) { + mockCA.setupForTest(allowPaste); + + const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + const CLIPBOARD_TEXT_STRING = "Some text"; + const CLIPBOARD_HTML_STRING = "<b>Some HTML</b>"; + { + trans.addDataFlavor("text/plain"); + const str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + str.data = CLIPBOARD_TEXT_STRING; + trans.setTransferData("text/plain", str); + } + { + trans.addDataFlavor("text/html"); + const str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + str.data = CLIPBOARD_HTML_STRING; + trans.setTransferData("text/html", str); + } + + // Write to clipboard. + Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL); + let browser = tab.linkedBrowser; + let result = await SpecialPowers.spawn(browser, [allowPaste], allowPaste => { + return new Promise(resolve => { + content.document.addEventListener("testresult", event => { + resolve(event.detail.result); + }); + content.document.getElementById("pasteAllowed").checked = allowPaste; + content.document.dispatchEvent(new content.CustomEvent("teststart", {})); + }); + }); + is(result, true, "Got unexpected result from page"); + + // Because we call event.clipboardData.getData in the test, this causes another call to + // content analysis. + is(mockCA.calls.length, 2, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0], CLIPBOARD_TEXT_STRING); + assertContentAnalysisRequest(mockCA.calls[1], CLIPBOARD_TEXT_STRING); + + BrowserTestUtils.removeTab(tab); +} + +function assertContentAnalysisRequest(request, expectedText) { + is(request.url.spec, PAGE_URL, "request has correct URL"); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.eBulkDataEntry, + "request has correct analysisType" + ); + is( + request.operationTypeForDisplay, + Ci.nsIContentAnalysisRequest.eClipboard, + "request has correct operationTypeForDisplay" + ); + is(request.filePath, "", "request filePath should match"); + is(request.textContent, expectedText, "request textContent should match"); + is(request.printDataHandle, 0, "request printDataHandle should not be 0"); + is(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} + +add_task( + async function testClipboardPasteNoFormattingWithContentAnalysisAllow() { + await testClipboardPasteNoFormatting(true); + } +); + +add_task( + async function testClipboardPasteNoFormattingWithContentAnalysisBlock() { + await testClipboardPasteNoFormatting(false); + } +); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_prompt_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_prompt_content_analysis.js new file mode 100644 index 0000000000..9e57250cc7 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_paste_prompt_content_analysis.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +let mockCA = makeMockContentAnalysis(); + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); +}); + +// Using an external page so the test can checks that the URL matches in the nsIContentAnalysisRequest +const PAGE_URL = + "https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_prompt.html"; +const CLIPBOARD_TEXT_STRING = "Just some text"; +async function testClipboardPaste(allowPaste) { + mockCA.setupForTest(allowPaste); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL); + let browser = tab.linkedBrowser; + + let promptPromise = SpecialPowers.spawn(browser, [], async () => { + return content.prompt(); + }); + + let prompt = await PromptTestUtils.waitForPrompt(browser, { + modalType: Services.prompt.MODAL_TYPE_CONTENT, + }); + // Paste text into prompt() in content + let pastePromise = new Promise(resolve => { + prompt.ui.loginTextbox.addEventListener( + "paste", + () => { + // Since mockCA uses setTimeout before invoking the callback, + // do it here too + setTimeout(() => { + resolve(); + }, 0); + }, + { once: true } + ); + }); + let ev = new ClipboardEvent("paste", { + dataType: "text/plain", + data: CLIPBOARD_TEXT_STRING, + }); + prompt.ui.loginTextbox.dispatchEvent(ev); + await pastePromise; + + // Close the prompt + await PromptTestUtils.handlePrompt(prompt); + + let result = await promptPromise; + is( + result, + allowPaste ? CLIPBOARD_TEXT_STRING : "", + "prompt has correct value" + ); + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0], CLIPBOARD_TEXT_STRING); + + BrowserTestUtils.removeTab(tab); +} + +function assertContentAnalysisRequest(request, expectedText) { + is(request.url.spec, PAGE_URL, "request has correct URL"); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.eBulkDataEntry, + "request has correct analysisType" + ); + is( + request.operationTypeForDisplay, + Ci.nsIContentAnalysisRequest.eClipboard, + "request has correct operationTypeForDisplay" + ); + is(request.filePath, null, "request filePath should match"); + is(request.textContent, expectedText, "request textContent should match"); + is(request.printDataHandle, 0, "request printDataHandle should not be 0"); + is(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} + +add_task(async function testClipboardPasteWithContentAnalysisAllow() { + await testClipboardPaste(true); +}); + +add_task(async function testClipboardPasteWithContentAnalysisBlock() { + await testClipboardPaste(false); +}); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_clipboard_read_async_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_read_async_content_analysis.js new file mode 100644 index 0000000000..7d180a048b --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_clipboard_read_async_content_analysis.js @@ -0,0 +1,195 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let mockCA = makeMockContentAnalysis(); + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.events.asyncClipboard.readText", true], + // This pref turns off the "Paste" popup + ["dom.events.testing.asyncClipboard", true], + ], + }); +}); + +const PAGE_URL = + "https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_read_async.html"; +const CLIPBOARD_TEXT_STRING = "Some plain text"; +const CLIPBOARD_HTML_STRING = "<b>Some HTML</b>"; +async function testClipboardReadAsync(allowPaste) { + mockCA.setupForTest(allowPaste); + + setClipboardData(); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL); + let browser = tab.linkedBrowser; + { + let result = await setDataAndStartTest(browser, allowPaste, "read"); + is(result, true, "Got unexpected result from page for read()"); + + is( + mockCA.calls.length, + 2, + "Correct number of calls to Content Analysis for read()" + ); + // On Windows, widget adds extra data into HTML clipboard. + let expectedHtml = navigator.platform.includes("Win") + ? `<html><body>\n<!--StartFragment-->${CLIPBOARD_HTML_STRING}<!--EndFragment-->\n</body>\n</html>` + : CLIPBOARD_HTML_STRING; + + assertContentAnalysisRequest(mockCA.calls[0], expectedHtml); + assertContentAnalysisRequest(mockCA.calls[1], CLIPBOARD_TEXT_STRING); + mockCA.clearCalls(); + } + + { + let result = await setDataAndStartTest(browser, allowPaste, "readText"); + is(result, true, "Got unexpected result from page for readText()"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis for read()" + ); + assertContentAnalysisRequest(mockCA.calls[0], CLIPBOARD_TEXT_STRING); + mockCA.clearCalls(); + } + + BrowserTestUtils.removeTab(tab); +} + +async function testClipboardReadAsyncWithErrorHelper() { + mockCA.setupForTestWithError(Cr.NS_ERROR_NOT_AVAILABLE); + + setClipboardData(); + + // This test throws a number of exceptions, so tell the framework this is OK. + // If an exception is thrown we won't get the right response from setDataAndStartTest() + // so this should be safe to do. + ignoreAllUncaughtExceptions(); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL); + let browser = tab.linkedBrowser; + { + let result = await setDataAndStartTest(browser, false, "read", true); + is(result, true, "Got unexpected result from page for read()"); + + is( + mockCA.calls.length, + 2, + "Correct number of calls to Content Analysis for read()" + ); + // On Windows, widget adds extra data into HTML clipboard. + let expectedHtml = navigator.platform.includes("Win") + ? `<html><body>\n<!--StartFragment-->${CLIPBOARD_HTML_STRING}<!--EndFragment-->\n</body>\n</html>` + : CLIPBOARD_HTML_STRING; + + assertContentAnalysisRequest(mockCA.calls[0], expectedHtml); + assertContentAnalysisRequest(mockCA.calls[1], CLIPBOARD_TEXT_STRING); + mockCA.clearCalls(); + } + + { + let result = await setDataAndStartTest(browser, false, "readText", true); + is(result, true, "Got unexpected result from page for readText()"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis for read()" + ); + assertContentAnalysisRequest(mockCA.calls[0], CLIPBOARD_TEXT_STRING); + mockCA.clearCalls(); + } + + BrowserTestUtils.removeTab(tab); +} + +function setDataAndStartTest( + browser, + allowPaste, + testType, + shouldError = false +) { + return SpecialPowers.spawn( + browser, + [allowPaste, testType, shouldError], + (allowPaste, testType, shouldError) => { + return new Promise(resolve => { + content.document.addEventListener( + "testresult", + event => { + resolve(event.detail.result); + }, + { once: true } + ); + content.document.getElementById("pasteAllowed").checked = allowPaste; + content.document.getElementById("contentAnalysisReturnsError").checked = + shouldError; + content.document.dispatchEvent( + new content.CustomEvent("teststart", { + detail: Cu.cloneInto({ testType }, content), + }) + ); + }); + } + ); +} + +function assertContentAnalysisRequest(request, expectedText) { + is(request.url.spec, PAGE_URL, "request has correct URL"); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.eBulkDataEntry, + "request has correct analysisType" + ); + is( + request.operationTypeForDisplay, + Ci.nsIContentAnalysisRequest.eClipboard, + "request has correct operationTypeForDisplay" + ); + is(request.filePath, "", "request filePath should match"); + is(request.textContent, expectedText, "request textContent should match"); + is(request.printDataHandle, 0, "request printDataHandle should not be 0"); + is(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} + +function setClipboardData() { + const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + { + trans.addDataFlavor("text/plain"); + const str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + str.data = CLIPBOARD_TEXT_STRING; + trans.setTransferData("text/plain", str); + } + { + trans.addDataFlavor("text/html"); + const str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + str.data = CLIPBOARD_HTML_STRING; + trans.setTransferData("text/html", str); + } + + // Write to clipboard. + Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard); +} + +add_task(async function testClipboardReadAsyncWithContentAnalysisAllow() { + await testClipboardReadAsync(true); +}); + +add_task(async function testClipboardReadAsyncWithContentAnalysisBlock() { + await testClipboardReadAsync(false); +}); + +add_task(async function testClipboardReadAsyncWithError() { + await testClipboardReadAsyncWithErrorHelper(); +}); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js b/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js index b226c0a37a..e7122508e1 100644 --- a/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js +++ b/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js @@ -25,7 +25,8 @@ const kAgentNamePref = "agent_name"; const kClientSignaturePref = "client_signature"; const kPerUserPref = "is_per_user"; const kShowBlockedPref = "show_blocked_result"; -const kDefaultAllowPref = "default_allow"; +const kDefaultResultPref = "default_result"; +const kBypassForSameTabOperationsPref = "bypass_for_same_tab_operations"; const ca = Cc["@mozilla.org/contentanalysis;1"].getService( Ci.nsIContentAnalysis @@ -87,7 +88,8 @@ add_task(async function test_ca_enterprise_config() { ClientSignature: string4, IsPerUser: true, ShowBlockedResult: false, - DefaultAllow: true, + DefaultResult: 1, + BypassForSameTabOperations: true, }, }, }); @@ -135,9 +137,16 @@ add_task(async function test_ca_enterprise_config() { "show blocked match" ); is( - Services.prefs.getBoolPref("browser.contentanalysis." + kDefaultAllowPref), + Services.prefs.getIntPref("browser.contentanalysis." + kDefaultResultPref), + 1, + "default result match" + ); + is( + Services.prefs.getBoolPref( + "browser.contentanalysis." + kBypassForSameTabOperationsPref + ), true, - "default allow match" + "bypass for same tab operations match" ); PoliciesPrefTracker.stop(); }); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_print_changing_page_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_print_changing_page_content_analysis.js index 72a7dcbb91..0f2d846627 100644 --- a/toolkit/components/contentanalysis/tests/browser/browser_print_changing_page_content_analysis.js +++ b/toolkit/components/contentanalysis/tests/browser/browser_print_changing_page_content_analysis.js @@ -12,69 +12,7 @@ const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( Ci.nsIPrintSettingsService ); -let mockCA = { - isActive: true, - mightBeActive: true, - errorValue: undefined, - - setupForTest(shouldAllowRequest) { - this.shouldAllowRequest = shouldAllowRequest; - this.errorValue = undefined; - this.calls = []; - }, - - setupForTestWithError(errorValue) { - this.errorValue = errorValue; - this.calls = []; - }, - - getAction() { - if (this.shouldAllowRequest === undefined) { - this.shouldAllowRequest = true; - } - return this.shouldAllowRequest - ? Ci.nsIContentAnalysisResponse.eAllow - : Ci.nsIContentAnalysisResponse.eBlock; - }, - - // nsIContentAnalysis methods - async analyzeContentRequest(request, _autoAcknowledge) { - info( - "Mock ContentAnalysis service: analyzeContentRequest, this.shouldAllowRequest=" + - this.shouldAllowRequest + - ", this.errorValue=" + - this.errorValue - ); - this.calls.push(request); - if (this.errorValue) { - throw this.errorValue; - } - // Use setTimeout to simulate an async activity - await new Promise(res => setTimeout(res, 0)); - return makeContentAnalysisResponse(this.getAction(), request.requestToken); - }, - - analyzeContentRequestCallback(request, autoAcknowledge, callback) { - info( - "Mock ContentAnalysis service: analyzeContentRequestCallback, this.shouldAllowRequest=" + - this.shouldAllowRequest + - ", this.errorValue=" + - this.errorValue - ); - this.calls.push(request); - if (this.errorValue) { - throw this.errorValue; - } - let response = makeContentAnalysisResponse( - this.getAction(), - request.requestToken - ); - // Use setTimeout to simulate an async activity - setTimeout(() => { - callback.contentResult(response); - }, 0); - }, -}; +let mockCA = makeMockContentAnalysis(); add_setup(async function test_setup() { mockCA = mockContentAnalysisService(mockCA); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js index 9b4c0ffa60..05897b5ca6 100644 --- a/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js +++ b/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js @@ -12,73 +12,7 @@ const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( Ci.nsIPrintSettingsService ); -let mockCA = { - isActive: true, - mightBeActive: true, - errorValue: undefined, - - setupForTest(shouldAllowRequest) { - this.shouldAllowRequest = shouldAllowRequest; - this.errorValue = undefined; - this.calls = []; - }, - - setupForTestWithError(errorValue) { - this.errorValue = errorValue; - this.calls = []; - }, - - clearCalls() { - this.calls = []; - }, - - getAction() { - if (this.shouldAllowRequest === undefined) { - this.shouldAllowRequest = true; - } - return this.shouldAllowRequest - ? Ci.nsIContentAnalysisResponse.eAllow - : Ci.nsIContentAnalysisResponse.eBlock; - }, - - // nsIContentAnalysis methods - async analyzeContentRequest(request, _autoAcknowledge) { - info( - "Mock ContentAnalysis service: analyzeContentRequest, this.shouldAllowRequest=" + - this.shouldAllowRequest + - ", this.errorValue=" + - this.errorValue - ); - this.calls.push(request); - if (this.errorValue) { - throw this.errorValue; - } - // Use setTimeout to simulate an async activity - await new Promise(res => setTimeout(res, 0)); - return makeContentAnalysisResponse(this.getAction(), request.requestToken); - }, - - analyzeContentRequestCallback(request, autoAcknowledge, callback) { - info( - "Mock ContentAnalysis service: analyzeContentRequestCallback, this.shouldAllowRequest=" + - this.shouldAllowRequest + - ", this.errorValue=" + - this.errorValue - ); - this.calls.push(request); - if (this.errorValue) { - throw this.errorValue; - } - let response = makeContentAnalysisResponse( - this.getAction(), - request.requestToken - ); - // Use setTimeout to simulate an async activity - setTimeout(() => { - callback.contentResult(response); - }, 0); - }, -}; +let mockCA = makeMockContentAnalysis(); add_setup(async function test_setup() { mockCA = mockContentAnalysisService(mockCA); diff --git a/toolkit/components/contentanalysis/tests/browser/clipboard_paste_file.html b/toolkit/components/contentanalysis/tests/browser/clipboard_paste_file.html new file mode 100644 index 0000000000..9604633842 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/clipboard_paste_file.html @@ -0,0 +1,61 @@ +<html><body> +<script> +async function checkPaste(event) { + let result = null; + try { + result = await checkPasteHelper(event); + } catch (e) { + result = e.toString(); + } + + document.dispatchEvent(new CustomEvent('testresult', { + detail: { result } + })); +} + +function is(a, b, msg) { + if (!Object.is(a, b)) { + throw new Error(`FAIL: expected ${b} got ${a} - ${msg}`); + } +} + +async function checkPasteHelper(event) { + let dt = event.clipboardData; + // Set by injected JS in the test + let filePasteAllowed = document.getElementById("pasteAllowed").checked; + + is(dt.types.length, 2, "Correct number of types"); + + // TODO: Remove application/x-moz-file from content. + is(dt.types[0], "application/x-moz-file", "First type") + is(dt.types[1], "Files", "Last type must be Files"); + + is(dt.getData("text/plain"), "", "text/plain found with getData"); + is(dt.getData("application/x-moz-file"), "", "application/x-moz-file found with getData"); + + if (!filePasteAllowed) { + is(dt.files.length, 0, "No files"); + } else { + is(dt.files.length, 1, "Correct number of files"); + is(dt.files[0].type, "text/plain", "Correct file type"); + } + is(dt.items.length, 1, "Correct number of items"); + is(dt.items[0].kind, "file", "Correct item kind"); + if (!filePasteAllowed) { + is(dt.items[0].type, "application/x-moz-file", "Correct item type"); + return ""; + } + is(dt.items[0].type, "text/plain", "Correct item type"); + + let file = dt.files[0]; + is(await file.text(), "Hello World!", "Pasted file contains right text"); + + return file.name; +} +</script> + +<input id="input" onpaste="checkPaste(event)"> + +<label for="pasteAllowed">Paste allowed?</label><input id="pasteAllowed" type="checkbox"> + +</body></html> diff --git a/toolkit/components/contentanalysis/tests/browser/clipboard_paste_inputandtextarea.html b/toolkit/components/contentanalysis/tests/browser/clipboard_paste_inputandtextarea.html new file mode 100644 index 0000000000..db93e85955 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/clipboard_paste_inputandtextarea.html @@ -0,0 +1,41 @@ +<html> +<body> + +<div id="content"> + <input + id="testInput" + type="text" onpaste="handlePaste(event)"> + <textarea id="testTextArea" onpaste="handlePaste(event)"></textarea> + + <label for="pasteAllowed">Paste allowed?</label><input id="pasteAllowed" type="checkbox"> +</div> +<script class="testbody" type="application/javascript"> +function is(a, b, msg) { + if (!Object.is(a, b)) { + throw new Error(`FAIL: expected ${b} got ${a} - ${msg}`); + } +} + +function checkPasteHelper(event) { + // Set by injected JS in the test + let filePasteAllowed = document.getElementById("pasteAllowed").checked; + is(event.clipboardData.getData('text/plain'), filePasteAllowed ? "Just some text" : "", "getData(text/plain) should return plain text"); + is(event.clipboardData.types.length, 1, "Correct number of types"); +} + +function handlePaste(e) { + let result = null; + try { + result = checkPasteHelper(e); + } catch (e) { + result = e.toString(); + } + + document.dispatchEvent(new CustomEvent('testresult', { + detail: { result } + })); +} +</script> + +</body> +</html> diff --git a/toolkit/components/contentanalysis/tests/browser/clipboard_paste_noformatting.html b/toolkit/components/contentanalysis/tests/browser/clipboard_paste_noformatting.html new file mode 100644 index 0000000000..eefc40de85 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/clipboard_paste_noformatting.html @@ -0,0 +1,46 @@ +<html> +<body> + +<script class="testbody" type="application/javascript"> +function is(a, b, msg) { + if (!Object.is(a, b)) { + throw new Error(`FAIL: expected ${b} got ${a} - ${msg}`); + } +} + +function checkPasteHelper(event) { + is(event.clipboardData.types.indexOf('text/html'), -1, "clipboardData shouldn't have text/html"); + // Set by injected JS in the test + let filePasteAllowed = document.getElementById("pasteAllowed").checked; + is(event.clipboardData.getData('text/plain'), filePasteAllowed ? "Some text" : "", "getData(text/plain) should return plain text"); + return true; +} + +window.addEventListener("paste", e => { + let result = null; + try { + result = checkPasteHelper(e); + } catch (e) { + result = e.toString(); + } + + document.dispatchEvent(new CustomEvent('testresult', { + detail: { result } + })); +}); + +document.addEventListener("teststart", _e => { + let editable = document.getElementById("editable1"); + editable.focus(); + + window.getSelection().selectAllChildren(editable); + + SpecialPowers.doCommand(window, "cmd_pasteNoFormatting"); +}); +</script> + +<div contenteditable="true" id="editable1"><b>Formatted Text</b><br></div> + +<label for="pasteAllowed">Paste allowed?</label><input id="pasteAllowed" type="checkbox"> +</body> +</html> diff --git a/toolkit/components/contentanalysis/tests/browser/clipboard_paste_prompt.html b/toolkit/components/contentanalysis/tests/browser/clipboard_paste_prompt.html new file mode 100644 index 0000000000..8017cc87a1 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/clipboard_paste_prompt.html @@ -0,0 +1,7 @@ +<html> +<body> + +<div id="content"></div> + +</body> +</html> diff --git a/toolkit/components/contentanalysis/tests/browser/clipboard_read_async.html b/toolkit/components/contentanalysis/tests/browser/clipboard_read_async.html new file mode 100644 index 0000000000..14414a0f28 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/clipboard_read_async.html @@ -0,0 +1,95 @@ +<html> +<body> + +<!--<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>--> +<script class="testbody" type="application/javascript"> +function is(a, b, msg) { + if (!Object.is(a, b)) { + throw new Error(`FAIL: expected ${b} got ${a} - ${msg}`); + } +} + +function isNotAllowedException(ex) { + return /NS_ERROR_CONTENT_BLOCKED/.test(ex.toString()) || + /DataError/.test(ex.toString()) || + /NotAllowedError/.test(ex.toString()); +} + +async function checkReadHelper(readResult, gotNotAllowedException, isReadTextTest) { + // Set by injected JS in the test + let filePasteAllowed = document.getElementById("pasteAllowed").checked; + let contentAnalysisReturnsError = document.getElementById("contentAnalysisReturnsError").checked; + filePasteAllowed = filePasteAllowed && !contentAnalysisReturnsError; + is(gotNotAllowedException, !filePasteAllowed && isReadTextTest, "Should get exception from readText() if not allowed"); + if (isReadTextTest) { + is(readResult, filePasteAllowed ? "Some plain text" : null, "Should get expected text from clipboard.readText()"); + } + else { + is(readResult.length, 1, "check number of ClipboardItems in response"); + is(readResult[0].types.length, 2, "check number of types in ClipboardItem"); + + { + let text = null; + let gotNotAllowedException = false; + try { + let textBlob = await readResult[0].getType("text/plain"); + text = await textBlob.text(); + } catch (ex) { + gotNotAllowedException = isNotAllowedException(ex); + } + is(gotNotAllowedException, !filePasteAllowed, "should get exception from reading text data when blocked"); + if (filePasteAllowed) { + is(text, "Some plain text", "check text/plain data"); + } + } + + { + let html = null; + let gotNotAllowedException = false; + try { + let htmlBlob = await readResult[0].getType("text/html"); + html = await htmlBlob.text(); + } catch (ex) { + gotNotAllowedException = isNotAllowedException(ex); + } + is(gotNotAllowedException, !filePasteAllowed, "should get exception from reading html data when blocked"); + if (filePasteAllowed) { + const CLIPBOARD_HTML_STRING = "<b>Some HTML</b>"; + let expectedHtml = navigator.platform.includes("Win") + ? `<html><body>\n<!--StartFragment-->${CLIPBOARD_HTML_STRING}<!--EndFragment-->\n</body>\n</html>` + : CLIPBOARD_HTML_STRING; + is(html, expectedHtml, "check text/html data"); + } + } + } + return true; +} + +document.addEventListener("teststart", async e => { + let isReadTextTest = e.detail.testType == "readText"; + let gotNotAllowedException = false; + let readResult = null; + try { + let readPromise = isReadTextTest ? navigator.clipboard.readText() : navigator.clipboard.read(); + readResult = await readPromise; + } catch (ex) { + gotNotAllowedException = isNotAllowedException(ex); + } + + let result = null; + try { + result = checkReadHelper(readResult, gotNotAllowedException, isReadTextTest); + } catch (ex) { + result = ex.toString(); + } + + document.dispatchEvent(new CustomEvent('testresult', { + detail: { result } + })); +}); +</script> + +<label for="pasteAllowed">Paste allowed?</label><input id="pasteAllowed" type="checkbox"> +<label for="contentAnalysisReturnsError">Content Analysis returns error?</label><input id="contentAnalysisReturnsError" type="checkbox"> +</body> +</html> diff --git a/toolkit/components/contentanalysis/tests/browser/head.js b/toolkit/components/contentanalysis/tests/browser/head.js index e645caa2d7..9422a62ff2 100644 --- a/toolkit/components/contentanalysis/tests/browser/head.js +++ b/toolkit/components/contentanalysis/tests/browser/head.js @@ -112,3 +112,127 @@ async function waitForFileToAlmostMatchSize(filePath, expectedSize) { return Math.abs(fileStat.size - expectedSize) <= maxSizeDifference; }, "Sizes should (almost) match"); } + +function makeMockContentAnalysis() { + return { + isActive: true, + mightBeActive: true, + errorValue: undefined, + + setupForTest(shouldAllowRequest) { + this.shouldAllowRequest = shouldAllowRequest; + this.errorValue = undefined; + this.calls = []; + }, + + setupForTestWithError(errorValue) { + this.errorValue = errorValue; + this.calls = []; + }, + + clearCalls() { + this.calls = []; + }, + + getAction() { + if (this.shouldAllowRequest === undefined) { + this.shouldAllowRequest = true; + } + return this.shouldAllowRequest + ? Ci.nsIContentAnalysisResponse.eAllow + : Ci.nsIContentAnalysisResponse.eBlock; + }, + + // nsIContentAnalysis methods + async analyzeContentRequest(request, _autoAcknowledge) { + info( + "Mock ContentAnalysis service: analyzeContentRequest, this.shouldAllowRequest=" + + this.shouldAllowRequest + + ", this.errorValue=" + + this.errorValue + ); + this.calls.push(request); + if (this.errorValue) { + throw this.errorValue; + } + // Use setTimeout to simulate an async activity + await new Promise(res => setTimeout(res, 0)); + return makeContentAnalysisResponse( + this.getAction(), + request.requestToken + ); + }, + + analyzeContentRequestCallback(request, autoAcknowledge, callback) { + info( + "Mock ContentAnalysis service: analyzeContentRequestCallback, this.shouldAllowRequest=" + + this.shouldAllowRequest + + ", this.errorValue=" + + this.errorValue + ); + this.calls.push(request); + if (this.errorValue) { + throw this.errorValue; + } + let response = makeContentAnalysisResponse( + this.getAction(), + request.requestToken + ); + // Use setTimeout to simulate an async activity + setTimeout(() => { + callback.contentResult(response); + }, 0); + }, + }; +} + +function whenTabLoaded(aTab, aCallback) { + promiseTabLoadEvent(aTab).then(aCallback); +} + +function promiseTabLoaded(aTab) { + return new Promise(resolve => { + whenTabLoaded(aTab, resolve); + }); +} + +/** + * Waits for a load (or custom) event to finish in a given tab. If provided + * load an uri into the tab. + * + * @param {object} tab + * The tab to load into. + * @param {string} [url] + * The url to load, or the current url. + * @returns {Promise<string>} resolved when the event is handled. Rejected if + * a valid load event is not received within a meaningful interval + */ +function promiseTabLoadEvent(tab, url) { + info("Wait tab event: load"); + + function handle(loadedUrl) { + if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) { + info(`Skipping spurious load event for ${loadedUrl}`); + return false; + } + + info("Tab event received: load"); + return true; + } + + let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle); + + if (url) { + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url); + } + + return loaded; +} + +function promisePopupShown(popup) { + return BrowserTestUtils.waitForPopupEvent(popup, "shown"); +} + +function promisePopupHidden(popup) { + return BrowserTestUtils.waitForPopupEvent(popup, "hidden"); +} diff --git a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.cpp b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.cpp index cd083a7779..d974ac78db 100644 --- a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.cpp +++ b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.cpp @@ -8,13 +8,25 @@ #include "mozilla/Assertions.h" #include "mozilla/Logging.h" #include "mozilla/Preferences.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsComponentManagerUtils.h" #include "nsNetUtil.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsIURI.h" +#include "nsIURIMutator.h" #include "ContentAnalysis.h" +#include "SpecialSystemDirectory.h" +#include "TestContentAnalysisUtils.h" #include <processenv.h> #include <synchapi.h> +#include <vector> const char* kAllowUrlPref = "browser.contentanalysis.allow_url_regex_list"; const char* kDenyUrlPref = "browser.contentanalysis.deny_url_regex_list"; +const char* kPipePathNamePref = "browser.contentanalysis.pipe_path_name"; +const char* kIsDLPEnabledPref = "browser.contentanalysis.enabled"; +const char* kTimeoutPref = "browser.contentanalysis.agent_timeout"; using namespace mozilla; using namespace mozilla::contentanalysis; @@ -24,6 +36,9 @@ class ContentAnalysisTest : public testing::Test { ContentAnalysisTest() { auto* logmodule = LogModule::Get("contentanalysis"); logmodule->SetLevel(LogLevel::Verbose); + MOZ_ALWAYS_SUCCEEDS( + Preferences::SetString(kPipePathNamePref, mPipeName.get())); + MOZ_ALWAYS_SUCCEEDS(Preferences::SetBool(kIsDLPEnabledPref, true)); nsCOMPtr<nsIContentAnalysis> caSvc = do_GetService("@mozilla.org/contentanalysis;1"); @@ -35,17 +50,39 @@ class ContentAnalysisTest : public testing::Test { mContentAnalysis->mAllowUrlList = {}; mContentAnalysis->mDenyUrlList = {}; + MOZ_ALWAYS_SUCCEEDS(mContentAnalysis->TestOnlySetCACmdLineArg(true)); + MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(kAllowUrlPref, "")); MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(kDenyUrlPref, "")); + + bool isActive = false; + MOZ_ALWAYS_SUCCEEDS(mContentAnalysis->GetIsActive(&isActive)); + EXPECT_TRUE(isActive); + } + + // Note that the constructor (and SetUp() method) get called once per test, + // not once for the whole fixture. Because Firefox does not currently + // reconnect to an agent after the DLP pipe is closed (bug 1888293), we only + // want to create the agent once and make sure the same process stays alive + // through all of these tests. + static void SetUpTestSuite() { + GeneratePipeName(L"contentanalysissdk-gtest-", mPipeName); + mAgentInfo = LaunchAgentNormal(L"block", mPipeName); } + static void TearDownTestSuite() { mAgentInfo.TerminateProcess(); } + void TearDown() override { mContentAnalysis->mParsedUrlLists = false; mContentAnalysis->mAllowUrlList = {}; mContentAnalysis->mDenyUrlList = {}; + MOZ_ALWAYS_SUCCEEDS(mContentAnalysis->TestOnlySetCACmdLineArg(false)); + MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(kAllowUrlPref, "")); MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(kDenyUrlPref, "")); + MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(kPipePathNamePref)); + MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(kIsDLPEnabledPref)); } already_AddRefed<nsIContentAnalysisRequest> CreateRequest(const char* aUrl) { @@ -62,6 +99,8 @@ class ContentAnalysisTest : public testing::Test { } RefPtr<ContentAnalysis> mContentAnalysis; + static nsString mPipeName; + static MozAgentInfo mAgentInfo; // Proxies for private members of ContentAnalysis. TEST_F // creates new subclasses -- they do not inherit `friend`s. @@ -71,6 +110,8 @@ class ContentAnalysisTest : public testing::Test { return mContentAnalysis->FilterByUrlLists(aReq); } }; +nsString ContentAnalysisTest::mPipeName; +MozAgentInfo ContentAnalysisTest::mAgentInfo; TEST_F(ContentAnalysisTest, AllowUrlList) { MOZ_ALWAYS_SUCCEEDS( @@ -124,3 +165,219 @@ TEST_F(ContentAnalysisTest, DenyOverridesAllowUrlList) { CreateRequest("https://example.org/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eDeny); } + +nsCOMPtr<nsIURI> GetExampleDotComURI() { + nsCOMPtr<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), "https://example.com")); + return uri; +} + +void SendRequestAndExpectResponse( + RefPtr<ContentAnalysis> contentAnalysis, + const nsCOMPtr<nsIContentAnalysisRequest>& request, + Maybe<bool> expectedShouldAllow, + Maybe<nsIContentAnalysisResponse::Action> expectedAction) { + std::atomic<bool> gotResponse = false; + std::atomic<bool> timedOut = false; + auto callback = MakeRefPtr<ContentAnalysisCallback>( + [&](nsIContentAnalysisResponse* response) { + if (expectedShouldAllow.isSome()) { + bool shouldAllow = false; + MOZ_ALWAYS_SUCCEEDS(response->GetShouldAllowContent(&shouldAllow)); + EXPECT_EQ(*expectedShouldAllow, shouldAllow); + } + if (expectedAction.isSome()) { + nsIContentAnalysisResponse::Action action; + MOZ_ALWAYS_SUCCEEDS(response->GetAction(&action)); + EXPECT_EQ(*expectedAction, action); + } + nsCString requestToken, originalRequestToken; + MOZ_ALWAYS_SUCCEEDS(response->GetRequestToken(requestToken)); + MOZ_ALWAYS_SUCCEEDS(request->GetRequestToken(originalRequestToken)); + EXPECT_EQ(originalRequestToken, requestToken); + gotResponse = true; + }, + [&gotResponse](nsresult error) { + EXPECT_EQ(NS_OK, error); + gotResponse = true; + // Make sure that we didn't somehow get passed NS_OK + FAIL() << "Got error response"; + }); + + MOZ_ALWAYS_SUCCEEDS( + contentAnalysis->AnalyzeContentRequestCallback(request, false, callback)); + RefPtr<CancelableRunnable> timer = + NS_NewCancelableRunnableFunction("Content Analysis timeout", [&] { + if (!gotResponse.load()) { + timedOut = true; + } + }); + NS_DelayedDispatchToCurrentThread(do_AddRef(timer), 10000); + mozilla::SpinEventLoopUntil("Waiting for ContentAnalysis result"_ns, [&]() { + return gotResponse.load() || timedOut.load(); + }); + timer->Cancel(); + EXPECT_TRUE(gotResponse); + EXPECT_FALSE(timedOut); +} + +TEST_F(ContentAnalysisTest, SendAllowedTextToAgent_GetAllowedResponse) { + nsCOMPtr<nsIURI> uri = GetExampleDotComURI(); + nsString allow(L"allow"); + nsCOMPtr<nsIContentAnalysisRequest> request = new ContentAnalysisRequest( + nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, std::move(allow), + false, EmptyCString(), uri, + nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); + + SendRequestAndExpectResponse(mContentAnalysis, request, Some(true), + Some(nsIContentAnalysisResponse::eAllow)); +} + +TEST_F(ContentAnalysisTest, SendBlockedTextToAgent_GetBlockResponse) { + nsCOMPtr<nsIURI> uri = GetExampleDotComURI(); + nsString block(L"block"); + nsCOMPtr<nsIContentAnalysisRequest> request = new ContentAnalysisRequest( + nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, std::move(block), + false, EmptyCString(), uri, + nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); + + SendRequestAndExpectResponse(mContentAnalysis, request, Some(false), + Some(nsIContentAnalysisResponse::eBlock)); +} + +class RawRequestObserver final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + RawRequestObserver() {} + + const std::vector<content_analysis::sdk::ContentAnalysisRequest>& + GetRequests() { + return mRequests; + } + + private: + ~RawRequestObserver() = default; + std::vector<content_analysis::sdk::ContentAnalysisRequest> mRequests; +}; + +NS_IMPL_ISUPPORTS(RawRequestObserver, nsIObserver); + +NS_IMETHODIMP RawRequestObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + std::wstring dataWideString(reinterpret_cast<const wchar_t*>(aData)); + std::vector<uint8_t> dataVector(dataWideString.size()); + for (size_t i = 0; i < dataWideString.size(); ++i) { + // Since this data is really bytes and not a null-terminated string, the + // calling code adds 0xFF00 to every member to ensure there are no 0 values. + dataVector[i] = static_cast<uint8_t>(dataWideString[i] - 0xFF00); + } + content_analysis::sdk::ContentAnalysisRequest request; + EXPECT_TRUE(request.ParseFromArray(dataVector.data(), dataVector.size())); + mRequests.push_back(std::move(request)); + return NS_OK; +} + +TEST_F(ContentAnalysisTest, CheckRawRequestWithText) { + MOZ_ALWAYS_SUCCEEDS(Preferences::SetInt(kTimeoutPref, 65)); + nsCOMPtr<nsIURI> uri = GetExampleDotComURI(); + nsString allow(L"allow"); + nsCOMPtr<nsIContentAnalysisRequest> request = new ContentAnalysisRequest( + nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, std::move(allow), + false, EmptyCString(), uri, + nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + auto rawRequestObserver = MakeRefPtr<RawRequestObserver>(); + MOZ_ALWAYS_SUCCEEDS( + obsServ->AddObserver(rawRequestObserver, "dlp-request-sent-raw", false)); + time_t now = time(nullptr); + + SendRequestAndExpectResponse(mContentAnalysis, request, Nothing(), Nothing()); + auto requests = rawRequestObserver->GetRequests(); + EXPECT_EQ(static_cast<size_t>(1), requests.size()); + time_t t = requests[0].expires_at(); + time_t secs_remaining = t - now; + // There should be around 65 seconds remaining + EXPECT_LE(abs(secs_remaining - 65), 2); + const auto& request_url = requests[0].request_data().url(); + EXPECT_EQ(uri->GetSpecOrDefault(), + nsCString(request_url.data(), request_url.size())); + nsCString request_user_action_id(requests[0].user_action_id().data(), + requests[0].user_action_id().size()); + // The user_action_id has a GUID appended to the end, just make sure the + // beginning is right. + request_user_action_id.Truncate(8); + EXPECT_EQ(nsCString("Firefox "), request_user_action_id); + const auto& request_text = requests[0].text_content(); + EXPECT_EQ(nsCString("allow"), + nsCString(request_text.data(), request_text.size())); + + MOZ_ALWAYS_SUCCEEDS( + obsServ->RemoveObserver(rawRequestObserver, "dlp-request-sent-raw")); + MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(kTimeoutPref)); +} + +TEST_F(ContentAnalysisTest, CheckRawRequestWithFile) { + nsCOMPtr<nsIURI> uri = GetExampleDotComURI(); + nsCOMPtr<nsIFile> file; + MOZ_ALWAYS_SUCCEEDS(GetSpecialSystemDirectory(OS_CurrentWorkingDirectory, + getter_AddRefs(file))); + nsString allowRelativePath(L"allowedFile.txt"); + MOZ_ALWAYS_SUCCEEDS(file->AppendRelativePath(allowRelativePath)); + nsString allowPath; + MOZ_ALWAYS_SUCCEEDS(file->GetPath(allowPath)); + + nsCOMPtr<nsIContentAnalysisRequest> request = new ContentAnalysisRequest( + nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, allowPath, true, + EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, + nullptr); + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + auto rawRequestObserver = MakeRefPtr<RawRequestObserver>(); + MOZ_ALWAYS_SUCCEEDS( + obsServ->AddObserver(rawRequestObserver, "dlp-request-sent-raw", false)); + + SendRequestAndExpectResponse(mContentAnalysis, request, Nothing(), Nothing()); + auto requests = rawRequestObserver->GetRequests(); + EXPECT_EQ(static_cast<size_t>(1), requests.size()); + const auto& request_url = requests[0].request_data().url(); + EXPECT_EQ(uri->GetSpecOrDefault(), + nsCString(request_url.data(), request_url.size())); + nsCString request_user_action_id(requests[0].user_action_id().data(), + requests[0].user_action_id().size()); + // The user_action_id has a GUID appended to the end, just make sure the + // beginning is right. + request_user_action_id.Truncate(8); + EXPECT_EQ(nsCString("Firefox "), request_user_action_id); + const auto& request_file_path = requests[0].file_path(); + EXPECT_EQ(NS_ConvertUTF16toUTF8(allowPath), + nsCString(request_file_path.data(), request_file_path.size())); + + MOZ_ALWAYS_SUCCEEDS( + obsServ->RemoveObserver(rawRequestObserver, "dlp-request-sent-raw")); +} + +TEST_F(ContentAnalysisTest, CheckTwoRequestsHaveSameUserActionId) { + nsCOMPtr<nsIURI> uri = GetExampleDotComURI(); + nsString allow(L"allow"); + nsCOMPtr<nsIContentAnalysisRequest> request = new ContentAnalysisRequest( + nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, std::move(allow), + false, EmptyCString(), uri, + nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + auto rawRequestObserver = MakeRefPtr<RawRequestObserver>(); + MOZ_ALWAYS_SUCCEEDS( + obsServ->AddObserver(rawRequestObserver, "dlp-request-sent-raw", false)); + + SendRequestAndExpectResponse(mContentAnalysis, request, Nothing(), Nothing()); + SendRequestAndExpectResponse(mContentAnalysis, request, Nothing(), Nothing()); + auto requests = rawRequestObserver->GetRequests(); + EXPECT_EQ(static_cast<size_t>(2), requests.size()); + EXPECT_EQ(requests[0].user_action_id(), requests[1].user_action_id()); + + MOZ_ALWAYS_SUCCEEDS( + obsServ->RemoveObserver(rawRequestObserver, "dlp-request-sent-raw")); +} diff --git a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisAgent.cpp b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisAgent.cpp index 5b2b76b963..d7a41cc5ff 100644 --- a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisAgent.cpp +++ b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisAgent.cpp @@ -8,29 +8,12 @@ #include "mozilla/Assertions.h" #include "mozilla/CmdLineAndEnvUtils.h" #include "content_analysis/sdk/analysis_client.h" -#include "TestContentAnalysisAgent.h" +#include "TestContentAnalysisUtils.h" #include <processenv.h> #include <synchapi.h> using namespace content_analysis::sdk; -MozAgentInfo LaunchAgentNormal(const wchar_t* aToBlock) { - nsString cmdLineArguments; - if (aToBlock && aToBlock[0] != 0) { - cmdLineArguments.Append(L" --toblock=.*"); - cmdLineArguments.Append(aToBlock); - cmdLineArguments.Append(L".*"); - } - cmdLineArguments.Append(L" --user"); - cmdLineArguments.Append(L" --path="); - nsString pipeName; - GeneratePipeName(L"contentanalysissdk-gtest-", pipeName); - cmdLineArguments.Append(pipeName); - MozAgentInfo agentInfo; - LaunchAgentWithCommandLineArguments(cmdLineArguments, pipeName, agentInfo); - return agentInfo; -} - TEST(ContentAnalysisAgent, TextShouldNotBeBlocked) { auto MozAgentInfo = LaunchAgentNormal(L"block"); @@ -49,10 +32,7 @@ TEST(ContentAnalysisAgent, TextShouldNotBeBlocked) response.results().Get(0).status()); ASSERT_EQ(0, response.results().Get(0).triggered_rules_size()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisAgent, TextShouldBeBlocked) @@ -75,10 +55,7 @@ TEST(ContentAnalysisAgent, TextShouldBeBlocked) ASSERT_EQ(ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK, response.results().Get(0).triggered_rules(0).action()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisAgent, FileShouldNotBeBlocked) @@ -99,10 +76,7 @@ TEST(ContentAnalysisAgent, FileShouldNotBeBlocked) response.results().Get(0).status()); ASSERT_EQ(0, response.results().Get(0).triggered_rules_size()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisAgent, FileShouldBeBlocked) @@ -125,8 +99,5 @@ TEST(ContentAnalysisAgent, FileShouldBeBlocked) ASSERT_EQ(ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK, response.results().Get(0).triggered_rules(0).action()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } diff --git a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisMisbehaving.cpp b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisMisbehaving.cpp index 7c944ed6e3..a120a82f7c 100644 --- a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisMisbehaving.cpp +++ b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisMisbehaving.cpp @@ -8,7 +8,7 @@ #include "mozilla/Assertions.h" #include "mozilla/CmdLineAndEnvUtils.h" #include "content_analysis/sdk/analysis_client.h" -#include "TestContentAnalysisAgent.h" +#include "TestContentAnalysisUtils.h" #include <processenv.h> #include <synchapi.h> #include <windows.h> @@ -70,10 +70,7 @@ TEST(ContentAnalysisMisbehaving, InvalidUtf8StringStartByteIsContinuationByte) // or invalid memory access or something. ASSERT_STREQ("\x80\x41\x41\x41", response.request_token().c_str()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, @@ -95,10 +92,7 @@ TEST(ContentAnalysisMisbehaving, // or invalid memory access or something. ASSERT_STREQ("\x41\xf0\x90\x8d", response.request_token().c_str()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, InvalidUtf8StringMultibyteSequenceTooShort) @@ -119,10 +113,7 @@ TEST(ContentAnalysisMisbehaving, InvalidUtf8StringMultibyteSequenceTooShort) // or invalid memory access or something. ASSERT_STREQ("\xf0\x90\x8d\x41", response.request_token().c_str()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, InvalidUtf8StringDecodesToInvalidCodePoint) @@ -143,10 +134,7 @@ TEST(ContentAnalysisMisbehaving, InvalidUtf8StringDecodesToInvalidCodePoint) // or invalid memory access or something. ASSERT_STREQ("\xf7\xbf\xbf\xbf", response.request_token().c_str()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, InvalidUtf8StringOverlongEncoding) @@ -167,10 +155,7 @@ TEST(ContentAnalysisMisbehaving, InvalidUtf8StringOverlongEncoding) // or invalid memory access or something. ASSERT_STREQ("\xf0\x82\x82\xac", response.request_token().c_str()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, StringWithEmbeddedNull) @@ -188,10 +173,7 @@ TEST(ContentAnalysisMisbehaving, StringWithEmbeddedNull) std::string expected("\x41\x00\x41"); ASSERT_EQ(expected, response.request_token()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, ZeroResults) @@ -208,10 +190,7 @@ TEST(ContentAnalysisMisbehaving, ZeroResults) ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); ASSERT_EQ(0, response.results().size()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, ResultWithInvalidStatus) @@ -232,10 +211,7 @@ TEST(ContentAnalysisMisbehaving, ResultWithInvalidStatus) // just make sure we can get the value without throwing ASSERT_GE(static_cast<int>(response.results(0).status()), 0); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, MessageTruncatedInMiddleOfString) @@ -252,10 +228,7 @@ TEST(ContentAnalysisMisbehaving, MessageTruncatedInMiddleOfString) // The response is an invalid serialization of protobuf, so this should fail ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, MessageWithInvalidWireType) @@ -271,10 +244,7 @@ TEST(ContentAnalysisMisbehaving, MessageWithInvalidWireType) // The response is an invalid serialization of protobuf, so this should fail ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, MessageWithUnusedFieldNumber) @@ -292,10 +262,7 @@ TEST(ContentAnalysisMisbehaving, MessageWithUnusedFieldNumber) // just make sure we can get a value without throwing ASSERT_STREQ("", response.request_token().c_str()); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, MessageWithWrongStringWireType) @@ -311,10 +278,7 @@ TEST(ContentAnalysisMisbehaving, MessageWithWrongStringWireType) // The response is an invalid serialization of protobuf, so this should fail ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, MessageWithZeroTag) @@ -330,10 +294,7 @@ TEST(ContentAnalysisMisbehaving, MessageWithZeroTag) // The response is an invalid serialization of protobuf, so this should fail ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, MessageWithZeroFieldButNonzeroWireType) @@ -350,10 +311,7 @@ TEST(ContentAnalysisMisbehaving, MessageWithZeroFieldButNonzeroWireType) // The response is an invalid serialization of protobuf, so this should fail ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, MessageWithGroupEnd) @@ -370,10 +328,7 @@ TEST(ContentAnalysisMisbehaving, MessageWithGroupEnd) // The response is an invalid serialization of protobuf, so this should fail ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, MessageTruncatedInMiddleOfVarint) @@ -390,10 +345,7 @@ TEST(ContentAnalysisMisbehaving, MessageTruncatedInMiddleOfVarint) // The response is an invalid serialization of protobuf, so this should fail ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } TEST(ContentAnalysisMisbehaving, MessageTruncatedInMiddleOfTag) @@ -409,8 +361,5 @@ TEST(ContentAnalysisMisbehaving, MessageTruncatedInMiddleOfTag) // The response is an invalid serialization of protobuf, so this should fail ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); - BOOL terminateResult = - ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); - ASSERT_NE(FALSE, terminateResult) - << "Failed to terminate content_analysis_sdk_agent process"; + MozAgentInfo.TerminateProcess(); } diff --git a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.cpp b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.cpp index 0e14de6b81..8bcfe018ee 100644 --- a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.cpp +++ b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.cpp @@ -3,13 +3,35 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -#include "TestContentAnalysisAgent.h" +#include "TestContentAnalysisUtils.h" #include <combaseapi.h> #include <pathcch.h> #include <shlwapi.h> #include <rpc.h> #include <windows.h> +MozAgentInfo LaunchAgentNormal(const wchar_t* aToBlock) { + nsString pipeName; + GeneratePipeName(L"contentanalysissdk-gtest-", pipeName); + return LaunchAgentNormal(aToBlock, pipeName); +} + +MozAgentInfo LaunchAgentNormal(const wchar_t* aToBlock, + const nsString& pipeName) { + nsString cmdLineArguments; + if (aToBlock && aToBlock[0] != 0) { + cmdLineArguments.Append(L" --toblock=.*"); + cmdLineArguments.Append(aToBlock); + cmdLineArguments.Append(L".*"); + } + cmdLineArguments.Append(L" --user"); + cmdLineArguments.Append(L" --path="); + cmdLineArguments.Append(pipeName); + MozAgentInfo agentInfo; + LaunchAgentWithCommandLineArguments(cmdLineArguments, pipeName, agentInfo); + return agentInfo; +} + void GeneratePipeName(const wchar_t* prefix, nsString& pipeName) { pipeName = u""_ns; pipeName.Append(prefix); diff --git a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisAgent.h b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.h index 9e31036262..fd437de3f7 100644 --- a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisAgent.h +++ b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.h @@ -15,10 +15,18 @@ struct MozAgentInfo { PROCESS_INFORMATION processInfo; std::unique_ptr<content_analysis::sdk::Client> client; + void TerminateProcess() { + BOOL terminateResult = ::TerminateProcess(processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; + } }; void GeneratePipeName(const wchar_t* prefix, nsString& pipeName); void LaunchAgentWithCommandLineArguments(const nsString& cmdLineArguments, const nsString& pipeName, MozAgentInfo& agentInfo); +MozAgentInfo LaunchAgentNormal(const wchar_t* aToBlock); +MozAgentInfo LaunchAgentNormal(const wchar_t* aToBlock, + const nsString& pipeName); #endif |