diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 96b8f2459f..154de89361 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -96,6 +96,30 @@ describe('AudiencesRouter', () => { }); }); + it('uses find condition from a where string in request.body', async () => { + const config = Config.get('test'); + await rest.create(config, auth.master(config), '_Audience', { + name: 'Android Users', + query: '{ "test": "android" }', + }); + await rest.create(config, auth.master(config), '_Audience', { + name: 'Iphone Users', + query: '{ "test": "ios" }', + }); + + const router = new AudiencesRouter(); + const res = await router.handleFind({ + config: config, + auth: auth.master(config), + body: { where: JSON.stringify({ query: '{ "test": "android" }' }) }, + query: {}, + info: {}, + }); + + expect(res.response.results.length).toEqual(1); + expect(res.response.results[0].name).toEqual('Android Users'); + }); + it('query installations with limit = 0', done => { const config = Config.get('test'); const androidAudienceRequest = { diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 1ccad5013d..4d3df95d00 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -1,6 +1,7 @@ const auth = require('../lib/Auth'); const Config = require('../lib/Config'); const rest = require('../lib/rest'); +const httpRequest = require('../lib/request'); const InstallationsRouter = require('../lib/Routers/InstallationsRouter').InstallationsRouter; describe('InstallationsRouter', () => { @@ -244,4 +245,79 @@ describe('InstallationsRouter', () => { done(); }); }); + + it('uses find condition from a where string in request.body', async () => { + const config = Config.get('test'); + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', + }); + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', + }); + + const router = new InstallationsRouter(); + const res = await router.handleFind({ + config: config, + auth: auth.master(config), + body: { where: JSON.stringify({ deviceType: 'android' }) }, + query: {}, + info: {}, + }); + + expect(res.response.results.length).toEqual(1); + expect(res.response.results[0].deviceType).toEqual('android'); + }); + + it('rejects an invalid where string in request.body', async () => { + const config = Config.get('test'); + const router = new InstallationsRouter(); + let error; + try { + await router.handleFind({ + config: config, + auth: auth.master(config), + body: { where: 'not json' }, + query: {}, + info: {}, + }); + fail('find should have been rejected'); + return; + } catch (e) { + error = e; + } + expect(error.code).toEqual(Parse.Error.INVALID_JSON); + expect(error.message).toEqual('where parameter is not valid JSON'); + }); + + it('finds installations when the client sends the find as POST with _method=GET', async () => { + const config = Config.get('test'); + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', + }); + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', + }); + + // A client that exceeds the maximum URL length sends the find as a POST + // with a urlencoded body, so `where` arrives as a string. + const response = await httpRequest({ + method: 'POST', + url: 'http://localhost:8378/1/installations', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `_method=GET&where=${encodeURIComponent( + JSON.stringify({ installationId: { $in: ['12345678-abcd-abcd-abcd-123456789abc'] } }) + )}`, + }); + + expect(response.data.results.length).toEqual(1); + expect(response.data.results[0].deviceType).toEqual('android'); + }); }); diff --git a/src/Routers/AudiencesRouter.js b/src/Routers/AudiencesRouter.js index dde95345da..089d0e0446 100644 --- a/src/Routers/AudiencesRouter.js +++ b/src/Routers/AudiencesRouter.js @@ -10,6 +10,7 @@ export class AudiencesRouter extends ClassesRouter { handleFind(req) { const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query)); const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); + ClassesRouter.decodeWhere(body); return rest .find( diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index b527131a39..fddd380f62 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -29,13 +29,7 @@ export class ClassesRouter extends PromiseRouter { if (body.redirectClassNameForKey) { options.redirectClassNameForKey = String(body.redirectClassNameForKey); } - if (typeof body.where === 'string') { - try { - body.where = JSON.parse(body.where); - } catch { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'where parameter is not valid JSON'); - } - } + ClassesRouter.decodeWhere(body); return rest .find( req.config, @@ -157,6 +151,28 @@ export class ClassesRouter extends PromiseRouter { return json; } + /** + * Decodes a `where` that arrives as a JSON string instead of an object. + * + * A client that exceeds the maximum URL length sends a find as + * `POST` + `_method=GET` with a urlencoded body, so `where` reaches the + * router as a string rather than being decoded by `JSONFromQuery`. Every + * router that serves a find has to decode it, otherwise the string is passed + * to the query layer and iterated character by character. + * + * Mutates `body` in place and returns the decoded `where`. + */ + static decodeWhere(body) { + if (typeof body.where === 'string') { + try { + body.where = JSON.parse(body.where); + } catch { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'where parameter is not valid JSON'); + } + } + return body.where; + } + static optionsFromBody(body, defaultLimit) { const allowConstraints = [ 'skip', diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index 91861a7ba2..fe99cba7a7 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -12,6 +12,7 @@ export class InstallationsRouter extends ClassesRouter { handleFind(req) { const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query)); const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); + ClassesRouter.decodeWhere(body); return rest .find( req.config,