summaryrefslogtreecommitdiffstats
path: root/dom/reporting
diff options
context:
space:
mode:
Diffstat (limited to 'dom/reporting')
-rw-r--r--dom/reporting/CrashReport.cpp59
-rw-r--r--dom/reporting/CrashReport.h23
-rw-r--r--dom/reporting/DeprecationReportBody.cpp81
-rw-r--r--dom/reporting/DeprecationReportBody.h55
-rw-r--r--dom/reporting/EndpointForReportChild.cpp30
-rw-r--r--dom/reporting/EndpointForReportChild.h32
-rw-r--r--dom/reporting/EndpointForReportParent.cpp44
-rw-r--r--dom/reporting/EndpointForReportParent.h42
-rw-r--r--dom/reporting/FeaturePolicyViolationReportBody.cpp79
-rw-r--r--dom/reporting/FeaturePolicyViolationReportBody.h53
-rw-r--r--dom/reporting/PEndpointForReport.ipdl20
-rw-r--r--dom/reporting/Report.cpp47
-rw-r--r--dom/reporting/Report.h59
-rw-r--r--dom/reporting/ReportBody.cpp27
-rw-r--r--dom/reporting/ReportBody.h44
-rw-r--r--dom/reporting/ReportDeliver.cpp410
-rw-r--r--dom/reporting/ReportDeliver.h65
-rw-r--r--dom/reporting/ReportingHeader.cpp774
-rw-r--r--dom/reporting/ReportingHeader.h142
-rw-r--r--dom/reporting/ReportingObserver.cpp152
-rw-r--r--dom/reporting/ReportingObserver.h77
-rw-r--r--dom/reporting/ReportingUtils.cpp45
-rw-r--r--dom/reporting/ReportingUtils.h28
-rw-r--r--dom/reporting/TestingDeprecatedInterface.cpp48
-rw-r--r--dom/reporting/TestingDeprecatedInterface.h50
-rw-r--r--dom/reporting/moz.build51
-rw-r--r--dom/reporting/tests/browser.toml7
-rw-r--r--dom/reporting/tests/browser_cleanup.js276
-rw-r--r--dom/reporting/tests/common_deprecated.js214
-rw-r--r--dom/reporting/tests/delivering.sjs111
-rw-r--r--dom/reporting/tests/empty.html1
-rw-r--r--dom/reporting/tests/gtest/TestReportToParser.cpp418
-rw-r--r--dom/reporting/tests/gtest/moz.build13
-rw-r--r--dom/reporting/tests/iframe_delivering.html92
-rw-r--r--dom/reporting/tests/mochitest.toml29
-rw-r--r--dom/reporting/tests/test_delivering.html45
-rw-r--r--dom/reporting/tests/test_deprecated.html51
-rw-r--r--dom/reporting/tests/test_memoryPressure.html33
-rw-r--r--dom/reporting/tests/worker_delivering.js5
-rw-r--r--dom/reporting/tests/worker_deprecated.js28
40 files changed, 3860 insertions, 0 deletions
diff --git a/dom/reporting/CrashReport.cpp b/dom/reporting/CrashReport.cpp
new file mode 100644
index 0000000000..9cfd5340d5
--- /dev/null
+++ b/dom/reporting/CrashReport.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/CrashReport.h"
+
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/ReportingHeader.h"
+#include "mozilla/dom/ReportDeliver.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+#include "nsIPrincipal.h"
+#include "nsIURIMutator.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+
+/* static */
+bool CrashReport::Deliver(nsIPrincipal* aPrincipal, bool aIsOOM) {
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoCString endpoint_url;
+ ReportingHeader::GetEndpointForReport(u"default"_ns, aPrincipal,
+ endpoint_url);
+ if (endpoint_url.IsEmpty()) {
+ return false;
+ }
+
+ nsCString safe_origin_spec;
+ aPrincipal->GetExposableSpec(safe_origin_spec);
+
+ ReportDeliver::ReportData data;
+ data.mType = u"crash"_ns;
+ data.mGroupName = u"default"_ns;
+ CopyUTF8toUTF16(safe_origin_spec, data.mURL);
+ data.mCreationTime = TimeStamp::Now();
+
+ Navigator::GetUserAgent(nullptr, nullptr, Nothing(), data.mUserAgent);
+ data.mPrincipal = aPrincipal;
+ data.mFailures = 0;
+ data.mEndpointURL = endpoint_url;
+
+ JSONStringWriteFunc<nsCString> body;
+ JSONWriter writer{body};
+
+ writer.Start();
+ if (aIsOOM) {
+ writer.StringProperty("reason", "oom");
+ }
+ writer.End();
+
+ data.mReportBodyJSON = std::move(body).StringRRef();
+
+ ReportDeliver::Fetch(data);
+ return true;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/CrashReport.h b/dom/reporting/CrashReport.h
new file mode 100644
index 0000000000..3a2ec64b05
--- /dev/null
+++ b/dom/reporting/CrashReport.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CrashReport_h
+#define mozilla_dom_CrashReport_h
+
+#include "nsCOMPtr.h"
+
+class nsIPrincipal;
+
+namespace mozilla::dom {
+
+class CrashReport {
+ public:
+ static bool Deliver(nsIPrincipal* aPrincipal, bool aIsOOM);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_CrashReport_h
diff --git a/dom/reporting/DeprecationReportBody.cpp b/dom/reporting/DeprecationReportBody.cpp
new file mode 100644
index 0000000000..1154121484
--- /dev/null
+++ b/dom/reporting/DeprecationReportBody.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/DeprecationReportBody.h"
+#include "mozilla/dom/ReportingBinding.h"
+#include "mozilla/JSONWriter.h"
+
+namespace mozilla::dom {
+
+DeprecationReportBody::DeprecationReportBody(
+ nsIGlobalObject* aGlobal, const nsAString& aId,
+ const Nullable<uint64_t>& aDate, const nsAString& aMessage,
+ const nsAString& aSourceFile, const Nullable<uint32_t>& aLineNumber,
+ const Nullable<uint32_t>& aColumnNumber)
+ : ReportBody(aGlobal),
+ mId(aId),
+ mDate(aDate),
+ mMessage(aMessage),
+ mSourceFile(aSourceFile),
+ mLineNumber(aLineNumber),
+ mColumnNumber(aColumnNumber) {
+ MOZ_ASSERT(aGlobal);
+}
+
+DeprecationReportBody::~DeprecationReportBody() = default;
+
+JSObject* DeprecationReportBody::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return DeprecationReportBody_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void DeprecationReportBody::GetId(nsAString& aId) const { aId = mId; }
+
+Nullable<uint64_t> DeprecationReportBody::GetAnticipatedRemoval() const {
+ return mDate;
+}
+
+void DeprecationReportBody::GetMessage(nsAString& aMessage) const {
+ aMessage = mMessage;
+}
+
+void DeprecationReportBody::GetSourceFile(nsAString& aSourceFile) const {
+ aSourceFile = mSourceFile;
+}
+
+Nullable<uint32_t> DeprecationReportBody::GetLineNumber() const {
+ return mLineNumber;
+}
+
+Nullable<uint32_t> DeprecationReportBody::GetColumnNumber() const {
+ return mColumnNumber;
+}
+
+void DeprecationReportBody::ToJSON(JSONWriter& aWriter) const {
+ aWriter.StringProperty("id", NS_ConvertUTF16toUTF8(mId));
+ // TODO: anticipatedRemoval? https://github.com/w3c/reporting/issues/132
+ aWriter.StringProperty("message", NS_ConvertUTF16toUTF8(mMessage));
+
+ if (mSourceFile.IsEmpty()) {
+ aWriter.NullProperty("sourceFile");
+ } else {
+ aWriter.StringProperty("sourceFile", NS_ConvertUTF16toUTF8(mSourceFile));
+ }
+
+ if (mLineNumber.IsNull()) {
+ aWriter.NullProperty("lineNumber");
+ } else {
+ aWriter.IntProperty("lineNumber", mLineNumber.Value());
+ }
+
+ if (mColumnNumber.IsNull()) {
+ aWriter.NullProperty("columnNumber");
+ } else {
+ aWriter.IntProperty("columnNumber", mColumnNumber.Value());
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/DeprecationReportBody.h b/dom/reporting/DeprecationReportBody.h
new file mode 100644
index 0000000000..9476899b12
--- /dev/null
+++ b/dom/reporting/DeprecationReportBody.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_DeprecationReportBody_h
+#define mozilla_dom_DeprecationReportBody_h
+
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/ReportBody.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+
+class DeprecationReportBody final : public ReportBody {
+ public:
+ DeprecationReportBody(nsIGlobalObject* aGlobal, const nsAString& aId,
+ const Nullable<uint64_t>& aDate,
+ const nsAString& aMessage, const nsAString& aSourceFile,
+ const Nullable<uint32_t>& aLineNumber,
+ const Nullable<uint32_t>& aColumnNumber);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetId(nsAString& aId) const;
+
+ Nullable<uint64_t> GetAnticipatedRemoval() const;
+
+ void GetMessage(nsAString& aMessage) const;
+
+ void GetSourceFile(nsAString& aSourceFile) const;
+
+ Nullable<uint32_t> GetLineNumber() const;
+
+ Nullable<uint32_t> GetColumnNumber() const;
+
+ protected:
+ void ToJSON(JSONWriter& aJSONWriter) const override;
+
+ private:
+ ~DeprecationReportBody();
+
+ const nsString mId;
+ const Nullable<uint64_t> mDate;
+ const nsString mMessage;
+ const nsString mSourceFile;
+ const Nullable<uint32_t> mLineNumber;
+ const Nullable<uint32_t> mColumnNumber;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_DeprecationReportBody_h
diff --git a/dom/reporting/EndpointForReportChild.cpp b/dom/reporting/EndpointForReportChild.cpp
new file mode 100644
index 0000000000..ba10e077b1
--- /dev/null
+++ b/dom/reporting/EndpointForReportChild.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/EndpointForReportChild.h"
+
+namespace mozilla::dom {
+
+EndpointForReportChild::EndpointForReportChild() = default;
+
+EndpointForReportChild::~EndpointForReportChild() = default;
+
+void EndpointForReportChild::Initialize(
+ const ReportDeliver::ReportData& aData) {
+ mReportData = aData;
+}
+
+mozilla::ipc::IPCResult EndpointForReportChild::Recv__delete__(
+ const nsCString& aEndpointURL) {
+ if (!aEndpointURL.IsEmpty()) {
+ mReportData.mEndpointURL = aEndpointURL;
+ ReportDeliver::Fetch(mReportData);
+ }
+
+ return IPC_OK();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/EndpointForReportChild.h b/dom/reporting/EndpointForReportChild.h
new file mode 100644
index 0000000000..bdc2d9c819
--- /dev/null
+++ b/dom/reporting/EndpointForReportChild.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_EndpointForReportChild_h
+#define mozilla_dom_EndpointForReportChild_h
+
+#include "mozilla/dom/ReportDeliver.h"
+#include "mozilla/dom/PEndpointForReportChild.h"
+
+namespace mozilla::dom {
+
+class EndpointForReport;
+
+class EndpointForReportChild final : public PEndpointForReportChild {
+ public:
+ EndpointForReportChild();
+ ~EndpointForReportChild();
+
+ void Initialize(const ReportDeliver::ReportData& aReportData);
+
+ mozilla::ipc::IPCResult Recv__delete__(const nsCString& aEndpointURL);
+
+ private:
+ ReportDeliver::ReportData mReportData;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_EndpointForReportChild_h
diff --git a/dom/reporting/EndpointForReportParent.cpp b/dom/reporting/EndpointForReportParent.cpp
new file mode 100644
index 0000000000..445cbf1818
--- /dev/null
+++ b/dom/reporting/EndpointForReportParent.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/EndpointForReportParent.h"
+#include "mozilla/dom/ReportingHeader.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/Unused.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::dom {
+
+EndpointForReportParent::EndpointForReportParent()
+ : mPBackgroundThread(NS_GetCurrentThread()), mActive(true) {}
+
+EndpointForReportParent::~EndpointForReportParent() = default;
+
+void EndpointForReportParent::ActorDestroy(ActorDestroyReason aWhy) {
+ mActive = false;
+}
+
+void EndpointForReportParent::Run(
+ const nsAString& aGroupName,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo) {
+ RefPtr<EndpointForReportParent> self = this;
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "EndpointForReportParent::Run",
+ [self, aGroupName = nsString(aGroupName), aPrincipalInfo]() {
+ nsAutoCString uri;
+ ReportingHeader::GetEndpointForReport(aGroupName, aPrincipalInfo, uri);
+ self->mPBackgroundThread->Dispatch(NS_NewRunnableFunction(
+ "EndpointForReportParent::Answer", [self, uri]() {
+ if (self->mActive) {
+ Unused << self->Send__delete__(self, uri);
+ }
+ }));
+ }));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/EndpointForReportParent.h b/dom/reporting/EndpointForReportParent.h
new file mode 100644
index 0000000000..42d5cce526
--- /dev/null
+++ b/dom/reporting/EndpointForReportParent.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_EndpointForReportParent_h
+#define mozilla_dom_EndpointForReportParent_h
+
+#include "mozilla/dom/PEndpointForReportParent.h"
+
+namespace mozilla {
+namespace ipc {
+class PrincipalInfo;
+}
+
+namespace dom {
+
+class EndpointForReport;
+
+class EndpointForReportParent final : public PEndpointForReportParent {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(EndpointForReportParent)
+
+ EndpointForReportParent();
+
+ void Run(const nsAString& aGroupName,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo);
+
+ private:
+ ~EndpointForReportParent();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ nsCOMPtr<nsIThread> mPBackgroundThread;
+ bool mActive;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_EndpointForReportParent_h
diff --git a/dom/reporting/FeaturePolicyViolationReportBody.cpp b/dom/reporting/FeaturePolicyViolationReportBody.cpp
new file mode 100644
index 0000000000..a8c46bf6ed
--- /dev/null
+++ b/dom/reporting/FeaturePolicyViolationReportBody.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/FeaturePolicyViolationReportBody.h"
+
+#include "mozilla/JSONWriter.h"
+#include "mozilla/dom/FeaturePolicyBinding.h"
+
+namespace mozilla::dom {
+
+FeaturePolicyViolationReportBody::FeaturePolicyViolationReportBody(
+ nsIGlobalObject* aGlobal, const nsAString& aFeatureId,
+ const nsAString& aSourceFile, const Nullable<int32_t>& aLineNumber,
+ const Nullable<int32_t>& aColumnNumber, const nsAString& aDisposition)
+ : ReportBody(aGlobal),
+ mFeatureId(aFeatureId),
+ mSourceFile(aSourceFile),
+ mLineNumber(aLineNumber),
+ mColumnNumber(aColumnNumber),
+ mDisposition(aDisposition) {}
+
+FeaturePolicyViolationReportBody::~FeaturePolicyViolationReportBody() = default;
+
+JSObject* FeaturePolicyViolationReportBody::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return FeaturePolicyViolationReportBody_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void FeaturePolicyViolationReportBody::GetFeatureId(
+ nsAString& aFeatureId) const {
+ aFeatureId = mFeatureId;
+}
+
+void FeaturePolicyViolationReportBody::GetSourceFile(
+ nsAString& aSourceFile) const {
+ aSourceFile = mSourceFile;
+}
+
+Nullable<int32_t> FeaturePolicyViolationReportBody::GetLineNumber() const {
+ return mLineNumber;
+}
+
+Nullable<int32_t> FeaturePolicyViolationReportBody::GetColumnNumber() const {
+ return mColumnNumber;
+}
+
+void FeaturePolicyViolationReportBody::GetDisposition(
+ nsAString& aDisposition) const {
+ aDisposition = mDisposition;
+}
+
+void FeaturePolicyViolationReportBody::ToJSON(JSONWriter& aWriter) const {
+ aWriter.StringProperty("featureId", NS_ConvertUTF16toUTF8(mFeatureId));
+
+ if (mSourceFile.IsEmpty()) {
+ aWriter.NullProperty("sourceFile");
+ } else {
+ aWriter.StringProperty("sourceFile", NS_ConvertUTF16toUTF8(mSourceFile));
+ }
+
+ if (mLineNumber.IsNull()) {
+ aWriter.NullProperty("lineNumber");
+ } else {
+ aWriter.IntProperty("lineNumber", mLineNumber.Value());
+ }
+
+ if (mColumnNumber.IsNull()) {
+ aWriter.NullProperty("columnNumber");
+ } else {
+ aWriter.IntProperty("columnNumber", mColumnNumber.Value());
+ }
+
+ aWriter.StringProperty("disposition", NS_ConvertUTF16toUTF8(mDisposition));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/FeaturePolicyViolationReportBody.h b/dom/reporting/FeaturePolicyViolationReportBody.h
new file mode 100644
index 0000000000..bf6f749256
--- /dev/null
+++ b/dom/reporting/FeaturePolicyViolationReportBody.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FeaturePolicyViolationReportBody_h
+#define mozilla_dom_FeaturePolicyViolationReportBody_h
+
+#include "mozilla/dom/ReportBody.h"
+#include "mozilla/dom/Nullable.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+
+class FeaturePolicyViolationReportBody final : public ReportBody {
+ public:
+ FeaturePolicyViolationReportBody(nsIGlobalObject* aGlobal,
+ const nsAString& aFeatureId,
+ const nsAString& aSourceFile,
+ const Nullable<int32_t>& aLineNumber,
+ const Nullable<int32_t>& aColumnNumber,
+ const nsAString& aDisposition);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetFeatureId(nsAString& aFeatureId) const;
+
+ void GetSourceFile(nsAString& aSourceFile) const;
+
+ Nullable<int32_t> GetLineNumber() const;
+
+ Nullable<int32_t> GetColumnNumber() const;
+
+ void GetDisposition(nsAString& aDisposition) const;
+
+ protected:
+ void ToJSON(JSONWriter& aJSONWriter) const override;
+
+ private:
+ ~FeaturePolicyViolationReportBody();
+
+ const nsString mFeatureId;
+ const nsString mSourceFile;
+ const Nullable<int32_t> mLineNumber;
+ const Nullable<int32_t> mColumnNumber;
+ const nsString mDisposition;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FeaturePolicyViolationReportBody_h
diff --git a/dom/reporting/PEndpointForReport.ipdl b/dom/reporting/PEndpointForReport.ipdl
new file mode 100644
index 0000000000..e04897402b
--- /dev/null
+++ b/dom/reporting/PEndpointForReport.ipdl
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+
+namespace mozilla {
+namespace dom {
+
+[ManualDealloc]
+protocol PEndpointForReport
+{
+ manager PBackground;
+
+child:
+ async __delete__(nsCString endpointURL);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/reporting/Report.cpp b/dom/reporting/Report.cpp
new file mode 100644
index 0000000000..3fae46709a
--- /dev/null
+++ b/dom/reporting/Report.cpp
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/Report.h"
+#include "mozilla/dom/ReportBody.h"
+#include "mozilla/dom/ReportingBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Report, mGlobal, mBody)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(Report)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(Report)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Report)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+Report::Report(nsIGlobalObject* aGlobal, const nsAString& aType,
+ const nsAString& aURL, ReportBody* aBody)
+ : mGlobal(aGlobal), mType(aType), mURL(aURL), mBody(aBody) {
+ MOZ_ASSERT(aGlobal);
+}
+
+Report::~Report() = default;
+
+already_AddRefed<Report> Report::Clone() {
+ RefPtr<Report> report = new Report(mGlobal, mType, mURL, mBody);
+ return report.forget();
+}
+
+JSObject* Report::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return Report_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void Report::GetType(nsAString& aType) const { aType = mType; }
+
+void Report::GetUrl(nsAString& aURL) const { aURL = mURL; }
+
+ReportBody* Report::GetBody() const { return mBody; }
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/Report.h b/dom/reporting/Report.h
new file mode 100644
index 0000000000..e139e27858
--- /dev/null
+++ b/dom/reporting/Report.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_Report_h
+#define mozilla_dom_Report_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::dom {
+
+class ReportBody;
+
+class Report final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Report)
+
+ Report(nsIGlobalObject* aGlobal, const nsAString& aType,
+ const nsAString& aURL, ReportBody* aBody);
+
+ already_AddRefed<Report> Clone();
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsIGlobalObject* GetParentObject() const { return mGlobal; }
+
+ void GetType(nsAString& aType) const;
+
+ void GetUrl(nsAString& aURL) const;
+
+ ReportBody* GetBody() const;
+
+ private:
+ ~Report();
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+
+ const nsString mType;
+ const nsString mURL;
+ RefPtr<ReportBody> mBody;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_Report_h
diff --git a/dom/reporting/ReportBody.cpp b/dom/reporting/ReportBody.cpp
new file mode 100644
index 0000000000..ef4330916e
--- /dev/null
+++ b/dom/reporting/ReportBody.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ReportBody.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ReportBody, mGlobal)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ReportBody)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ReportBody)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReportBody)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+ReportBody::ReportBody(nsIGlobalObject* aGlobal) : mGlobal(aGlobal) {
+ MOZ_ASSERT(aGlobal);
+}
+
+ReportBody::~ReportBody() = default;
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/ReportBody.h b/dom/reporting/ReportBody.h
new file mode 100644
index 0000000000..9043dba16d
--- /dev/null
+++ b/dom/reporting/ReportBody.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ReportBody_h
+#define mozilla_dom_ReportBody_h
+
+#include "mozilla/Assertions.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+class JSONWriter;
+
+namespace dom {
+
+class ReportBody : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ReportBody)
+
+ explicit ReportBody(nsIGlobalObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const { return mGlobal; }
+
+ virtual void ToJSON(JSONWriter& aJSONWriter) const = 0;
+
+ protected:
+ virtual ~ReportBody();
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ReportBody_h
diff --git a/dom/reporting/ReportDeliver.cpp b/dom/reporting/ReportDeliver.cpp
new file mode 100644
index 0000000000..08a31e57ee
--- /dev/null
+++ b/dom/reporting/ReportDeliver.cpp
@@ -0,0 +1,410 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/JSONStringWriteFuncs.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/EndpointForReportChild.h"
+#include "mozilla/dom/Fetch.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ReportBody.h"
+#include "mozilla/dom/ReportDeliver.h"
+#include "mozilla/dom/Request.h"
+#include "mozilla/dom/RequestBinding.h"
+#include "mozilla/dom/Response.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIGlobalObject.h"
+#include "nsIXPConnect.h"
+#include "nsNetUtil.h"
+#include "nsStringStream.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+StaticRefPtr<ReportDeliver> gReportDeliver;
+
+class ReportFetchHandler final : public PromiseNativeHandler {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit ReportFetchHandler(
+ const nsTArray<ReportDeliver::ReportData>& aReportData)
+ : mReports(aReportData.Clone()) {}
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ if (!gReportDeliver) {
+ return;
+ }
+
+ if (NS_WARN_IF(!aValue.isObject())) {
+ return;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
+ MOZ_ASSERT(obj);
+
+ {
+ Response* response = nullptr;
+ if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Response, &obj, response)))) {
+ return;
+ }
+
+ if (response->Status() == 410) {
+ mozilla::ipc::PBackgroundChild* actorChild =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+
+ for (const auto& report : mReports) {
+ mozilla::ipc::PrincipalInfo principalInfo;
+ nsresult rv =
+ PrincipalToPrincipalInfo(report.mPrincipal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ actorChild->SendRemoveEndpoint(report.mGroupName, report.mEndpointURL,
+ principalInfo);
+ }
+ }
+ }
+ }
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ if (gReportDeliver) {
+ for (auto& report : mReports) {
+ ++report.mFailures;
+ gReportDeliver->AppendReportData(report);
+ }
+ }
+ }
+
+ private:
+ ~ReportFetchHandler() = default;
+
+ nsTArray<ReportDeliver::ReportData> mReports;
+};
+
+NS_IMPL_ISUPPORTS0(ReportFetchHandler)
+
+class ReportJSONWriter final : public JSONWriter {
+ public:
+ explicit ReportJSONWriter(JSONStringWriteFunc<nsAutoCString>& aOutput)
+ : JSONWriter(aOutput) {}
+
+ void JSONProperty(const Span<const char>& aProperty,
+ const Span<const char>& aJSON) {
+ Separator();
+ PropertyNameAndColon(aProperty);
+ mWriter.Write(aJSON);
+ }
+};
+
+void SendReports(nsTArray<ReportDeliver::ReportData>& aReports,
+ const nsCString& aEndPointUrl, nsIPrincipal* aPrincipal) {
+ if (NS_WARN_IF(aReports.IsEmpty())) {
+ return;
+ }
+
+ nsIXPConnect* xpc = nsContentUtils::XPConnect();
+ MOZ_ASSERT(xpc, "This should never be null!");
+
+ nsCOMPtr<nsIGlobalObject> globalObject;
+ {
+ AutoJSAPI jsapi;
+ jsapi.Init();
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JSObject*> sandbox(cx);
+ nsresult rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // The JSContext is not in a realm, so CreateSandbox returned an unwrapped
+ // global.
+ MOZ_ASSERT(JS_IsGlobalObject(sandbox));
+
+ globalObject = xpc::NativeGlobal(sandbox);
+ }
+
+ if (NS_WARN_IF(!globalObject)) {
+ return;
+ }
+
+ // The body
+ JSONStringWriteFunc<nsAutoCString> body;
+ ReportJSONWriter w(body);
+
+ w.StartArrayElement();
+ for (const auto& report : aReports) {
+ MOZ_ASSERT(report.mPrincipal == aPrincipal);
+ MOZ_ASSERT(report.mEndpointURL == aEndPointUrl);
+ w.StartObjectElement();
+ w.IntProperty("age",
+ (TimeStamp::Now() - report.mCreationTime).ToMilliseconds());
+ w.StringProperty("type", NS_ConvertUTF16toUTF8(report.mType));
+ w.StringProperty("url", NS_ConvertUTF16toUTF8(report.mURL));
+ w.StringProperty("user_agent", NS_ConvertUTF16toUTF8(report.mUserAgent));
+ w.JSONProperty(MakeStringSpan("body"),
+ Span<const char>(report.mReportBodyJSON.Data(),
+ report.mReportBodyJSON.Length()));
+ w.EndObject();
+ }
+ w.EndArray();
+
+ // The body as stream
+ nsCOMPtr<nsIInputStream> streamBody;
+ nsresult rv =
+ NS_NewCStringInputStream(getter_AddRefs(streamBody), body.StringCRef());
+
+ // Headers
+ IgnoredErrorResult error;
+ RefPtr<InternalHeaders> internalHeaders =
+ new InternalHeaders(HeadersGuardEnum::Request);
+ internalHeaders->Set("Content-Type"_ns, "application/reports+json"_ns, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return;
+ }
+
+ // URL and fragments
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), aEndPointUrl);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uriClone;
+ rv = NS_GetURIWithoutRef(uri, getter_AddRefs(uriClone));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsAutoCString uriSpec;
+ rv = uriClone->GetSpec(uriSpec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsAutoCString uriFragment;
+ rv = uri->GetRef(uriFragment);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ auto internalRequest = MakeSafeRefPtr<InternalRequest>(uriSpec, uriFragment);
+
+ internalRequest->SetMethod("POST"_ns);
+ internalRequest->SetBody(streamBody, body.StringCRef().Length());
+ internalRequest->SetHeaders(internalHeaders);
+ internalRequest->SetSkipServiceWorker();
+ // TODO: internalRequest->SetContentPolicyType(TYPE_REPORT);
+ internalRequest->SetMode(RequestMode::Cors);
+ internalRequest->SetCredentialsMode(RequestCredentials::Include);
+
+ RefPtr<Request> request =
+ new Request(globalObject, std::move(internalRequest), nullptr);
+
+ RequestOrUSVString fetchInput;
+ fetchInput.SetAsRequest() = request;
+
+ RootedDictionary<RequestInit> requestInit(RootingCx());
+ RefPtr<Promise> promise = FetchRequest(globalObject, fetchInput, requestInit,
+ CallerType::NonSystem, error);
+ if (error.Failed()) {
+ for (auto& report : aReports) {
+ ++report.mFailures;
+ if (gReportDeliver) {
+ gReportDeliver->AppendReportData(report);
+ }
+ }
+ return;
+ }
+
+ RefPtr<ReportFetchHandler> handler = new ReportFetchHandler(aReports);
+ promise->AppendNativeHandler(handler);
+}
+
+} // namespace
+
+/* static */
+void ReportDeliver::Record(nsPIDOMWindowInner* aWindow, const nsAString& aType,
+ const nsAString& aGroupName, const nsAString& aURL,
+ ReportBody* aBody) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aBody);
+
+ JSONStringWriteFunc<nsAutoCString> reportBodyJSON;
+ ReportJSONWriter w(reportBodyJSON);
+
+ w.Start();
+ aBody->ToJSON(w);
+ w.End();
+
+ nsCOMPtr<nsIPrincipal> principal =
+ nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ return;
+ }
+
+ mozilla::ipc::PrincipalInfo principalInfo;
+ nsresult rv = PrincipalToPrincipalInfo(principal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ mozilla::ipc::PBackgroundChild* actorChild =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+
+ PEndpointForReportChild* actor =
+ actorChild->SendPEndpointForReportConstructor(nsString(aGroupName),
+ principalInfo);
+ if (NS_WARN_IF(!actor)) {
+ return;
+ }
+
+ ReportData data;
+ data.mType = aType;
+ data.mGroupName = aGroupName;
+ data.mURL = aURL;
+ data.mCreationTime = TimeStamp::Now();
+ data.mReportBodyJSON = std::move(reportBodyJSON).StringRRef();
+ data.mPrincipal = principal;
+ data.mFailures = 0;
+
+ Navigator* navigator = aWindow->Navigator();
+ MOZ_ASSERT(navigator);
+
+ IgnoredErrorResult error;
+ navigator->GetUserAgent(data.mUserAgent, CallerType::NonSystem, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return;
+ }
+
+ static_cast<EndpointForReportChild*>(actor)->Initialize(data);
+}
+
+/* static */
+void ReportDeliver::Fetch(const ReportData& aReportData) {
+ if (!gReportDeliver) {
+ RefPtr<ReportDeliver> rd = new ReportDeliver();
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return;
+ }
+
+ obs->AddObserver(rd, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ gReportDeliver = rd;
+ }
+
+ gReportDeliver->AppendReportData(aReportData);
+}
+
+void ReportDeliver::AppendReportData(const ReportData& aReportData) {
+ if (aReportData.mFailures >
+ StaticPrefs::dom_reporting_delivering_maxFailures()) {
+ return;
+ }
+
+ if (NS_WARN_IF(!mReportQueue.AppendElement(aReportData, fallible))) {
+ return;
+ }
+
+ while (mReportQueue.Length() >
+ StaticPrefs::dom_reporting_delivering_maxReports()) {
+ mReportQueue.RemoveElementAt(0);
+ }
+
+ if (!mTimer) {
+ uint32_t timeout = StaticPrefs::dom_reporting_delivering_timeout() * 1000;
+ nsresult rv = NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, timeout,
+ nsITimer::TYPE_ONE_SHOT);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+}
+
+NS_IMETHODIMP
+ReportDeliver::Notify(nsITimer* aTimer) {
+ mTimer = nullptr;
+
+ nsTArray<ReportData> reports = std::move(mReportQueue);
+
+ // group reports by endpoint and nsIPrincipal
+ std::map<std::pair<nsCString, nsCOMPtr<nsIPrincipal>>, nsTArray<ReportData>>
+ reportsByPrincipal;
+ for (ReportData& report : reports) {
+ auto already_seen =
+ reportsByPrincipal.find({report.mEndpointURL, report.mPrincipal});
+ if (already_seen == reportsByPrincipal.end()) {
+ reportsByPrincipal.emplace(
+ std::make_pair(report.mEndpointURL, report.mPrincipal),
+ nsTArray<ReportData>({report}));
+ } else {
+ already_seen->second.AppendElement(report);
+ }
+ }
+
+ for (auto& iter : reportsByPrincipal) {
+ std::pair<nsCString, nsCOMPtr<nsIPrincipal>> key = iter.first;
+ nsTArray<ReportData>& value = iter.second;
+ nsCString url = key.first;
+ nsCOMPtr<nsIPrincipal> principal = key.second;
+ nsAutoCString u(url);
+ SendReports(value, url, principal);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReportDeliver::GetName(nsACString& aName) {
+ aName.AssignLiteral("ReportDeliver");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReportDeliver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID));
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return NS_OK;
+ }
+
+ obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ gReportDeliver = nullptr;
+ return NS_OK;
+}
+
+ReportDeliver::ReportDeliver() = default;
+
+ReportDeliver::~ReportDeliver() = default;
+
+NS_INTERFACE_MAP_BEGIN(ReportDeliver)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(ReportDeliver)
+NS_IMPL_RELEASE(ReportDeliver)
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/ReportDeliver.h b/dom/reporting/ReportDeliver.h
new file mode 100644
index 0000000000..f5d3f0ab6e
--- /dev/null
+++ b/dom/reporting/ReportDeliver.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ReportDeliver_h
+#define mozilla_dom_ReportDeliver_h
+
+#include "nsIObserver.h"
+#include "nsITimer.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+// XXX Avoid including this here by moving function bodies to the cpp file
+#include "nsIPrincipal.h"
+
+class nsIPrincipal;
+class nsPIDOMWindowInner;
+
+namespace mozilla::dom {
+
+class ReportBody;
+
+class ReportDeliver final : public nsIObserver,
+ public nsITimerCallback,
+ public nsINamed {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ struct ReportData {
+ nsString mType;
+ nsString mGroupName;
+ nsString mURL;
+ nsCString mEndpointURL;
+ nsString mUserAgent;
+ TimeStamp mCreationTime;
+ nsCString mReportBodyJSON;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ uint32_t mFailures;
+ };
+
+ static void Record(nsPIDOMWindowInner* aWindow, const nsAString& aType,
+ const nsAString& aGroupName, const nsAString& aURL,
+ ReportBody* aBody);
+
+ static void Fetch(const ReportData& aReportData);
+
+ void AppendReportData(const ReportData& aReportData);
+
+ private:
+ ReportDeliver();
+ ~ReportDeliver();
+
+ nsTArray<ReportData> mReportQueue;
+
+ nsCOMPtr<nsITimer> mTimer;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_ReportDeliver_h
diff --git a/dom/reporting/ReportingHeader.cpp b/dom/reporting/ReportingHeader.cpp
new file mode 100644
index 0000000000..552036852d
--- /dev/null
+++ b/dom/reporting/ReportingHeader.cpp
@@ -0,0 +1,774 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ReportingHeader.h"
+
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
+#include "js/JSON.h"
+#include "js/PropertyAndElement.h" // JS_GetElement
+#include "mozilla/dom/ReportingBinding.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/SimpleGlobalObject.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPtr.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpProtocolHandler.h"
+#include "nsIObserverService.h"
+#include "nsIPrincipal.h"
+#include "nsIRandomGenerator.h"
+#include "nsIScriptError.h"
+#include "nsNetUtil.h"
+#include "nsXULAppAPI.h"
+
+#define REPORTING_PURGE_ALL "reporting:purge-all"
+#define REPORTING_PURGE_HOST "reporting:purge-host"
+
+namespace mozilla::dom {
+
+namespace {
+
+StaticRefPtr<ReportingHeader> gReporting;
+
+} // namespace
+
+/* static */
+void ReportingHeader::Initialize() {
+ MOZ_ASSERT(!gReporting);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ RefPtr<ReportingHeader> service = new ReportingHeader();
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return;
+ }
+
+ obs->AddObserver(service, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC, false);
+ obs->AddObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ obs->AddObserver(service, "clear-origin-attributes-data", false);
+ obs->AddObserver(service, REPORTING_PURGE_HOST, false);
+ obs->AddObserver(service, REPORTING_PURGE_ALL, false);
+
+ gReporting = service;
+}
+
+/* static */
+void ReportingHeader::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!gReporting) {
+ return;
+ }
+
+ RefPtr<ReportingHeader> service = gReporting;
+ gReporting = nullptr;
+
+ if (service->mCleanupTimer) {
+ service->mCleanupTimer->Cancel();
+ service->mCleanupTimer = nullptr;
+ }
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return;
+ }
+
+ obs->RemoveObserver(service, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC);
+ obs->RemoveObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ obs->RemoveObserver(service, "clear-origin-attributes-data");
+ obs->RemoveObserver(service, REPORTING_PURGE_HOST);
+ obs->RemoveObserver(service, REPORTING_PURGE_ALL);
+}
+
+ReportingHeader::ReportingHeader() = default;
+ReportingHeader::~ReportingHeader() = default;
+
+NS_IMETHODIMP
+ReportingHeader::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ Shutdown();
+ return NS_OK;
+ }
+
+ // Pref disabled.
+ if (!StaticPrefs::dom_reporting_header_enabled()) {
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC)) {
+ nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aSubject);
+ if (NS_WARN_IF(!channel)) {
+ return NS_OK;
+ }
+
+ ReportingFromChannel(channel);
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, REPORTING_PURGE_HOST)) {
+ RemoveOriginsFromHost(nsDependentString(aData));
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, "clear-origin-attributes-data")) {
+ OriginAttributesPattern pattern;
+ if (!pattern.Init(nsDependentString(aData))) {
+ NS_ERROR("Cannot parse origin attributes pattern");
+ return NS_ERROR_FAILURE;
+ }
+
+ RemoveOriginsFromOriginAttributesPattern(pattern);
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, REPORTING_PURGE_ALL)) {
+ RemoveOrigins();
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+void ReportingHeader::ReportingFromChannel(nsIHttpChannel* aChannel) {
+ MOZ_ASSERT(aChannel);
+
+ if (!StaticPrefs::dom_reporting_header_enabled()) {
+ return;
+ }
+
+ // We want to use the final URI to check if Report-To should be allowed or
+ // not.
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (!IsSecureURI(uri)) {
+ return;
+ }
+
+ if (NS_UsePrivateBrowsing(aChannel)) {
+ return;
+ }
+
+ nsAutoCString headerValue;
+ rv = aChannel->GetResponseHeader("Report-To"_ns, headerValue);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+ if (NS_WARN_IF(!ssm)) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ rv = ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(principal));
+ if (NS_WARN_IF(NS_FAILED(rv)) || !principal) {
+ return;
+ }
+
+ nsAutoCString origin;
+ rv = principal->GetOrigin(origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ UniquePtr<Client> client = ParseHeader(aChannel, uri, headerValue);
+ if (!client) {
+ return;
+ }
+
+ // Here we override the previous data.
+ mOrigins.InsertOrUpdate(origin, std::move(client));
+
+ MaybeCreateCleanupTimer();
+}
+
+/* static */ UniquePtr<ReportingHeader::Client> ReportingHeader::ParseHeader(
+ nsIHttpChannel* aChannel, nsIURI* aURI, const nsACString& aHeaderValue) {
+ MOZ_ASSERT(aURI);
+ // aChannel can be null in gtest
+
+ AutoJSAPI jsapi;
+
+ JSObject* cleanGlobal =
+ SimpleGlobalObject::Create(SimpleGlobalObject::GlobalType::BindingDetail);
+ if (NS_WARN_IF(!cleanGlobal)) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!jsapi.Init(cleanGlobal))) {
+ return nullptr;
+ }
+
+ // WebIDL dictionary parses single items. Let's create a object to parse the
+ // header.
+ nsAutoString json;
+ json.AppendASCII("{ \"items\": [");
+ json.Append(NS_ConvertUTF8toUTF16(aHeaderValue));
+ json.AppendASCII("]}");
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> jsonValue(cx);
+ bool ok = JS_ParseJSON(cx, json.BeginReading(), json.Length(), &jsonValue);
+ if (!ok) {
+ LogToConsoleInvalidJSON(aChannel, aURI);
+ return nullptr;
+ }
+
+ dom::ReportingHeaderValue data;
+ if (!data.Init(cx, jsonValue)) {
+ LogToConsoleInvalidJSON(aChannel, aURI);
+ return nullptr;
+ }
+
+ if (!data.mItems.WasPassed() || data.mItems.Value().IsEmpty()) {
+ return nullptr;
+ }
+
+ UniquePtr<Client> client = MakeUnique<Client>();
+
+ for (const dom::ReportingItem& item : data.mItems.Value()) {
+ nsAutoString groupName;
+
+ if (item.mGroup.isUndefined()) {
+ groupName.AssignLiteral("default");
+ } else if (!item.mGroup.isString()) {
+ LogToConsoleInvalidNameItem(aChannel, aURI);
+ continue;
+ } else {
+ JS::Rooted<JSString*> groupStr(cx, item.mGroup.toString());
+ MOZ_ASSERT(groupStr);
+
+ nsAutoJSString string;
+ if (NS_WARN_IF(!string.init(cx, groupStr))) {
+ continue;
+ }
+
+ groupName = string;
+ }
+
+ if (!item.mMax_age.isNumber() || !item.mEndpoints.isObject()) {
+ LogToConsoleIncompleteItem(aChannel, aURI, groupName);
+ continue;
+ }
+
+ JS::Rooted<JSObject*> endpoints(cx, &item.mEndpoints.toObject());
+ MOZ_ASSERT(endpoints);
+
+ bool isArray = false;
+ if (!JS::IsArrayObject(cx, endpoints, &isArray) || !isArray) {
+ LogToConsoleIncompleteItem(aChannel, aURI, groupName);
+ continue;
+ }
+
+ uint32_t endpointsLength;
+ if (!JS::GetArrayLength(cx, endpoints, &endpointsLength) ||
+ endpointsLength == 0) {
+ LogToConsoleIncompleteItem(aChannel, aURI, groupName);
+ continue;
+ }
+
+ const auto [begin, end] = client->mGroups.NonObservingRange();
+ if (std::any_of(begin, end, [&groupName](const Group& group) {
+ return group.mName == groupName;
+ })) {
+ LogToConsoleDuplicateGroup(aChannel, aURI, groupName);
+ continue;
+ }
+
+ Group* group = client->mGroups.AppendElement();
+ group->mName = groupName;
+ group->mIncludeSubdomains = item.mInclude_subdomains;
+ group->mTTL = item.mMax_age.toNumber();
+ group->mCreationTime = TimeStamp::Now();
+
+ for (uint32_t i = 0; i < endpointsLength; ++i) {
+ JS::Rooted<JS::Value> element(cx);
+ if (!JS_GetElement(cx, endpoints, i, &element)) {
+ return nullptr;
+ }
+
+ RootedDictionary<ReportingEndpoint> endpoint(cx);
+ if (!endpoint.Init(cx, element)) {
+ LogToConsoleIncompleteEndpoint(aChannel, aURI, groupName);
+ continue;
+ }
+
+ if (!endpoint.mUrl.isString() ||
+ (!endpoint.mPriority.isUndefined() &&
+ (!endpoint.mPriority.isNumber() ||
+ endpoint.mPriority.toNumber() < 0)) ||
+ (!endpoint.mWeight.isUndefined() &&
+ (!endpoint.mWeight.isNumber() || endpoint.mWeight.toNumber() < 0))) {
+ LogToConsoleIncompleteEndpoint(aChannel, aURI, groupName);
+ continue;
+ }
+
+ JS::Rooted<JSString*> endpointUrl(cx, endpoint.mUrl.toString());
+ MOZ_ASSERT(endpointUrl);
+
+ nsAutoJSString endpointString;
+ if (NS_WARN_IF(!endpointString.init(cx, endpointUrl))) {
+ continue;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), endpointString);
+ if (NS_FAILED(rv)) {
+ LogToConsoleInvalidURLEndpoint(aChannel, aURI, groupName,
+ endpointString);
+ continue;
+ }
+
+ Endpoint* ep = group->mEndpoints.AppendElement();
+ ep->mUrl = uri;
+ ep->mPriority =
+ endpoint.mPriority.isUndefined() ? 1 : endpoint.mPriority.toNumber();
+ ep->mWeight =
+ endpoint.mWeight.isUndefined() ? 1 : endpoint.mWeight.toNumber();
+ }
+ }
+
+ if (client->mGroups.IsEmpty()) {
+ return nullptr;
+ }
+
+ return client;
+}
+
+bool ReportingHeader::IsSecureURI(nsIURI* aURI) const {
+ MOZ_ASSERT(aURI);
+
+ bool prioriAuthenticated = false;
+ if (NS_WARN_IF(NS_FAILED(NS_URIChainHasFlags(
+ aURI, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY,
+ &prioriAuthenticated)))) {
+ return false;
+ }
+
+ return prioriAuthenticated;
+}
+
+/* static */
+void ReportingHeader::LogToConsoleInvalidJSON(nsIHttpChannel* aChannel,
+ nsIURI* aURI) {
+ nsTArray<nsString> params;
+ LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidJSON", params);
+}
+
+/* static */
+void ReportingHeader::LogToConsoleDuplicateGroup(nsIHttpChannel* aChannel,
+ nsIURI* aURI,
+ const nsAString& aName) {
+ nsTArray<nsString> params;
+ params.AppendElement(aName);
+
+ LogToConsoleInternal(aChannel, aURI, "ReportingHeaderDuplicateGroup", params);
+}
+
+/* static */
+void ReportingHeader::LogToConsoleInvalidNameItem(nsIHttpChannel* aChannel,
+ nsIURI* aURI) {
+ nsTArray<nsString> params;
+ LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidNameItem",
+ params);
+}
+
+/* static */
+void ReportingHeader::LogToConsoleIncompleteItem(nsIHttpChannel* aChannel,
+ nsIURI* aURI,
+ const nsAString& aName) {
+ nsTArray<nsString> params;
+ params.AppendElement(aName);
+
+ LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidItem", params);
+}
+
+/* static */
+void ReportingHeader::LogToConsoleIncompleteEndpoint(nsIHttpChannel* aChannel,
+ nsIURI* aURI,
+ const nsAString& aName) {
+ nsTArray<nsString> params;
+ params.AppendElement(aName);
+
+ LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidEndpoint",
+ params);
+}
+
+/* static */
+void ReportingHeader::LogToConsoleInvalidURLEndpoint(nsIHttpChannel* aChannel,
+ nsIURI* aURI,
+ const nsAString& aName,
+ const nsAString& aURL) {
+ nsTArray<nsString> params;
+ params.AppendElement(aURL);
+ params.AppendElement(aName);
+
+ LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidURLEndpoint",
+ params);
+}
+
+/* static */
+void ReportingHeader::LogToConsoleInternal(nsIHttpChannel* aChannel,
+ nsIURI* aURI, const char* aMsg,
+ const nsTArray<nsString>& aParams) {
+ MOZ_ASSERT(aURI);
+
+ if (!aChannel) {
+ // We are in a gtest.
+ return;
+ }
+
+ uint64_t windowID = 0;
+
+ nsresult rv = aChannel->GetTopLevelContentWindowId(&windowID);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (!windowID) {
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ nsresult rv = aChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (loadGroup) {
+ windowID = nsContentUtils::GetInnerWindowID(loadGroup);
+ }
+ }
+
+ nsAutoString localizedMsg;
+ rv = nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eSECURITY_PROPERTIES, aMsg, aParams, localizedMsg);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ rv = nsContentUtils::ReportToConsoleByWindowID(
+ localizedMsg, nsIScriptError::infoFlag, "Reporting"_ns, windowID, aURI);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+/* static */
+void ReportingHeader::GetEndpointForReport(
+ const nsAString& aGroupName,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ nsACString& aEndpointURI) {
+ auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo);
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ GetEndpointForReport(aGroupName, principal, aEndpointURI);
+}
+
+/* static */
+void ReportingHeader::GetEndpointForReport(const nsAString& aGroupName,
+ nsIPrincipal* aPrincipal,
+ nsACString& aEndpointURI) {
+ MOZ_ASSERT(aEndpointURI.IsEmpty());
+
+ if (!gReporting) {
+ return;
+ }
+
+ nsAutoCString origin;
+ nsresult rv = aPrincipal->GetOrigin(origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ Client* client = gReporting->mOrigins.Get(origin);
+ if (!client) {
+ return;
+ }
+
+ const auto [begin, end] = client->mGroups.NonObservingRange();
+ const auto foundIt = std::find_if(
+ begin, end,
+ [&aGroupName](const Group& group) { return group.mName == aGroupName; });
+ if (foundIt != end) {
+ GetEndpointForReportInternal(*foundIt, aEndpointURI);
+ }
+
+ // XXX More explicitly report an error if not found?
+}
+
+/* static */
+void ReportingHeader::GetEndpointForReportInternal(
+ const ReportingHeader::Group& aGroup, nsACString& aEndpointURI) {
+ TimeDuration diff = TimeStamp::Now() - aGroup.mCreationTime;
+ if (diff.ToSeconds() > aGroup.mTTL) {
+ // Expired.
+ return;
+ }
+
+ if (aGroup.mEndpoints.IsEmpty()) {
+ return;
+ }
+
+ int64_t minPriority = -1;
+ uint32_t totalWeight = 0;
+
+ for (const Endpoint& endpoint : aGroup.mEndpoints.NonObservingRange()) {
+ if (minPriority == -1 || minPriority > endpoint.mPriority) {
+ minPriority = endpoint.mPriority;
+ totalWeight = endpoint.mWeight;
+ } else if (minPriority == endpoint.mPriority) {
+ totalWeight += endpoint.mWeight;
+ }
+ }
+
+ nsCOMPtr<nsIRandomGenerator> randomGenerator =
+ do_GetService("@mozilla.org/security/random-generator;1");
+ if (NS_WARN_IF(!randomGenerator)) {
+ return;
+ }
+
+ uint32_t randomNumber = 0;
+
+ nsresult rv = randomGenerator->GenerateRandomBytesInto(randomNumber);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ totalWeight = randomNumber % totalWeight;
+
+ const auto [begin, end] = aGroup.mEndpoints.NonObservingRange();
+ const auto foundIt = std::find_if(
+ begin, end, [minPriority, totalWeight](const Endpoint& endpoint) {
+ return minPriority == endpoint.mPriority &&
+ totalWeight < endpoint.mWeight;
+ });
+ if (foundIt != end) {
+ Unused << NS_WARN_IF(NS_FAILED(foundIt->mUrl->GetSpec(aEndpointURI)));
+ }
+ // XXX More explicitly report an error if not found?
+}
+
+/* static */
+void ReportingHeader::RemoveEndpoint(
+ const nsAString& aGroupName, const nsACString& aEndpointURL,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo) {
+ if (!gReporting) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aEndpointURL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo);
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return;
+ }
+
+ nsAutoCString origin;
+ rv = principalOrErr.unwrap()->GetOrigin(origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ Client* client = gReporting->mOrigins.Get(origin);
+ if (!client) {
+ return;
+ }
+
+ // Scope for the group iterator.
+ {
+ nsTObserverArray<Group>::BackwardIterator iter(client->mGroups);
+ while (iter.HasMore()) {
+ const Group& group = iter.GetNext();
+ if (group.mName != aGroupName) {
+ continue;
+ }
+
+ // Scope for the endpoint iterator.
+ {
+ nsTObserverArray<Endpoint>::BackwardIterator endpointIter(
+ group.mEndpoints);
+ while (endpointIter.HasMore()) {
+ const Endpoint& endpoint = endpointIter.GetNext();
+
+ bool equal = false;
+ rv = endpoint.mUrl->Equals(uri, &equal);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ if (equal) {
+ endpointIter.Remove();
+ break;
+ }
+ }
+ }
+
+ if (group.mEndpoints.IsEmpty()) {
+ iter.Remove();
+ }
+
+ break;
+ }
+ }
+
+ if (client->mGroups.IsEmpty()) {
+ gReporting->mOrigins.Remove(origin);
+ gReporting->MaybeCancelCleanupTimer();
+ }
+}
+
+void ReportingHeader::RemoveOriginsFromHost(const nsAString& aHost) {
+ nsCOMPtr<nsIEffectiveTLDService> tldService =
+ do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+ if (NS_WARN_IF(!tldService)) {
+ return;
+ }
+
+ NS_ConvertUTF16toUTF8 host(aHost);
+
+ for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
+ bool hasRootDomain = false;
+ nsresult rv = tldService->HasRootDomain(iter.Key(), host, &hasRootDomain);
+ if (NS_WARN_IF(NS_FAILED(rv)) || !hasRootDomain) {
+ continue;
+ }
+
+ iter.Remove();
+ }
+
+ MaybeCancelCleanupTimer();
+}
+
+void ReportingHeader::RemoveOriginsFromOriginAttributesPattern(
+ const OriginAttributesPattern& aPattern) {
+ for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
+ nsAutoCString suffix;
+ OriginAttributes attr;
+ if (NS_WARN_IF(!attr.PopulateFromOrigin(iter.Key(), suffix))) {
+ continue;
+ }
+
+ if (aPattern.Matches(attr)) {
+ iter.Remove();
+ }
+ }
+
+ MaybeCancelCleanupTimer();
+}
+
+void ReportingHeader::RemoveOrigins() {
+ mOrigins.Clear();
+ MaybeCancelCleanupTimer();
+}
+
+void ReportingHeader::RemoveOriginsForTTL() {
+ TimeStamp now = TimeStamp::Now();
+
+ for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
+ Client* client = iter.UserData();
+
+ // Scope of the iterator.
+ {
+ nsTObserverArray<Group>::BackwardIterator groupIter(client->mGroups);
+ while (groupIter.HasMore()) {
+ const Group& group = groupIter.GetNext();
+ TimeDuration diff = now - group.mCreationTime;
+ if (diff.ToSeconds() > group.mTTL) {
+ groupIter.Remove();
+ return;
+ }
+ }
+ }
+
+ if (client->mGroups.IsEmpty()) {
+ iter.Remove();
+ }
+ }
+}
+
+/* static */
+bool ReportingHeader::HasReportingHeaderForOrigin(const nsACString& aOrigin) {
+ if (!gReporting) {
+ return false;
+ }
+
+ return gReporting->mOrigins.Contains(aOrigin);
+}
+
+NS_IMETHODIMP
+ReportingHeader::Notify(nsITimer* aTimer) {
+ mCleanupTimer = nullptr;
+
+ RemoveOriginsForTTL();
+ MaybeCreateCleanupTimer();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReportingHeader::GetName(nsACString& aName) {
+ aName.AssignLiteral("ReportingHeader");
+ return NS_OK;
+}
+
+void ReportingHeader::MaybeCreateCleanupTimer() {
+ if (mCleanupTimer) {
+ return;
+ }
+
+ if (mOrigins.Count() == 0) {
+ return;
+ }
+
+ uint32_t timeout = StaticPrefs::dom_reporting_cleanup_timeout() * 1000;
+ nsresult rv =
+ NS_NewTimerWithCallback(getter_AddRefs(mCleanupTimer), this, timeout,
+ nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+void ReportingHeader::MaybeCancelCleanupTimer() {
+ if (!mCleanupTimer) {
+ return;
+ }
+
+ if (mOrigins.Count() != 0) {
+ return;
+ }
+
+ mCleanupTimer->Cancel();
+ mCleanupTimer = nullptr;
+}
+
+NS_INTERFACE_MAP_BEGIN(ReportingHeader)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(ReportingHeader)
+NS_IMPL_RELEASE(ReportingHeader)
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/ReportingHeader.h b/dom/reporting/ReportingHeader.h
new file mode 100644
index 0000000000..d0db5f612b
--- /dev/null
+++ b/dom/reporting/ReportingHeader.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ReportingHeader_h
+#define mozilla_dom_ReportingHeader_h
+
+#include "mozilla/TimeStamp.h"
+#include "nsClassHashtable.h"
+#include "nsIObserver.h"
+#include "nsITimer.h"
+#include "nsTObserverArray.h"
+
+class nsIHttpChannel;
+class nsIPrincipal;
+class nsIURI;
+
+namespace mozilla {
+
+class OriginAttributesPattern;
+
+namespace ipc {
+class PrincipalInfo;
+}
+
+namespace dom {
+
+class ReportingHeader final : public nsIObserver,
+ public nsITimerCallback,
+ public nsINamed {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ static void Initialize();
+
+ // Exposed structs for gtests
+
+ struct Endpoint {
+ nsCOMPtr<nsIURI> mUrl;
+ uint32_t mPriority;
+ uint32_t mWeight;
+ };
+
+ struct Group {
+ nsString mName;
+ bool mIncludeSubdomains;
+ int32_t mTTL;
+ TimeStamp mCreationTime;
+ nsTObserverArray<Endpoint> mEndpoints;
+ };
+
+ struct Client {
+ nsTObserverArray<Group> mGroups;
+ };
+
+ static UniquePtr<Client> ParseHeader(nsIHttpChannel* aChannel, nsIURI* aURI,
+ const nsACString& aHeaderValue);
+
+ static void GetEndpointForReport(
+ const nsAString& aGroupName,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ nsACString& aEndpointURI);
+
+ static void GetEndpointForReport(const nsAString& aGroupName,
+ nsIPrincipal* aPrincipal,
+ nsACString& aEndpointURI);
+
+ static void RemoveEndpoint(const nsAString& aGroupName,
+ const nsACString& aEndpointURL,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo);
+
+ // ChromeOnly-WebIDL methods
+
+ static bool HasReportingHeaderForOrigin(const nsACString& aOrigin);
+
+ private:
+ ReportingHeader();
+ ~ReportingHeader();
+
+ static void Shutdown();
+
+ // Checks if a channel contains a Report-To header and parses its value.
+ void ReportingFromChannel(nsIHttpChannel* aChannel);
+
+ // This method checks if the protocol handler of the URI has the
+ // URI_IS_POTENTIALLY_TRUSTWORTHY flag.
+ bool IsSecureURI(nsIURI* aURI) const;
+
+ void RemoveOriginsFromHost(const nsAString& aHost);
+
+ void RemoveOriginsFromOriginAttributesPattern(
+ const OriginAttributesPattern& aPattern);
+
+ void RemoveOrigins();
+
+ void RemoveOriginsForTTL();
+
+ void MaybeCreateCleanupTimer();
+
+ void MaybeCancelCleanupTimer();
+
+ static void LogToConsoleInvalidJSON(nsIHttpChannel* aChannel, nsIURI* aURI);
+
+ static void LogToConsoleDuplicateGroup(nsIHttpChannel* aChannel, nsIURI* aURI,
+ const nsAString& aName);
+
+ static void LogToConsoleInvalidNameItem(nsIHttpChannel* aChannel,
+ nsIURI* aURI);
+
+ static void LogToConsoleIncompleteItem(nsIHttpChannel* aChannel, nsIURI* aURI,
+ const nsAString& aName);
+
+ static void LogToConsoleIncompleteEndpoint(nsIHttpChannel* aChannel,
+ nsIURI* aURI,
+ const nsAString& aName);
+
+ static void LogToConsoleInvalidURLEndpoint(nsIHttpChannel* aChannel,
+ nsIURI* aURI,
+ const nsAString& aName,
+ const nsAString& aURL);
+
+ static void LogToConsoleInternal(nsIHttpChannel* aChannel, nsIURI* aURI,
+ const char* aMsg,
+ const nsTArray<nsString>& aParams);
+
+ static void GetEndpointForReportInternal(const ReportingHeader::Group& aGrup,
+ nsACString& aEndpointURI);
+
+ nsClassHashtable<nsCStringHashKey, Client> mOrigins;
+
+ nsCOMPtr<nsITimer> mCleanupTimer;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ReportingHeader_h
diff --git a/dom/reporting/ReportingObserver.cpp b/dom/reporting/ReportingObserver.cpp
new file mode 100644
index 0000000000..808824ce95
--- /dev/null
+++ b/dom/reporting/ReportingObserver.cpp
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ReportingObserver.h"
+#include "mozilla/dom/Report.h"
+#include "mozilla/dom/ReportingBinding.h"
+#include "nsContentUtils.h"
+#include "nsIGlobalObject.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ReportingObserver)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ReportingObserver)
+ tmp->Disconnect();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mReports)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ReportingObserver)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReports)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+/* static */
+already_AddRefed<ReportingObserver> ReportingObserver::Constructor(
+ const GlobalObject& aGlobal, ReportingObserverCallback& aCallback,
+ const ReportingObserverOptions& aOptions, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ MOZ_ASSERT(global);
+
+ nsTArray<nsString> types;
+ if (aOptions.mTypes.WasPassed()) {
+ types = aOptions.mTypes.Value();
+ }
+
+ RefPtr<ReportingObserver> ro =
+ new ReportingObserver(global, aCallback, types, aOptions.mBuffered);
+
+ return ro.forget();
+}
+
+ReportingObserver::ReportingObserver(nsIGlobalObject* aGlobal,
+ ReportingObserverCallback& aCallback,
+ const nsTArray<nsString>& aTypes,
+ bool aBuffered)
+ : mGlobal(aGlobal),
+ mCallback(&aCallback),
+ mTypes(aTypes.Clone()),
+ mBuffered(aBuffered) {
+ MOZ_ASSERT(aGlobal);
+}
+
+ReportingObserver::~ReportingObserver() { Disconnect(); }
+
+JSObject* ReportingObserver::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ReportingObserver_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void ReportingObserver::Observe() {
+ mGlobal->RegisterReportingObserver(this, mBuffered);
+}
+
+void ReportingObserver::Disconnect() {
+ if (mGlobal) {
+ mGlobal->UnregisterReportingObserver(this);
+ }
+}
+
+void ReportingObserver::TakeRecords(nsTArray<RefPtr<Report>>& aRecords) {
+ mReports.SwapElements(aRecords);
+}
+
+namespace {
+
+class ReportRunnable final : public DiscardableRunnable {
+ public:
+ explicit ReportRunnable(nsIGlobalObject* aGlobal)
+ : DiscardableRunnable("ReportRunnable"), mGlobal(aGlobal) {}
+
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
+ // bug 1535398.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ MOZ_KnownLive(mGlobal)->NotifyReportingObservers();
+ return NS_OK;
+ }
+
+ private:
+ const nsCOMPtr<nsIGlobalObject> mGlobal;
+};
+
+} // namespace
+
+void ReportingObserver::MaybeReport(Report* aReport) {
+ MOZ_ASSERT(aReport);
+
+ if (!mTypes.IsEmpty()) {
+ nsAutoString type;
+ aReport->GetType(type);
+
+ if (!mTypes.Contains(type)) {
+ return;
+ }
+ }
+
+ bool wasEmpty = mReports.IsEmpty();
+
+ RefPtr<Report> report = aReport->Clone();
+ MOZ_ASSERT(report);
+
+ if (NS_WARN_IF(!mReports.AppendElement(report, fallible))) {
+ return;
+ }
+
+ if (!wasEmpty) {
+ return;
+ }
+
+ RefPtr<ReportRunnable> r = new ReportRunnable(mGlobal);
+ NS_DispatchToCurrentThread(r);
+}
+
+void ReportingObserver::MaybeNotify() {
+ if (mReports.IsEmpty()) {
+ return;
+ }
+
+ // Let's take the ownership of the reports.
+ nsTArray<RefPtr<Report>> list = std::move(mReports);
+
+ Sequence<OwningNonNull<Report>> reports;
+ for (Report* report : list) {
+ if (NS_WARN_IF(!reports.AppendElement(*report, fallible))) {
+ return;
+ }
+ }
+
+ // We should report if this throws exception. But where?
+ RefPtr<ReportingObserverCallback> callback(mCallback);
+ callback->Call(reports, *this);
+}
+
+void ReportingObserver::ForgetReports() { mReports.Clear(); }
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/ReportingObserver.h b/dom/reporting/ReportingObserver.h
new file mode 100644
index 0000000000..b89ee33adf
--- /dev/null
+++ b/dom/reporting/ReportingObserver.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ReportingObserver_h
+#define mozilla_dom_ReportingObserver_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class GlobalObject;
+class Report;
+class ReportingObserverCallback;
+struct ReportingObserverOptions;
+
+class ReportingObserver final : public nsWrapperCache {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ReportingObserver)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(ReportingObserver)
+
+ static already_AddRefed<ReportingObserver> Constructor(
+ const GlobalObject& aGlobal, ReportingObserverCallback& aCallback,
+ const ReportingObserverOptions& aOptions, ErrorResult& aRv);
+
+ ReportingObserver(nsIGlobalObject* aGlobal,
+ ReportingObserverCallback& aCallback,
+ const nsTArray<nsString>& aTypes, bool aBuffered);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsIGlobalObject* GetParentObject() const { return mGlobal; }
+
+ void Observe();
+
+ void Disconnect();
+
+ void TakeRecords(nsTArray<RefPtr<Report>>& aRecords);
+
+ void MaybeReport(Report* aReport);
+
+ MOZ_CAN_RUN_SCRIPT void MaybeNotify();
+
+ void ForgetReports();
+
+ private:
+ ~ReportingObserver();
+
+ nsTArray<RefPtr<Report>> mReports;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ReportingObserverCallback> mCallback;
+ nsTArray<nsString> mTypes;
+ bool mBuffered;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ReportingObserver_h
diff --git a/dom/reporting/ReportingUtils.cpp b/dom/reporting/ReportingUtils.cpp
new file mode 100644
index 0000000000..ce7966a0a3
--- /dev/null
+++ b/dom/reporting/ReportingUtils.cpp
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ReportingUtils.h"
+#include "mozilla/dom/ReportBody.h"
+#include "mozilla/dom/ReportDeliver.h"
+#include "mozilla/dom/Report.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "nsAtom.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::dom {
+
+/* static */
+void ReportingUtils::Report(nsIGlobalObject* aGlobal, nsAtom* aType,
+ const nsAString& aGroupName, const nsAString& aURL,
+ ReportBody* aBody) {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(aBody);
+
+ nsDependentAtomString type(aType);
+
+ RefPtr<mozilla::dom::Report> report =
+ new mozilla::dom::Report(aGlobal, type, aURL, aBody);
+ aGlobal->BroadcastReport(report);
+
+ if (!NS_IsMainThread()) {
+ return;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal);
+ if (!window) {
+ return;
+ }
+
+ // Send the report to the server.
+ ReportDeliver::Record(window, type, aGroupName, aURL, aBody);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/ReportingUtils.h b/dom/reporting/ReportingUtils.h
new file mode 100644
index 0000000000..ee9726bed7
--- /dev/null
+++ b/dom/reporting/ReportingUtils.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ReportingUtils_h
+#define mozilla_dom_ReportingUtils_h
+
+#include "nsString.h"
+
+class nsAtom;
+class nsIGlobalObject;
+
+namespace mozilla::dom {
+
+class ReportBody;
+
+class ReportingUtils final {
+ public:
+ static void Report(nsIGlobalObject* aGlobal, nsAtom* aType,
+ const nsAString& aGroupName, const nsAString& aURL,
+ ReportBody* aBody);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_ReportingUtils_h
diff --git a/dom/reporting/TestingDeprecatedInterface.cpp b/dom/reporting/TestingDeprecatedInterface.cpp
new file mode 100644
index 0000000000..9c03f3125d
--- /dev/null
+++ b/dom/reporting/TestingDeprecatedInterface.cpp
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/TestingDeprecatedInterface.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/ReportingBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TestingDeprecatedInterface, mGlobal)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TestingDeprecatedInterface)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TestingDeprecatedInterface)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TestingDeprecatedInterface)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* static */
+already_AddRefed<TestingDeprecatedInterface>
+TestingDeprecatedInterface::Constructor(const GlobalObject& aGlobal) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ MOZ_ASSERT(global);
+
+ RefPtr<TestingDeprecatedInterface> obj =
+ new TestingDeprecatedInterface(global);
+ return obj.forget();
+}
+
+TestingDeprecatedInterface::TestingDeprecatedInterface(nsIGlobalObject* aGlobal)
+ : mGlobal(aGlobal) {}
+
+TestingDeprecatedInterface::~TestingDeprecatedInterface() = default;
+
+JSObject* TestingDeprecatedInterface::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return TestingDeprecatedInterface_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void TestingDeprecatedInterface::DeprecatedMethod() const {}
+
+bool TestingDeprecatedInterface::DeprecatedAttribute() const { return true; }
+
+} // namespace mozilla::dom
diff --git a/dom/reporting/TestingDeprecatedInterface.h b/dom/reporting/TestingDeprecatedInterface.h
new file mode 100644
index 0000000000..7056a91ed7
--- /dev/null
+++ b/dom/reporting/TestingDeprecatedInterface.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_TestingDeprecatedInterface_h
+#define mozilla_dom_TestingDeprecatedInterface_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::dom {
+class GlobalObject;
+
+class TestingDeprecatedInterface final : public nsISupports,
+ public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TestingDeprecatedInterface)
+
+ static already_AddRefed<TestingDeprecatedInterface> Constructor(
+ const GlobalObject& aGlobal);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsIGlobalObject* GetParentObject() const { return mGlobal; }
+
+ void DeprecatedMethod() const;
+
+ bool DeprecatedAttribute() const;
+
+ private:
+ explicit TestingDeprecatedInterface(nsIGlobalObject* aGlobal);
+ ~TestingDeprecatedInterface();
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_TestingDeprecatedInterface_h
diff --git a/dom/reporting/moz.build b/dom/reporting/moz.build
new file mode 100644
index 0000000000..73a091dc72
--- /dev/null
+++ b/dom/reporting/moz.build
@@ -0,0 +1,51 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom = [
+ "CrashReport.h",
+ "DeprecationReportBody.h",
+ "EndpointForReportChild.h",
+ "EndpointForReportParent.h",
+ "FeaturePolicyViolationReportBody.h",
+ "Report.h",
+ "ReportBody.h",
+ "ReportDeliver.h",
+ "ReportingHeader.h",
+ "ReportingObserver.h",
+ "ReportingUtils.h",
+ "TestingDeprecatedInterface.h",
+]
+
+UNIFIED_SOURCES += [
+ "CrashReport.cpp",
+ "DeprecationReportBody.cpp",
+ "EndpointForReportChild.cpp",
+ "EndpointForReportParent.cpp",
+ "FeaturePolicyViolationReportBody.cpp",
+ "Report.cpp",
+ "ReportBody.cpp",
+ "ReportDeliver.cpp",
+ "ReportingHeader.cpp",
+ "ReportingObserver.cpp",
+ "ReportingUtils.cpp",
+ "TestingDeprecatedInterface.cpp",
+]
+
+IPDL_SOURCES += [
+ "PEndpointForReport.ipdl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Security")
+
+FINAL_LIBRARY = "xul"
+
+MOCHITEST_MANIFESTS += ["tests/mochitest.toml"]
+BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
+
+TEST_DIRS += ["tests/gtest"]
diff --git a/dom/reporting/tests/browser.toml b/dom/reporting/tests/browser.toml
new file mode 100644
index 0000000000..1f6354edd4
--- /dev/null
+++ b/dom/reporting/tests/browser.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files = [
+ "delivering.sjs",
+ "empty.html",
+]
+
+["browser_cleanup.js"]
diff --git a/dom/reporting/tests/browser_cleanup.js b/dom/reporting/tests/browser_cleanup.js
new file mode 100644
index 0000000000..e50b8db1da
--- /dev/null
+++ b/dom/reporting/tests/browser_cleanup.js
@@ -0,0 +1,276 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const TEST_HOST = "example.org";
+const TEST_DOMAIN = "https://" + TEST_HOST;
+const TEST_PATH = "/browser/dom/reporting/tests/";
+const TEST_TOP_PAGE = TEST_DOMAIN + TEST_PATH + "empty.html";
+const TEST_SJS = TEST_DOMAIN + TEST_PATH + "delivering.sjs";
+
+async function storeReportingHeader(browser, extraParams = "") {
+ await SpecialPowers.spawn(
+ browser,
+ [{ url: TEST_SJS, extraParams }],
+ async obj => {
+ await content
+ .fetch(
+ obj.url +
+ "?task=header" +
+ (obj.extraParams.length ? "&" + obj.extraParams : "")
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "OK", "Report-to header sent");
+ });
+ }
+ );
+}
+
+add_task(async function () {
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.reporting.enabled", true],
+ ["dom.reporting.header.enabled", true],
+ ["dom.reporting.testing.enabled", true],
+ ["dom.reporting.delivering.timeout", 1],
+ ["dom.reporting.cleanup.timeout", 1],
+ ["privacy.userContext.enabled", true],
+ ],
+ });
+});
+
+add_task(async function () {
+ info("Testing a total cleanup");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "No data before the test"
+ );
+
+ await storeReportingHeader(browser);
+ ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data");
+
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "No data before a full cleanup"
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Testing a total QuotaManager cleanup");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "No data before the test"
+ );
+
+ await storeReportingHeader(browser);
+ ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data");
+
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_REPORTS, value =>
+ resolve()
+ );
+ });
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "No data before a reports cleanup"
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Testing a QuotaManager host cleanup");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "No data before the test"
+ );
+
+ await storeReportingHeader(browser);
+ ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data");
+
+ await new Promise(resolve => {
+ Services.clearData.deleteDataFromHost(
+ TEST_HOST,
+ true,
+ Ci.nsIClearDataService.CLEAR_REPORTS,
+ value => resolve()
+ );
+ });
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "No data before a reports cleanup"
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Testing a 410 endpoint status");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "No data before the test"
+ );
+
+ await storeReportingHeader(browser, "410=true");
+ ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data");
+
+ await SpecialPowers.spawn(browser, [], async _ => {
+ let testingInterface = new content.TestingDeprecatedInterface();
+ ok(!!testingInterface, "Created a deprecated interface");
+ });
+
+ await new Promise((resolve, reject) => {
+ let count = 0;
+ let id = setInterval(_ => {
+ ++count;
+ if (count > 10) {
+ ok(false, "Something went wrong.");
+ clearInterval(id);
+ reject();
+ }
+
+ if (!ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN)) {
+ ok(true, "No data after a 410!");
+ clearInterval(id);
+ resolve();
+ }
+ }, 1000);
+ });
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Creating a new container");
+
+ let identity = ContextualIdentityService.create(
+ "Report-To-Test",
+ "fingerprint",
+ "orange"
+ );
+
+ info("Creating a new container tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE, {
+ userContextId: identity.userContextId,
+ });
+ is(
+ tab.getAttribute("usercontextid"),
+ "" + identity.userContextId,
+ "New tab has the right UCI"
+ );
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "No data before the test"
+ );
+
+ await storeReportingHeader(browser);
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "We don't have data for the origin"
+ );
+ ok(
+ ChromeUtils.hasReportingHeaderForOrigin(
+ TEST_DOMAIN + "^userContextId=" + identity.userContextId
+ ),
+ "We have data for the origin + userContextId"
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ ContextualIdentityService.remove(identity.userContextId);
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(
+ TEST_DOMAIN + "^userContextId=" + identity.userContextId
+ ),
+ "No more data after a container removal"
+ );
+});
+
+add_task(async function () {
+ info("TTL cleanup");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ ok(
+ !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "No data before the test"
+ );
+
+ await storeReportingHeader(browser);
+ ok(
+ ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN),
+ "We have data for the origin"
+ );
+
+ // Let's wait a bit.
+ await new Promise(resolve => {
+ setTimeout(resolve, 5000);
+ });
+
+ ok(!ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "No data anymore");
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/dom/reporting/tests/common_deprecated.js b/dom/reporting/tests/common_deprecated.js
new file mode 100644
index 0000000000..8f8415fcf3
--- /dev/null
+++ b/dom/reporting/tests/common_deprecated.js
@@ -0,0 +1,214 @@
+let testingInterface;
+
+// eslint-disable-next-line no-unused-vars
+function test_deprecatedInterface() {
+ info("Testing DeprecatedTestingInterface report");
+ return new Promise(resolve => {
+ let obs = new ReportingObserver((reports, o) => {
+ is(obs, o, "Same observer!");
+ ok(reports.length == 1, "We have 1 report");
+
+ let report = reports[0];
+ is(report.type, "deprecation", "Deprecation report received");
+ is(report.url, location.href, "URL is location");
+ ok(!!report.body, "The report has a body");
+ ok(
+ report.body instanceof DeprecationReportBody,
+ "Correct type for the body"
+ );
+ is(
+ report.body.id,
+ "DeprecatedTestingInterface",
+ "report.body.id matches DeprecatedTestingMethod"
+ );
+ ok(!report.body.anticipatedRemoval, "We don't have a anticipatedRemoval");
+ ok(
+ report.body.message.includes("TestingDeprecatedInterface"),
+ "We have a message"
+ );
+ is(
+ report.body.sourceFile,
+ location.href
+ .split("?")[0]
+ .replace("test_deprecated.html", "common_deprecated.js")
+ .replace("worker_deprecated.js", "common_deprecated.js"),
+ "We have a sourceFile"
+ );
+ is(report.body.lineNumber, 48, "We have a lineNumber");
+ is(report.body.columnNumber, 24, "We have a columnNumber");
+
+ obs.disconnect();
+ resolve();
+ });
+ ok(!!obs, "ReportingObserver is a thing");
+
+ obs.observe();
+ ok(true, "ReportingObserver.observe() is callable");
+
+ testingInterface = new TestingDeprecatedInterface();
+ ok(true, "Created a deprecated interface");
+ });
+}
+
+// eslint-disable-next-line no-unused-vars
+function test_deprecatedMethod() {
+ info("Testing DeprecatedTestingMethod report");
+ return new Promise(resolve => {
+ let obs = new ReportingObserver((reports, o) => {
+ is(obs, o, "Same observer!");
+ ok(reports.length == 1, "We have 1 report");
+
+ let report = reports[0];
+ is(report.type, "deprecation", "Deprecation report received");
+ is(report.url, location.href, "URL is location");
+ ok(!!report.body, "The report has a body");
+ ok(
+ report.body instanceof DeprecationReportBody,
+ "Correct type for the body"
+ );
+ is(
+ report.body.id,
+ "DeprecatedTestingMethod",
+ "report.body.id matches DeprecatedTestingMethod"
+ );
+ ok(!report.body.anticipatedRemoval, "We don't have a anticipatedRemoval");
+ ok(
+ report.body.message.includes(
+ "TestingDeprecatedInterface.deprecatedMethod"
+ ),
+ "We have a message"
+ );
+ is(
+ report.body.sourceFile,
+ location.href
+ .split("?")[0]
+ .replace("test_deprecated.html", "common_deprecated.js")
+ .replace("worker_deprecated.js", "common_deprecated.js"),
+ "We have a sourceFile"
+ );
+ is(report.body.lineNumber, 100, "We have a lineNumber");
+ is(report.body.columnNumber, 22, "We have a columnNumber");
+
+ obs.disconnect();
+ resolve();
+ });
+ ok(!!obs, "ReportingObserver is a thing");
+
+ obs.observe();
+ ok(true, "ReportingObserver.observe() is callable");
+
+ testingInterface.deprecatedMethod();
+ ok(true, "Run a deprecated method.");
+ });
+}
+
+// eslint-disable-next-line no-unused-vars
+function test_deprecatedMethodWithDataURI() {
+ info("Testing deprecatedMethodWithDataURI report");
+
+ const uri = `data:text/html,<script>
+ window.onload = () => {
+ let obs = new ReportingObserver((reports, o) => {
+ obs.disconnect();
+ let report = reports[0];
+ const message = (report.url == "data:...") ? "passed" : "failed";
+ window.opener.postMessage(message, "http://mochi.test:8888");
+ close();
+ });
+
+ obs.observe();
+ let testingInterface = new TestingDeprecatedInterface();
+ testingInterface.deprecatedMethod();
+ };
+ </script>`;
+
+ return new Promise((resolve, reject) => {
+ window.open(uri);
+ window.addEventListener("message", e => {
+ is(e.data, "passed", "The data URI is truncated");
+ resolve();
+ });
+ });
+}
+
+// eslint-disable-next-line no-unused-vars
+function test_deprecatedAttribute() {
+ info("Testing DeprecatedTestingAttribute report");
+ return new Promise(resolve => {
+ let obs = new ReportingObserver((reports, o) => {
+ is(obs, o, "Same observer!");
+ ok(reports.length == 1, "We have 1 report");
+
+ let report = reports[0];
+ is(report.type, "deprecation", "Deprecation report received");
+ is(report.url, location.href, "URL is location");
+ ok(!!report.body, "The report has a body");
+ ok(
+ report.body instanceof DeprecationReportBody,
+ "Correct type for the body"
+ );
+ is(
+ report.body.id,
+ "DeprecatedTestingAttribute",
+ "report.body.id matches DeprecatedTestingAttribute"
+ );
+ ok(!report.body.anticipatedRemoval, "We don't have a anticipatedRemoval");
+ ok(
+ report.body.message.includes(
+ "TestingDeprecatedInterface.deprecatedAttribute"
+ ),
+ "We have a message"
+ );
+ is(
+ report.body.sourceFile,
+ location.href
+ .split("?")[0]
+ .replace("test_deprecated.html", "common_deprecated.js")
+ .replace("worker_deprecated.js", "common_deprecated.js"),
+ "We have a sourceFile"
+ );
+ is(report.body.lineNumber, 181, "We have a lineNumber");
+ is(report.body.columnNumber, 8, "We have a columnNumber");
+
+ obs.disconnect();
+ resolve();
+ });
+ ok(!!obs, "ReportingObserver is a thing");
+
+ obs.observe();
+ ok(true, "ReportingObserver.observe() is callable");
+
+ ok(testingInterface.deprecatedAttribute, "Attributed called");
+ });
+}
+
+// eslint-disable-next-line no-unused-vars
+function test_takeRecords() {
+ info("Testing ReportingObserver.takeRecords()");
+ let p = new Promise(resolve => {
+ let obs = new ReportingObserver((reports, o) => {
+ is(obs, o, "Same observer!");
+ resolve(obs);
+ });
+ ok(!!obs, "ReportingObserver is a thing");
+
+ obs.observe();
+ ok(true, "ReportingObserver.observe() is callable");
+
+ testingInterface.deprecatedMethod();
+ ok(true, "Run a deprecated method.");
+ });
+
+ return p.then(obs => {
+ let reports = obs.takeRecords();
+ is(reports.length, 0, "No reports after an callback");
+
+ testingInterface.deprecatedAttribute + 1;
+
+ reports = obs.takeRecords();
+ ok(reports.length >= 1, "We have at least 1 report");
+
+ reports = obs.takeRecords();
+ is(reports.length, 0, "No more reports");
+ });
+}
diff --git a/dom/reporting/tests/delivering.sjs b/dom/reporting/tests/delivering.sjs
new file mode 100644
index 0000000000..5bc3e17f3e
--- /dev/null
+++ b/dom/reporting/tests/delivering.sjs
@@ -0,0 +1,111 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(aRequest, aResponse) {
+ var params = new URLSearchParams(aRequest.queryString);
+
+ // Report-to setter
+ if (aRequest.method == "GET" && params.get("task") == "header") {
+ let extraParams = [];
+
+ if (params.has("410")) {
+ extraParams.push("410=true");
+ }
+
+ if (params.has("worker")) {
+ extraParams.push("worker=true");
+ }
+
+ let body = {
+ max_age: 1,
+ endpoints: [
+ {
+ url:
+ "https://example.org/tests/dom/reporting/tests/delivering.sjs" +
+ (extraParams.length ? "?" + extraParams.join("&") : ""),
+ priority: 1,
+ weight: 1,
+ },
+ ],
+ };
+
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+ aResponse.setHeader("Report-to", JSON.stringify(body), false);
+ aResponse.write("OK");
+ return;
+ }
+
+ // Report check
+ if (aRequest.method == "GET" && params.get("task") == "check") {
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+
+ let reports = getState("report");
+ if (!reports) {
+ aResponse.write("");
+ return;
+ }
+
+ if (params.has("min")) {
+ let json = JSON.parse(reports);
+ if (json.length < params.get("min")) {
+ aResponse.write("");
+ return;
+ }
+ }
+
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+ aResponse.write(getState("report"));
+
+ setState("report", "");
+ return;
+ }
+
+ if (aRequest.method == "POST") {
+ var body = new BinaryInputStream(aRequest.bodyInputStream);
+
+ var avail;
+ var bytes = [];
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+
+ let reports = getState("report");
+ if (!reports) {
+ reports = [];
+ } else {
+ reports = JSON.parse(reports);
+ }
+
+ const incoming_reports = JSON.parse(String.fromCharCode.apply(null, bytes));
+ for (let report of incoming_reports) {
+ let data = {
+ contentType: aRequest.getHeader("content-type"),
+ origin: aRequest.getHeader("origin"),
+ body: report,
+ url:
+ aRequest.scheme +
+ "://" +
+ aRequest.host +
+ aRequest.path +
+ (aRequest.queryString ? "&" + aRequest.queryString : ""),
+ };
+ reports.push(data);
+ }
+
+ setState("report", JSON.stringify(reports));
+
+ if (params.has("410")) {
+ aResponse.setStatusLine(aRequest.httpVersion, 410, "Gone");
+ } else {
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+ }
+ return;
+ }
+
+ aResponse.setStatusLine(aRequest.httpVersion, 500, "Internal error");
+ aResponse.write("Invalid request");
+}
diff --git a/dom/reporting/tests/empty.html b/dom/reporting/tests/empty.html
new file mode 100644
index 0000000000..cd0875583a
--- /dev/null
+++ b/dom/reporting/tests/empty.html
@@ -0,0 +1 @@
+Hello world!
diff --git a/dom/reporting/tests/gtest/TestReportToParser.cpp b/dom/reporting/tests/gtest/TestReportToParser.cpp
new file mode 100644
index 0000000000..a3549886a3
--- /dev/null
+++ b/dom/reporting/tests/gtest/TestReportToParser.cpp
@@ -0,0 +1,418 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/dom/ReportingHeader.h"
+#include "nsNetUtil.h"
+#include "nsIURI.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+TEST(ReportToParser, Basic)
+{
+ nsCOMPtr<nsIURI> uri;
+
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), "https://example.com");
+ ASSERT_EQ(NS_OK, rv);
+
+ bool urlEqual = false;
+
+ // Empty header.
+ UniquePtr<ReportingHeader::Client> client =
+ ReportingHeader::ParseHeader(nullptr, uri, ""_ns);
+ ASSERT_TRUE(!client);
+
+ // Empty header.
+ client = ReportingHeader::ParseHeader(nullptr, uri, " "_ns);
+ ASSERT_TRUE(!client);
+
+ // No minimal attributes
+ client = ReportingHeader::ParseHeader(nullptr, uri, "{}"_ns);
+ ASSERT_TRUE(!client);
+
+ // Single client
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 42, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default"));
+ ASSERT_FALSE(client->mGroups.ElementAt(0).mIncludeSubdomains);
+ ASSERT_EQ(42, client->mGroups.ElementAt(0).mTTL);
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)1,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)2,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+
+ // 2 clients, same group name.
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 43, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]},"
+ "{\"max_age\": 44, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default"));
+ ASSERT_EQ(43, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first one with an invalid group name.
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 43, \"group\": 123, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]},"
+ "{\"max_age\": 44, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default"));
+ ASSERT_EQ(44, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first one with an invalid group name.
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 43, \"group\": null, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]},"
+ "{\"max_age\": 44, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default"));
+ ASSERT_EQ(44, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first one with an invalid group name.
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 43, \"group\": {}, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]},"
+ "{\"max_age\": 44, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default"));
+ ASSERT_EQ(44, client->mGroups.ElementAt(0).mTTL);
+
+ // Single client: optional params
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 45, \"group\": \"foobar\", \"include_subdomains\": "
+ "true, \"endpoints\": [{\"url\": \"https://example.com\", "
+ "\"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("foobar"));
+ ASSERT_TRUE(client->mGroups.ElementAt(0).mIncludeSubdomains);
+ ASSERT_EQ(45, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: missing max_age.
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"endpoints\": [{\"url\": \"https://example.com\", \"priority\": "
+ "1, \"weight\": 2}]},"
+ "{\"max_age\": 46, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(46, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: invalid max_age.
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": null, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]},"
+ "{\"max_age\": 46, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(46, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: invalid max_age.
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": \"foobar\", \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]},"
+ "{\"max_age\": 46, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(46, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: invalid max_age.
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": {}, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]},"
+ "{\"max_age\": 46, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(46, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: missing endpoints
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 47},"
+ "{\"max_age\": 48, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: invalid endpoints
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 47, \"endpoints\": null },"
+ "{\"max_age\": 48, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: invalid endpoints
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 47, \"endpoints\": \"abc\" },"
+ "{\"max_age\": 48, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: invalid endpoints
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 47, \"endpoints\": 42 },"
+ "{\"max_age\": 48, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: invalid endpoints
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 47, \"endpoints\": {} },"
+ "{\"max_age\": 48, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 clients, the first incomplete: empty endpoints
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 49, \"endpoints\": []},"
+ "{\"max_age\": 50, \"endpoints\": [{\"url\": "
+ "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ(50, client->mGroups.ElementAt(0).mTTL);
+
+ // 2 endpoints, the first incomplete: missing url
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString("{\"max_age\": 51, \"endpoints\": ["
+ " {\"priority\": 1, \"weight\": 2},"
+ " {\"url\": \"https://example.com\", \"priority\": 1, "
+ "\"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)1,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)2,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+
+ // 2 endpoints, the first incomplete: invalid url
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString("{\"max_age\": 51, \"endpoints\": ["
+ " {\"url\": 42, \"priority\": 1, \"weight\": 2},"
+ " {\"url\": \"https://example.com\", \"priority\": 1, "
+ "\"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)1,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)2,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+
+ // 2 endpoints, the first incomplete: invalid url
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 51, \"endpoints\": ["
+ " {\"url\": \"something here\", \"priority\": 1, \"weight\": 2},"
+ " {\"url\": \"https://example.com\", \"priority\": 1, \"weight\": "
+ "2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)1,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)2,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+
+ // 2 endpoints, the first incomplete: invalid url
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString("{\"max_age\": 51, \"endpoints\": ["
+ " {\"url\": {}, \"priority\": 1, \"weight\": 2},"
+ " {\"url\": \"https://example.com\", \"priority\": 1, "
+ "\"weight\": 2}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)1,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)2,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+
+ // 2 endpoints, the first incomplete: missing priority
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString("{\"max_age\": 52, \"endpoints\": ["
+ " {\"url\": \"https://example.com\", \"weight\": 3}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)1,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)3,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+
+ // 2 endpoints, the first incomplete: invalid priority
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString("{\"max_age\": 52, \"endpoints\": ["
+ " {\"url\": \"https://example.com\", \"priority\": "
+ "{}, \"weight\": 2},"
+ " {\"url\": \"https://example.com\", \"priority\": 2, "
+ "\"weight\": 3}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)2,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)3,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+
+ // 2 endpoints, the first incomplete: invalid priority
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString("{\"max_age\": 52, \"endpoints\": ["
+ " {\"url\": \"https://example.com\", \"priority\": "
+ "\"ok\", \"weight\": 2},"
+ " {\"url\": \"https://example.com\", \"priority\": 2, "
+ "\"weight\": 3}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)2,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)3,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+
+ // 2 endpoints, the first incomplete: missing weight
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString(
+ "{\"max_age\": 52, \"endpoints\": ["
+ " {\"url\": \"https://example.com\", \"priority\": 5}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)5,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)1,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+
+ // 2 endpoints, the first incomplete: invalid weight
+ client = ReportingHeader::ParseHeader(
+ nullptr, uri,
+ nsLiteralCString("{\"max_age\": 52, \"endpoints\": ["
+ " {\"url\": \"https://example.com\", \"priority\": 4, "
+ "\"weight\": []},"
+ " {\"url\": \"https://example.com\", \"priority\": 5, "
+ "\"weight\": 6}]}"));
+ ASSERT_TRUE(!!client);
+ ASSERT_EQ((uint32_t)1, client->mGroups.Length());
+ ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length());
+ ASSERT_TRUE(
+ NS_SUCCEEDED(
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals(
+ uri, &urlEqual)) &&
+ urlEqual);
+ ASSERT_EQ((uint32_t)5,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority);
+ ASSERT_EQ((uint32_t)6,
+ client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight);
+}
diff --git a/dom/reporting/tests/gtest/moz.build b/dom/reporting/tests/gtest/moz.build
new file mode 100644
index 0000000000..860ef48d1e
--- /dev/null
+++ b/dom/reporting/tests/gtest/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestReportToParser.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/reporting/tests/iframe_delivering.html b/dom/reporting/tests/iframe_delivering.html
new file mode 100644
index 0000000000..e8c5e9e3a4
--- /dev/null
+++ b/dom/reporting/tests/iframe_delivering.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for delivering reports</title>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function ok(a, msg) {
+ parent.postMessage({type: "test", check: !!a, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+function finish() {
+ parent.postMessage({type: "finish" }, "*");
+}
+
+function checkReport() {
+ return new Promise(resolve => {
+ let id = setInterval(_ => {
+ fetch("delivering.sjs?task=check&min=3")
+ .then(r => r.text())
+ .then(text => {
+ if (text) {
+ resolve(JSON.parse(text));
+ clearInterval(id);
+ }
+ });
+ }, 1000);
+ });
+}
+
+function runTests(extraParams = "") {
+ // Call a deprecating operation.
+ for (let i = 0; i < 100; ++i) {
+ new TestingDeprecatedInterface();
+ }
+ ok(true, "Created a deprecated interface");
+
+ // Check if the report has been received.
+ return checkReport()
+ .then(reports => {
+ is(reports.length, 3, "We have 1 report");
+
+ let report = reports[0];
+ is(report.contentType, "application/reports+json", "Correct mime-type");
+ is(report.origin, "https://example.org", "Origin correctly set");
+ is(report.url, "https://example.org/tests/dom/reporting/tests/delivering.sjs" + extraParams, "URL is correctly set");
+ ok(!!report.body, "We have a report.body");
+ ok(report.body.age > 0, "Age is correctly set");
+ is(report.body.url, window.location.href, "URL is correctly set");
+ is(report.body.user_agent, navigator.userAgent, "User-agent matches");
+ ok(report.body.type, "deprecation", "Type is fine.");
+ ok(!!report.body.body, "We have the real report.body");
+ is(report.body.body.id, "DeprecatedTestingInterface", "Correct report.body.id");
+ is(report.body.body.message, "TestingDeprecatedInterface is a testing-only interface and this is its testing deprecation message.", "We have a report.body.message");
+ is(report.body.body.sourceFile, "https://example.org/tests/dom/reporting/tests/iframe_delivering.html", "report.body.sourceFile");
+ is(report.body.body.lineNumber, 40, "report.body.lineNumber");
+ is(report.body.body.columnNumber, 5, "report.body.columnNumber");
+ });
+}
+
+// Let's register a group + endpoint
+fetch("delivering.sjs?task=header")
+.then(r => r.text())
+.then(text => {
+ is(text, "OK", "Report-to header sent");
+})
+.then(_ => {
+ return runTests();
+})
+
+// Let's register a group + endpoint from a worker
+.then(_ => {
+ return new Promise(resolve => {
+ let w = new Worker("worker_delivering.js");
+ w.onmessage = e => resolve();
+ });
+})
+.then(_ => {
+ return runTests("&worker=true");
+})
+
+.then(finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/reporting/tests/mochitest.toml b/dom/reporting/tests/mochitest.toml
new file mode 100644
index 0000000000..4faddd217c
--- /dev/null
+++ b/dom/reporting/tests/mochitest.toml
@@ -0,0 +1,29 @@
+[DEFAULT]
+prefs = [
+ "dom.reporting.enabled=true",
+ "dom.reporting.header.enabled=true",
+ "dom.reporting.testing.enabled=true",
+]
+
+["test_delivering.html"]
+support-files = [
+ "delivering.sjs",
+ "iframe_delivering.html",
+ "worker_delivering.js",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_deprecated.html"]
+support-files = [
+ "common_deprecated.js",
+ "worker_deprecated.js",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_memoryPressure.html"]
diff --git a/dom/reporting/tests/test_delivering.html b/dom/reporting/tests/test_delivering.html
new file mode 100644
index 0000000000..2c552273e0
--- /dev/null
+++ b/dom/reporting/tests/test_delivering.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for delivering reports</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="application/javascript">
+
+// Setting prefs.
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom_reporting_delivering_timeout", 1],
+ ["dom_reporting_delivering_maxFailures", 2],
+ ["dom.reporting.delivering.maxReports", 3],
+]})
+
+// Tests run in iframes because the origin must be secure for report-to header.
+.then(_ => {
+ window.addEventListener("message", e => {
+ if (e.data.type == "finish") {
+ SimpleTest.finish();
+ return;
+ }
+
+ if (e.data.type == "test") {
+ ok(e.data.check, e.data.msg);
+ return;
+ }
+
+ ok(false, "Invalid message");
+ });
+
+ let ifr = document.createElement("iframe");
+ ifr.src = "https://example.org/tests/dom/reporting/tests/iframe_delivering.html";
+
+ document.body.appendChild(ifr);
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/reporting/tests/test_deprecated.html b/dom/reporting/tests/test_deprecated.html
new file mode 100644
index 0000000000..da55978e9b
--- /dev/null
+++ b/dom/reporting/tests/test_deprecated.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Deprecated reports</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="common_deprecated.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="application/javascript">
+
+test_deprecatedInterface()
+.then(() => test_deprecatedMethod())
+.then(() => test_deprecatedMethodWithDataURI())
+.then(() => test_deprecatedAttribute())
+.then(() => test_takeRecords())
+.then(() => {
+ info("Workers!");
+
+ return new Promise(resolve => {
+ const w = new Worker("worker_deprecated.js");
+ w.onmessage = e => {
+ switch (e.data.type) {
+ case "info":
+ info(e.data.msg);
+ break;
+
+ case "check":
+ ok(e.data.check, e.data.msg);
+ break;
+
+ case "finish":
+ resolve();
+ break;
+
+ default:
+ ok(false, "Invalid message");
+ break;
+ }
+ }
+ });
+})
+
+.then(() => { SimpleTest.finish(); });
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/reporting/tests/test_memoryPressure.html b/dom/reporting/tests/test_memoryPressure.html
new file mode 100644
index 0000000000..1bb887b05e
--- /dev/null
+++ b/dom/reporting/tests/test_memoryPressure.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for ReportingObserver + memory-pressure</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="application/javascript">
+
+info("Testing TakeRecords() without memory-pressure");
+let o = new ReportingObserver(() => {});
+o.observe();
+
+new TestingDeprecatedInterface();
+let r = o.takeRecords();
+is(r.length, 1, "We have 1 report");
+
+r = o.takeRecords();
+is(r.length, 0, "We have 0 reports after a takeRecords()");
+
+info("Testing DeprecatedTestingMethod report");
+
+new TestingDeprecatedInterface();
+SpecialPowers.Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize");
+
+r = o.takeRecords();
+is(r.length, 0, "We have 0 reports after a memory-pressure");
+
+</script>
+</body>
+</html>
diff --git a/dom/reporting/tests/worker_delivering.js b/dom/reporting/tests/worker_delivering.js
new file mode 100644
index 0000000000..539bcd231c
--- /dev/null
+++ b/dom/reporting/tests/worker_delivering.js
@@ -0,0 +1,5 @@
+fetch("delivering.sjs?task=header&worker=true")
+ .then(r => r.text())
+ .then(text => {
+ postMessage("All good!");
+ });
diff --git a/dom/reporting/tests/worker_deprecated.js b/dom/reporting/tests/worker_deprecated.js
new file mode 100644
index 0000000000..f6b57896f6
--- /dev/null
+++ b/dom/reporting/tests/worker_deprecated.js
@@ -0,0 +1,28 @@
+/* eslint-disable no-undef */
+
+// eslint-disable-next-line no-unused-vars
+function ok(a, msg) {
+ postMessage({ type: "check", check: !!a, msg });
+}
+
+// eslint-disable-next-line no-unused-vars
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+// eslint-disable-next-line no-unused-vars
+function info(msg) {
+ postMessage({ type: "info", msg });
+}
+
+function finish() {
+ postMessage({ type: "finish" });
+}
+
+importScripts("common_deprecated.js");
+
+test_deprecatedInterface()
+ .then(() => test_deprecatedMethod())
+ .then(() => test_deprecatedAttribute())
+ .then(() => test_takeRecords())
+ .then(() => finish());