implement tracking protection per site

This commit is contained in:
Fabian Freund
2026-01-16 07:36:38 +01:00
parent d416b8963d
commit 2c00504b42
20 changed files with 1277 additions and 13 deletions
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tracking_protection.g.dart';
/// Repository for managing per-site Enhanced Tracking Protection exceptions
///
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
/// automatically after mutations.
@Riverpod(keepAlive: true)
class TrackingProtectionRepository extends _$TrackingProtectionRepository {
final _api = GeckoTrackingProtectionApi();
/// Check if a tab has a tracking protection exception
///
/// Returns true if ETP is disabled for this site
Future<bool> containsException(String tabId) {
return _api.containsException(tabId);
}
/// Add tracking protection exception for a tab (disable ETP for this site)
Future<void> addException(String tabId) async {
await _api.addException(tabId);
await _invalidateWithDelay();
}
/// Remove tracking protection exception for a tab (enable ETP for this site)
Future<void> removeException(String tabId) async {
await _api.removeException(tabId);
await _invalidateWithDelay();
}
/// Remove a specific exception by URL and refresh the exceptions list
Future<void> removeExceptionByUrl(String url) async {
await _api.removeExceptionByUrl(url);
await _invalidateWithDelay();
}
/// Remove all tracking protection exceptions
Future<void> removeAllExceptions() async {
await _api.removeAllExceptions();
await _invalidateWithDelay();
}
/// Helper method to invalidate repository state after mutations with delay
Future<void> _invalidateWithDelay() async {
await Future.delayed(const Duration(milliseconds: 100)).whenComplete(() {
if (ref.mounted) {
ref.invalidateSelf();
}
});
}
@override
Future<List<TrackingProtectionException>> build() {
return _api.fetchExceptions();
}
}
@@ -0,0 +1,86 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tracking_protection.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Repository for managing per-site Enhanced Tracking Protection exceptions
///
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
/// automatically after mutations.
@ProviderFor(TrackingProtectionRepository)
final trackingProtectionRepositoryProvider =
TrackingProtectionRepositoryProvider._();
/// Repository for managing per-site Enhanced Tracking Protection exceptions
///
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
/// automatically after mutations.
final class TrackingProtectionRepositoryProvider
extends
$AsyncNotifierProvider<
TrackingProtectionRepository,
List<TrackingProtectionException>
> {
/// Repository for managing per-site Enhanced Tracking Protection exceptions
///
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
/// automatically after mutations.
TrackingProtectionRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'trackingProtectionRepositoryProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$trackingProtectionRepositoryHash();
@$internal
@override
TrackingProtectionRepository create() => TrackingProtectionRepository();
}
String _$trackingProtectionRepositoryHash() =>
r'6d4407034b2c67317d0a0e106002f1785fd86a34';
/// Repository for managing per-site Enhanced Tracking Protection exceptions
///
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
/// automatically after mutations.
abstract class _$TrackingProtectionRepository
extends $AsyncNotifier<List<TrackingProtectionException>> {
FutureOr<List<TrackingProtectionException>> build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<
AsyncValue<List<TrackingProtectionException>>,
List<TrackingProtectionException>
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<List<TrackingProtectionException>>,
List<TrackingProtectionException>
>,
AsyncValue<List<TrackingProtectionException>>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -28,17 +28,14 @@ Future<SitePermissions?> sitePermissions(
Ref ref,
String origin,
bool isPrivate,
) async {
) {
final api = GeckoSitePermissionsApi();
return api.getSitePermissions(origin, isPrivate);
}
/// Provider to get the public suffix plus one (eTLD+1) for a host
@Riverpod()
Future<String> publicSuffixPlusOne(
Ref ref,
String host,
) async {
Future<String> publicSuffixPlusOne(Ref ref, String host) {
final api = GeckoPublicSuffixListApi();
return api.getPublicSuffixPlusOne(host);
}
@@ -49,10 +46,7 @@ class SelectedClearDataTypes extends _$SelectedClearDataTypes {
@override
Set<ClearDataType> build() {
// Default to clearing all site data and auth sessions (most common use case)
return {
ClearDataType.allSiteData,
ClearDataType.authSessions,
};
return {ClearDataType.allSiteData, ClearDataType.authSessions};
}
void toggle(ClearDataType type) {
@@ -68,7 +68,7 @@ final class SitePermissionsProvider
}
}
String _$sitePermissionsHash() => r'c605e9a1acb261ec46659431b5bb1e105b8aa2a1';
String _$sitePermissionsHash() => r'785af0f938bfe53799fc8b6ee8be71015afbc86c';
/// Provider to fetch site permissions for a given origin
@@ -147,7 +147,7 @@ final class PublicSuffixPlusOneProvider
}
String _$publicSuffixPlusOneHash() =>
r'e259f66e3ebf278468223d8e25618eb9f67a3504';
r'2871740bee033e634047b1374ae8dfad508f2cb2';
/// Provider to get the public suffix plus one (eTLD+1) for a host
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart';
part 'tracking_protection_provider.g.dart';
/// Provider that checks if a specific tab has a tracking protection exception
/// Automatically rebuilds when repository changes after mutations
@Riverpod()
Future<bool> hasTrackingProtectionException(Ref ref, String tabId) {
ref.watch(trackingProtectionRepositoryProvider);
return ref
.read(trackingProtectionRepositoryProvider.notifier)
.containsException(tabId);
}
@@ -0,0 +1,95 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tracking_protection_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider that checks if a specific tab has a tracking protection exception
/// Automatically rebuilds when repository changes after mutations
@ProviderFor(hasTrackingProtectionException)
final hasTrackingProtectionExceptionProvider =
HasTrackingProtectionExceptionFamily._();
/// Provider that checks if a specific tab has a tracking protection exception
/// Automatically rebuilds when repository changes after mutations
final class HasTrackingProtectionExceptionProvider
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
with $FutureModifier<bool>, $FutureProvider<bool> {
/// Provider that checks if a specific tab has a tracking protection exception
/// Automatically rebuilds when repository changes after mutations
HasTrackingProtectionExceptionProvider._({
required HasTrackingProtectionExceptionFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'hasTrackingProtectionExceptionProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$hasTrackingProtectionExceptionHash();
@override
String toString() {
return r'hasTrackingProtectionExceptionProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<bool> create(Ref ref) {
final argument = this.argument as String;
return hasTrackingProtectionException(ref, argument);
}
@override
bool operator ==(Object other) {
return other is HasTrackingProtectionExceptionProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$hasTrackingProtectionExceptionHash() =>
r'3c6c2d4a2c75d52c33211bc8da8191185fb6c542';
/// Provider that checks if a specific tab has a tracking protection exception
/// Automatically rebuilds when repository changes after mutations
final class HasTrackingProtectionExceptionFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<bool>, String> {
HasTrackingProtectionExceptionFamily._()
: super(
retry: null,
name: r'hasTrackingProtectionExceptionProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider that checks if a specific tab has a tracking protection exception
/// Automatically rebuilds when repository changes after mutations
HasTrackingProtectionExceptionProvider call(String tabId) =>
HasTrackingProtectionExceptionProvider._(argument: tabId, from: this);
@override
String toString() => r'hasTrackingProtectionExceptionProvider';
}
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart';
import 'package:weblibre/utils/ui_helper.dart';
/// Section widget displaying Enhanced Tracking Protection toggle
class TrackingProtectionSection extends HookConsumerWidget {
final String tabId;
const TrackingProtectionSection({required this.tabId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final hasExceptionAsync = ref.watch(
hasTrackingProtectionExceptionProvider(tabId),
);
return hasExceptionAsync.when(
data: (hasException) => _TrackingProtectionTile(
tabId: tabId,
isEnabled: !hasException, // ETP enabled when NOT in exceptions
),
loading: () => const SizedBox.shrink(),
error: (error, stack) => const SizedBox.shrink(),
);
}
}
class _TrackingProtectionTile extends ConsumerWidget {
final String tabId;
final bool isEnabled;
const _TrackingProtectionTile({required this.tabId, required this.isEnabled});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SwitchListTile.adaptive(
value: isEnabled,
onChanged: (enabled) => _toggleProtection(context, ref, enabled),
title: const Text('Enhanced Tracking Protection'),
subtitle: Text(
isEnabled
? 'Trackers on this site are being blocked'
: 'Trackers on this site are allowed',
),
secondary: Icon(
isEnabled ? Icons.shield : Icons.shield_outlined,
color: isEnabled
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
);
}
Future<void> _toggleProtection(
BuildContext context,
WidgetRef ref,
bool enabled,
) async {
try {
if (enabled) {
// Remove exception to enable ETP
await ref
.read(trackingProtectionRepositoryProvider.notifier)
.removeException(tabId);
} else {
// Add exception to disable ETP
await ref
.read(trackingProtectionRepositoryProvider.notifier)
.addException(tabId);
}
// Reload tab to apply changes
await ref.read(selectedTabSessionProvider).reload();
} catch (e) {
if (context.mounted) {
showErrorMessage(context, 'Failed to toggle tracking protection: $e');
}
}
}
}
@@ -29,6 +29,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_section.dart';
import 'package:weblibre/presentation/widgets/website_title_tile.dart';
class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics {
@@ -208,6 +209,11 @@ class ViewTabSheetWidget extends HookConsumerWidget {
),
),
const Divider(),
// Tracking Protection Section
TrackingProtectionSection(
tabId: initialTabState.id,
),
const Divider(),
// Permissions Section
PermissionsSection(
origin: initialTabState.url.origin,