From 702b4b27ca2c3f523069a3a8b8e08139474e9c19 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 24 Feb 2026 06:19:14 +0100 Subject: [PATCH] refactor uri scheme handling --- app/lib/domain/services/generic_website.dart | 5 ++--- app/lib/domain/services/generic_website.g.dart | 2 +- app/lib/extensions/uri.dart | 4 ++++ .../providers/site_settings_badge_provider.dart | 6 ++---- .../providers/site_settings_badge_provider.g.dart | 2 +- .../widgets/browser_modules/app_bar_title.dart | 3 ++- .../presentation/widgets/certificate_tile.dart | 3 ++- .../domain/services/url_cleaner_service.dart | 4 ++-- .../domain/services/url_unshortener_service.dart | 14 ++++++-------- .../domain/services/url_unshortener_service.g.dart | 2 +- .../presentation/dialogs/open_shared_content.dart | 11 ++++------- .../presentation/controllers/website_title.dart | 6 ++---- .../presentation/controllers/website_title.g.dart | 2 +- app/lib/utils/form_validators.dart | 4 ++-- 14 files changed, 32 insertions(+), 36 deletions(-) diff --git a/app/lib/domain/services/generic_website.dart b/app/lib/domain/services/generic_website.dart index e50a7a17..a914098a 100644 --- a/app/lib/domain/services/generic_website.dart +++ b/app/lib/domain/services/generic_website.dart @@ -33,6 +33,7 @@ import 'package:socks5_proxy/socks_client.dart'; import 'package:weblibre/core/http_error_handler.dart'; import 'package:weblibre/data/models/web_page_info.dart'; import 'package:weblibre/extensions/http_encoding.dart'; +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/features/geckoview/domain/entities/browser_icon.dart'; import 'package:weblibre/features/user/domain/repositories/cache.dart'; import 'package:weblibre/features/web_feed/utils/feed_finder.dart'; @@ -129,9 +130,7 @@ class GenericWebsiteService extends _$GenericWebsiteService { return uri; } - static bool _isHttpUrl(Uri url) { - return url.scheme.startsWith('https') || url.scheme.startsWith('http'); - } + static bool _isHttpUrl(Uri url) => url.isHttpOrHttps; static List _extractIcons(Uri baseUrl, Document document) { final List icons = []; diff --git a/app/lib/domain/services/generic_website.g.dart b/app/lib/domain/services/generic_website.g.dart index d256c006..eebdc04e 100644 --- a/app/lib/domain/services/generic_website.g.dart +++ b/app/lib/domain/services/generic_website.g.dart @@ -42,7 +42,7 @@ final class GenericWebsiteServiceProvider } String _$genericWebsiteServiceHash() => - r'b320635f651574a500ee6764688ef26b6d39ef8f'; + r'a2bd892f0ca07467eaa906648e57f232a7e50155'; abstract class _$GenericWebsiteService extends $Notifier { void build(); diff --git a/app/lib/extensions/uri.dart b/app/lib/extensions/uri.dart index 0405ba06..62feda4d 100644 --- a/app/lib/extensions/uri.dart +++ b/app/lib/extensions/uri.dart @@ -24,4 +24,8 @@ extension UriX on Uri { Uri get base => Uri.parse('$scheme://$authority'); bool get hasSupportedScheme => _supportedSchemes.contains(scheme); + + bool get isHttp => isScheme('http'); + bool get isHttps => isScheme('https'); + bool get isHttpOrHttps => isHttp || isHttps; } diff --git a/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart b/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart index 09a0cc0a..27a92171 100644 --- a/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart +++ b/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart @@ -19,14 +19,13 @@ */ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/repositories/site_permissions.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart'; part 'site_settings_badge_provider.g.dart'; -const _supportedSchemes = {'http', 'https'}; - /// Provider that determines whether to show the site settings badge on the tab icon. /// Returns true if any site-specific setting has been altered from defaults. @Riverpod() @@ -41,8 +40,7 @@ Future showSiteSettingsBadge(Ref ref) async { hasTrackingProtectionExceptionProvider(tabState.id).future, ); - if (!tabState.url.hasScheme || - !_supportedSchemes.contains(tabState.url.scheme)) { + if (!tabState.url.hasScheme || !tabState.url.isHttpOrHttps) { return false; } diff --git a/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.g.dart b/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.g.dart index 21c8aa00..e27d6569 100644 --- a/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.g.dart +++ b/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.g.dart @@ -48,4 +48,4 @@ final class ShowSiteSettingsBadgeProvider } String _$showSiteSettingsBadgeHash() => - r'e326617d2d794844c6bbe6d909042b91d95c2996'; + r'd1499809901363705d23b17aab375fd414080544'; diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart index 30bee790..797c63c4 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart @@ -25,6 +25,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:text_scroll/text_scroll.dart'; import 'package:weblibre/core/design/app_colors.dart'; import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart'; @@ -52,7 +53,7 @@ class AppBarTitle extends HookConsumerWidget { } final icon = useMemoized(() { - if (tabState.url.isScheme('http')) { + if (tabState.url.isHttp) { return Icon( MdiIcons.lockOff, color: Theme.of(context).colorScheme.error, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/certificate_tile.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/certificate_tile.dart index e3bd7fcb..4d697905 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/certificate_tile.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/certificate_tile.dart @@ -22,6 +22,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; class CertificateTile extends HookConsumerWidget { @@ -36,7 +37,7 @@ class CertificateTile extends HookConsumerWidget { } final icon = useMemoized(() { - if (tabState.url.isScheme('http')) { + if (tabState.url.isHttp) { return ListTile( leading: Icon( MdiIcons.lockOff, diff --git a/app/lib/features/geckoview/features/open_link_tools/domain/services/url_cleaner_service.dart b/app/lib/features/geckoview/features/open_link_tools/domain/services/url_cleaner_service.dart index 8def0d40..24db9f6d 100644 --- a/app/lib/features/geckoview/features/open_link_tools/domain/services/url_cleaner_service.dart +++ b/app/lib/features/geckoview/features/open_link_tools/domain/services/url_cleaner_service.dart @@ -17,6 +17,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/features/geckoview/features/open_link_tools/domain/entities/url_cleaner_result.dart'; import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_rule.dart'; @@ -243,8 +244,7 @@ List _findQueryParamMatchesWithRegex(String url, RegExp paramRegex) { bool _hasSafeScheme(String url) { final parsed = Uri.tryParse(url); if (parsed == null || !parsed.hasScheme) return true; // schemeless is ok - final scheme = parsed.scheme.toLowerCase(); - return scheme == 'http' || scheme == 'https'; + return parsed.isHttpOrHttps; } // Pre-compiled regexes for URL normalization — avoids recompiling on every call. diff --git a/app/lib/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.dart b/app/lib/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.dart index e8b53dd6..e1619e72 100644 --- a/app/lib/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.dart +++ b/app/lib/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.dart @@ -22,6 +22,7 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/features/geckoview/features/open_link_tools/data/models/unshorten_response_data.dart'; import 'package:weblibre/features/geckoview/features/open_link_tools/domain/entities/unshorten_result.dart'; @@ -136,14 +137,11 @@ class UrlUnshortenerService extends _$UrlUnshortenerService { // Reject non-HTTP(S) URLs from the API to prevent scheme-based attacks. if (result.success && result.finalUrl != null) { final parsed = Uri.tryParse(result.finalUrl!); - if (parsed != null && parsed.hasScheme) { - final scheme = parsed.scheme.toLowerCase(); - if (scheme != 'http' && scheme != 'https') { - return UnshortenResult( - success: false, - error: 'Unsupported URL scheme: $scheme', - ); - } + if (parsed != null && parsed.hasScheme && !parsed.isHttpOrHttps) { + return UnshortenResult( + success: false, + error: 'Unsupported URL scheme: ${parsed.scheme}', + ); } } diff --git a/app/lib/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.g.dart b/app/lib/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.g.dart index 8a0cf19e..c38bea04 100644 --- a/app/lib/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.g.dart +++ b/app/lib/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.g.dart @@ -34,7 +34,7 @@ final class UrlUnshortenerServiceProvider } String _$urlUnshortenerServiceHash() => - r'627fd3cc6f6c2251f8f582fcff27cb5a7e2c7813'; + r'19dae758ed0c48cb183e3da814d01039548fae40'; abstract class _$UrlUnshortenerService extends $AsyncNotifier> { FutureOr> build(); diff --git a/app/lib/features/geckoview/features/open_link_tools/presentation/dialogs/open_shared_content.dart b/app/lib/features/geckoview/features/open_link_tools/presentation/dialogs/open_shared_content.dart index 19a61f64..365eb8ee 100644 --- a/app/lib/features/geckoview/features/open_link_tools/presentation/dialogs/open_shared_content.dart +++ b/app/lib/features/geckoview/features/open_link_tools/presentation/dialogs/open_shared_content.dart @@ -68,13 +68,10 @@ class OpenSharedContent extends HookConsumerWidget { // Debounce the URL to avoid running expensive operations on every keystroke. final debouncedUrl = useState(currentUrl); final debouncer = useDebouncer(const Duration(milliseconds: 300)); - useEffect( - () { - debouncer.eventOccured(() => debouncedUrl.value = currentUrl); - return null; - }, - [currentUrl], - ); + useEffect(() { + debouncer.eventOccured(() => debouncedUrl.value = currentUrl); + return null; + }, [currentUrl]); final parsedDebouncedUrl = Uri.tryParse(debouncedUrl.value); final hasExternalApp = useCachedFuture( diff --git a/app/lib/presentation/controllers/website_title.dart b/app/lib/presentation/controllers/website_title.dart index 8e3d4ac6..5a826ee0 100644 --- a/app/lib/presentation/controllers/website_title.dart +++ b/app/lib/presentation/controllers/website_title.dart @@ -23,6 +23,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:weblibre/data/models/web_page_info.dart'; import 'package:weblibre/domain/services/generic_website.dart'; import 'package:weblibre/extensions/ref_cache.dart'; +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; @@ -32,16 +33,13 @@ import 'package:weblibre/features/user/domain/repositories/tor_settings.dart'; part 'website_title.g.dart'; -const _supportedFetchSchemes = {'http', 'https'}; - @Riverpod() class CompletePageInfo extends _$CompletePageInfo { @override AsyncValue build(TabState cached) { ref.cacheFor(const Duration(minutes: 2)); - if (cached.isPageInfoComplete || - !_supportedFetchSchemes.contains(cached.url.scheme)) { + if (cached.isPageInfoComplete || !cached.url.isHttpOrHttps) { return AsyncData(cached); } diff --git a/app/lib/presentation/controllers/website_title.g.dart b/app/lib/presentation/controllers/website_title.g.dart index 0ae20567..8a7682eb 100644 --- a/app/lib/presentation/controllers/website_title.g.dart +++ b/app/lib/presentation/controllers/website_title.g.dart @@ -58,7 +58,7 @@ final class CompletePageInfoProvider } } -String _$completePageInfoHash() => r'edf5538b913d1ebb285c15ec83644818fe90707d'; +String _$completePageInfoHash() => r'4af0e7c06f07e95aa7e0037782e1a7e661ee61d1'; final class CompletePageInfoFamily extends $Family with diff --git a/app/lib/utils/form_validators.dart b/app/lib/utils/form_validators.dart index 1b5e5f5e..c9392d51 100644 --- a/app/lib/utils/form_validators.dart +++ b/app/lib/utils/form_validators.dart @@ -21,6 +21,7 @@ import 'dart:io'; import 'package:nullability/nullability.dart'; import 'package:path/path.dart' as p; +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/utils/uri_parser.dart' as uri_parser; String? validateUrl( @@ -41,8 +42,7 @@ String? validateUrl( if (uri_parser.tryParseUrl(value, eagerParsing: eagerParsing) case final Uri url) { if (!requireAuthority || url.authority.isNotEmpty) { - if (!onlyHttpProtocol || - (url.isScheme('https') || url.isScheme('http'))) { + if (!onlyHttpProtocol || url.isHttpOrHttps) { return null; } }