diff --git a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs index 25ef6c3843cc..c710f098cbf0 100644 --- a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs +++ b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs @@ -300,10 +300,10 @@ export const ContentAnalysis = { ); return; } - const operation = request.analysisType; + const analysisType = request.analysisType; // For operations that block browser interaction, show the "slow content analysis" // dialog faster - let slowTimeoutMs = this._shouldShowBlockingNotification(operation) + let slowTimeoutMs = this._shouldShowBlockingNotification(analysisType) ? this._SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS : this._SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS; let browsingContext = request.windowGlobalParent?.browsingContext; @@ -333,7 +333,7 @@ export const ContentAnalysis = { timer: lazy.setTimeout(() => { this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, { notification: this._showSlowCAMessage( - operation, + analysisType, request, resourceNameOrOperationType, browsingContext @@ -450,7 +450,10 @@ export const ContentAnalysis = { } if (this._SHOW_NOTIFICATIONS) { - const notification = new aBrowsingContext.topChromeWindow.Notification( + let topWindow = + aBrowsingContext.topChromeWindow ?? + aBrowsingContext.embedderWindowGlobal.browsingContext.topChromeWindow; + const notification = new topWindow.Notification( this.l10n.formatValueSync("contentanalysis-notification-title"), { body: aMessage, @@ -469,10 +472,10 @@ export const ContentAnalysis = { return null; }, - _shouldShowBlockingNotification(aOperation) { + _shouldShowBlockingNotification(aAnalysisType) { return !( - aOperation == Ci.nsIContentAnalysisRequest.eFileDownloaded || - aOperation == Ci.nsIContentAnalysisRequest.ePrint + aAnalysisType == Ci.nsIContentAnalysisRequest.eFileDownloaded || + aAnalysisType == Ci.nsIContentAnalysisRequest.ePrint ); }, @@ -488,6 +491,9 @@ export const ContentAnalysis = { case Ci.nsIContentAnalysisRequest.eDroppedText: l10nId = "contentanalysis-operationtype-dropped-text"; break; + case Ci.nsIContentAnalysisRequest.eOperationPrint: + l10nId = "contentanalysis-operationtype-print"; + break; } if (!l10nId) { console.error( @@ -596,10 +602,14 @@ export const ContentAnalysis = { case Ci.nsIContentAnalysisRequest.eDroppedText: l10nId = "contentanalysis-slow-agent-dialog-body-dropped-text"; break; + case Ci.nsIContentAnalysisRequest.eOperationPrint: + l10nId = "contentanalysis-slow-agent-dialog-body-print"; + break; } if (!l10nId) { console.error( - "Unknown operationTypeForDisplay: " + aResourceNameOrOperationType + "Unknown operationTypeForDisplay: ", + aResourceNameOrOperationType ); return ""; } diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp index bcfdf71b00d5..4c92988c9b90 100644 --- a/docshell/base/CanonicalBrowsingContext.cpp +++ b/docshell/base/CanonicalBrowsingContext.cpp @@ -6,8 +6,10 @@ #include "mozilla/dom/CanonicalBrowsingContext.h" +#include "ContentAnalysis.h" #include "ErrorList.h" #include "mozilla/CheckedInt.h" +#include "mozilla/Components.h" #include "mozilla/ErrorResult.h" #include "mozilla/EventForwards.h" #include "mozilla/AsyncEventDispatcher.h" @@ -47,6 +49,7 @@ #include "nsFrameLoader.h" #include "nsFrameLoaderOwner.h" #include "nsGlobalWindowOuter.h" +#include "nsIContentAnalysis.h" #include "nsIWebBrowserChrome.h" #include "nsIXULRuntime.h" #include "nsNetUtil.h" @@ -668,6 +671,9 @@ CanonicalBrowsingContext::ReplaceLoadingSessionHistoryEntryForLoad( using PrintPromise = CanonicalBrowsingContext::PrintPromise; #ifdef NS_PRINTING +// Clients must call StaticCloneForPrintingCreated or +// NoStaticCloneForPrintingWillBeCreated before the underlying promise can +// resolve. class PrintListenerAdapter final : public nsIWebProgressListener { public: explicit PrintListenerAdapter(PrintPromise::Private* aPromise) @@ -678,10 +684,14 @@ class PrintListenerAdapter final : public nsIWebProgressListener { // NS_DECL_NSIWEBPROGRESSLISTENER NS_IMETHOD OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aStateFlags, nsresult aStatus) override { + MOZ_ASSERT(NS_IsMainThread()); if (aStateFlags & nsIWebProgressListener::STATE_STOP && aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT && mPromise) { - mPromise->Resolve(true, __func__); - mPromise = nullptr; + mPrintJobFinished = true; + if (mHaveSetBrowsingContext) { + mPromise->Resolve(mClonedStaticBrowsingContext, __func__); + mPromise = nullptr; + } } return NS_OK; } @@ -716,10 +726,28 @@ class PrintListenerAdapter final : public nsIWebProgressListener { return NS_OK; } + void StaticCloneForPrintingCreated( + MaybeDiscardedBrowsingContext&& aClonedStaticBrowsingContext) { + MOZ_ASSERT(NS_IsMainThread()); + mClonedStaticBrowsingContext = std::move(aClonedStaticBrowsingContext); + mHaveSetBrowsingContext = true; + if (mPrintJobFinished && mPromise) { + mPromise->Resolve(mClonedStaticBrowsingContext, __func__); + mPromise = nullptr; + } + } + + void NoStaticCloneForPrintingWillBeCreated() { + StaticCloneForPrintingCreated(nullptr); + } + private: ~PrintListenerAdapter() = default; RefPtr mPromise; + MaybeDiscardedBrowsingContext mClonedStaticBrowsingContext = nullptr; + bool mHaveSetBrowsingContext = false; + bool mPrintJobFinished = false; }; NS_IMPL_ISUPPORTS(PrintListenerAdapter, nsIWebProgressListener) @@ -735,7 +763,9 @@ already_AddRefed CanonicalBrowsingContext::PrintJS( Print(aPrintSettings) ->Then( GetCurrentSerialEventTarget(), __func__, - [promise](bool) { promise->MaybeResolveWithUndefined(); }, + [promise](MaybeDiscardedBrowsingContext) { + promise->MaybeResolveWithUndefined(); + }, [promise](nsresult aResult) { promise->MaybeReject(aResult); }); return promise.forget(); } @@ -745,7 +775,72 @@ RefPtr CanonicalBrowsingContext::Print( #ifndef NS_PRINTING return PrintPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); #else +// Content analysis is not supported on non-Windows platforms. +# if defined(XP_WIN) + bool needContentAnalysis = false; + nsCOMPtr contentAnalysis = + mozilla::components::nsIContentAnalysis::Service(); + Unused << NS_WARN_IF(!contentAnalysis); + if (contentAnalysis) { + nsresult rv = contentAnalysis->GetIsActive(&needContentAnalysis); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + if (needContentAnalysis) { + auto done = MakeRefPtr(__func__); + contentanalysis::ContentAnalysis::PrintToPDFToDetermineIfPrintAllowed( + this, aPrintSettings) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [done, aPrintSettings = RefPtr{aPrintSettings}, + self = RefPtr{this}]( + contentanalysis::ContentAnalysis::PrintAllowedResult aResponse) + MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable { + if (aResponse.mAllowed) { + self->PrintWithNoContentAnalysis( + aPrintSettings, false, + aResponse.mCachedStaticDocumentBrowsingContext) + ->ChainTo(done.forget(), __func__); + } else { + // Since we are not doing the second print in this case, + // release the clone that is no longer needed. + self->ReleaseClonedPrint( + aResponse.mCachedStaticDocumentBrowsingContext); + done->Reject(NS_ERROR_CONTENT_BLOCKED, __func__); + } + }, + [done, self = RefPtr{this}]( + contentanalysis::ContentAnalysis::PrintAllowedError + aErrorResponse) MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA { + // Since we are not doing the second print in this case, release + // the clone that is no longer needed. + self->ReleaseClonedPrint( + aErrorResponse.mCachedStaticDocumentBrowsingContext); + done->Reject(aErrorResponse.mError, __func__); + }); + return done; + } +# endif + return PrintWithNoContentAnalysis(aPrintSettings, false, nullptr); +#endif +} +void CanonicalBrowsingContext::ReleaseClonedPrint( + const MaybeDiscardedBrowsingContext& aClonedStaticBrowsingContext) { +#ifdef NS_PRINTING + auto* browserParent = GetBrowserParent(); + if (NS_WARN_IF(!browserParent)) { + return; + } + Unused << browserParent->SendDestroyPrintClone(aClonedStaticBrowsingContext); +#endif +} + +RefPtr CanonicalBrowsingContext::PrintWithNoContentAnalysis( + nsIPrintSettings* aPrintSettings, bool aForceStaticDocument, + const MaybeDiscardedBrowsingContext& aCachedStaticDocument) { +#ifndef NS_PRINTING + return PrintPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); +#else auto promise = MakeRefPtr(__func__); auto listener = MakeRefPtr(promise); if (IsInProcess()) { @@ -757,12 +852,14 @@ RefPtr CanonicalBrowsingContext::Print( } ErrorResult rv; + listener->NoStaticCloneForPrintingWillBeCreated(); outerWindow->Print(aPrintSettings, /* aRemotePrintJob = */ nullptr, listener, /* aDocShellToCloneInto = */ nullptr, nsGlobalWindowOuter::IsPreview::No, nsGlobalWindowOuter::IsForWindowDotPrint::No, - /* aPrintPreviewCallback = */ nullptr, rv); + /* aPrintPreviewCallback = */ nullptr, + /* aCachedBrowsingContext = */ nullptr, rv); if (rv.Failed()) { promise->Reject(rv.StealNSResult(), __func__); } @@ -805,12 +902,31 @@ RefPtr CanonicalBrowsingContext::Print( printData.remotePrintJob() = browserParent->Manager()->SendPRemotePrintJobConstructor(remotePrintJob); - if (listener) { - remotePrintJob->RegisterListener(listener); - } + remotePrintJob->RegisterListener(listener); - if (NS_WARN_IF(!browserParent->SendPrint(this, printData))) { - promise->Reject(NS_ERROR_FAILURE, __func__); + if (!aCachedStaticDocument.IsNullOrDiscarded()) { + // There is no cloned static browsing context that + // SendPrintClonedPage() will return, so indicate this + // so listener can resolve its promise. + listener->NoStaticCloneForPrintingWillBeCreated(); + if (NS_WARN_IF(!browserParent->SendPrintClonedPage( + this, printData, aCachedStaticDocument))) { + promise->Reject(NS_ERROR_FAILURE, __func__); + } + } else { + RefPtr printPromise = + browserParent->SendPrint(this, printData, aForceStaticDocument); + printPromise->Then( + GetMainThreadSerialEventTarget(), __func__, + [listener](MaybeDiscardedBrowsingContext cachedStaticDocument) { + // promise will get resolved by the listener + listener->StaticCloneForPrintingCreated( + std::move(cachedStaticDocument)); + }, + [promise](ResponseRejectReason reason) { + NS_WARNING("SendPrint() failed"); + promise->Reject(NS_ERROR_FAILURE, __func__); + }); } return promise.forget(); #endif diff --git a/docshell/base/CanonicalBrowsingContext.h b/docshell/base/CanonicalBrowsingContext.h index 132c9f21578c..ccbdf9ed9655 100644 --- a/docshell/base/CanonicalBrowsingContext.h +++ b/docshell/base/CanonicalBrowsingContext.h @@ -136,11 +136,16 @@ class CanonicalBrowsingContext final : public BrowsingContext { UniquePtr ReplaceLoadingSessionHistoryEntryForLoad( LoadingSessionHistoryInfo* aInfo, nsIChannel* aNewChannel); - using PrintPromise = MozPromise; + using PrintPromise = + MozPromise; MOZ_CAN_RUN_SCRIPT RefPtr Print(nsIPrintSettings*); MOZ_CAN_RUN_SCRIPT already_AddRefed PrintJS(nsIPrintSettings*, ErrorResult&); - + MOZ_CAN_RUN_SCRIPT RefPtr PrintWithNoContentAnalysis( + nsIPrintSettings* aPrintSettings, bool aForceStaticDocument, + const MaybeDiscardedBrowsingContext& aClonedStaticBrowsingContext); + MOZ_CAN_RUN_SCRIPT void ReleaseClonedPrint( + const MaybeDiscardedBrowsingContext& aClonedStaticBrowsingContext); // Call the given callback on all top-level descendant BrowsingContexts. // Return Callstate::Stop from the callback to stop calling further children. // diff --git a/dom/base/nsFrameLoader.cpp b/dom/base/nsFrameLoader.cpp index eca528f2588b..1e3fb93aa821 100644 --- a/dom/base/nsFrameLoader.cpp +++ b/dom/base/nsFrameLoader.cpp @@ -3388,7 +3388,8 @@ already_AddRefed nsFrameLoader::PrintPreview( /* aListener = */ nullptr, docShellToCloneInto, nsGlobalWindowOuter::IsPreview::Yes, nsGlobalWindowOuter::IsForWindowDotPrint::No, - [resolve](const PrintPreviewResultInfo& aInfo) { resolve(aInfo); }, rv); + [resolve](const PrintPreviewResultInfo& aInfo) { resolve(aInfo); }, + nullptr, rv); if (NS_WARN_IF(rv.Failed())) { promise->MaybeReject(std::move(rv)); } diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp index 3dc63b83c25d..7dcd265ca4a7 100644 --- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp @@ -3752,7 +3752,7 @@ Nullable nsGlobalWindowInner::PrintPreview( /* aRemotePrintJob = */ nullptr, aListener, aDocShellToCloneInto, nsGlobalWindowOuter::IsPreview::Yes, nsGlobalWindowOuter::IsForWindowDotPrint::No, - /* aPrintPreviewCallback = */ nullptr, aError), + /* aPrintPreviewCallback = */ nullptr, nullptr, aError), aError, nullptr); } diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index 7a7bd503daf0..c678a0a94150 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -5006,7 +5006,7 @@ void nsGlobalWindowOuter::PrintOuter(ErrorResult& aError) { const bool forPreview = !StaticPrefs::print_always_print_silent(); Print(nullptr, nullptr, nullptr, nullptr, IsPreview(forPreview), - IsForWindowDotPrint::Yes, nullptr, aError); + IsForWindowDotPrint::Yes, nullptr, nullptr, aError); #endif } @@ -5028,7 +5028,8 @@ Nullable nsGlobalWindowOuter::Print( nsIPrintSettings* aPrintSettings, RemotePrintJobChild* aRemotePrintJob, nsIWebProgressListener* aListener, nsIDocShell* aDocShellToCloneInto, IsPreview aIsPreview, IsForWindowDotPrint aForWindowDotPrint, - PrintPreviewResolver&& aPrintPreviewCallback, ErrorResult& aError) { + PrintPreviewResolver&& aPrintPreviewCallback, + RefPtr* aCachedBrowsingContext, ErrorResult& aError) { #ifdef NS_PRINTING nsCOMPtr printSettingsService = do_GetService("@mozilla.org/gfx/printsettings-service;1"); @@ -5064,16 +5065,36 @@ Nullable nsGlobalWindowOuter::Print( nsCOMPtr viewer; RefPtr bc; bool hasPrintCallbacks = false; - if (docToPrint->IsStaticDocument()) { + bool wasStaticDocument = docToPrint->IsStaticDocument(); + bool usingCachedBrowsingContext = false; + if (aCachedBrowsingContext && *aCachedBrowsingContext) { + MOZ_ASSERT(!wasStaticDocument, + "Why pass in non-empty aCachedBrowsingContext if original " + "document is already static?"); + if (!wasStaticDocument) { + // The passed in document is not a static clone and the caller passed in a + // static clone to reuse, so swap it in. + docToPrint = (*aCachedBrowsingContext)->GetDocument(); + MOZ_ASSERT(docToPrint); + MOZ_ASSERT(docToPrint->IsStaticDocument()); + wasStaticDocument = true; + usingCachedBrowsingContext = true; + } + } + if (wasStaticDocument) { if (aForWindowDotPrint == IsForWindowDotPrint::Yes) { aError.ThrowNotSupportedError( "Calling print() from a print preview is unsupported, did you intend " "to call printPreview() instead?"); return nullptr; } - // We're already a print preview window, just reuse our browsing context / - // content viewer. - bc = sourceBC; + if (usingCachedBrowsingContext) { + bc = docToPrint->GetBrowsingContext(); + } else { + // We're already a print preview window, just reuse our browsing context / + // content viewer. + bc = sourceBC; + } nsCOMPtr docShell = bc->GetDocShell(); if (!docShell) { aError.ThrowNotSupportedError("No docshell"); @@ -5115,6 +5136,10 @@ Nullable nsGlobalWindowOuter::Print( if (NS_WARN_IF(aError.Failed())) { return nullptr; } + if (aCachedBrowsingContext) { + MOZ_ASSERT(!*aCachedBrowsingContext); + *aCachedBrowsingContext = bc; + } } if (!bc) { aError.ThrowNotAllowedError("No browsing context"); @@ -5170,6 +5195,24 @@ Nullable nsGlobalWindowOuter::Print( "Content viewer didn't implement nsIWebBrowserPrint"); return nullptr; } + bool closeWindowAfterPrint; + if (wasStaticDocument) { + // Here the document was a static clone to begin with that this code did not + // create, so we should not clean it up. + // The exception is if we're using the passed-in aCachedBrowsingContext, in + // which case this is the second print with this static document clone that + // we created the first time through, and we are responsible for cleaning it + // up. + closeWindowAfterPrint = usingCachedBrowsingContext; + } else { + // In this case the document was not a static clone, so we made a static + // clone for printing purposes and must clean it up after the print is done. + // The exception is if aCachedBrowsingContext is non-NULL, meaning the + // caller is intending to print this document again, so we need to defer the + // cleanup until after the second print. + closeWindowAfterPrint = !aCachedBrowsingContext; + } + webBrowserPrint->SetCloseWindowAfterPrint(closeWindowAfterPrint); // For window.print(), we postpone making these calls until the round-trip to // the parent process (triggered by the OpenInternal call above) calls us diff --git a/dom/base/nsGlobalWindowOuter.h b/dom/base/nsGlobalWindowOuter.h index e9172f8f0534..3c26344c3d45 100644 --- a/dom/base/nsGlobalWindowOuter.h +++ b/dom/base/nsGlobalWindowOuter.h @@ -580,7 +580,8 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget, Print(nsIPrintSettings*, mozilla::layout::RemotePrintJobChild* aRemotePrintJob, nsIWebProgressListener*, nsIDocShell*, IsPreview, IsForWindowDotPrint, - PrintPreviewResolver&&, mozilla::ErrorResult&); + PrintPreviewResolver&&, RefPtr*, + mozilla::ErrorResult&); mozilla::dom::Selection* GetSelectionOuter(); already_AddRefed GetSelection() override; nsScreen* GetScreen(); diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp index bdd10fdcb285..3d1f399edb4f 100644 --- a/dom/ipc/BrowserChild.cpp +++ b/dom/ipc/BrowserChild.cpp @@ -2342,7 +2342,7 @@ mozilla::ipc::IPCResult BrowserChild::RecvPrintPreview( /* aListener = */ nullptr, docShellToCloneInto, nsGlobalWindowOuter::IsPreview::Yes, nsGlobalWindowOuter::IsForWindowDotPrint::No, - std::move(aCallback), IgnoreErrors()); + std::move(aCallback), nullptr, IgnoreErrors()); #endif return IPC_OK(); } @@ -2359,8 +2359,9 @@ mozilla::ipc::IPCResult BrowserChild::RecvExitPrintPreview() { return IPC_OK(); } -mozilla::ipc::IPCResult BrowserChild::RecvPrint( - const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData) { +mozilla::ipc::IPCResult BrowserChild::CommonPrint( + const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData, + RefPtr* aCachedBrowsingContext) { #ifdef NS_PRINTING if (NS_WARN_IF(aBc.IsNullOrDiscarded())) { return IPC_OK(); @@ -2389,12 +2390,12 @@ mozilla::ipc::IPCResult BrowserChild::RecvPrint( IgnoredErrorResult rv; RefPtr printJob = static_cast( aPrintData.remotePrintJob().AsChild()); - outerWindow->Print(printSettings, printJob, - /* aListener = */ nullptr, - /* aWindowToCloneInto = */ nullptr, - nsGlobalWindowOuter::IsPreview::No, - nsGlobalWindowOuter::IsForWindowDotPrint::No, - /* aPrintPreviewCallback = */ nullptr, rv); + outerWindow->Print( + printSettings, printJob, + /* aListener = */ nullptr, + /* aWindowToCloneInto = */ nullptr, nsGlobalWindowOuter::IsPreview::No, + nsGlobalWindowOuter::IsForWindowDotPrint::No, + /* aPrintPreviewCallback = */ nullptr, aCachedBrowsingContext, rv); if (NS_WARN_IF(rv.Failed())) { return IPC_OK(); } @@ -2403,6 +2404,49 @@ mozilla::ipc::IPCResult BrowserChild::RecvPrint( return IPC_OK(); } +mozilla::ipc::IPCResult BrowserChild::RecvPrint( + const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData, + bool aReturnStaticClone, PrintResolver&& aResolve) { +#ifdef NS_PRINTING + RefPtr browsingContext; + auto result = CommonPrint(aBc, aPrintData, + aReturnStaticClone ? &browsingContext : nullptr); + aResolve(browsingContext); + return result; +#else + aResolve(nullptr); + return IPC_OK(); +#endif +} + +mozilla::ipc::IPCResult BrowserChild::RecvPrintClonedPage( + const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData, + const MaybeDiscardedBrowsingContext& aClonedBc) { +#ifdef NS_PRINTING + if (aClonedBc.IsNullOrDiscarded()) { + return IPC_OK(); + } + RefPtr clonedBc = aClonedBc.get(); + return CommonPrint(aBc, aPrintData, &clonedBc); +#else + return IPC_OK(); +#endif +} + +mozilla::ipc::IPCResult BrowserChild::RecvDestroyPrintClone( + const MaybeDiscardedBrowsingContext& aCachedPage) { +#ifdef NS_PRINTING + if (aCachedPage) { + RefPtr window = aCachedPage->GetDOMWindow(); + if (NS_WARN_IF(!window)) { + return IPC_OK(); + } + window->Close(); + } +#endif + return IPC_OK(); +} + mozilla::ipc::IPCResult BrowserChild::RecvUpdateNativeWindowHandle( const uintptr_t& aNewHandle) { #if defined(XP_WIN) && defined(ACCESSIBILITY) diff --git a/dom/ipc/BrowserChild.h b/dom/ipc/BrowserChild.h index ddbf8f0b74fd..90cb7124763b 100644 --- a/dom/ipc/BrowserChild.h +++ b/dom/ipc/BrowserChild.h @@ -507,7 +507,15 @@ class BrowserChild final : public nsMessageManagerScriptExecutor, mozilla::ipc::IPCResult RecvExitPrintPreview(); MOZ_CAN_RUN_SCRIPT_BOUNDARY mozilla::ipc::IPCResult RecvPrint( - const MaybeDiscardedBrowsingContext&, const PrintData&); + const MaybeDiscardedBrowsingContext&, const PrintData&, bool, + PrintResolver&&); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY mozilla::ipc::IPCResult RecvPrintClonedPage( + const MaybeDiscardedBrowsingContext&, const PrintData&, + const MaybeDiscardedBrowsingContext&); + + mozilla::ipc::IPCResult RecvDestroyPrintClone( + const MaybeDiscardedBrowsingContext&); mozilla::ipc::IPCResult RecvUpdateNativeWindowHandle( const uintptr_t& aNewHandle); @@ -712,6 +720,11 @@ class BrowserChild final : public nsMessageManagerScriptExecutor, void InternalSetDocShellIsActive(bool aIsActive); + MOZ_CAN_RUN_SCRIPT + mozilla::ipc::IPCResult CommonPrint( + const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData, + RefPtr* aCachedBrowsingContext); + bool CreateRemoteLayerManager( mozilla::layers::PCompositorBridgeChild* aCompositorChild); diff --git a/dom/ipc/PBrowser.ipdl b/dom/ipc/PBrowser.ipdl index 727b579dd8ac..372a81b13934 100644 --- a/dom/ipc/PBrowser.ipdl +++ b/dom/ipc/PBrowser.ipdl @@ -960,8 +960,33 @@ child: * * @param aBrowsingContext the browsing context to print. * @param aPrintData the serialized settings to print with + * @param aReturnStaticClone If the document in aBrowsingContext is not a static clone, whether + * to return the static document clone created. + * Note that if you call this with true but do not later call PrintClonedPage(), + * you must call DestroyPrintCache() to avoid leaks. */ - async Print(MaybeDiscardedBrowsingContext aBC, PrintData aPrintData); + async Print(MaybeDiscardedBrowsingContext aBC, PrintData aPrintData, bool aReturnStaticClone) returns(MaybeDiscardedBrowsingContext staticCloneBrowsingContext); + + /** + * Tell the child to print the passed in static clone browsing context with the given settings. + * + * @param aBrowsingContext the browsing context to print. + * @param aPrintData the serialized settings to print with + * @param aStaticCloneBrowsingContext The static clone of aBrowsingContext that + * was created by an earlier call to Print(). This is the page that will actually be + * printed. + */ + async PrintClonedPage(MaybeDiscardedBrowsingContext aBC, PrintData aPrintData, MaybeDiscardedBrowsingContext aStaticCloneBrowsingContext); + + /** + * Destroy the static document clone for printing, if present. See Print() for details. + * For callers' simplicity, it is safe to call this method even if aStaticCloneBrowsingContext + * is null or has already been discarded. + * + * @param aStaticCloneBrowsingContext The static clone that was created by + * an earlier call to Print(). + */ + async DestroyPrintClone(MaybeDiscardedBrowsingContext aStaticCloneBrowsingContext); /** * Update the child with the tab's current top-level native window handle. diff --git a/layout/base/nsDocumentViewer.cpp b/layout/base/nsDocumentViewer.cpp index d1cf9bf2371a..8ae0e001b87f 100644 --- a/layout/base/nsDocumentViewer.cpp +++ b/layout/base/nsDocumentViewer.cpp @@ -449,6 +449,7 @@ class nsDocumentViewer final : public nsIDocumentViewer, #ifdef NS_PRINTING unsigned mClosingWhilePrinting : 1; + unsigned mCloseWindowAfterPrint : 1; # if NS_PRINT_PREVIEW RefPtr mPrintJob; @@ -520,6 +521,7 @@ nsDocumentViewer::nsDocumentViewer() mInPermitUnloadPrompt(false), #ifdef NS_PRINTING mClosingWhilePrinting(false), + mCloseWindowAfterPrint(false), #endif // NS_PRINTING mReloadEncodingSource(kCharsetUninitialized), mReloadEncoding(nullptr), @@ -3139,6 +3141,20 @@ nsDocumentViewer::GetDoingPrintPreview(bool* aDoingPrintPreview) { return NS_OK; } +NS_IMETHODIMP +nsDocumentViewer::GetCloseWindowAfterPrint(bool* aCloseWindowAfterPrint) { + NS_ENSURE_ARG_POINTER(aCloseWindowAfterPrint); + + *aCloseWindowAfterPrint = mCloseWindowAfterPrint; + return NS_OK; +} + +NS_IMETHODIMP +nsDocumentViewer::SetCloseWindowAfterPrint(bool aCloseWindowAfterPrint) { + mCloseWindowAfterPrint = aCloseWindowAfterPrint; + return NS_OK; +} + NS_IMETHODIMP nsDocumentViewer::ExitPrintPreview() { NS_ENSURE_TRUE(mPrintJob, NS_ERROR_FAILURE); @@ -3301,15 +3317,23 @@ void nsDocumentViewer::OnDonePrinting() { printJob->Destroy(); } - // We are done printing, now clean up. - // - // For non-print-preview jobs, we are actually responsible for cleaning up - // our whole or window (see the OPEN_PRINT_BROWSER code), so gotta - // run window.close(), which will take care of this. - // - // For print preview jobs the front-end code is responsible for cleaning the - // UI. - if (!printJob->CreatedForPrintPreview()) { +// We are done printing, now clean up. +// +// If the original document to print was not a static clone, we opened a new +// window and are responsible for cleaning up the whole or window +// (see the OPEN_PRINT_BROWSER code, specifically +// handleStaticCloneCreatedForPrint()), so gotta run window.close(), which +// will take care of this. +// +// Otherwise the front-end code is responsible for cleaning the UI. +# ifdef ANDROID + // Android doesn't support Content Analysis and prints in a different way, + // so use different logic to clean up. + bool closeWindowAfterPrint = !printJob->CreatedForPrintPreview(); +# else + bool closeWindowAfterPrint = GetCloseWindowAfterPrint(); +# endif + if (closeWindowAfterPrint) { if (mContainer) { if (nsCOMPtr win = mContainer->GetWindow()) { win->Close(); diff --git a/toolkit/components/browser/nsIWebBrowserPrint.idl b/toolkit/components/browser/nsIWebBrowserPrint.idl index 5a900b7b6583..2b5d7669bcb7 100644 --- a/toolkit/components/browser/nsIWebBrowserPrint.idl +++ b/toolkit/components/browser/nsIWebBrowserPrint.idl @@ -30,7 +30,7 @@ native PrintPreviewResolver(std::function # define SECURITY_WIN32 1 # include +# include "mozilla/NativeNt.h" # include "mozilla/WinDllServices.h" #endif // XP_WIN @@ -114,6 +119,11 @@ nsIContentAnalysisAcknowledgement::FinalAction ConvertResult( } // anonymous namespace namespace mozilla::contentanalysis { +ContentAnalysisRequest::~ContentAnalysisRequest() { +#ifdef XP_WIN + CloseHandle(mPrintDataHandle); +#endif +} NS_IMETHODIMP ContentAnalysisRequest::GetAnalysisType(AnalysisType* aAnalysisType) { @@ -133,6 +143,34 @@ ContentAnalysisRequest::GetFilePath(nsAString& aFilePath) { return NS_OK; } +NS_IMETHODIMP +ContentAnalysisRequest::GetPrintDataHandle(uint64_t* aPrintDataHandle) { +#ifdef XP_WIN + uintptr_t printDataHandle = reinterpret_cast(mPrintDataHandle); + uint64_t printDataValue = static_cast(printDataHandle); + *aPrintDataHandle = printDataValue; + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +NS_IMETHODIMP +ContentAnalysisRequest::GetPrinterName(nsAString& aPrinterName) { + aPrinterName = mPrinterName; + return NS_OK; +} + +NS_IMETHODIMP +ContentAnalysisRequest::GetPrintDataSize(uint64_t* aPrintDataSize) { +#ifdef XP_WIN + *aPrintDataSize = mPrintDataSize; + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + NS_IMETHODIMP ContentAnalysisRequest::GetUrl(nsIURI** aUrl) { NS_ENSURE_ARG_POINTER(aUrl); @@ -234,6 +272,8 @@ ContentAnalysisRequest::ContentAnalysisRequest( mUrl(std::move(aUrl)), mSha256Digest(std::move(aSha256Digest)), mWindowGlobalParent(aWindowGlobalParent) { + MOZ_ASSERT(aAnalysisType != AnalysisType::ePrint, + "Print should use other ContentAnalysisRequest constructor!"); if (aStringIsFilePath) { mFilePath = std::move(aString); } else { @@ -251,6 +291,32 @@ ContentAnalysisRequest::ContentAnalysisRequest( mRequestToken = GenerateRequestToken(); } +ContentAnalysisRequest::ContentAnalysisRequest( + const nsTArray aPrintData, nsCOMPtr aUrl, + nsString aPrinterName, dom::WindowGlobalParent* aWindowGlobalParent) + : mAnalysisType(AnalysisType::ePrint), + mUrl(std::move(aUrl)), + mPrinterName(std::move(aPrinterName)), + mWindowGlobalParent(aWindowGlobalParent) { +#ifdef XP_WIN + LARGE_INTEGER dataContentLength; + dataContentLength.QuadPart = static_cast(aPrintData.Length()); + mPrintDataHandle = ::CreateFileMappingW( + INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, dataContentLength.HighPart, + dataContentLength.LowPart, nullptr); + if (mPrintDataHandle) { + mozilla::nt::AutoMappedView view(mPrintDataHandle, FILE_MAP_ALL_ACCESS); + memcpy(view.as(), aPrintData.Elements(), aPrintData.Length()); + mPrintDataSize = aPrintData.Length(); + } +#else + MOZ_ASSERT_UNREACHABLE( + "Content Analysis is not supported on non-Windows platforms"); +#endif + mOperationTypeForDisplay = OperationType::eOperationPrint; + mRequestToken = GenerateRequestToken(); +} + nsresult ContentAnalysisRequest::GetFileDigest(const nsAString& aFilePath, nsCString& aDigestString) { MOZ_DIAGNOSTIC_ASSERT( @@ -366,22 +432,44 @@ static nsresult ConvertToProtobuf( requestData->set_digest(sha256Digest.get()); } - nsString filePath; - rv = aIn->GetFilePath(filePath); - NS_ENSURE_SUCCESS(rv, rv); - if (!filePath.IsEmpty()) { - std::string filePathStr = NS_ConvertUTF16toUTF8(filePath).get(); - aOut->set_file_path(filePathStr); - auto filename = filePathStr.substr(filePathStr.find_last_of("/\\") + 1); - if (!filename.empty()) { - requestData->set_filename(filename); + if (analysisType == nsIContentAnalysisRequest::AnalysisType::ePrint) { +#if XP_WIN + uint64_t printDataHandle; + MOZ_TRY(aIn->GetPrintDataHandle(&printDataHandle)); + if (!printDataHandle) { + return NS_ERROR_OUT_OF_MEMORY; } + aOut->mutable_print_data()->set_handle(printDataHandle); + + uint64_t printDataSize; + MOZ_TRY(aIn->GetPrintDataSize(&printDataSize)); + aOut->mutable_print_data()->set_size(printDataSize); + + nsString printerName; + MOZ_TRY(aIn->GetPrinterName(printerName)); + requestData->mutable_print_metadata()->set_printer_name( + NS_ConvertUTF16toUTF8(printerName).get()); +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif } else { - nsString textContent; - rv = aIn->GetTextContent(textContent); + nsString filePath; + rv = aIn->GetFilePath(filePath); NS_ENSURE_SUCCESS(rv, rv); - MOZ_ASSERT(!textContent.IsEmpty()); - aOut->set_text_content(NS_ConvertUTF16toUTF8(textContent).get()); + if (!filePath.IsEmpty()) { + std::string filePathStr = NS_ConvertUTF16toUTF8(filePath).get(); + aOut->set_file_path(filePathStr); + auto filename = filePathStr.substr(filePathStr.find_last_of("/\\") + 1); + if (!filename.empty()) { + requestData->set_filename(filename); + } + } else { + nsString textContent; + rv = aIn->GetTextContent(textContent); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(!textContent.IsEmpty()); + aOut->set_text_content(NS_ConvertUTF16toUTF8(textContent).get()); + } } #ifdef XP_WIN @@ -1409,6 +1497,181 @@ ContentAnalysis::RespondToWarnDialog(const nsACString& aRequestToken, return NS_OK; } +#if defined(XP_WIN) +RefPtr +ContentAnalysis::PrintToPDFToDetermineIfPrintAllowed( + dom::CanonicalBrowsingContext* aBrowsingContext, + nsIPrintSettings* aPrintSettings) { + // Note that the IsChrome() check here excludes a few + // common about pages like about:config, about:preferences, + // and about:support, but other about: pages may still + // go through content analysis. + if (aBrowsingContext->IsChrome()) { + return PrintAllowedPromise::CreateAndResolve(PrintAllowedResult(true), + __func__); + } + nsCOMPtr contentAnalysisPrintSettings; + if (NS_WARN_IF(NS_FAILED(aPrintSettings->Clone( + getter_AddRefs(contentAnalysisPrintSettings)))) || + NS_WARN_IF(!aBrowsingContext->GetCurrentWindowGlobal())) { + return PrintAllowedPromise::CreateAndReject( + PrintAllowedError(NS_ERROR_FAILURE), __func__); + } + contentAnalysisPrintSettings->SetOutputDestination( + nsIPrintSettings::OutputDestinationType::kOutputDestinationStream); + contentAnalysisPrintSettings->SetOutputFormat( + nsIPrintSettings::kOutputFormatPDF); + nsCOMPtr storageStream = + do_CreateInstance("@mozilla.org/storagestream;1"); + if (!storageStream) { + return PrintAllowedPromise::CreateAndReject( + PrintAllowedError(NS_ERROR_FAILURE), __func__); + } + // Use segment size of 512K + nsresult rv = storageStream->Init(0x80000, UINT32_MAX); + if (NS_WARN_IF(NS_FAILED(rv))) { + return PrintAllowedPromise::CreateAndReject(PrintAllowedError(rv), + __func__); + } + + nsCOMPtr outputStream; + storageStream->QueryInterface(NS_GET_IID(nsIOutputStream), + getter_AddRefs(outputStream)); + MOZ_ASSERT(outputStream); + + contentAnalysisPrintSettings->SetOutputStream(outputStream.get()); + RefPtr browsingContext = aBrowsingContext; + auto promise = MakeRefPtr(__func__); + nsCOMPtr finalPrintSettings(aPrintSettings); + aBrowsingContext + ->PrintWithNoContentAnalysis(contentAnalysisPrintSettings, true, nullptr) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [browsingContext, contentAnalysisPrintSettings, finalPrintSettings, + promise]( + dom::MaybeDiscardedBrowsingContext cachedStaticBrowsingContext) + MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable { + nsCOMPtr outputStream; + contentAnalysisPrintSettings->GetOutputStream( + getter_AddRefs(outputStream)); + nsCOMPtr storageStream = + do_QueryInterface(outputStream); + MOZ_ASSERT(storageStream); + nsTArray printData; + uint32_t length = 0; + storageStream->GetLength(&length); + if (!printData.SetLength(length, fallible)) { + promise->Reject( + PrintAllowedError(NS_ERROR_OUT_OF_MEMORY, + cachedStaticBrowsingContext), + __func__); + return; + } + nsCOMPtr inputStream; + nsresult rv = storageStream->NewInputStream( + 0, getter_AddRefs(inputStream)); + if (NS_FAILED(rv)) { + promise->Reject( + PrintAllowedError(rv, cachedStaticBrowsingContext), + __func__); + return; + } + uint32_t currentPosition = 0; + while (currentPosition < length) { + uint32_t elementsRead = 0; + // Make sure the reinterpret_cast<> below is safe + static_assert(std::is_trivially_assignable_v< + decltype(*printData.Elements()), char>); + rv = inputStream->Read( + reinterpret_cast(printData.Elements()) + + currentPosition, + length - currentPosition, &elementsRead); + if (NS_WARN_IF(NS_FAILED(rv) || !elementsRead)) { + promise->Reject( + PrintAllowedError(NS_FAILED(rv) ? rv : NS_ERROR_FAILURE, + cachedStaticBrowsingContext), + __func__); + return; + } + currentPosition += elementsRead; + } + + nsString printerName; + rv = contentAnalysisPrintSettings->GetPrinterName(printerName); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->Reject( + PrintAllowedError(rv, cachedStaticBrowsingContext), + __func__); + return; + } + + auto* windowParent = browsingContext->GetCurrentWindowGlobal(); + if (!windowParent) { + // The print window may have been closed by the user by now. + // Cancel the print. + promise->Reject( + PrintAllowedError(NS_ERROR_ABORT, + cachedStaticBrowsingContext), + __func__); + return; + } + nsCOMPtr uri = windowParent->GetDocumentURI(); + nsCOMPtr contentAnalysisRequest = + new contentanalysis::ContentAnalysisRequest( + std::move(printData), std::move(uri), + std::move(printerName), windowParent); + auto callback = + MakeRefPtr( + [browsingContext, cachedStaticBrowsingContext, promise, + finalPrintSettings = std::move(finalPrintSettings)]( + nsIContentAnalysisResponse* aResponse) + MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable { + bool shouldAllow = false; + DebugOnly rv = + aResponse->GetShouldAllowContent( + &shouldAllow); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + promise->Resolve( + PrintAllowedResult( + shouldAllow, cachedStaticBrowsingContext), + __func__); + }, + [promise, + cachedStaticBrowsingContext](nsresult aError) { + promise->Reject( + PrintAllowedError(aError, + cachedStaticBrowsingContext), + __func__); + }); + nsCOMPtr contentAnalysis = + mozilla::components::nsIContentAnalysis::Service(); + if (NS_WARN_IF(!contentAnalysis)) { + promise->Reject( + PrintAllowedError(rv, cachedStaticBrowsingContext), + __func__); + } else { + bool isActive = false; + nsresult rv = contentAnalysis->GetIsActive(&isActive); + // Should not be called if content analysis is not active + MOZ_ASSERT(isActive); + Unused << NS_WARN_IF(NS_FAILED(rv)); + rv = contentAnalysis->AnalyzeContentRequestCallback( + contentAnalysisRequest, /* aAutoAcknowledge */ true, + callback); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->Reject( + PrintAllowedError(rv, cachedStaticBrowsingContext), + __func__); + } + } + }, + [promise](nsresult aError) { + promise->Reject(PrintAllowedError(aError), __func__); + }); + return promise; +} +#endif + NS_IMETHODIMP ContentAnalysisResponse::Acknowledge( nsIContentAnalysisAcknowledgement* aAcknowledgement) { diff --git a/toolkit/components/contentanalysis/ContentAnalysis.h b/toolkit/components/contentanalysis/ContentAnalysis.h index a99bbe80c37d..f2545624fd0f 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.h +++ b/toolkit/components/contentanalysis/ContentAnalysis.h @@ -8,6 +8,8 @@ #include "mozilla/DataMutex.h" #include "mozilla/MozPromise.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/MaybeDiscarded.h" #include "mozilla/dom/Promise.h" #include "nsIContentAnalysis.h" #include "nsProxyRelease.h" @@ -18,10 +20,16 @@ #include #include +#ifdef XP_WIN +# include +#endif // XP_WIN + class nsIPrincipal; +class nsIPrintSettings; class ContentAnalysisTest; namespace mozilla::dom { +class CanonicalBrowsingContext; class DataTransfer; class WindowGlobalParent; } // namespace mozilla::dom @@ -64,11 +72,15 @@ class ContentAnalysisRequest final : public nsIContentAnalysisRequest { bool aStringIsFilePath, nsCString aSha256Digest, nsCOMPtr aUrl, OperationType aOperationType, dom::WindowGlobalParent* aWindowGlobalParent); + ContentAnalysisRequest(const nsTArray aPrintData, + nsCOMPtr aUrl, nsString aPrinterName, + dom::WindowGlobalParent* aWindowGlobalParent); static nsresult GetFileDigest(const nsAString& aFilePath, nsCString& aDigestString); private: - ~ContentAnalysisRequest() = default; + ~ContentAnalysisRequest(); + // Remove unneeded copy constructor/assignment ContentAnalysisRequest(const ContentAnalysisRequest&) = delete; ContentAnalysisRequest& operator=(ContentAnalysisRequest&) = delete; @@ -105,7 +117,16 @@ class ContentAnalysisRequest final : public nsIContentAnalysisRequest { // OPERATION_CUSTOMDISPLAYSTRING nsString mOperationDisplayString; + // The name of the printer being printed to + nsString mPrinterName; + RefPtr mWindowGlobalParent; +#ifdef XP_WIN + // The printed data to analyze, in PDF format + HANDLE mPrintDataHandle = 0; + // The size of the printed data in mPrintDataHandle + uint64_t mPrintDataSize = 0; +#endif friend class ::ContentAnalysisTest; }; @@ -128,6 +149,39 @@ class ContentAnalysis final : public nsIContentAnalysis { nsCString GetUserActionId(); void SetLastResult(nsresult aLastResult) { mLastResult = aLastResult; } + struct PrintAllowedResult final { + bool mAllowed; + dom::MaybeDiscarded + mCachedStaticDocumentBrowsingContext; + PrintAllowedResult(bool aAllowed, dom::MaybeDiscarded + aCachedStaticDocumentBrowsingContext) + : mAllowed(aAllowed), + mCachedStaticDocumentBrowsingContext( + aCachedStaticDocumentBrowsingContext) {} + explicit PrintAllowedResult(bool aAllowed) + : PrintAllowedResult(aAllowed, dom::MaybeDiscardedBrowsingContext()) {} + }; + struct PrintAllowedError final { + nsresult mError; + dom::MaybeDiscarded + mCachedStaticDocumentBrowsingContext; + PrintAllowedError(nsresult aError, dom::MaybeDiscarded + aCachedStaticDocumentBrowsingContext) + : mError(aError), + mCachedStaticDocumentBrowsingContext( + aCachedStaticDocumentBrowsingContext) {} + explicit PrintAllowedError(nsresult aError) + : PrintAllowedError(aError, dom::MaybeDiscardedBrowsingContext()) {} + }; + using PrintAllowedPromise = + MozPromise; +#if defined(XP_WIN) + MOZ_CAN_RUN_SCRIPT static RefPtr + PrintToPDFToDetermineIfPrintAllowed( + dom::CanonicalBrowsingContext* aBrowsingContext, + nsIPrintSettings* aPrintSettings); +#endif // defined(XP_WIN) + private: ~ContentAnalysis(); // Remove unneeded copy constructor/assignment diff --git a/toolkit/components/contentanalysis/components.conf b/toolkit/components/contentanalysis/components.conf index 82236cb1b920..1683ef99d752 100644 --- a/toolkit/components/contentanalysis/components.conf +++ b/toolkit/components/contentanalysis/components.conf @@ -11,5 +11,6 @@ Classes = [ 'contract_ids': ['@mozilla.org/contentanalysis;1'], 'type': 'mozilla::contentanalysis::ContentAnalysis', 'headers': ['/toolkit/components/contentanalysis/ContentAnalysis.h'], + 'overridable': True, }, ] diff --git a/toolkit/components/contentanalysis/nsIContentAnalysis.idl b/toolkit/components/contentanalysis/nsIContentAnalysis.idl index a183af9035b6..63ac8d12fbfe 100644 --- a/toolkit/components/contentanalysis/nsIContentAnalysis.idl +++ b/toolkit/components/contentanalysis/nsIContentAnalysis.idl @@ -131,6 +131,7 @@ interface nsIContentAnalysisRequest : nsISupports eCustomDisplayString = 0, eClipboard = 1, eDroppedText = 2, + eOperationPrint = 3, }; readonly attribute nsIContentAnalysisRequest_OperationType operationTypeForDisplay; readonly attribute AString operationDisplayString; @@ -141,6 +142,15 @@ interface nsIContentAnalysisRequest : nsISupports // Name of file to analyze. Only one of textContent or filePath is defined. readonly attribute AString filePath; + // HANDLE to the printed data in PDF format. + readonly attribute unsigned long long printDataHandle; + + // Size of the data stored in printDataHandle. + readonly attribute unsigned long long printDataSize; + + // Name of the printer being printed to. + readonly attribute AString printerName; + // The URL containing the file download/upload or to which web content is // being uploaded. readonly attribute nsIURI url; diff --git a/toolkit/components/contentanalysis/tests/browser/browser.toml b/toolkit/components/contentanalysis/tests/browser/browser.toml index 0e210902996d..bdbf35059324 100644 --- a/toolkit/components/contentanalysis/tests/browser/browser.toml +++ b/toolkit/components/contentanalysis/tests/browser/browser.toml @@ -1,3 +1,20 @@ [DEFAULT] +run-if = ["os == 'win'"] +support-files = [ + "head.js", +] ["browser_content_analysis_policies.js"] + +["browser_print_changing_page_content_analysis.js"] +support-files = [ + "!/toolkit/components/printing/tests/head.js", + "changing_page_for_print.html", +] + +["browser_print_content_analysis.js"] +support-files = [ + "!/toolkit/components/printing/tests/head.js", + "!/toolkit/components/printing/tests/longerArticle.html", + "!/toolkit/components/printing/tests/simplifyArticleSample.html", +] 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 new file mode 100644 index 000000000000..72a7dcbb9107 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_print_changing_page_content_analysis.js @@ -0,0 +1,339 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/printing/tests/head.js", + this +); + +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); + }, +}; + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); +}); + +const TEST_PAGE_URL = PrintHelper.getTestPageUrlHTTPS( + "changing_page_for_print.html" +); + +function addUniqueSuffix(prefix) { + return `${prefix}-${Services.uuid + .generateUUID() + .toString() + .slice(1, -1)}.pdf`; +} + +async function printToDestination(aBrowser, aDestination) { + let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + let fileName = addUniqueSuffix(`printDestinationTest-${aDestination}`); + let filePath = PathUtils.join(tmpDir.path, fileName); + + info(`Printing to ${filePath}`); + + let settings = PSSVC.createNewPrintSettings(); + settings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; + settings.outputDestination = aDestination; + + settings.headerStrCenter = ""; + settings.headerStrLeft = ""; + settings.headerStrRight = ""; + settings.footerStrCenter = ""; + settings.footerStrLeft = ""; + settings.footerStrRight = ""; + + settings.unwriteableMarginTop = 1; /* Just to ensure settings are respected on both */ + let outStream = null; + if (aDestination == Ci.nsIPrintSettings.kOutputDestinationFile) { + settings.toFileName = PathUtils.join(tmpDir.path, fileName); + } else { + is(aDestination, Ci.nsIPrintSettings.kOutputDestinationStream); + outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + let tmpFile = tmpDir.clone(); + tmpFile.append(fileName); + outStream.init(tmpFile, -1, 0o666, 0); + settings.outputStream = outStream; + } + + await aBrowser.browsingContext.print(settings); + + return filePath; +} + +function assertContentAnalysisRequest(request) { + is(request.url.spec, TEST_PAGE_URL, "request has correct URL"); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.ePrint, + "request has print analysisType" + ); + is( + request.operationTypeForDisplay, + Ci.nsIContentAnalysisRequest.eOperationPrint, + "request has print operationTypeForDisplay" + ); + is(request.textContent, "", "request textContent should be empty"); + is(request.filePath, "", "request filePath should be empty"); + isnot(request.printDataHandle, 0, "request printDataHandle should not be 0"); + isnot(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} + +add_task( + async function testPrintToStreamWithContentAnalysisActiveAndAllowing() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + let filePath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + + // This effectively tests that the PDF content sent to Content Analysis + // and the content that is actually printed matches. This is necessary + // because a previous iteration of the Content Analysis code didn't use + // a static Document clone for this and so the content would differ. (since + // the .html file in question adds content to the page when print events + // happen) + await waitForFileToAlmostMatchSize( + filePath, + mockCA.calls[0].printDataSize + ); + + await IOUtils.remove(filePath); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintToStreamWithContentAnalysisActiveAndBlocking() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(false); + + try { + await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + ok(false, "Content analysis should make this fail to print"); + } catch (e) { + ok( + /NS_ERROR_CONTENT_BLOCKED/.test(e.toString()), + "Got content blocked error" + ); + } + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task(async function testPrintToStreamWithContentAnalysisReturningError() { + await PrintHelper.withTestPage( + async helper => { + expectUncaughtException(); + mockCA.setupForTestWithError(Cr.NS_ERROR_NOT_AVAILABLE); + + try { + await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + ok(false, "Content analysis should make this fail to print"); + } catch (e) { + ok( + /NS_ERROR_NOT_AVAILABLE/.test(e.toString()), + "Error in mock CA was propagated out" + ); + } + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); +}); + +add_task(async function testPrintThroughDialogWithContentAnalysisActive() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + await helper.startPrint(); + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0]); + + await waitForFileToAlmostMatchSize( + file.path, + mockCA.calls[0].printDataSize + ); + }, + TEST_PAGE_URL, + true + ); +}); + +add_task( + async function testPrintThroughDialogWithContentAnalysisActiveAndBlocking() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(false); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + try { + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + } catch (e) { + ok( + /Wait for target file to get created/.test(e.toString()), + "Target file should not get created" + ); + } + ok(!file.exists(), "File should not exist"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintThroughDialogWithContentAnalysisReturningError() { + await PrintHelper.withTestPage( + async helper => { + expectUncaughtException(); + mockCA.setupForTestWithError(Cr.NS_ERROR_NOT_AVAILABLE); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + try { + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + } catch (e) { + ok( + /Wait for target file to get created/.test(e.toString()), + "Target file should not get created" + ); + } + ok(!file.exists(), "File should not exist"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js new file mode 100644 index 000000000000..9b4c0ffa6090 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js @@ -0,0 +1,390 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/printing/tests/head.js", + this +); + +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); + }, +}; + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); +}); + +const TEST_PAGE_URL = + "https://example.com/browser/toolkit/components/printing/tests/simplifyArticleSample.html"; +const TEST_PAGE_URL_2 = + "https://example.com/browser/toolkit/components/printing/tests/longerArticle.html"; + +function addUniqueSuffix(prefix) { + return `${prefix}-${Services.uuid + .generateUUID() + .toString() + .slice(1, -1)}.pdf`; +} + +async function printToDestination(aBrowser, aDestination) { + let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + let fileName = addUniqueSuffix(`printDestinationTest-${aDestination}`); + let filePath = PathUtils.join(tmpDir.path, fileName); + + info(`Printing to ${filePath}`); + + let settings = PSSVC.createNewPrintSettings(); + settings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; + settings.outputDestination = aDestination; + + settings.headerStrCenter = ""; + settings.headerStrLeft = ""; + settings.headerStrRight = ""; + settings.footerStrCenter = ""; + settings.footerStrLeft = ""; + settings.footerStrRight = ""; + + settings.unwriteableMarginTop = 1; /* Just to ensure settings are respected on both */ + let outStream = null; + if (aDestination == Ci.nsIPrintSettings.kOutputDestinationFile) { + settings.toFileName = PathUtils.join(tmpDir.path, fileName); + } else { + is(aDestination, Ci.nsIPrintSettings.kOutputDestinationStream); + outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + let tmpFile = tmpDir.clone(); + tmpFile.append(fileName); + outStream.init(tmpFile, -1, 0o666, 0); + settings.outputStream = outStream; + } + + await aBrowser.browsingContext.print(settings); + + return filePath; +} + +function assertContentAnalysisRequest(request, expectedUrl) { + is(request.url.spec, expectedUrl ?? TEST_PAGE_URL, "request has correct URL"); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.ePrint, + "request has print analysisType" + ); + is( + request.operationTypeForDisplay, + Ci.nsIContentAnalysisRequest.eOperationPrint, + "request has print operationTypeForDisplay" + ); + is(request.textContent, "", "request textContent should be empty"); + is(request.filePath, "", "request filePath should be empty"); + isnot(request.printDataHandle, 0, "request printDataHandle should not be 0"); + isnot(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} + +// Printing to a stream is different than going through the print preview dialog because it +// doesn't make a static clone of the document before the print, which causes the +// Content Analysis code to go through a different code path. This is similar to what +// happens when various preferences are set to skip the print preview dialog, for example +// print.prefer_system_dialog. +add_task( + async function testPrintToStreamWithContentAnalysisActiveAndAllowing() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + let filePath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + + await waitForFileToAlmostMatchSize( + filePath, + mockCA.calls[0].printDataSize + ); + + await IOUtils.remove(filePath); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintToStreamAfterNavigationWithContentAnalysisActiveAndAllowing() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + let filePath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + mockCA.clearCalls(); + + await IOUtils.remove(filePath); + + BrowserTestUtils.startLoadingURIString( + helper.sourceBrowser, + TEST_PAGE_URL_2 + ); + await BrowserTestUtils.browserLoaded(helper.sourceBrowser); + + filePath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0], TEST_PAGE_URL_2); + await waitForFileToAlmostMatchSize( + filePath, + mockCA.calls[0].printDataSize + ); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintToStreamWithContentAnalysisActiveAndBlocking() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(false); + + try { + await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + ok(false, "Content analysis should make this fail to print"); + } catch (e) { + ok( + /NS_ERROR_CONTENT_BLOCKED/.test(e.toString()), + "Got content blocked error" + ); + } + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task(async function testPrintToStreamWithContentAnalysisReturningError() { + await PrintHelper.withTestPage( + async helper => { + expectUncaughtException(); + mockCA.setupForTestWithError(Cr.NS_ERROR_NOT_AVAILABLE); + + try { + await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + ok(false, "Content analysis should make this fail to print"); + } catch (e) { + ok( + /NS_ERROR_NOT_AVAILABLE/.test(e.toString()), + "Error in mock CA was propagated out" + ); + } + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); +}); + +add_task(async function testPrintThroughDialogWithContentAnalysisActive() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0]); + + await waitForFileToAlmostMatchSize( + file.path, + mockCA.calls[0].printDataSize + ); + }, + TEST_PAGE_URL, + true + ); +}); + +add_task( + async function testPrintThroughDialogWithContentAnalysisActiveAndBlocking() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(false); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + try { + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + } catch (e) { + ok( + /Wait for target file to get created/.test(e.toString()), + "Target file should not get created" + ); + } + ok(!file.exists(), "File should not exist"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintThroughDialogWithContentAnalysisReturningError() { + await PrintHelper.withTestPage( + async helper => { + expectUncaughtException(); + mockCA.setupForTestWithError(Cr.NS_ERROR_NOT_AVAILABLE); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + try { + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + } catch (e) { + ok( + /Wait for target file to get created/.test(e.toString()), + "Target file should not get created" + ); + } + ok(!file.exists(), "File should not exist"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); diff --git a/toolkit/components/contentanalysis/tests/browser/changing_page_for_print.html b/toolkit/components/contentanalysis/tests/browser/changing_page_for_print.html new file mode 100644 index 000000000000..de6f9001aa82 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/changing_page_for_print.html @@ -0,0 +1,12 @@ + +

