import { Parse } from 'parse/node'; import RestQuery from '../RestQuery'; import RestWrite from '../RestWrite'; import { master } from '../Auth'; import { pushStatusHandler } from '../StatusHandler'; import { applyDeviceTokenExists } from '../Push/utils'; export class PushController { sendPush( body = {}, where = {}, config, auth, onPushStatusSaved = () => {}, now = new Date() ) { if (!config.hasPushSupport) { throw new Parse.Error( Parse.Error.PUSH_MISCONFIGURED, 'Missing push configuration' ); } // Replace the expiration_time and push_time with a valid Unix epoch milliseconds time body.expiration_time = PushController.getExpirationTime(body); body.expiration_interval = PushController.getExpirationInterval(body); if (body.expiration_time && body.expiration_interval) { throw new Parse.Error( Parse.Error.PUSH_MISCONFIGURED, 'Both expiration_time and expiration_interval cannot be set' ); } // Immediate push if (body.expiration_interval && !body.hasOwnProperty('push_time')) { const ttlMs = body.expiration_interval * 1000; body.expiration_time = new Date(now.valueOf() + ttlMs).valueOf(); } const pushTime = PushController.getPushTime(body); if (pushTime && pushTime.date !== 'undefined') { body['push_time'] = PushController.formatPushTime(pushTime); } // 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 = () => { return Promise.resolve(); }; if (body.data && body.data.badge) { const badge = body.data.badge; let restUpdate = {}; if (typeof badge == 'string' && badge.toLowerCase() === 'increment') { restUpdate = { badge: { __op: 'Increment', amount: 1 } }; } else if ( typeof badge == 'object' && typeof badge.__op == 'string' && badge.__op.toLowerCase() == 'increment' && Number(badge.amount) ) { restUpdate = { badge: { __op: 'Increment', amount: badge.amount } }; } else if (Number(badge)) { restUpdate = { badge: badge }; } else { throw "Invalid value for badge, expected number or 'Increment' or {increment: number}"; } // Force filtering on only valid device tokens const updateWhere = applyDeviceTokenExists(where); badgeUpdate = () => { // Build a real RestQuery so we can use it in RestWrite const restQuery = new RestQuery( config, master(config), '_Installation', updateWhere ); return restQuery.buildRestWhere().then(() => { const write = new RestWrite( config, master(config), '_Installation', restQuery.restWhere, restUpdate ); write.runOptions.many = true; return write.execute(); }); }; } const pushStatus = pushStatusHandler(config); return Promise.resolve() .then(() => { return pushStatus.setInitial(body, where); }) .then(() => { onPushStatusSaved(pushStatus.objectId); return badgeUpdate(); }) .then(() => { // Update audience lastUsed and timesUsed if (body.audience_id) { const audienceId = body.audience_id; var updateAudience = { lastUsed: { __type: 'Date', iso: new Date().toISOString() }, timesUsed: { __op: 'Increment', amount: 1 }, }; const write = new RestWrite( config, master(config), '_Audience', { objectId: audienceId }, updateAudience ); write.execute(); } // Don't wait for the audience update promise to resolve. return Promise.resolve(); }) .then(() => { if ( body.hasOwnProperty('push_time') && config.hasPushScheduledSupport ) { return Promise.resolve(); } return config.pushControllerQueue.enqueue( body, where, config, auth, pushStatus ); }) .catch(err => { return pushStatus.fail(err).then(() => { throw err; }); }); } /** * Get expiration time from the request body. * @param {Object} request A request object * @returns {Number|undefined} The expiration time if it exists in the request */ static getExpirationTime(body = {}) { var hasExpirationTime = body.hasOwnProperty('expiration_time'); if (!hasExpirationTime) { return; } var expirationTimeParam = body['expiration_time']; var expirationTime; if (typeof expirationTimeParam === 'number') { expirationTime = new Date(expirationTimeParam * 1000); } else if (typeof expirationTimeParam === 'string') { expirationTime = new Date(expirationTimeParam); } else { throw new Parse.Error( Parse.Error.PUSH_MISCONFIGURED, body['expiration_time'] + ' is not valid time.' ); } // Check expirationTime is valid or not, if it is not valid, expirationTime is NaN if (!isFinite(expirationTime)) { throw new Parse.Error( Parse.Error.PUSH_MISCONFIGURED, body['expiration_time'] + ' is not valid time.' ); } return expirationTime.valueOf(); } static getExpirationInterval(body = {}) { const hasExpirationInterval = body.hasOwnProperty('expiration_interval'); if (!hasExpirationInterval) { return; } var expirationIntervalParam = body['expiration_interval']; if ( typeof expirationIntervalParam !== 'number' || expirationIntervalParam <= 0 ) { throw new Parse.Error( Parse.Error.PUSH_MISCONFIGURED, `expiration_interval must be a number greater than 0` ); } return expirationIntervalParam; } /** * 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 date; var isLocalTime = true; if (typeof pushTimeParam === 'number') { date = new Date(pushTimeParam * 1000); } else if (typeof pushTimeParam === 'string') { isLocalTime = !PushController.pushTimeHasTimezoneComponent(pushTimeParam); date = 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(date)) { throw new Parse.Error( Parse.Error.PUSH_MISCONFIGURED, body['push_time'] + ' is not valid time.' ); } return { date, isLocalTime, }; } /** * Checks if a ISO8601 formatted date contains a timezone component * @param pushTimeParam {string} * @returns {boolean} */ static pushTimeHasTimezoneComponent(pushTimeParam: string): boolean { const offsetPattern = /(.+)([+-])\d\d:\d\d$/; return ( pushTimeParam.indexOf('Z') === pushTimeParam.length - 1 || // 2007-04-05T12:30Z offsetPattern.test(pushTimeParam) ); // 2007-04-05T12:30.000+02:00, 2007-04-05T12:30.000-02:00 } /** * Converts a date to ISO format in UTC time and strips the timezone if `isLocalTime` is true * @param date {Date} * @param isLocalTime {boolean} * @returns {string} */ static formatPushTime({ date, isLocalTime, }: { date: Date, isLocalTime: boolean, }) { if (isLocalTime) { // Strip 'Z' const isoString = date.toISOString(); return isoString.substring(0, isoString.indexOf('Z')); } return date.toISOString(); } } export default PushController;