Bug 1875481 - Part 3: Add content analysis support to printing operations r=handyman,spohl,fluent-reviewers,kpatenio,smaug,emilio

Differential Revision: https://phabricator.services.mozilla.com/D200979
This commit is contained in:
Greg Stoll 2024-04-10 00:15:47 +00:00
parent d0d35d1366
commit b32aa334d7
24 changed files with 1563 additions and 64 deletions

View File

@ -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 "";
}

View File

@ -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<PrintPromise::Private> mPromise;
MaybeDiscardedBrowsingContext mClonedStaticBrowsingContext = nullptr;
bool mHaveSetBrowsingContext = false;
bool mPrintJobFinished = false;
};
NS_IMPL_ISUPPORTS(PrintListenerAdapter, nsIWebProgressListener)
@ -735,7 +763,9 @@ already_AddRefed<Promise> 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<PrintPromise> 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<nsIContentAnalysis> 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<PrintPromise::Private>(__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<PrintPromise> CanonicalBrowsingContext::PrintWithNoContentAnalysis(
nsIPrintSettings* aPrintSettings, bool aForceStaticDocument,
const MaybeDiscardedBrowsingContext& aCachedStaticDocument) {
#ifndef NS_PRINTING
return PrintPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
#else
auto promise = MakeRefPtr<PrintPromise::Private>(__func__);
auto listener = MakeRefPtr<PrintListenerAdapter>(promise);
if (IsInProcess()) {
@ -757,12 +852,14 @@ RefPtr<PrintPromise> 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<PrintPromise> 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<PBrowserParent::PrintPromise> 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

View File

@ -136,11 +136,16 @@ class CanonicalBrowsingContext final : public BrowsingContext {
UniquePtr<LoadingSessionHistoryInfo> ReplaceLoadingSessionHistoryEntryForLoad(
LoadingSessionHistoryInfo* aInfo, nsIChannel* aNewChannel);
using PrintPromise = MozPromise</* unused */ bool, nsresult, false>;
using PrintPromise =
MozPromise<MaybeDiscardedBrowsingContext, nsresult, false>;
MOZ_CAN_RUN_SCRIPT RefPtr<PrintPromise> Print(nsIPrintSettings*);
MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> PrintJS(nsIPrintSettings*,
ErrorResult&);
MOZ_CAN_RUN_SCRIPT RefPtr<PrintPromise> 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.
//

View File

@ -3388,7 +3388,8 @@ already_AddRefed<Promise> 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));
}

View File

@ -3752,7 +3752,7 @@ Nullable<WindowProxyHolder> nsGlobalWindowInner::PrintPreview(
/* aRemotePrintJob = */ nullptr, aListener, aDocShellToCloneInto,
nsGlobalWindowOuter::IsPreview::Yes,
nsGlobalWindowOuter::IsForWindowDotPrint::No,
/* aPrintPreviewCallback = */ nullptr, aError),
/* aPrintPreviewCallback = */ nullptr, nullptr, aError),
aError, nullptr);
}

View File

