Skip to content

Commit a7d50cf

Browse files
committed
fix: Special characters in list-based where constraints break URL parsing
`whereContainedIn`, `whereNotContainedIn`, and `whereArrayContainsAll` embed String elements into the `where={...}` querystring without URL encoding. `Uri(query: ...)` leaves `&`, `=`, `+`, and `#` as valid query sub-delimiters, so an element like "Peanut Butter & Jelly" causes the server to split the querystring at the literal `&` and reject the request with `code: 102 Invalid parameter for query`. Apply `Uri.encodeComponent` to String elements at constraint-build time, following the pattern introduced in #866 for the scalar `whereEqualTo` / `whereContains` family. Non-String elements (numbers, pointers, etc.) are left untouched.
1 parent 785f76b commit a7d50cf

2 files changed

Lines changed: 60 additions & 3 deletions

File tree

packages/dart/lib/src/network/parse_query.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ class QueryBuilder<T extends ParseObject> {
252252
void whereContainedIn(String column, List<dynamic> value) {
253253
queries.add(
254254
_buildQueryWithColumnValueAndOperator(
255-
MapEntry<String, dynamic>(column, value),
255+
MapEntry<String, dynamic>(column, _encodeStringElements(value)),
256256
'\$in',
257257
),
258258
);
@@ -262,7 +262,7 @@ class QueryBuilder<T extends ParseObject> {
262262
void whereNotContainedIn(String column, List<dynamic> value) {
263263
queries.add(
264264
_buildQueryWithColumnValueAndOperator(
265-
MapEntry<String, dynamic>(column, value),
265+
MapEntry<String, dynamic>(column, _encodeStringElements(value)),
266266
'\$nin',
267267
),
268268
);
@@ -312,12 +312,23 @@ class QueryBuilder<T extends ParseObject> {
312312
void whereArrayContainsAll(String column, List<dynamic> value) {
313313
queries.add(
314314
_buildQueryWithColumnValueAndOperator(
315-
MapEntry<String, dynamic>(column, value),
315+
MapEntry<String, dynamic>(column, _encodeStringElements(value)),
316316
'\$all',
317317
),
318318
);
319319
}
320320

321+
/// Percent-encodes String elements of [value] so that characters such as
322+
/// `&`, `=`, `+`, and `#` survive URL querystring parsing when the list is
323+
/// serialized into `where={...}` via [buildQuery]. Non-String elements are
324+
/// left untouched. Mirrors the per-argument encoding applied by the scalar
325+
/// [whereEqualTo] / [whereContains] family of methods.
326+
List<dynamic> _encodeStringElements(List<dynamic> value) {
327+
return value
328+
.map<dynamic>((dynamic e) => e is String ? Uri.encodeComponent(e) : e)
329+
.toList();
330+
}
331+
321332
/// Returns an object where the [String] column has a regEx performed on,
322333
/// this can include ^StringsWith, or ^EndsWith. This can be manipulated to the users desire
323334
void regEx(String column, String value) {

packages/dart/test/src/network/parse_query_test.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,5 +695,51 @@ void main() {
695695

696696
expect(queryString, equals(expectedQueryString.toString()));
697697
});
698+
699+
test(
700+
'list-based where methods encode special characters in String elements',
701+
() {
702+
// arrange
703+
final queryBuilder = QueryBuilder.name('Diet_Plans');
704+
705+
// act
706+
queryBuilder.whereContainedIn('topics', <dynamic>[
707+
"RFI's & Change Orders",
708+
'Schedule (Impacts, Delays, Inspections)',
709+
]);
710+
queryBuilder.whereNotContainedIn('excluded', <dynamic>['a=b', 'c+d']);
711+
queryBuilder.whereArrayContainsAll('tags', <dynamic>['x#y', 'z&w']);
712+
713+
// assert
714+
final queryString = queryBuilder.buildQuery();
715+
const encodedAmp = '%26'; // &
716+
const encodedEq = '%3D'; // =
717+
const encodedPlus = '%2B'; // +
718+
const encodedHash = '%23'; // #
719+
720+
expect(queryString, contains(encodedAmp));
721+
expect(queryString, contains(encodedEq));
722+
expect(queryString, contains(encodedPlus));
723+
expect(queryString, contains(encodedHash));
724+
725+
// Ensure the raw unencoded delimiters do NOT appear inside the JSON
726+
// values (they would break querystring parsing on the server).
727+
expect(queryString.contains('& Change Orders'), isFalse);
728+
expect(queryString.contains('a=b'), isFalse);
729+
expect(queryString.contains('c+d'), isFalse);
730+
expect(queryString.contains('x#y'), isFalse);
731+
});
732+
733+
test('list-based where methods leave non-String elements untouched', () {
734+
// arrange
735+
final queryBuilder = QueryBuilder.name('Diet_Plans');
736+
737+
// act
738+
queryBuilder.whereContainedIn('nums', <dynamic>[1, 2, 3]);
739+
740+
// assert
741+
final queryString = queryBuilder.buildQuery();
742+
expect(queryString, contains('"\$in":[1,2,3]'));
743+
});
698744
});
699745
}

0 commit comments

Comments
 (0)