diff --git a/app/lib/common/utils/utils.dart b/app/lib/common/utils/utils.dart index c2229c2aff25..135a78f37bea 100644 --- a/app/lib/common/utils/utils.dart +++ b/app/lib/common/utils/utils.dart @@ -36,9 +36,14 @@ final idMatrixRegexp = RegExp( r'matrix:roomid/(?[^?]+)(\?via=(?[^&]+))?(&via=(?[^&]+))?(&via=(?[^&]+))?', ); -final urlValidatorRegexp = RegExp( - r'^[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_+.~#?&/=]*$', -); +bool isValidUrl(String url) { +// Regular expression to validate URLs + final RegExp urlPattern = RegExp( + r"^([a-zA-Z][a-zA-Z\d+\-.]*):\/\/([\w\-])+\.{1}([a-zA-Z]{2,63})([\w\-\._~:/?#[\]@!\$&'()*+,;=.]+)?$", + caseSensitive: false, + ); + return urlPattern.hasMatch(url); +} /// Get provider right from the context no matter where we are extension Context on BuildContext { diff --git a/app/lib/features/attachments/actions/add_edit_link_bottom_sheet.dart b/app/lib/features/attachments/actions/add_edit_link_bottom_sheet.dart index 8cd90fa50526..ab1e9d103c38 100644 --- a/app/lib/features/attachments/actions/add_edit_link_bottom_sheet.dart +++ b/app/lib/features/attachments/actions/add_edit_link_bottom_sheet.dart @@ -50,13 +50,11 @@ class _PinLinkBottomSheet extends ConsumerState { final _formKey = GlobalKey(); final _titleController = TextEditingController(); final _linkController = TextEditingController(); - final prefixText = 'https://'; @override void initState() { super.initState(); _titleController.text = widget.pinTitle ?? ''; - _linkController.text = (widget.pinLink ?? '').replaceAll(prefixText, ''); } @override @@ -105,7 +103,7 @@ class _PinLinkBottomSheet extends ConsumerState { // Need to update change of tile widget.onSave( _titleController.text.trim(), - '$prefixText${_linkController.text.trim()}', + _linkController.text.trim(), ); }, child: Text(L10n.of(context).save), @@ -150,11 +148,10 @@ class _PinLinkBottomSheet extends ConsumerState { controller: _linkController, minLines: 1, maxLines: 1, - decoration: InputDecoration(prefixText: prefixText), validator: (value) { if (value == null || value.trim().isEmpty) { return L10n.of(context).pleaseEnterALink; - } else if (!urlValidatorRegexp.hasMatch(value)) { + } else if (!isValidUrl(value)) { return L10n.of(context).pleaseEnterAValidLink; } return null; diff --git a/app/test/features/general/url_validation_test.dart b/app/test/features/general/url_validation_test.dart new file mode 100644 index 000000000000..e030ea51ea1f --- /dev/null +++ b/app/test/features/general/url_validation_test.dart @@ -0,0 +1,42 @@ +import 'package:acter/common/utils/utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('URL Validation Tests', () { + test('Valid URL with http', () { + expect(isValidUrl('http://example.com'), true); + }); + + test('Valid URL with https', () { + expect(isValidUrl('https://example.com'), true); + }); + + test('Valid URL with custom schema', () { + expect(isValidUrl('custom://example.com'), true); + }); + + test('Valid URL with subdomains', () { + expect(isValidUrl('https://sub.example.com'), true); + }); + + test('Valid URL with query parameters', () { + expect(isValidUrl('https://example.com?query=flutter'), true); + }); + + test('Invalid URL without scheme', () { + expect(isValidUrl('example.com'), false); + }); + + test('Invalid URL with invalid characters', () { + expect(isValidUrl('https://example!.com'), false); + }); + + test('Invalid URL with missing domain', () { + expect(isValidUrl('https://.com'), false); + }); + + test('Empty URL string', () { + expect(isValidUrl(''), false); + }); + }); +}