@ -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<WindowProxyHolder> nsGlobalWindowOuter::Print(
nsIPrintSettings* aPrintSettings, RemotePrintJobChild* aRemotePrintJob,
nsIWebProgressListener* aListener, nsIDocShell* aDocShellToCloneInto,
IsPreview aIsPreview, IsForWindowDotPrint aForWindowDotPrint,
PrintPreviewResolver&& aPrintPreviewCallback, ErrorResult& aError) {
PrintPreviewResolver&& aPrintPreviewCallback,
RefPtr<BrowsingContext>* aCachedBrowsingContext, ErrorResult& aError) {
#ifdef NS_PRINTING
nsCOMPtr<nsIPrintSettingsService> printSettingsService =
do_GetService("@mozilla.org/gfx/printsettings-service;1");
@ -5064,16 +5065,36 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print(
nsCOMPtr<nsIDocumentViewer> viewer;
RefPtr<BrowsingContext> 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<nsIDocShell> docShell = bc->GetDocShell();
if (!docShell) {
aError.ThrowNotSupportedError("No docshell");
@ -5115,6 +5136,10 @@ Nullable<WindowProxyHolder> 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<WindowProxyHolder> 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

View File

@ -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::dom::BrowsingContext>*,
mozilla::ErrorResult&);
mozilla::dom::Selection* GetSelectionOuter();
already_AddRefed<mozilla::dom::Selection> GetSelection() override;
nsScreen* GetScreen();

View File

@ -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<BrowsingContext>* 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<RemotePrintJobChild*>(
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> 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<BrowsingContext> 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<nsPIDOMWindowOuter> 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)

View File

@ -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<BrowsingContext>* aCachedBrowsingContext);
bool CreateRemoteLayerManager(
mozilla::layers::PCompositorBridgeChild* aCompositorChild);

View File

@ -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.

View File

@ -449,6 +449,7 @@ class nsDocumentViewer final : public nsIDocumentViewer,
#ifdef NS_PRINTING
unsigned mClosingWhilePrinting : 1;
unsigned mCloseWindowAfterPrint : 1;
# if NS_PRINT_PREVIEW
RefPtr<nsPrintJob> 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 <browser> 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 <browser> 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<nsPIDOMWindowOuter> win = mContainer->GetWindow()) {
win->Close();

View File

@ -30,7 +30,7 @@ native PrintPreviewResolver(std::function<void(const mozilla::dom::PrintPreviewR
* nsIWebBrowserPrint corresponds to the main interface
* for printing an embedded Gecko web browser window/document
*/
[scriptable, uuid(c9a934ed-fff1-4971-bfba-6c25ad70e1e6)]
[scriptable, builtinclass, uuid(c9a934ed-fff1-4971-bfba-6c25ad70e1e6)]
interface nsIWebBrowserPrint : nsISupports
{
/**
@ -85,6 +85,13 @@ interface nsIWebBrowserPrint : nsISupports
*/
readonly attribute long printPreviewCurrentPageNumber;
/*
* Whether the document to print needs to have its window closed after printing
* is done - see OPEN_PRINT_BROWSER and specifically handleStaticCloneCreatedForPrint().
* This is set if the document is not a static clone.
*/
[infallible] attribute boolean closeWindowAfterPrint;
/**
* Print the specified DOM window
*

View File

@ -11,6 +11,7 @@
#include "base/process_util.h"
#include "GMPUtils.h" // ToHexString
#include "mozilla/Components.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/Logging.h"
@ -24,6 +25,9 @@
#include "nsIFile.h"
#include "nsIGlobalObject.h"
#include "nsIObserverService.h"
#include "nsIOutputStream.h"
#include "nsIPrintSettings.h"
#include "nsIStorageStream.h"
#include "ScopedNSSTypes.h"
#include "xpcpublic.h"
@ -35,6 +39,7 @@
# include <windows.h>
# define SECURITY_WIN32 1
# include <security.h>
# 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<uintptr_t>(mPrintDataHandle);
uint64_t printDataValue = static_cast<uint64_t>(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<uint8_t> aPrintData, nsCOMPtr<nsIURI> 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<LONGLONG>(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<uint8_t>(), 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::PrintAllowedPromise>
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<nsIPrintSettings> 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<nsIStorageStream> 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<nsIOutputStream> outputStream;
storageStream->QueryInterface(NS_GET_IID(nsIOutputStream),
getter_AddRefs(outputStream));
MOZ_ASSERT(outputStream);
contentAnalysisPrintSettings->SetOutputStream(outputStream.get());
RefPtr<dom::CanonicalBrowsingContext> browsingContext = aBrowsingContext;
auto promise = MakeRefPtr<PrintAllowedPromise::Private>(__func__);
nsCOMPtr<nsIPrintSettings> 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<nsIOutputStream> outputStream;
contentAnalysisPrintSettings->GetOutputStream(
getter_AddRefs(outputStream));
nsCOMPtr<nsIStorageStream> storageStream =
do_QueryInterface(outputStream);
MOZ_ASSERT(storageStream);
nsTArray<uint8_t> 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<nsIInputStream> 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<char*>(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<nsIURI> uri = windowParent->GetDocumentURI();
nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest =
new contentanalysis::ContentAnalysisRequest(
std::move(printData), std::move(uri),
std::move(printerName), windowParent);
auto callback =
MakeRefPtr<contentanalysis::ContentAnalysisCallback>(
[browsingContext, cachedStaticBrowsingContext, promise,
finalPrintSettings = std::move(finalPrintSettings)](
nsIContentAnalysisResponse* aResponse)
MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable {
bool shouldAllow = false;
DebugOnly<nsresult> 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<nsIContentAnalysis> 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) {

View File

@ -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 <regex>
#include <string>
#ifdef XP_WIN
# include <windows.h>
#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<nsIURI> aUrl, OperationType aOperationType,
dom::WindowGlobalParent* aWindowGlobalParent);
ContentAnalysisRequest(const nsTArray<uint8_t> aPrintData,
nsCOMPtr<nsIURI> 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<dom::WindowGlobalParent> 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<dom::BrowsingContext>
mCachedStaticDocumentBrowsingContext;
PrintAllowedResult(bool aAllowed, dom::MaybeDiscarded<dom::BrowsingContext>
aCachedStaticDocumentBrowsingContext)
: mAllowed(aAllowed),
mCachedStaticDocumentBrowsingContext(
aCachedStaticDocumentBrowsingContext) {}
explicit PrintAllowedResult(bool aAllowed)
: PrintAllowedResult(aAllowed, dom::MaybeDiscardedBrowsingContext()) {}
};
struct PrintAllowedError final {
nsresult mError;
dom::MaybeDiscarded<dom::BrowsingContext>
mCachedStaticDocumentBrowsingContext;
PrintAllowedError(nsresult aError, dom::MaybeDiscarded<dom::BrowsingContext>
aCachedStaticDocumentBrowsingContext)
: mError(aError),
mCachedStaticDocumentBrowsingContext(
aCachedStaticDocumentBrowsingContext) {}
explicit PrintAllowedError(nsresult aError)
: PrintAllowedError(aError, dom::MaybeDiscardedBrowsingContext()) {}
};
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)
private:
~ContentAnalysis();
// Remove unneeded copy constructor/assignment

View File

@ -11,5 +11,6 @@ Classes = [
'contract_ids': ['@mozilla.org/contentanalysis;1'],
'type': 'mozilla::contentanalysis::ContentAnalysis',
'headers': ['/toolkit/components/contentanalysis/ContentAnalysis.h'],
'overridable': True,
},
]

View File

@ -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;

View File

@ -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",
]

View File

@ -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
);
}
);

View File

@ -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
);
}
);

View File

@ -0,0 +1,12 @@
<!doctype html>
<p>Some random text</p>
<button onclick="print()">Print the page</button>
<pre id="log"></pre>
<script>
let i = 0;
for (let t of ["beforeprint", "afterprint"]) {
addEventListener(t, () => {
document.getElementById("log").appendChild(document.createTextNode(`[${i++}] ${t}\n`));
});
}
</script>

View File

@ -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");
}

View File

@ -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"

View File

@ -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 organizations 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 organizations 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 }”

View File

@ -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 */