refactor uri scheme handling

This commit is contained in:
Fabian Freund
2026-02-24 06:19:14 +01:00
parent 25b153af97
commit 702b4b27ca
14 changed files with 32 additions and 36 deletions
@@ -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<bool> 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;
}
@@ -48,4 +48,4 @@ final class ShowSiteSettingsBadgeProvider
}
String _$showSiteSettingsBadgeHash() =>
r'e326617d2d794844c6bbe6d909042b91d95c2996';
r'd1499809901363705d23b17aab375fd414080544';
@@ -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,
@@ -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,
@@ -17,6 +17,7 @@
* 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: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<String> _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.
@@ -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}',
);
}
}
@@ -34,7 +34,7 @@ final class UrlUnshortenerServiceProvider
}
String _$urlUnshortenerServiceHash() =>
r'627fd3cc6f6c2251f8f582fcff27cb5a7e2c7813';
r'19dae758ed0c48cb183e3da814d01039548fae40';
abstract class _$UrlUnshortenerService extends $AsyncNotifier<Set<String>> {
FutureOr<Set<String>> build();
@@ -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(