diff options
Diffstat (limited to 'toolkit/components/antitracking/ContentBlockingLog.h')
-rw-r--r-- | toolkit/components/antitracking/ContentBlockingLog.h | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/ContentBlockingLog.h b/toolkit/components/antitracking/ContentBlockingLog.h new file mode 100644 index 0000000000..f71b2fc0ff --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingLog.h @@ -0,0 +1,427 @@ +/* -*- 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_ContentBlockingLog_h +#define mozilla_ContentBlockingLog_h + +#include "mozilla/ContentBlockingNotifier.h" +#include "mozilla/JSONStringWriteFuncs.h" +#include "mozilla/Maybe.h" +#include "mozilla/StaticPrefs_browser.h" + +#include "mozilla/UniquePtr.h" +#include "nsIWebProgressListener.h" +#include "nsReadableUtils.h" +#include "nsTArray.h" +#include "nsWindowSizes.h" + +class nsIPrincipal; + +namespace mozilla { + +class ContentBlockingLog final { + typedef ContentBlockingNotifier::StorageAccessPermissionGrantedReason + StorageAccessPermissionGrantedReason; + + struct LogEntry { + uint32_t mType; + uint32_t mRepeatCount; + bool mBlocked; + Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason> + mReason; + nsTArray<nsCString> mTrackingFullHashes; + }; + + struct OriginDataEntry { + OriginDataEntry() + : mHasLevel1TrackingContentLoaded(false), + mHasLevel2TrackingContentLoaded(false) {} + + bool mHasLevel1TrackingContentLoaded; + bool mHasLevel2TrackingContentLoaded; + Maybe<bool> mHasCookiesLoaded; + Maybe<bool> mHasTrackerCookiesLoaded; + Maybe<bool> mHasSocialTrackerCookiesLoaded; + nsTArray<LogEntry> mLogs; + }; + + struct OriginEntry { + OriginEntry() { mData = MakeUnique<OriginDataEntry>(); } + + nsCString mOrigin; + UniquePtr<OriginDataEntry> mData; + }; + + typedef nsTArray<OriginEntry> OriginDataTable; + + struct Comparator { + public: + bool Equals(const OriginDataTable::value_type& aLeft, + const OriginDataTable::value_type& aRight) const { + return aLeft.mOrigin.Equals(aRight.mOrigin); + } + + bool Equals(const OriginDataTable::value_type& aLeft, + const nsACString& aRight) const { + return aLeft.mOrigin.Equals(aRight); + } + }; + + public: + static const nsLiteralCString kDummyOriginHash; + + ContentBlockingLog() = default; + ~ContentBlockingLog() = default; + + // Record the log in the parent process. This should be called only in the + // parent process and will replace the RecordLog below after we remove the + // ContentBlockingLog from content processes. + Maybe<uint32_t> RecordLogParent( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe< + ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const nsTArray<nsCString>& aTrackingFullHashes); + + void RecordLog( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe< + ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const nsTArray<nsCString>& aTrackingFullHashes) { + RecordLogInternal(aOrigin, aType, aBlocked, aReason, aTrackingFullHashes); + } + + void ReportLog(nsIPrincipal* aFirstPartyPrincipal); + void ReportEmailTrackingLog(nsIPrincipal* aFirstPartyPrincipal); + + nsAutoCString Stringify() { + nsAutoCString buffer; + + JSONStringRefWriteFunc js(buffer); + JSONWriter w(js); + w.Start(); + + for (const OriginEntry& entry : mLog) { + if (!entry.mData) { + continue; + } + + w.StartArrayProperty(entry.mOrigin, w.SingleLineStyle); + + StringifyCustomFields(entry, w); + for (const LogEntry& item : entry.mData->mLogs) { + w.StartArrayElement(w.SingleLineStyle); + { + w.IntElement(item.mType); + w.BoolElement(item.mBlocked); + w.IntElement(item.mRepeatCount); + if (item.mReason.isSome()) { + w.IntElement(item.mReason.value()); + } + } + w.EndArray(); + } + w.EndArray(); + } + + w.End(); + + return buffer; + } + + bool HasBlockedAnyOfType(uint32_t aType) const { + // Note: nothing inside this loop should return false, the goal for the + // loop is to scan the log to see if we find a matching entry, and if so + // we would return true, otherwise in the end of the function outside of + // the loop we take the common `return false;` statement. + for (const OriginEntry& entry : mLog) { + if (!entry.mData) { + continue; + } + + if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) { + if (entry.mData->mHasLevel1TrackingContentLoaded) { + return true; + } + } else if (aType == nsIWebProgressListener:: + STATE_LOADED_LEVEL_2_TRACKING_CONTENT) { + if (entry.mData->mHasLevel2TrackingContentLoaded) { + return true; + } + } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) { + if (entry.mData->mHasCookiesLoaded.isSome() && + entry.mData->mHasCookiesLoaded.value()) { + return true; + } + } else if (aType == + nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) { + if (entry.mData->mHasTrackerCookiesLoaded.isSome() && + entry.mData->mHasTrackerCookiesLoaded.value()) { + return true; + } + } else if (aType == + nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) { + if (entry.mData->mHasSocialTrackerCookiesLoaded.isSome() && + entry.mData->mHasSocialTrackerCookiesLoaded.value()) { + return true; + } + } else { + for (const auto& item : entry.mData->mLogs) { + if (((item.mType & aType) != 0) && item.mBlocked) { + return true; + } + } + } + } + return false; + } + + void AddSizeOfExcludingThis(nsWindowSizes& aSizes) const { + aSizes.mDOMSizes.mDOMOtherSize += + mLog.ShallowSizeOfExcludingThis(aSizes.mState.mMallocSizeOf); + + // Now add the sizes of each origin log queue. + for (const OriginEntry& entry : mLog) { + if (entry.mData) { + aSizes.mDOMSizes.mDOMOtherSize += + aSizes.mState.mMallocSizeOf(entry.mData.get()) + + entry.mData->mLogs.ShallowSizeOfExcludingThis( + aSizes.mState.mMallocSizeOf); + } + } + } + + uint32_t GetContentBlockingEventsInLog() { + uint32_t events = 0; + + // We iterate the whole log to produce the overview of blocked events. + for (const OriginEntry& entry : mLog) { + if (!entry.mData) { + continue; + } + + if (entry.mData->mHasLevel1TrackingContentLoaded) { + events |= nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT; + } + + if (entry.mData->mHasLevel2TrackingContentLoaded) { + events |= nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT; + } + + if (entry.mData->mHasCookiesLoaded.isSome() && + entry.mData->mHasCookiesLoaded.value()) { + events |= nsIWebProgressListener::STATE_COOKIES_LOADED; + } + + if (entry.mData->mHasTrackerCookiesLoaded.isSome() && + entry.mData->mHasTrackerCookiesLoaded.value()) { + events |= nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER; + } + + if (entry.mData->mHasSocialTrackerCookiesLoaded.isSome() && + entry.mData->mHasSocialTrackerCookiesLoaded.value()) { + events |= nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER; + } + + for (const auto& item : entry.mData->mLogs) { + if (item.mBlocked) { + events |= item.mType; + } + } + } + + return events; + } + + private: + void RecordLogInternal( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe< + ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason = Nothing(), + const nsTArray<nsCString>& aTrackingFullHashes = nsTArray<nsCString>()) { + DebugOnly<bool> isCookiesBlockedTracker = + aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER || + aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER; + MOZ_ASSERT_IF(aBlocked, aReason.isNothing()); + MOZ_ASSERT_IF(!isCookiesBlockedTracker, aReason.isNothing()); + MOZ_ASSERT_IF(isCookiesBlockedTracker && !aBlocked, aReason.isSome()); + + if (aOrigin.IsVoid()) { + return; + } + auto index = mLog.IndexOf(aOrigin, 0, Comparator()); + if (index != OriginDataTable::NoIndex) { + OriginEntry& entry = mLog[index]; + if (!entry.mData) { + return; + } + + if (RecordLogEntryInCustomField(aType, entry, aBlocked)) { + return; + } + if (!entry.mData->mLogs.IsEmpty()) { + auto& last = entry.mData->mLogs.LastElement(); + if (last.mType == aType && last.mBlocked == aBlocked) { + ++last.mRepeatCount; + // Don't record recorded events. This helps compress our log. + // We don't care about if the the reason is the same, just keep the + // first one. + // Note: {aReason, aTrackingFullHashes} are not compared here and we + // simply keep the first for the reason, and merge hashes to make sure + // they can be correctly recorded. + for (const auto& hash : aTrackingFullHashes) { + if (!last.mTrackingFullHashes.Contains(hash)) { + last.mTrackingFullHashes.AppendElement(hash); + } + } + return; + } + } + if (entry.mData->mLogs.Length() == + std::max(1u, + StaticPrefs::browser_contentblocking_originlog_length())) { + // Cap the size at the maximum length adjustable by the pref + entry.mData->mLogs.RemoveElementAt(0); + } + entry.mData->mLogs.AppendElement( + LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone()}); + return; + } + + // The entry has not been found. + OriginEntry* entry = mLog.AppendElement(); + if (NS_WARN_IF(!entry || !entry->mData)) { + return; + } + + entry->mOrigin = aOrigin; + + if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) { + entry->mData->mHasLevel1TrackingContentLoaded = aBlocked; + } else if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT) { + entry->mData->mHasLevel2TrackingContentLoaded = aBlocked; + } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) { + MOZ_ASSERT(entry->mData->mHasCookiesLoaded.isNothing()); + entry->mData->mHasCookiesLoaded.emplace(aBlocked); + } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) { + MOZ_ASSERT(entry->mData->mHasTrackerCookiesLoaded.isNothing()); + entry->mData->mHasTrackerCookiesLoaded.emplace(aBlocked); + } else if (aType == + nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) { + MOZ_ASSERT(entry->mData->mHasSocialTrackerCookiesLoaded.isNothing()); + entry->mData->mHasSocialTrackerCookiesLoaded.emplace(aBlocked); + } else { + entry->mData->mLogs.AppendElement( + LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone()}); + } + } + + bool RecordLogEntryInCustomField(uint32_t aType, OriginEntry& aEntry, + bool aBlocked) { + if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) { + aEntry.mData->mHasLevel1TrackingContentLoaded = aBlocked; + return true; + } + if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT) { + aEntry.mData->mHasLevel2TrackingContentLoaded = aBlocked; + return true; + } + if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) { + if (aEntry.mData->mHasCookiesLoaded.isSome()) { + aEntry.mData->mHasCookiesLoaded.ref() = aBlocked; + } else { + aEntry.mData->mHasCookiesLoaded.emplace(aBlocked); + } + return true; + } + if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) { + if (aEntry.mData->mHasTrackerCookiesLoaded.isSome()) { + aEntry.mData->mHasTrackerCookiesLoaded.ref() = aBlocked; + } else { + aEntry.mData->mHasTrackerCookiesLoaded.emplace(aBlocked); + } + return true; + } + if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) { + if (aEntry.mData->mHasSocialTrackerCookiesLoaded.isSome()) { + aEntry.mData->mHasSocialTrackerCookiesLoaded.ref() = aBlocked; + } else { + aEntry.mData->mHasSocialTrackerCookiesLoaded.emplace(aBlocked); + } + return true; + } + return false; + } + + void StringifyCustomFields(const OriginEntry& aEntry, JSONWriter& aWriter) { + if (aEntry.mData->mHasLevel1TrackingContentLoaded) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement( + nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT); + aWriter.BoolElement(true); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + if (aEntry.mData->mHasLevel2TrackingContentLoaded) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement( + nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT); + aWriter.BoolElement(true); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + if (aEntry.mData->mHasCookiesLoaded.isSome()) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement(nsIWebProgressListener::STATE_COOKIES_LOADED); + aWriter.BoolElement( + aEntry.mData->mHasCookiesLoaded.value()); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + if (aEntry.mData->mHasTrackerCookiesLoaded.isSome()) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement( + nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER); + aWriter.BoolElement( + aEntry.mData->mHasTrackerCookiesLoaded.value()); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + if (aEntry.mData->mHasSocialTrackerCookiesLoaded.isSome()) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement( + nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER); + aWriter.BoolElement( + aEntry.mData->mHasSocialTrackerCookiesLoaded.value()); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + } + + private: + OriginDataTable mLog; +}; + +} // namespace mozilla + +#endif |