|
| 1 | +import 'package:qs_dart/qs_dart.dart' as qs; |
| 2 | +import 'package:validators/validators.dart' as validators; |
| 3 | + |
1 | 4 | /// Takes a string and appends [parameters] as query parameters of [url]. |
2 | 5 | /// |
3 | | -/// It validates the URL structure and properly encodes both keys and values |
4 | | -/// to prevent URL injection attacks. |
| 6 | +/// Throws [ArgumentError] if [url] is not a valid URL. |
5 | 7 | String buildUrlString(String url, Map<String, dynamic>? parameters) { |
6 | | - // Avoids unnecessary processing. |
7 | | - if (parameters == null) return url; |
8 | | - |
9 | | - // Check if there are parameters to add. |
10 | | - if (parameters.isNotEmpty) { |
11 | | - // Validate URL structure to prevent injection |
12 | | - // First check if it looks like a valid HTTP/HTTPS URL |
13 | | - if (!url.startsWith('http://') && !url.startsWith('https://')) { |
14 | | - throw ArgumentError( |
15 | | - 'Invalid URL structure: $url - must be a valid HTTP/HTTPS URL', |
16 | | - ); |
17 | | - } |
18 | | - |
19 | | - try { |
20 | | - final uri = Uri.parse(url); |
21 | | - // Additional validation: ensure it has a host |
22 | | - if (uri.host.isEmpty) { |
23 | | - throw ArgumentError( |
24 | | - 'Invalid URL structure: $url - must have a valid host', |
25 | | - ); |
26 | | - } |
27 | | - } catch (e) { |
28 | | - if (e is ArgumentError) { |
29 | | - rethrow; |
30 | | - } |
31 | | - throw ArgumentError('Invalid URL structure: $url'); |
32 | | - } |
| 8 | + late final Uri uri; |
33 | 9 |
|
34 | | - // Checks if the string url already has parameters. |
35 | | - if (url.contains("?")) { |
36 | | - url += "&"; |
37 | | - } else { |
38 | | - url += "?"; |
| 10 | + try { |
| 11 | + if (!validators.isURL(url)) { |
| 12 | + throw FormatException('Invalid URL format'); |
39 | 13 | } |
40 | | - |
41 | | - // Concat every parameter to the string url with proper encoding |
42 | | - parameters.forEach((key, value) { |
43 | | - // Encode the key to prevent injection |
44 | | - final encodedKey = Uri.encodeQueryComponent(key); |
45 | | - |
46 | | - if (value is List) { |
47 | | - if (value is List<String>) { |
48 | | - for (String singleValue in value) { |
49 | | - url += "$encodedKey=${Uri.encodeQueryComponent(singleValue)}&"; |
50 | | - } |
51 | | - } else { |
52 | | - for (dynamic singleValue in value) { |
53 | | - url += |
54 | | - "$encodedKey=${Uri.encodeQueryComponent(singleValue.toString())}&"; |
55 | | - } |
56 | | - } |
57 | | - } else if (value is String) { |
58 | | - url += "$encodedKey=${Uri.encodeQueryComponent(value)}&"; |
59 | | - } else { |
60 | | - url += "$encodedKey=${Uri.encodeQueryComponent(value.toString())}&"; |
61 | | - } |
62 | | - }); |
63 | | - |
64 | | - // Remove last '&' character. |
65 | | - url = url.substring(0, url.length - 1); |
| 14 | + uri = Uri.parse(url); |
| 15 | + } on FormatException { |
| 16 | + throw ArgumentError.value(url, 'url', 'Must be a valid URL'); |
66 | 17 | } |
67 | 18 |
|
68 | | - return url; |
| 19 | + return parameters?.isNotEmpty ?? false |
| 20 | + ? uri |
| 21 | + .replace( |
| 22 | + query: qs.encode( |
| 23 | + <String, dynamic>{ |
| 24 | + ...uri.queryParametersAll, |
| 25 | + ...?parameters, |
| 26 | + }, |
| 27 | + qs.EncodeOptions( |
| 28 | + listFormat: qs.ListFormat.repeat, |
| 29 | + skipNulls: false, |
| 30 | + strictNullHandling: false, |
| 31 | + ), |
| 32 | + ), |
| 33 | + queryParameters: null) |
| 34 | + .toString() |
| 35 | + : url; |
69 | 36 | } |
0 commit comments