diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index e0f51e6fe8..1eb1607daf 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -531,5 +531,130 @@ 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 + })).toEqual(new Date(1000 * 1000)); + expect(PushController.getPushTime({ + 'push_time': '2017-01-01' + })).toEqual(new Date('2017-01-01')); + expect(() => {PushController.getPushTime({ + 'push_time': 'gibberish-time' + })}).toThrow(); + expect(() => {PushController.getPushTime({ + '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')).not.toBe('scheduled'); + 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/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..4e25b7ad52 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 - body['expiration_time'] = PushController.getExpirationTime(body); + // 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 && config.hasPushScheduledSupport) { + return Promise.resolve(); + } return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus); }).catch((err) => { return pushStatus.fail(err).then(() => { @@ -63,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; } @@ -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.hasOwnProperty('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; + } } 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..9a7896ca9e 100644 --- a/src/StatusHandler.js +++ b/src/StatusHandler.js @@ -110,6 +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(); + let pushTime = new Date(); + let status = 'pending'; + if (body.hasOwnProperty('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; @@ -123,13 +135,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! 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",