From c12e8c5ad342c23982c267a9927cdb2248a0c803 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Wed, 24 Jan 2024 15:14:19 +0100 Subject: [PATCH] Added ignored domains list config --- lib/l10n/app_en.arb | 4 +- lib/l10n/app_es.arb | 4 +- lib/models/querylog_config.dart | 27 ++++ .../logs_settings/config_widgets.dart | 117 ++++++++++++++++-- .../settings/logs_settings/logs_settings.dart | 79 +++++++++--- lib/services/api_client.dart | 18 ++- 6 files changed, 218 insertions(+), 31 deletions(-) create mode 100644 lib/models/querylog_config.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 79081e5..b7e9999 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -727,5 +727,7 @@ "dnsCacheNumber": "DNS cache size must be a number", "errors": "Errors", "redirectHttpsWarning": "If you have enabled \"Redirect to HTTPS automatically\" on your AdGuard Home server, you must select an HTTPS connection and use the HTTPS port of your server.", - "logsSettingsDescription": "Configure query logs" + "logsSettingsDescription": "Configure query logs", + "ignoredDomains": "Ignored domains", + "noIgnoredDomainsAdded": "No domains to ignore added" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3846777..0ea54a0 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -727,5 +727,7 @@ "dnsCacheNumber": "El tamaño de caché de DNS debe ser un número", "errors": "Errores", "redirectHttpsWarning": "Si tienes activado \"Redireccionar a HTTPS automáticamente\" en tu servidor AdGuard Home, debes seleccionar una conexión HTTPS y utilizar el puerto de HTTPS de tu servidor.", - "logsSettingsDescription": "Configura los registros de peticiones" + "logsSettingsDescription": "Configura los registros de peticiones", + "ignoredDomains": "Dominios ignorados", + "noIgnoredDomainsAdded": "No hay añadidos dominios para ignorar" } \ No newline at end of file diff --git a/lib/models/querylog_config.dart b/lib/models/querylog_config.dart new file mode 100644 index 0000000..dfb80b4 --- /dev/null +++ b/lib/models/querylog_config.dart @@ -0,0 +1,27 @@ +class QueryLogConfig { + final List? ignored; + final int? interval; + final bool? enabled; + final bool? anonymizeClientIp; + + QueryLogConfig({ + this.ignored, + this.interval, + this.enabled, + this.anonymizeClientIp, + }); + + factory QueryLogConfig.fromJson(Map json) => QueryLogConfig( + ignored: json["ignored"] == null ? [] : List.from(json["ignored"]!.map((x) => x)), + interval: json["interval"], + enabled: json["enabled"], + anonymizeClientIp: json["anonymize_client_ip"], + ); + + Map toJson() => { + "ignored": ignored == null ? [] : List.from(ignored!.map((x) => x)), + "interval": interval, + "enabled": enabled, + "anonymize_client_ip": anonymizeClientIp, + }; +} diff --git a/lib/screens/settings/logs_settings/config_widgets.dart b/lib/screens/settings/logs_settings/config_widgets.dart index 3374854..4dbaee7 100644 --- a/lib/screens/settings/logs_settings/config_widgets.dart +++ b/lib/screens/settings/logs_settings/config_widgets.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:uuid/uuid.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/screens/settings/logs_settings/logs_settings.dart'; +import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/custom_checkbox_list_tile.dart'; class LogsConfigOptions extends StatelessWidget { @@ -13,6 +16,8 @@ class LogsConfigOptions extends StatelessWidget { final void Function(double?) updateRetentionTime; final void Function() onClear; final void Function() onConfirm; + final List ignoredDomainsControllers; + final void Function(List) updateIgnoredDomainsControllers; const LogsConfigOptions({ super.key, @@ -24,11 +29,15 @@ class LogsConfigOptions extends StatelessWidget { required this.retentionTime, required this.updateRetentionTime, required this.onClear, - required this.onConfirm + required this.onConfirm, + required this.ignoredDomainsControllers, + required this.updateIgnoredDomainsControllers }); @override Widget build(BuildContext context) { + const Uuid uuid = Uuid(); + final List dropdownItemTranslation = [ AppLocalizations.of(context)!.hours6, AppLocalizations.of(context)!.hours24, @@ -37,7 +46,28 @@ class LogsConfigOptions extends StatelessWidget { AppLocalizations.of(context)!.days90, ]; - return Column( + void validateDomain(String value, String id) { + final domainRegex = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$'); + bool error = false; + if (domainRegex.hasMatch(value)) { + error = false; + } + else { + error = true; + } + updateIgnoredDomainsControllers( + ignoredDomainsControllers.map((entry) { + if (entry.id != id) return entry; + return DomainListItemController( + id: id, + controller: entry.controller, + error: error + ); + }).toList() + ); + } + + return ListView( children: [ const SizedBox(height: 16), Padding( @@ -100,12 +130,83 @@ class LogsConfigOptions extends StatelessWidget { borderRadius: BorderRadius.circular(20), ), ), - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: onClear, - icon: const Icon(Icons.delete_rounded), - label: Text(AppLocalizations.of(context)!.clearLogs), - ) + Padding( + padding: const EdgeInsets.only(top: 24, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SectionLabel( + label: AppLocalizations.of(context)!.ignoredDomains, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 0), + ), + Padding( + padding: const EdgeInsets.only(right: 10), + child: IconButton( + onPressed: () => updateIgnoredDomainsControllers([ + ...ignoredDomainsControllers, + DomainListItemController( + id: uuid.v4(), + controller: TextEditingController(), + error: false + ), + ]), + icon: const Icon(Icons.add) + ), + ) + ], + ), + ), + if (ignoredDomainsControllers.isNotEmpty) ...ignoredDomainsControllers.map((controller) => Padding( + padding: const EdgeInsets.only( + top: 12, bottom: 12, left: 24, right: 10 + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextFormField( + controller: controller.controller, + onChanged: (v) => validateDomain(v, controller.id), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.domain, + errorText: controller.error + ? AppLocalizations.of(context)!.invalidDomain + : null + ), + ), + ), + const SizedBox(width: 12), + Padding( + padding: controller.error + ? const EdgeInsets.only(bottom: 24) + : const EdgeInsets.all(0), + child: IconButton( + onPressed: () => updateIgnoredDomainsControllers( + ignoredDomainsControllers.where((e) => e.id != controller.id).toList() + ), + icon: const Icon(Icons.remove_circle_outline_outlined) + ), + ) + ], + ), + )), + if (ignoredDomainsControllers.isEmpty) Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + AppLocalizations.of(context)!.noIgnoredDomainsAdded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), ], ); } diff --git a/lib/screens/settings/logs_settings/logs_settings.dart b/lib/screens/settings/logs_settings/logs_settings.dart index 2fc6892..8c651e0 100644 --- a/lib/screens/settings/logs_settings/logs_settings.dart +++ b/lib/screens/settings/logs_settings/logs_settings.dart @@ -1,15 +1,29 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:uuid/uuid.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/settings/logs_settings/config_widgets.dart'; +import 'package:adguard_home_manager/models/querylog_config.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; +class DomainListItemController { + final String id; + final TextEditingController controller; + bool error; + + DomainListItemController({ + required this.id, + required this.controller, + required this.error + }); +} + class LogsSettings extends StatefulWidget { const LogsSettings({super.key}); @@ -18,9 +32,12 @@ class LogsSettings extends StatefulWidget { } class _LogsSettingsState extends State { + final Uuid uuid = const Uuid(); + bool generalSwitch = false; bool anonymizeClientIp = false; double? retentionTime; + List _ignoredDomainsControllers = []; List retentionItems = [ 21600000, @@ -37,20 +54,27 @@ class _LogsSettingsState extends State { final result = await serversProvider.apiClient2!.getQueryLogInfo(); - if (mounted) { - if (result.successful == true) { - setState(() { - generalSwitch = result.content['enabled']; - anonymizeClientIp = result.content['anonymize_client_ip']; - retentionTime = result.content['interval'] != null - ? double.parse(result.content['interval'].toString()) - : null; - loadStatus = LoadStatus.loaded; - }); - } - else { - setState(() => loadStatus = LoadStatus.error); - } + if (!mounted) return; + if (result.successful == true) { + final data = result.content as QueryLogConfig; + setState(() { + generalSwitch = data.enabled ?? false; + anonymizeClientIp = data.anonymizeClientIp ?? false; + retentionTime = data.interval != null + ? double.parse(data.interval.toString()) + : null; + if (data.ignored != null) { + _ignoredDomainsControllers = data.ignored!.map((e) => DomainListItemController( + id: uuid.v4(), + controller: TextEditingController(text: e), + error: false + )).toList(); + } + loadStatus = LoadStatus.loaded; + }); + } + else { + setState(() => loadStatus = LoadStatus.error); } } @@ -65,6 +89,10 @@ class _LogsSettingsState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final validValues = _ignoredDomainsControllers.where( + (d) => d.controller.text == "" || d.error == true + ).isEmpty; + void clearQueries() async { ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.updatingSettings); @@ -99,7 +127,8 @@ class _LogsSettingsState extends State { data: { "enabled": generalSwitch, "interval": retentionTime, - "anonymize_client_ip": anonymizeClientIp + "anonymize_client_ip": anonymizeClientIp, + "ignored": _ignoredDomainsControllers.map((e) => e.controller.text).toList() } ); @@ -128,10 +157,24 @@ class _LogsSettingsState extends State { title: Text(AppLocalizations.of(context)!.logsSettings), actions: [ if (loadStatus == LoadStatus.loaded) IconButton( - onPressed: updateConfig, + onPressed: validValues ? () => updateConfig() : null, icon: const Icon(Icons.save_rounded), tooltip: AppLocalizations.of(context)!.save, ), + if (loadStatus == LoadStatus.loaded) PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + onTap: clearQueries, + child: Row( + children: [ + const Icon(Icons.delete_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.clearLogs), + ], + ) + ) + ], + ), const SizedBox(width: 8) ], ), @@ -151,7 +194,9 @@ class _LogsSettingsState extends State { retentionTime: retentionTime, updateRetentionTime: (v) => setState(() => retentionTime = v), onClear: clearQueries, - onConfirm: updateConfig + onConfirm: updateConfig, + ignoredDomainsControllers: _ignoredDomainsControllers, + updateIgnoredDomainsControllers: (v) => setState(() => _ignoredDomainsControllers = v), ); case LoadStatus.error: diff --git a/lib/services/api_client.dart b/lib/services/api_client.dart index f48d6d7..b122a8c 100644 --- a/lib/services/api_client.dart +++ b/lib/services/api_client.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:adguard_home_manager/models/blocked_services.dart'; +import 'package:adguard_home_manager/models/querylog_config.dart'; import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/models/encryption.dart'; import 'package:adguard_home_manager/models/dhcp.dart'; @@ -627,10 +628,19 @@ class ApiClientV2 { Future getQueryLogInfo() async { final result = await HttpRequestClient.get(urlPath: '/querylog/config', server: server); if (result.successful) { - return ApiResponse( - successful: true, - content: jsonDecode(result.body!) - ); + try { + return ApiResponse( + successful: true, + content: QueryLogConfig.fromJson(jsonDecode(result.body!)) + ); + } catch (e, stackTrace) { + Sentry.captureException( + e, + stackTrace: stackTrace, + hint: Hint.withMap({ "statusCode": result.statusCode.toString() }) + ); + return const ApiResponse(successful: false); + } } else { return const ApiResponse(successful: false);