improved uri parsing

This commit is contained in:
Fabian Freund
2026-02-27 08:31:19 +01:00
parent d24675094d
commit d25229cb95
19 changed files with 1132 additions and 95 deletions
@@ -88,7 +88,14 @@ class EditBangScreen extends HookConsumerWidget {
return;
}
final uri = Uri.parse(urlTextController.text);
final uri = parseValidatedUrl(
urlTextController.text,
eagerParsing: false,
onlyHttpProtocol: true,
);
if (uri == null) {
return;
}
final bang = Bang(
group: BangGroup.user,
@@ -28,7 +28,7 @@ import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositori
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/dialogs/delete_bookmark_dialog.dart';
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/widgets/folder_tree_picker.dart';
import 'package:weblibre/utils/form_validators.dart';
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
import 'package:weblibre/utils/uri_input_parser.dart';
class BookmarkEntryEditScreen extends HookConsumerWidget {
final BookmarkInfo? initialInfo;
@@ -66,10 +66,17 @@ class BookmarkEntryEditScreen extends HookConsumerWidget {
IconButton(
onPressed: () async {
if (formKey.currentState?.validate() ?? false) {
final newUrl = uri_parser.tryParseUrl(
var newUrl = parseValidatedUrl(
urlTextController.text,
eagerParsing: true,
)!;
onlyHttpProtocol: true,
);
if (newUrl == null) {
return;
}
newUrl = redactUriCredentials(newUrl);
if (exisitingEntry != null) {
await ref
@@ -23,6 +23,7 @@
import 'dart:convert';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/utils/uri_input_parser.dart';
class BookmarkJSONUtils {
final GeckoBookmarksService _service;
@@ -156,7 +157,11 @@ class BookmarkJSONUtils {
await _service.addItem(parentGuid, uri, title, i);
count++;
} else {
logger.w('Skipping invalid URL: $url');
final parsed = Uri.tryParse(url);
final redacted = parsed != null
? redactUriCredentials(parsed)
: url;
logger.w('Skipping invalid URL: $redacted');
}
} catch (e) {
logger.e('Failed to import bookmark "$title": $e');
@@ -74,7 +74,10 @@ class OpenSharedContent extends HookConsumerWidget {
return null;
}, [currentUrl]);
final parsedDebouncedUrl = Uri.tryParse(debouncedUrl.value);
final parsedDebouncedUrl = parseValidatedUrl(
debouncedUrl.value,
eagerParsing: false,
);
final hasExternalApp = useCachedFuture(
// ignore: discarded_futures useFuture
() => parsedDebouncedUrl != null
@@ -163,10 +166,18 @@ class OpenSharedContent extends HookConsumerWidget {
Future<void> openTab(TabMode tabMode) async {
if (formKey.currentState?.validate() == true) {
final parsedUrl = parseValidatedUrl(
textController.text,
eagerParsing: false,
);
if (parsedUrl == null) {
return;
}
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: Uri.parse(textController.text),
url: parsedUrl,
tabMode: tabMode,
containerSelection: selectedContainer.value == null
? const TabContainerSelection.unassigned()
@@ -183,8 +194,16 @@ class OpenSharedContent extends HookConsumerWidget {
Future<void> openCustomTab(bool isPrivate) async {
if (formKey.currentState?.validate() == true) {
final parsedUrl = parseValidatedUrl(
textController.text,
eagerParsing: false,
);
if (parsedUrl == null) {
return;
}
await GeckoBrowserService().openInCustomTab(
url: Uri.parse(textController.text),
url: parsedUrl,
private: isPrivate,
contextId: selectedContainer.value?.id,
);
@@ -197,7 +216,7 @@ class OpenSharedContent extends HookConsumerWidget {
Future<void> openInApp() async {
if (formKey.currentState?.validate() == true) {
final uri = Uri.tryParse(textController.text);
final uri = parseValidatedUrl(textController.text, eagerParsing: false);
if (uri == null) return;
final success = await _appLinksService.openAppLink(uri);
@@ -20,14 +20,27 @@
import 'package:flutter/material.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'
show GeckoBrowserService;
import 'package:weblibre/utils/ui_helper.dart';
import 'package:weblibre/utils/uri_input_parser.dart';
import 'package:weblibre/utils/uri_policy.dart';
Future<void> openInPrivateCustomTab(BuildContext context, String url) async {
try {
await GeckoBrowserService().openInCustomTab(
url: Uri.parse(url),
private: true,
final parsedUrl = parseUserInputUrl(
url,
policy: SchemePolicy.internalIntent,
allowSchemelessHosts: true,
enforceMaxInputLength: true,
);
if (parsedUrl == null) {
if (context.mounted) {
showErrorMessage(context, 'Could not open link: $url');
}
return;
}
await GeckoBrowserService().openInCustomTab(url: parsedUrl, private: true);
} catch (e) {
if (context.mounted) {
showErrorMessage(context, 'Could not open link: $url');
@@ -47,8 +47,9 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/c
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/on_listenable_change_selector.dart';
import 'package:weblibre/presentation/hooks/sampled_value_notifier.dart';
import 'package:weblibre/utils/input_classification.dart';
import 'package:weblibre/utils/text_field_line_count.dart';
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
class SearchScreen extends HookConsumerWidget {
final String? initialSearchText;
@@ -358,12 +359,26 @@ class SearchScreen extends HookConsumerWidget {
unfocusOnTapOutside: false,
onSubmitted: (value) async {
if (value.isNotEmpty) {
var newUrl = uri_parser.tryParseUrl(
value,
eagerParsing: true,
);
final classification = classifyAddressBarInput(value);
Uri? newUrl;
String? searchQuery;
if (newUrl == null) {
switch (classification) {
case NavigateInputClassification(:final uri):
newUrl = uri;
case SearchInputClassification(:final query):
searchQuery = query;
case InvalidInputClassification():
if (context.mounted) {
ui_helper.showErrorMessage(
context,
'Invalid address',
);
}
return;
}
if (newUrl == null && searchQuery != null) {
// Read from both providers - use site if set, otherwise global
final siteBang = isEditMode
? ref.read(
@@ -383,12 +398,12 @@ class SearchScreen extends HookConsumerWidget {
);
if (bang != null) {
newUrl = bang.getTemplateUrl(value);
newUrl = bang.getTemplateUrl(searchQuery);
if (!privateTabMode) {
await ref
.read(bangSearchProvider.notifier)
.triggerBangSearch(bang, value);
.triggerBangSearch(bang, searchQuery);
}
}
}
@@ -26,7 +26,6 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/co
import 'package:weblibre/presentation/widgets/url_icon.dart';
import 'package:weblibre/utils/form_validators.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
class ContainerSitesScreen extends HookConsumerWidget {
final Set<Uri> initialSites;
@@ -91,11 +90,16 @@ class ContainerSitesScreen extends HookConsumerWidget {
return uriValid;
}
final origin = Uri.parse(
uri_parser
.tryParseUrl(value, eagerParsing: true)!
.origin,
final parsedUrl = parseValidatedUrl(
value,
eagerParsing: true,
onlyHttpProtocol: true,
);
if (parsedUrl == null) {
return 'Invalid URL';
}
final origin = Uri.parse(parsedUrl.origin);
if (sites.value.contains(origin)) {
return 'This site has been already assigned';
@@ -104,11 +108,16 @@ class ContainerSitesScreen extends HookConsumerWidget {
return null;
},
onSaved: (newValue) async {
final origin = Uri.parse(
uri_parser
.tryParseUrl(newValue, eagerParsing: true)!
.origin,
final parsedUrl = parseValidatedUrl(
newValue,
eagerParsing: true,
onlyHttpProtocol: true,
);
if (parsedUrl == null) {
return;
}
final origin = Uri.parse(parsedUrl.origin);
final isAssigned = await ref
.read(containerRepositoryProvider.notifier)
@@ -18,14 +18,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:fast_equatable/fast_equatable.dart';
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
import 'package:weblibre/utils/input_classification.dart';
sealed class SharedContent with FastEquatable {
SharedContent();
factory SharedContent.parse(String content) {
if (uri_parser.tryParseUrl(content, eagerParsing: true)
case final Uri uri) {
if (parseSharedIntentUrl(content) case final Uri uri) {
return SharedUrl(uri);
} else {
return SharedText(content);
@@ -24,7 +24,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/web_feed/domain/providers/add_dialog_blocking.dart';
import 'package:weblibre/utils/form_validators.dart';
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
class AddFeedDialog extends HookConsumerWidget {
final Uri? initialUri;
@@ -83,9 +82,16 @@ class AddFeedDialog extends HookConsumerWidget {
TextButton(
onPressed: () {
if (formKey.currentState?.validate() == true) {
FeedCreateRoute(
feedId: uri_parser.tryParseUrl(textController.text)!,
).pushReplacement(context);
final feedId = parseValidatedUrl(
textController.text,
eagerParsing: false,
onlyHttpProtocol: true,
);
if (feedId == null) {
return;
}
FeedCreateRoute(feedId: feedId).pushReplacement(context);
}
},
child: const Text('Add'),
@@ -34,7 +34,6 @@ import 'package:weblibre/features/web_feed/presentation/widgets/tag_field.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
import 'package:weblibre/utils/form_validators.dart';
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
enum _DialogMode { create, edit }
@@ -156,19 +155,22 @@ class _FeedEditContent extends HookConsumerWidget {
onPressed: () async {
if (formKey.currentState?.validate() ?? false) {
final feedData = FeedData(
url: uri_parser.tryParseUrl(
url: parseValidatedUrl(
urlTextController.text,
eagerParsing: true,
eagerParsing: false,
onlyHttpProtocol: true,
)!,
authors: initialFeed.authors,
description: descriptionTextController.text.whenNotEmpty,
icon: uri_parser.tryParseUrl(
icon: parseValidatedUrl(
iconUrlTextController.text,
eagerParsing: true,
eagerParsing: false,
onlyHttpProtocol: true,
),
siteLink: uri_parser.tryParseUrl(
siteLink: parseValidatedUrl(
siteLinkTextController.text,
eagerParsing: true,
eagerParsing: false,
onlyHttpProtocol: true,
),
tags: tags.value.map((tag) => FeedCategory(id: tag)).toList(),
title: titleTextController.text.whenNotEmpty,