From 3eabfa94783003f92eddd4e3e79eec70e896ec68 Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Thu, 13 Apr 2017 11:40:38 -0300 Subject: [PATCH 1/8] Add support for push scheduling Add a configuration flag on the server to handle the availability of push scheduling. --- src/Config.js | 1 + src/Controllers/PushController.js | 34 ++++++++++++++++++++++++++++++- src/ParseServer.js | 5 ++++- src/Routers/FeaturesRouter.js | 2 +- src/StatusHandler.js | 6 ++++-- 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/Config.js b/src/Config.js index a1a51abc08..8958a079c5 100644 --- a/src/Config.js +++ b/src/Config.js @@ -61,6 +61,7 @@ export class Config { this.pushControllerQueue = cacheInfo.pushControllerQueue; this.pushWorker = cacheInfo.pushWorker; this.hasPushSupport = cacheInfo.hasPushSupport; + this.hasPushScheduledSupport = cacheInfo.hasPushScheduledSupport; this.loggerController = cacheInfo.loggerController; this.userController = cacheInfo.userController; this.authDataManager = cacheInfo.authDataManager; diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index a390d31ec5..255ef5bdeb 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -12,8 +12,9 @@ export class PushController { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Missing push configuration'); } - // Replace the expiration_time with a valid Unix epoch milliseconds time + // Replace the expiration_time and push_time with a valid Unix epoch milliseconds time body['expiration_time'] = PushController.getExpirationTime(body); + body['push_time'] = PushController.getPushTime(body); // TODO: If the req can pass the checking, we return immediately instead of waiting // pushes to be sent. We probably change this behaviour in the future. let badgeUpdate = () => { @@ -49,6 +50,9 @@ export class PushController { onPushStatusSaved(pushStatus.objectId); return badgeUpdate(); }).then(() => { + if (body.push_time) { + return Promise.resolve(); + } return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus); }).catch((err) => { return pushStatus.fail(err).then(() => { @@ -84,6 +88,34 @@ export class PushController { } return expirationTime.valueOf(); } + + /** + * Get push time from the request body. + * @param {Object} request A request object + * @returns {Number|undefined} The push time if it exists in the request + */ + static getPushTime(body = {}) { + var hasPushTime = !!body['push_time']; + if (!hasPushTime) { + return; + } + var pushTimeParam = body['push_time']; + var pushTime; + if (typeof pushTimeParam === 'number') { + pushTime = new Date(pushTimeParam * 1000); + } else if (typeof pushTimeParam === 'string') { + pushTime = new Date(pushTimeParam); + } else { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + body['push_time'] + ' is not valid time.'); + } + // Check pushTime is valid or not, if it is not valid, pushTime is NaN + if (!isFinite(pushTime)) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + body['push_time'] + ' is not valid time.'); + } + return pushTime.valueOf(); + } } export default PushController; diff --git a/src/ParseServer.js b/src/ParseServer.js index 11cb38f56e..79c388bfee 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -95,6 +95,7 @@ class ParseServer { analyticsAdapter, filesAdapter, push, + scheduledPush = false, loggerAdapter, jsonLogs = defaults.jsonLogs, logsFolder = defaults.logsFolder, @@ -182,6 +183,7 @@ class ParseServer { const pushController = new PushController(); const hasPushSupport = pushAdapter && push; + const hasPushScheduledSupport = pushAdapter && push && scheduledPush; const { disablePushWorker @@ -259,7 +261,8 @@ class ParseServer { userSensitiveFields, pushWorker, pushControllerQueue, - hasPushSupport + hasPushSupport, + hasPushScheduledSupport }); Config.validate(AppCache.get(appId)); diff --git a/src/Routers/FeaturesRouter.js b/src/Routers/FeaturesRouter.js index 74e45eb141..a82488ab4c 100644 --- a/src/Routers/FeaturesRouter.js +++ b/src/Routers/FeaturesRouter.js @@ -30,7 +30,7 @@ export class FeaturesRouter extends PromiseRouter { }, push: { immediatePush: req.config.hasPushSupport, - scheduledPush: false, + scheduledPush: req.config.hasPushScheduledSupport, storedPushData: req.config.hasPushSupport, pushAudiences: false, }, diff --git a/src/StatusHandler.js b/src/StatusHandler.js index c92933d992..1d46bcbe89 100644 --- a/src/StatusHandler.js +++ b/src/StatusHandler.js @@ -110,6 +110,8 @@ export function pushStatusHandler(config, objectId = newObjectId()) { const handler = statusHandler(PUSH_STATUS_COLLECTION, database); const setInitial = function(body = {}, where, options = {source: 'rest'}) { const now = new Date(); + const pushTime = body.push_time || new Date(); + const status = body.push_time ? "scheduled" : "pending"; const data = body.data || {}; const payloadString = JSON.stringify(data); let pushHash; @@ -123,13 +125,13 @@ export function pushStatusHandler(config, objectId = newObjectId()) { const object = { objectId, createdAt: now, - pushTime: now.toISOString(), + pushTime: pushTime.toISOString(), query: JSON.stringify(where), payload: payloadString, source: options.source, title: options.title, expiry: body.expiration_time, - status: "pending", + status: status, numSent: 0, pushHash, // lockdown! From 2f8edd8e2b5e0d24159dfcc4b27be6dbcf798de8 Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Thu, 13 Apr 2017 12:07:10 -0300 Subject: [PATCH 2/8] Update push controller to skip sending only if scheduling is configured Only skip push sending if scheduling is configured --- src/Controllers/PushController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 255ef5bdeb..03e926dbfc 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -50,7 +50,7 @@ export class PushController { onPushStatusSaved(pushStatus.objectId); return badgeUpdate(); }).then(() => { - if (body.push_time) { + if (body.push_time && config.hasPushScheduledSupport) { return Promise.resolve(); } return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus); From 5e09c688e72bfefa448775ca64a7406301549f3d Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Thu, 13 Apr 2017 16:19:25 -0300 Subject: [PATCH 3/8] Update bad conventions --- src/Controllers/PushController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 03e926dbfc..20c363ff55 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -67,7 +67,7 @@ export class PushController { * @returns {Number|undefined} The expiration time if it exists in the request */ static getExpirationTime(body = {}) { - var hasExpirationTime = !!body['expiration_time']; + var hasExpirationTime = body.hasOwnProperty('expiration_time'); if (!hasExpirationTime) { return; } @@ -95,7 +95,7 @@ export class PushController { * @returns {Number|undefined} The push time if it exists in the request */ static getPushTime(body = {}) { - var hasPushTime = !!body['push_time']; + var hasPushTime = body.hasOwnProperty('push_time'); if (!hasPushTime) { return; } From 919cfc665729df6e71b50af96b056373338da212 Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Thu, 13 Apr 2017 16:24:18 -0300 Subject: [PATCH 4/8] Add CLI definitions for push scheduling --- src/cli/definitions/parse-server.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js index 16e857a860..81c2cb428d 100644 --- a/src/cli/definitions/parse-server.js +++ b/src/cli/definitions/parse-server.js @@ -81,6 +81,11 @@ export default { help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push", action: objectParser }, + "scheduledPush": { + env: "PARSE_SERVER_SCHEDULED_PUSH", + help: "Configuration for push scheduling. Defaults to false.", + action: booleanParser + }, "oauth": { env: "PARSE_SERVER_OAUTH_PROVIDERS", help: "[DEPRECATED (use auth option)] Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth", From 7c13a548b6a030863a4bc91b6b9167550330403e Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sat, 15 Apr 2017 12:16:47 -0400 Subject: [PATCH 5/8] Adds tests for pushTime --- spec/PushController.spec.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index e0f51e6fe8..d70255b79a 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -531,5 +531,21 @@ describe('PushController', () => { it('should flatten', () => { var res = StatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]]) expect(res).toEqual([1,2,3,4,5,6]); - }) + }); + + it('properly transforms push time', () => { + expect(PushController.getPushTime()).toBe(undefined); + expect(PushController.getPushTime({ + 'push_time': 1000 + })).toBe(new Date(1000 * 1000).valueOf()); + expect(PushController.getPushTime({ + 'push_time': '2017-01-01' + })).toBe(new Date('2017-01-01').valueOf()); + expect(() => {PushController.getPushTime({ + 'push_time': 'gibberish-time' + })}).toThrow(); + expect(() => {PushController.getPushTime({ + 'push_time': Number.NaN + })}).toThrow(); + }); }); From 809fba4da4e789b4c0a72cc4d903211c3395ba2f Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sat, 15 Apr 2017 12:59:59 -0400 Subject: [PATCH 6/8] Adds test for scheduling --- spec/PushController.spec.js | 113 +++++++++++++++++++++++++++++- src/Controllers/PushController.js | 2 +- src/StatusHandler.js | 14 +++- 3 files changed, 124 insertions(+), 5 deletions(-) diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index d70255b79a..6859d8d136 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -537,10 +537,10 @@ describe('PushController', () => { expect(PushController.getPushTime()).toBe(undefined); expect(PushController.getPushTime({ 'push_time': 1000 - })).toBe(new Date(1000 * 1000).valueOf()); + })).toEqual(new Date(1000 * 1000)); expect(PushController.getPushTime({ 'push_time': '2017-01-01' - })).toBe(new Date('2017-01-01').valueOf()); + })).toEqual(new Date('2017-01-01')); expect(() => {PushController.getPushTime({ 'push_time': 'gibberish-time' })}).toThrow(); @@ -548,4 +548,113 @@ describe('PushController', () => { 'push_time': Number.NaN })}).toThrow(); }); + + it('should not schedule push when not configured', (done) => { + var config = new Config(Parse.applicationId); + var auth = { + isMaster: true + } + var pushAdapter = { + send: function(body, installations) { + return successfulTransmissions(body, installations); + }, + getValidPushTypes: function() { + return ["ios"]; + } + } + + var pushController = new PushController(); + const payload = { + data: { + alert: 'hello', + }, + push_time: new Date().getTime() + } + + var installations = []; + while(installations.length != 10) { + const installation = new Parse.Object("_Installation"); + installation.set("installationId", "installation_" + installations.length); + installation.set("deviceToken","device_token_" + installations.length) + installation.set("badge", installations.length); + installation.set("originalBadge", installations.length); + installation.set("deviceType", "ios"); + installations.push(installation); + } + + reconfigureServer({ + push: { adapter: pushAdapter } + }).then(() => { + return Parse.Object.saveAll(installations).then(() => { + return pushController.sendPush(payload, {}, config, auth); + }); + }).then(() => { + const query = new Parse.Query('_PushStatus'); + return query.find({useMasterKey: true}).then((results) => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('status')).toBe('succeeded'); + done(); + }); + }).catch((err) => { + console.error(err); + fail('should not fail'); + done(); + }); + }); + + it('should not schedule push when configured', (done) => { + var auth = { + isMaster: true + } + var pushAdapter = { + send: function(body, installations) { + return successfulTransmissions(body, installations); + }, + getValidPushTypes: function() { + return ["ios"]; + } + } + + var pushController = new PushController(); + const payload = { + data: { + alert: 'hello', + }, + push_time: new Date().getTime() / 1000 + } + + var installations = []; + while(installations.length != 10) { + const installation = new Parse.Object("_Installation"); + installation.set("installationId", "installation_" + installations.length); + installation.set("deviceToken","device_token_" + installations.length) + installation.set("badge", installations.length); + installation.set("originalBadge", installations.length); + installation.set("deviceType", "ios"); + installations.push(installation); + } + + reconfigureServer({ + push: { adapter: pushAdapter }, + scheduledPush: true + }).then(() => { + var config = new Config(Parse.applicationId); + return Parse.Object.saveAll(installations).then(() => { + return pushController.sendPush(payload, {}, config, auth); + }); + }).then(() => { + const query = new Parse.Query('_PushStatus'); + return query.find({useMasterKey: true}).then((results) => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('status')).toBe('scheduled'); + done(); + }); + }).catch((err) => { + console.error(err); + fail('should not fail'); + done(); + }); + }); }); diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 20c363ff55..887dc4968c 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -114,7 +114,7 @@ export class PushController { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, body['push_time'] + ' is not valid time.'); } - return pushTime.valueOf(); + return pushTime; } } diff --git a/src/StatusHandler.js b/src/StatusHandler.js index 1d46bcbe89..11c3230e37 100644 --- a/src/StatusHandler.js +++ b/src/StatusHandler.js @@ -110,8 +110,18 @@ export function pushStatusHandler(config, objectId = newObjectId()) { const handler = statusHandler(PUSH_STATUS_COLLECTION, database); const setInitial = function(body = {}, where, options = {source: 'rest'}) { const now = new Date(); - const pushTime = body.push_time || new Date(); - const status = body.push_time ? "scheduled" : "pending"; + let pushTime = new Date(); + let status = 'pending'; + if (body.push_time) { + if (config.hasPushScheduledSupport) { + pushTime = body.push_time; + status = 'scheduled'; + } else { + logger.warn('Trying to schedule a push while server is not configured.'); + logger.warn('Push will be sent immediately'); + } + } + const data = body.data || {}; const payloadString = JSON.stringify(data); let pushHash; From 89c966d8ca8a3f82eb0bf14de51f195bb680ad45 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sat, 15 Apr 2017 13:24:57 -0400 Subject: [PATCH 7/8] nits --- src/Controllers/PushController.js | 4 ++-- src/StatusHandler.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 887dc4968c..4e25b7ad52 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -13,8 +13,8 @@ export class PushController { 'Missing push configuration'); } // Replace the expiration_time and push_time with a valid Unix epoch milliseconds time - body['expiration_time'] = PushController.getExpirationTime(body); - body['push_time'] = PushController.getPushTime(body); + body.expiration_time = PushController.getExpirationTime(body); + body.push_time = PushController.getPushTime(body); // TODO: If the req can pass the checking, we return immediately instead of waiting // pushes to be sent. We probably change this behaviour in the future. let badgeUpdate = () => { diff --git a/src/StatusHandler.js b/src/StatusHandler.js index 11c3230e37..9a7896ca9e 100644 --- a/src/StatusHandler.js +++ b/src/StatusHandler.js @@ -112,7 +112,7 @@ export function pushStatusHandler(config, objectId = newObjectId()) { const now = new Date(); let pushTime = new Date(); let status = 'pending'; - if (body.push_time) { + if (body.hasOwnProperty('push_time')) { if (config.hasPushScheduledSupport) { pushTime = body.push_time; status = 'scheduled'; From 22dc33648276360d0f0ac7c910f2db377a46c9be Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sat, 15 Apr 2017 14:42:56 -0400 Subject: [PATCH 8/8] Test for not scheduled --- spec/PushController.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 6859d8d136..1eb1607daf 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -593,7 +593,7 @@ describe('PushController', () => { return query.find({useMasterKey: true}).then((results) => { expect(results.length).toBe(1); const pushStatus = results[0]; - expect(pushStatus.get('status')).toBe('succeeded'); + expect(pushStatus.get('status')).not.toBe('scheduled'); done(); }); }).catch((err) => {