Bug 1491835 - Store User-Interaction for AntiTracking purposes - part 4 - update permissions after X seconds, r=ehsan

This commit is contained in:
Andrea Marchesini 2018-09-24 12:54:54 +02:00
parent a7bdca9758
commit 25f9ad890f
5 changed files with 256 additions and 6 deletions

View File

@ -1495,6 +1495,7 @@ nsIDocument::nsIDocument()
mChildDocumentUseCounters(0),
mNotifiedPageForUseCounter(0),
mUserHasInteracted(false),
mHasUserInteractionTimerScheduled(false),
mUserGestureActivated(false),
mStackRefCnt(0),
mUpdateNestLevel(0),
@ -12626,6 +12627,10 @@ nsIDocument::SetUserHasInteracted()
{
MOZ_LOG(gUserInteractionPRLog, LogLevel::Debug,
("Document %p has been interacted by user.", this));
// We maybe need to update the user-interaction permission.
MaybeStoreUserInteractionAsPermission();
if (mUserHasInteracted) {
return;
}
@ -12638,7 +12643,6 @@ nsIDocument::SetUserHasInteracted()
}
MaybeAllowStorageForOpener();
MaybeStoreUserInteractionAsPermission();
}
void
@ -12758,13 +12762,190 @@ nsIDocument::MaybeAllowStorageForOpener()
AntiTrackingCommon::eHeuristic);
}
namespace {
// Documents can stay alive for days. We don't want to update the permission
// value at any user-interaction, and, using a timer triggered any X seconds
// should be good enough. 'X' is taken from
// privacy.userInteraction.document.interval pref.
// We also want to store the user-interaction before shutting down, and, for
// this reason, this class implements nsIAsyncShutdownBlocker interface.
class UserIntractionTimer final : public Runnable
, public nsITimerCallback
, public nsIAsyncShutdownBlocker
{
public:
NS_DECL_ISUPPORTS_INHERITED
explicit UserIntractionTimer(nsIDocument* aDocument)
: Runnable("UserIntractionTimer")
, mPrincipal(aDocument->NodePrincipal())
, mDocument(do_GetWeakReference(aDocument))
{
static int32_t userInteractionTimerId = 0;
// Blocker names must be unique. Let's create it now because when needed,
// the document could be already gone.
mBlockerName.AppendPrintf("UserInteractionTimer %d for document %p",
++userInteractionTimerId, aDocument);
}
// Runnable interface
NS_IMETHOD
Run() override
{
uint32_t interval =
StaticPrefs::privacy_userInteraction_document_interval();
if (!interval) {
return NS_OK;
}
RefPtr<UserIntractionTimer> self = this;
auto raii = MakeScopeExit([self] {
self->CancelTimerAndStoreUserInteraction();
});
nsresult rv = NS_NewTimerWithCallback(getter_AddRefs(mTimer),
this, interval * 1000,
nsITimer::TYPE_ONE_SHOT,
SystemGroup::EventTargetFor(TaskCategory::Other));
NS_ENSURE_SUCCESS(rv, NS_OK);
nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase();
NS_ENSURE_TRUE(!!phase, NS_OK);
rv = phase->AddBlocker(this, NS_LITERAL_STRING(__FILE__), __LINE__,
NS_LITERAL_STRING("UserIntractionTimer shutdown"));
NS_ENSURE_SUCCESS(rv, NS_OK);
raii.release();
return NS_OK;
}
// nsITimerCallback interface
NS_IMETHOD
Notify(nsITimer* aTimer) override
{
StoreUserInteraction();
return NS_OK;
}
#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
using nsINamed::GetName;
#endif
// nsIAsyncShutdownBlocker interface
NS_IMETHOD
GetName(nsAString& aName) override
{
aName = mBlockerName;
return NS_OK;
}
NS_IMETHOD
BlockShutdown(nsIAsyncShutdownClient* aClient) override
{
CancelTimerAndStoreUserInteraction();
return NS_OK;
}
NS_IMETHOD
GetState(nsIPropertyBag**) override
{
return NS_OK;
}
private:
~UserIntractionTimer() = default;
void
StoreUserInteraction()
{
// Remove the shutting down blocker
nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase();
if (phase) {
phase->RemoveBlocker(this);
}
// If the document is not gone, let's reset its timer flag.
nsCOMPtr<nsIDocument> document = do_QueryReferent(mDocument);
if (document) {
AntiTrackingCommon::StoreUserInteractionFor(mPrincipal);
document->ResetUserInteractionTimer();
}
}
void
CancelTimerAndStoreUserInteraction()
{
if (mTimer) {
mTimer->Cancel();
mTimer = nullptr;
}
StoreUserInteraction();
}
static already_AddRefed<nsIAsyncShutdownClient>
GetShutdownPhase()
{
nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdown();
NS_ENSURE_TRUE(!!svc, nullptr);
nsCOMPtr<nsIAsyncShutdownClient> phase;
nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(phase));
NS_ENSURE_SUCCESS(rv, nullptr);
return phase.forget();
}
nsCOMPtr<nsIPrincipal> mPrincipal;
nsWeakPtr mDocument;
nsCOMPtr<nsITimer> mTimer;
nsString mBlockerName;
};
NS_IMPL_ISUPPORTS_INHERITED(UserIntractionTimer, Runnable, nsITimerCallback,
nsIAsyncShutdownBlocker)
} // anonymous
void
nsIDocument::MaybeStoreUserInteractionAsPermission()
{
// We care about user-interaction stored only for top-level documents.
if (!mParentDocument) {
AntiTrackingCommon::StoreUserInteractionFor(NodePrincipal());
if (GetSameTypeParentDocument()) {
return;
}
if (!mUserHasInteracted) {
// First interaction, let's store this info now.
AntiTrackingCommon::StoreUserInteractionFor(NodePrincipal());
return;
}
if (mHasUserInteractionTimerScheduled) {
return;
}
nsCOMPtr<nsIRunnable> task = new UserIntractionTimer(this);
nsresult rv = NS_IdleDispatchToCurrentThread(task.forget(), 2500);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
// This value will be reset by the timer.
mHasUserInteractionTimerScheduled = true;
}
void
nsIDocument::ResetUserInteractionTimer()
{
mHasUserInteractionTimerScheduled = false;
}
bool

View File

@ -3505,6 +3505,7 @@ public:
{
return mUserHasInteracted;
}
void ResetUserInteractionTimer();
// This should be called when this document receives events which are likely
// to be user interaction with the document, rather than the byproduct of
@ -4497,6 +4498,11 @@ protected:
// Whether the user has interacted with the document or not:
bool mUserHasInteracted;
// We constantly update the user-interaction anti-tracking permission at any
// user-interaction using a timer. This boolean value is set to true when this
// timer is scheduled.
bool mHasUserInteractionTimerScheduled;
// Whether the user has interacted with the document via a restricted
// set of gestures which are likely to be interaction with the document,
// and not events that are fired as a byproduct of the user interacting

View File

@ -504,7 +504,7 @@ EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
aEvent->mMessage == eWheel || aEvent->mMessage == eTouchEnd ||
aEvent->mMessage == ePointerUp)) {
nsIDocument* doc = node->OwnerDoc();
while (doc && !doc->UserHasInteracted()) {
while (doc) {
doc->SetUserHasInteracted();
doc = nsContentUtils::IsChildOfSameType(doc) ?
doc->GetParentDocument() : nullptr;

View File

@ -1628,6 +1628,13 @@ VARCACHE_PREF(
uint32_t, 2592000 // 30 days (in seconds)
)
// Anti-tracking user-interaction document interval
VARCACHE_PREF(
"privacy.userInteraction.document.interval",
privacy_userInteraction_document_interval,
uint32_t, 1800 // 30 minutes (in seconds)
)
// Anti-fingerprinting, disabled by default
VARCACHE_PREF(
"privacy.resistFingerprinting",

View File

@ -1,3 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
ChromeUtils.import("resource://gre/modules/Services.jsm");
add_task(async function() {
@ -5,6 +10,7 @@ add_task(async function() {
await SpecialPowers.flushPrefEnv();
await SpecialPowers.pushPrefEnv({"set": [
["privacy.userInteraction.document.interval", 1],
["browser.contentblocking.enabled", true],
["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER],
["privacy.trackingprotection.enabled", false],
@ -22,8 +28,8 @@ add_task(async function() {
await BrowserTestUtils.browserLoaded(browser);
let uri = Services.io.newURI(TEST_DOMAIN);
is (Services.perms.testPermission(uri, "storageAccessAPI"), Services.perms.UNKNOWN_ACTION,
"Before user-interaction we don't have a permission");
is(Services.perms.testPermission(uri, "storageAccessAPI"), Services.perms.UNKNOWN_ACTION,
"Before user-interaction we don't have a permission");
let promise = TestUtils.topicObserved("perm-changed", (aSubject, aData) => {
let permission = aSubject.QueryInterface(Ci.nsIPermission);
@ -39,6 +45,56 @@ add_task(async function() {
info("Waiting to have a permissions set.");
await promise;
// Let's see if the document is able to update the permission correctly.
for (var i = 0; i < 3; ++i) {
// Another perm-changed event should be triggered by the timer.
promise = TestUtils.topicObserved("perm-changed", (aSubject, aData) => {
let permission = aSubject.QueryInterface(Ci.nsIPermission);
return permission.type == "storageAccessAPI" &&
permission.principal.URI.equals(uri);
});
info("Simulating another user-interaction.");
await ContentTask.spawn(browser, null, async function() {
content.document.userInteractionForTesting();
});
info("Waiting to have a permissions set.");
await promise;
}
// Let's disable the document.interval.
await SpecialPowers.pushPrefEnv({"set": [
["privacy.userInteraction.document.interval", 0],
]});
promise = new Promise(resolve => {
let id;
function observer(subject, topic, data) {
ok(false, "Notification received!");
Services.obs.removeObserver(observer, "perm-changed");
clearTimeout(id);
resolve();
}
Services.obs.addObserver(observer, "perm-changed");
id = setTimeout(() => {
ok(true, "No notification received!");
Services.obs.removeObserver(observer, "perm-changed");
resolve();
}, 2000);
});
info("Simulating another user-interaction.");
await ContentTask.spawn(browser, null, async function() {
content.document.userInteractionForTesting();
});
info("Waiting to have a permissions set.");
await promise;
info("Removing the tab");
BrowserTestUtils.removeTab(tab);
});