diff --git a/packages/dart/lib/src/network/parse_query.dart b/packages/dart/lib/src/network/parse_query.dart index 311bf461..7fe0b4d7 100644 --- a/packages/dart/lib/src/network/parse_query.dart +++ b/packages/dart/lib/src/network/parse_query.dart @@ -129,7 +129,7 @@ class QueryBuilder { String prefix, { bool caseSensitive = false, }) { - prefix = Uri.encodeComponent(prefix); + prefix = _encodeStringElement(prefix); if (caseSensitive) { queries.add( @@ -155,7 +155,7 @@ class QueryBuilder { String prefix, { bool caseSensitive = false, }) { - prefix = Uri.encodeComponent(prefix); + prefix = _encodeStringElement(prefix); if (caseSensitive) { queries.add( @@ -178,7 +178,7 @@ class QueryBuilder { /// to be equal to the provided [value] void whereEqualTo(String column, dynamic value) { if (value is String) { - value = Uri.encodeComponent(value); + value = _encodeStringElement(value); } queries.add( @@ -237,7 +237,7 @@ class QueryBuilder { /// to be not equal to the provided [value] void whereNotEqualTo(String column, dynamic value) { if (value is String) { - value = Uri.encodeComponent(value); + value = _encodeStringElement(value); } queries.add( @@ -252,7 +252,7 @@ class QueryBuilder { void whereContainedIn(String column, List value) { queries.add( _buildQueryWithColumnValueAndOperator( - MapEntry(column, value), + MapEntry(column, _encodeStringElements(value)), '\$in', ), ); @@ -262,7 +262,7 @@ class QueryBuilder { void whereNotContainedIn(String column, List value) { queries.add( _buildQueryWithColumnValueAndOperator( - MapEntry(column, value), + MapEntry(column, _encodeStringElements(value)), '\$nin', ), ); @@ -312,12 +312,30 @@ class QueryBuilder { void whereArrayContainsAll(String column, List value) { queries.add( _buildQueryWithColumnValueAndOperator( - MapEntry(column, value), + MapEntry(column, _encodeStringElements(value)), '\$all', ), ); } + /// JSON-escapes [value] and then percent-encodes the escaped content. The + /// JSON escaping keeps `"`, `\` and newlines valid inside the surrounding + /// string literal once the server URL-decodes the query; the percent + /// encoding keeps `&`, `=`, `+` and `#` from being chewed up by querystring + /// parsing on the way in. + String _encodeStringElement(String value) { + final String jsonString = jsonEncode(value); + return Uri.encodeComponent(jsonString.substring(1, jsonString.length - 1)); + } + + /// Runs [_encodeStringElement] on each String in [value]; other elements + /// are passed through unchanged. + List _encodeStringElements(List value) { + return value + .map((dynamic e) => e is String ? _encodeStringElement(e) : e) + .toList(); + } + /// Returns an object where the [String] column has a regEx performed on, /// this can include ^StringsWith, or ^EndsWith. This can be manipulated to the users desire void regEx(String column, String value) { @@ -336,7 +354,7 @@ class QueryBuilder { String substring, { bool caseSensitive = false, }) { - substring = Uri.encodeComponent(substring); + substring = _encodeStringElement(substring); if (caseSensitive) { queries.add( @@ -365,7 +383,7 @@ class QueryBuilder { bool orderByScore = true, bool diacriticSensitive = false, }) { - searchTerm = Uri.encodeComponent(searchTerm); + searchTerm = _encodeStringElement(searchTerm); queries.add( MapEntry( diff --git a/packages/dart/test/src/network/parse_query_test.dart b/packages/dart/test/src/network/parse_query_test.dart index 5234ae4b..6136f52d 100644 --- a/packages/dart/test/src/network/parse_query_test.dart +++ b/packages/dart/test/src/network/parse_query_test.dart @@ -695,5 +695,94 @@ void main() { expect(queryString, equals(expectedQueryString.toString())); }); + + test( + 'list-based where methods encode special characters in String elements', + () { + final queryBuilder = QueryBuilder.name('Diet_Plans'); + + queryBuilder.whereContainedIn('topics', [ + "RFI's & Change Orders", + 'Schedule (Impacts, Delays, Inspections)', + ]); + queryBuilder.whereNotContainedIn('excluded', ['a=b', 'c+d']); + queryBuilder.whereArrayContainsAll('tags', ['x#y', 'z&w']); + + final queryString = queryBuilder.buildQuery(); + const encodedAmp = '%26'; // & + const encodedEq = '%3D'; // = + const encodedPlus = '%2B'; // + + const encodedHash = '%23'; // # + + expect(queryString, contains(encodedAmp)); + expect(queryString, contains(encodedEq)); + expect(queryString, contains(encodedPlus)); + expect(queryString, contains(encodedHash)); + + // Raw delimiters would break querystring parsing on the server, so + // verify they are not present in the encoded output. + expect(queryString.contains('& Change Orders'), isFalse); + expect(queryString.contains('a=b'), isFalse); + expect(queryString.contains('c+d'), isFalse); + expect(queryString.contains('x#y'), isFalse); + }, + ); + + test( + 'list-based where methods JSON-escape quotes and backslashes in String elements', + () { + final queryBuilder = QueryBuilder.name('Diet_Plans'); + + queryBuilder.whereContainedIn('topics', [ + 'He said "hi"', + r'C:\path', + ]); + + final queryString = queryBuilder.buildQuery(); + + // Encoded form of `\"` and `\\`: the backslash must survive URL decoding + // so the server sees valid JSON (e.g. "He said \"hi\""). + expect(queryString, contains('%5C%22')); + expect(queryString, contains('%5C%5C')); + + // Unescaped `"` and `\` inside the string values would corrupt the JSON. + expect(queryString.contains('"He said "hi""'), isFalse); + expect(queryString.contains(r'C:\path'), isFalse); + }, + ); + + test('list-based where methods leave non-String elements untouched', () { + final queryBuilder = QueryBuilder.name('Diet_Plans'); + + queryBuilder.whereContainedIn('nums', [1, 2, 3]); + + final queryString = queryBuilder.buildQuery(); + expect(queryString, contains('"\$in":[1,2,3]')); + }); + + test( + 'scalar where methods JSON-escape quotes and backslashes in String values', + () { + final queryBuilder = QueryBuilder.name('Diet_Plans'); + + queryBuilder.whereEqualTo('title', 'He said "hi"'); + queryBuilder.whereNotEqualTo('note', r'C:\path'); + queryBuilder.whereStartsWith('name', 'quote"prefix'); + queryBuilder.whereContains('body', r'back\slash'); + + final queryString = queryBuilder.buildQuery(); + + // `"` and `\` must come through percent-encoded as `\"` / `\\` so the + // JSON stays valid after the server URL-decodes the query. + expect(queryString, contains('%5C%22')); + expect(queryString, contains('%5C%5C')); + + // Raw `"` and `\` inside the values would break JSON parsing. + expect(queryString.contains('"He said "hi""'), isFalse); + expect(queryString.contains(r'C:\path'), isFalse); + expect(queryString.contains('quote"prefix'), isFalse); + expect(queryString.contains(r'back\slash'), isFalse); + }, + ); }); }