Some random text

+ +

+
diff --git a/toolkit/components/contentanalysis/tests/browser/head.js b/toolkit/components/contentanalysis/tests/browser/head.js
new file mode 100644
index 000000000000..e645caa2d71f
--- /dev/null
+++ b/toolkit/components/contentanalysis/tests/browser/head.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+  "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+// Wraps the given object in an XPConnect wrapper and, if an interface
+// is passed, queries the result to that interface.
+function xpcWrap(obj, iface) {
+  let ifacePointer = Cc[
+    "@mozilla.org/supports-interface-pointer;1"
+  ].createInstance(Ci.nsISupportsInterfacePointer);
+
+  ifacePointer.data = obj;
+  if (iface) {
+    return ifacePointer.data.QueryInterface(iface);
+  }
+  return ifacePointer.data;
+}
+
+/**
+ * Mock a (set of) service(s) as the object mockService.
+ *
+ * @param {[string]} serviceNames
+ *                   array of services names that mockService will be
+ *                   allowed to QI to.  Must include the name of the
+ *                   service referenced by contractId.
+ * @param {string}   contractId
+ *                   the component ID that will reference the mock object
+ *                   instead of the original service
+ * @param {object}   interfaceObj
+ *                   interface object for the component
+ * @param {object}   mockService
+ *                   object that satisfies the contract well
+ *                   enough to use as a mock of it
+ * @returns {object} The newly-mocked service
+ */
+function mockService(serviceNames, contractId, interfaceObj, mockService) {
+  // xpcWrap allows us to mock [implicit_jscontext] methods.
+  let newService = {
+    ...mockService,
+    QueryInterface: ChromeUtils.generateQI(serviceNames),
+  };
+  let o = xpcWrap(newService, interfaceObj);
+  let cid = MockRegistrar.register(contractId, o);
+  registerCleanupFunction(() => {
+    MockRegistrar.unregister(cid);
+  });
+  return newService;
+}
+
+/**
+ * Mock the nsIContentAnalysis service with the object mockCAService.
+ *
+ * @param {object}    mockCAService
+ *                    the service to mock for nsIContentAnalysis
+ * @returns {object}  The newly-mocked service
+ */
+function mockContentAnalysisService(mockCAService) {
+  return mockService(
+    ["nsIContentAnalysis"],
+    "@mozilla.org/contentanalysis;1",
+    Ci.nsIContentAnalysis,
+    mockCAService
+  );
+}
+
+/**
+ * Make an nsIContentAnalysisResponse.
+ *
+ * @param {number} action The action to take, from the
+ *  nsIContentAnalysisResponse.Action enum.
+ * @param {string} token The requestToken.
+ * @returns {object} An object that conforms to nsIContentAnalysisResponse.
+ */
+function makeContentAnalysisResponse(action, token) {
+  return {
+    action,
+    shouldAllowContent: action != Ci.nsIContentAnalysisResponse.eBlock,
+    requestToken: token,
+    acknowledge: _acknowledgement => {},
+  };
+}
+
+async function waitForFileToAlmostMatchSize(filePath, expectedSize) {
+  // In Cocoa the CGContext adds a hash, plus there are other minor
+  // non-user-visible differences, so we need to be a bit more sloppy there.
+  //
+  // We see one byte difference in Windows and Linux on automation sometimes,
+  // though files are consistently the same locally, that needs
+  // investigation, but it's probably harmless.
+  // Note that this is copied from browser_print_stream.js.
+  const maxSizeDifference = AppConstants.platform == "macosx" ? 100 : 3;
+
+  // Buffering shenanigans? Wait for sizes to match... There's no great
+  // IOUtils methods to force a flush without writing anything...
+  // Note that this means if this results in a timeout this is exactly
+  // the same as a test failure.
+  // This is taken from toolkit/components/printing/tests/browser_print_stream.js
+  await TestUtils.waitForCondition(async function () {
+    let fileStat = await IOUtils.stat(filePath);
+
+    info("got size: " + fileStat.size + " expected: " + expectedSize);
+    Assert.greater(
+      fileStat.size,
+      0,
+      "File should not be empty: " + fileStat.size
+    );
+    return Math.abs(fileStat.size - expectedSize) <= maxSizeDifference;
+  }, "Sizes should (almost) match");
+}
diff --git a/toolkit/components/printing/tests/head.js b/toolkit/components/printing/tests/head.js
index 8e8c2d1754c9..e2a83463a65e 100644
--- a/toolkit/components/printing/tests/head.js
+++ b/toolkit/components/printing/tests/head.js
@@ -60,6 +60,9 @@ class PrintHelper {
   }
 
   static getTestPageUrl(pathName) {
+    if (pathName.startsWith("http://")) {
+      return pathName;
+    }
     const testPath = getRootDirectory(gTestPath).replace(
       "chrome://mochitests/content",
       "http://example.com"
@@ -68,6 +71,9 @@ class PrintHelper {
   }
 
   static getTestPageUrlHTTPS(pathName) {
+    if (pathName.startsWith("https://")) {
+      return pathName;
+    }
     const testPath = getRootDirectory(gTestPath).replace(
       "chrome://mochitests/content",
       "https://example.com"
diff --git a/toolkit/locales/en-US/toolkit/contentanalysis/contentanalysis.ftl b/toolkit/locales/en-US/toolkit/contentanalysis/contentanalysis.ftl
index 932507e5ad46..41c18a48e37e 100644
--- a/toolkit/locales/en-US/toolkit/contentanalysis/contentanalysis.ftl
+++ b/toolkit/locales/en-US/toolkit/contentanalysis/contentanalysis.ftl
@@ -20,8 +20,12 @@ contentanalysis-slow-agent-dialog-body-clipboard = { $agent } is reviewing what
 # Variables:
 #   $agent - The name of the DLP agent doing the analysis
 contentanalysis-slow-agent-dialog-body-dropped-text = { $agent } is reviewing the text you dropped against your organization’s data policies. This may take a moment.
+# Variables:
+#   $agent - The name of the DLP agent doing the analysis
+contentanalysis-slow-agent-dialog-body-print = { $agent } is reviewing what you printed against your organization’s data policies. This may take a moment.
 contentanalysis-operationtype-clipboard = clipboard
 contentanalysis-operationtype-dropped-text = dropped text
+contentanalysis-operationtype-print = print
 #   $filename - The filename associated with the request, such as "aFile.txt"
 contentanalysis-customdisplaystring-description = upload of “{ $filename }”
 
diff --git a/widget/nsIPrintSettings.idl b/widget/nsIPrintSettings.idl
index 2355e776e012..daf0e145f429 100644
--- a/widget/nsIPrintSettings.idl
+++ b/widget/nsIPrintSettings.idl
@@ -335,7 +335,7 @@ interface nsIPrintSettings : nsISupports
    */
   attribute AString toFileName;
 
-  attribute nsIOutputStream outputStream; /* for kOutputDestinationPrinter */
+  attribute nsIOutputStream outputStream; /* for kOutputDestinationStream */
 
   [infallible] attribute long printPageDelay; /* in milliseconds */