/*! firebase-admin v6.0.0 */ "use strict"; /*! * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); var index_1 = require("../utils/index"); var deep_copy_1 = require("../utils/deep-copy"); var messaging_api_request_1 = require("./messaging-api-request"); var error_1 = require("../utils/error"); var utils = require("../utils"); var validator = require("../utils/validator"); // FCM endpoints var FCM_SEND_HOST = 'fcm.googleapis.com'; var FCM_SEND_PATH = '/fcm/send'; var FCM_TOPIC_MANAGEMENT_HOST = 'iid.googleapis.com'; var FCM_TOPIC_MANAGEMENT_ADD_PATH = '/iid/v1:batchAdd'; var FCM_TOPIC_MANAGEMENT_REMOVE_PATH = '/iid/v1:batchRemove'; // Key renames for the messaging notification payload object. var CAMELCASED_NOTIFICATION_PAYLOAD_KEYS_MAP = { bodyLocArgs: 'body_loc_args', bodyLocKey: 'body_loc_key', clickAction: 'click_action', titleLocArgs: 'title_loc_args', titleLocKey: 'title_loc_key', }; // Key renames for the messaging options object. var CAMELCASE_OPTIONS_KEYS_MAP = { dryRun: 'dry_run', timeToLive: 'time_to_live', collapseKey: 'collapse_key', mutableContent: 'mutable_content', contentAvailable: 'content_available', restrictedPackageName: 'restricted_package_name', }; // Key renames for the MessagingDeviceResult object. var MESSAGING_DEVICE_RESULT_KEYS_MAP = { message_id: 'messageId', registration_id: 'canonicalRegistrationToken', }; // Key renames for the MessagingDevicesResponse object. var MESSAGING_DEVICES_RESPONSE_KEYS_MAP = { canonical_ids: 'canonicalRegistrationTokenCount', failure: 'failureCount', success: 'successCount', multicast_id: 'multicastId', }; // Key renames for the MessagingDeviceGroupResponse object. var MESSAGING_DEVICE_GROUP_RESPONSE_KEYS_MAP = { success: 'successCount', failure: 'failureCount', failed_registration_ids: 'failedRegistrationTokens', }; // Key renames for the MessagingTopicResponse object. var MESSAGING_TOPIC_RESPONSE_KEYS_MAP = { message_id: 'messageId', }; // Key renames for the MessagingConditionResponse object. var MESSAGING_CONDITION_RESPONSE_KEYS_MAP = { message_id: 'messageId', }; // Keys which are not allowed in the messaging data payload object. exports.BLACKLISTED_DATA_PAYLOAD_KEYS = ['from']; // Keys which are not allowed in the messaging options object. exports.BLACKLISTED_OPTIONS_KEYS = [ 'condition', 'data', 'notification', 'registrationIds', 'registration_ids', 'to', ]; /** * Checks if the given object only contains strings as child values. * * @param {object} map An object to be validated. * @param {string} label A label to be included in the errors thrown. */ function validateStringMap(map, label) { if (typeof map === 'undefined') { return; } else if (!validator.isNonNullObject(map)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, label + " must be a non-null object"); } Object.keys(map).forEach(function (key) { if (!validator.isString(map[key])) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, label + " must only contain string values"); } }); } /** * Checks if the given WebpushConfig object is valid. The object must have valid headers and data. * * @param {WebpushConfig} config An object to be validated. */ function validateWebpushConfig(config) { if (typeof config === 'undefined') { return; } else if (!validator.isNonNullObject(config)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'webpush must be a non-null object'); } validateStringMap(config.headers, 'webpush.headers'); validateStringMap(config.data, 'webpush.data'); } /** * Checks if the given ApnsConfig object is valid. The object must have valid headers and a * payload. * * @param {ApnsConfig} config An object to be validated. */ function validateApnsConfig(config) { if (typeof config === 'undefined') { return; } else if (!validator.isNonNullObject(config)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns must be a non-null object'); } validateStringMap(config.headers, 'apns.headers'); validateApnsPayload(config.payload); } /** * Checks if the given ApnsPayload object is valid. The object must have a valid aps value. * * @param {ApnsPayload} payload An object to be validated. */ function validateApnsPayload(payload) { if (typeof payload === 'undefined') { return; } else if (!validator.isNonNullObject(payload)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload must be a non-null object'); } validateAps(payload.aps); } /** * Checks if the given Aps object is valid. The object must have a valid alert. If the validation * is successful, transforms the input object by renaming the keys to valid APNS payload keys. * * @param {Aps} aps An object to be validated. */ function validateAps(aps) { if (typeof aps === 'undefined') { return; } else if (!validator.isNonNullObject(aps)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps must be a non-null object'); } validateApsAlert(aps.alert); var propertyMappings = { contentAvailable: 'content-available', mutableContent: 'mutable-content', threadId: 'thread-id', }; Object.keys(propertyMappings).forEach(function (key) { if (key in aps && propertyMappings[key] in aps) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, "Multiple specifications for " + key + " in Aps"); } }); index_1.renameProperties(aps, propertyMappings); var contentAvailable = aps['content-available']; if (typeof contentAvailable !== 'undefined' && contentAvailable !== 1) { if (contentAvailable === true) { aps['content-available'] = 1; } else { delete aps['content-available']; } } var mutableContent = aps['mutable-content']; if (typeof mutableContent !== 'undefined' && mutableContent !== 1) { if (mutableContent === true) { aps['mutable-content'] = 1; } else { delete aps['mutable-content']; } } } /** * Checks if the given alert object is valid. Alert could be a string or a complex object. * If specified as an object, it must have valid localization parameters. If successful, transforms * the input object by renaming the keys to valid APNS payload keys. * * @param {string | ApsAlert} alert An alert string or an object to be validated. */ function validateApsAlert(alert) { if (typeof alert === 'undefined' || validator.isString(alert)) { return; } else if (!validator.isNonNullObject(alert)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert must be a string or a non-null object'); } var apsAlert = alert; if (validator.isNonEmptyArray(apsAlert.locArgs) && !validator.isNonEmptyString(apsAlert.locKey)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.locKey is required when specifying locArgs'); } if (validator.isNonEmptyArray(apsAlert.titleLocArgs) && !validator.isNonEmptyString(apsAlert.titleLocKey)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.titleLocKey is required when specifying titleLocArgs'); } var propertyMappings = { locKey: 'loc-key', locArgs: 'loc-args', titleLocKey: 'title-loc-key', titleLocArgs: 'title-loc-args', actionLocKey: 'action-loc-key', launchImage: 'launch-image', }; index_1.renameProperties(apsAlert, propertyMappings); } /** * Checks if the given AndroidConfig object is valid. The object must have valid ttl, data, * and notification fields. If successful, transforms the input object by renaming keys to valid * Android keys. Also transforms the ttl value to the format expected by FCM service. * * @param {AndroidConfig} config An object to be validated. */ function validateAndroidConfig(config) { if (typeof config === 'undefined') { return; } else if (!validator.isNonNullObject(config)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android must be a non-null object'); } if (typeof config.ttl !== 'undefined') { if (!validator.isNumber(config.ttl) || config.ttl < 0) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'TTL must be a non-negative duration in milliseconds'); } var seconds = Math.floor(config.ttl / 1000); var nanos = (config.ttl - seconds * 1000) * 1000000; var duration = void 0; if (nanos > 0) { var nanoString = nanos.toString(); while (nanoString.length < 9) { nanoString = '0' + nanoString; } duration = seconds + "." + nanoString + "s"; } else { duration = seconds + "s"; } config.ttl = duration; } validateStringMap(config.data, 'android.data'); validateAndroidNotification(config.notification); var propertyMappings = { collapseKey: 'collapse_key', restrictedPackageName: 'restricted_package_name', }; index_1.renameProperties(config, propertyMappings); } /** * Checks if the given AndroidNotification object is valid. The object must have valid color and * localization parameters. If successful, transforms the input object by renaming keys to valid * Android keys. * * @param {AndroidNotification} notification An object to be validated. */ function validateAndroidNotification(notification) { if (typeof notification === 'undefined') { return; } else if (!validator.isNonNullObject(notification)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification must be a non-null object'); } if (typeof notification.color !== 'undefined' && !/^#[0-9a-fA-F]{6}$/.test(notification.color)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.color must be in the form #RRGGBB'); } if (validator.isNonEmptyArray(notification.bodyLocArgs) && !validator.isNonEmptyString(notification.bodyLocKey)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.bodyLocKey is required when specifying bodyLocArgs'); } if (validator.isNonEmptyArray(notification.titleLocArgs) && !validator.isNonEmptyString(notification.titleLocKey)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.titleLocKey is required when specifying titleLocArgs'); } var propertyMappings = { clickAction: 'click_action', bodyLocKey: 'body_loc_key', bodyLocArgs: 'body_loc_args', titleLocKey: 'title_loc_key', titleLocArgs: 'title_loc_args', }; index_1.renameProperties(notification, propertyMappings); } /** * Checks if the given Message object is valid. Recursively validates all the child objects * included in the message (android, apns, data etc.). If successful, transforms the message * in place by renaming the keys to what's expected by the remote FCM service. * * @param {Message} Message An object to be validated. */ function validateMessage(message) { if (!validator.isNonNullObject(message)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'Message must be a non-null object'); } var anyMessage = message; if (anyMessage.topic) { // If the topic name is prefixed, remove it. if (anyMessage.topic.startsWith('/topics/')) { anyMessage.topic = anyMessage.topic.replace(/^\/topics\//, ''); } // Checks for illegal characters and empty string. if (!/^[a-zA-Z0-9-_.~%]+$/.test(anyMessage.topic)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'Malformed topic name'); } } var targets = [anyMessage.token, anyMessage.topic, anyMessage.condition]; if (targets.filter(function (v) { return validator.isNonEmptyString(v); }).length !== 1) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'Exactly one of topic, token or condition is required'); } validateStringMap(message.data, 'data'); validateAndroidConfig(message.android); validateWebpushConfig(message.webpush); validateApnsConfig(message.apns); } /** * Maps a raw FCM server response to a MessagingDevicesResponse object. * * @param {object} response The raw FCM server response to map. * * @return {MessagingDeviceGroupResponse} The mapped MessagingDevicesResponse object. */ function mapRawResponseToDevicesResponse(response) { // Rename properties on the server response utils.renameProperties(response, MESSAGING_DEVICES_RESPONSE_KEYS_MAP); if ('results' in response) { response.results.forEach(function (messagingDeviceResult) { utils.renameProperties(messagingDeviceResult, MESSAGING_DEVICE_RESULT_KEYS_MAP); // Map the FCM server's error strings to actual error objects. if ('error' in messagingDeviceResult) { var newError = error_1.FirebaseMessagingError.fromServerError(messagingDeviceResult.error, /* message */ undefined, messagingDeviceResult.error); messagingDeviceResult.error = newError; } }); } return response; } /** * Maps a raw FCM server response to a MessagingDeviceGroupResponse object. * * @param {object} response The raw FCM server response to map. * * @return {MessagingDeviceGroupResponse} The mapped MessagingDeviceGroupResponse object. */ function mapRawResponseToDeviceGroupResponse(response) { // Rename properties on the server response utils.renameProperties(response, MESSAGING_DEVICE_GROUP_RESPONSE_KEYS_MAP); // Add the 'failedRegistrationTokens' property if it does not exist on the response, which // it won't when the 'failureCount' property has a value of 0) response.failedRegistrationTokens = response.failedRegistrationTokens || []; return response; } /** * Maps a raw FCM server response to a MessagingTopicManagementResponse object. * * @param {object} response The raw FCM server response to map. * * @return {MessagingTopicManagementResponse} The mapped MessagingTopicManagementResponse object. */ function mapRawResponseToTopicManagementResponse(response) { // Add the success and failure counts. var result = { successCount: 0, failureCount: 0, errors: [], }; var errors = []; if ('results' in response) { response.results.forEach(function (tokenManagementResult, index) { // Map the FCM server's error strings to actual error objects. if ('error' in tokenManagementResult) { result.failureCount += 1; var newError = error_1.FirebaseMessagingError.fromTopicManagementServerError(tokenManagementResult.error, /* message */ undefined, tokenManagementResult.error); result.errors.push({ index: index, error: newError, }); } else { result.successCount += 1; } }); } return result; } /** * Internals of a Messaging instance. */ var MessagingInternals = /** @class */ (function () { function MessagingInternals() { } /** * Deletes the service and its associated resources. * * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. */ MessagingInternals.prototype.delete = function () { // There are no resources to clean up. return Promise.resolve(undefined); }; return MessagingInternals; }()); /** * Messaging service bound to the provided app. */ var Messaging = /** @class */ (function () { /** * @param {FirebaseApp} app The app for this Messaging service. * @constructor */ function Messaging(app) { this.INTERNAL = new MessagingInternals(); if (!validator.isNonNullObject(app) || !('options' in app)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_ARGUMENT, 'First argument passed to admin.messaging() must be a valid Firebase app instance.'); } var projectId = utils.getProjectId(app); if (!validator.isNonEmptyString(projectId)) { // Assert for an explicit projct ID (either via AppOptions or the cert itself). throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_ARGUMENT, 'Failed to determine project ID for Messaging. Initialize the ' + 'SDK with service account credentials or set project ID as an app option. ' + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.'); } this.urlPath = "/v1/projects/" + projectId + "/messages:send"; this.appInternal = app; this.messagingRequestHandler = new messaging_api_request_1.FirebaseMessagingRequestHandler(app); } Object.defineProperty(Messaging.prototype, "app", { /** * Returns the app associated with this Messaging instance. * * @return {FirebaseApp} The app associated with this Messaging instance. */ get: function () { return this.appInternal; }, enumerable: true, configurable: true }); /** * Sends a message via Firebase Cloud Messaging (FCM). * * @param {Message} message The message to be sent. * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. * * @return {Promise} A Promise fulfilled with a message ID string. */ Messaging.prototype.send = function (message, dryRun) { var _this = this; var copy = deep_copy_1.deepCopy(message); validateMessage(copy); if (typeof dryRun !== 'undefined' && !validator.isBoolean(dryRun)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); } return Promise.resolve() .then(function () { var request = { message: copy }; if (dryRun) { request.validate_only = true; } return _this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, _this.urlPath, request); }) .then(function (response) { return response.name; }); }; /** * Sends an FCM message to a single device or an array of devices. * * @param {string|string[]} registrationTokenOrTokens The registration token or an array of * registration tokens for the device(s) to which to send the message. * @param {MessagingPayload} payload The message payload. * @param {MessagingOptions} [options = {}] Optional options to alter the message. * * @return {Promise} A Promise fulfilled * with the server's response after the message has been sent. */ Messaging.prototype.sendToDevice = function (registrationTokenOrTokens, payload, options) { var _this = this; if (options === void 0) { options = {}; } // Validate the input argument types. Since these are common developer errors when getting // started, throw an error instead of returning a rejected promise. this.validateRegistrationTokensType(registrationTokenOrTokens, 'sendToDevice', error_1.MessagingClientErrorCode.INVALID_RECIPIENT); this.validateMessagingPayloadAndOptionsTypes(payload, options); return Promise.resolve() .then(function () { // Validate the contents of the input arguments. Because we are now in a promise, any thrown // error will cause this method to return a rejected promise. _this.validateRegistrationTokens(registrationTokenOrTokens, 'sendToDevice', error_1.MessagingClientErrorCode.INVALID_RECIPIENT); var payloadCopy = _this.validateMessagingPayload(payload); var optionsCopy = _this.validateMessagingOptions(options); var request = deep_copy_1.deepCopy(payloadCopy); deep_copy_1.deepExtend(request, optionsCopy); if (validator.isString(registrationTokenOrTokens)) { request.to = registrationTokenOrTokens; } else { request.registration_ids = registrationTokenOrTokens; } return _this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, FCM_SEND_PATH, request); }) .then(function (response) { // The sendToDevice() and sendToDeviceGroup() methods both set the `to` query parameter in // the underlying FCM request. If the provided registration token argument is actually a // valid notification key, the response from the FCM server will be a device group response. // If that is the case, we map the response to a MessagingDeviceGroupResponse. // See b/35394951 for more context. if ('multicast_id' in response) { return mapRawResponseToDevicesResponse(response); } else { return mapRawResponseToDeviceGroupResponse(response); } }); }; /** * Sends an FCM message to a device group. * * @param {string} notificationKey The notification key representing the device group to which to * send the message. * @param {MessagingPayload} payload The message payload. * @param {MessagingOptions} [options = {}] Optional options to alter the message. * * @return {Promise} A Promise fulfilled * with the server's response after the message has been sent. */ Messaging.prototype.sendToDeviceGroup = function (notificationKey, payload, options) { var _this = this; if (options === void 0) { options = {}; } if (!validator.isNonEmptyString(notificationKey)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_RECIPIENT, 'Notification key provided to sendToDeviceGroup() must be a non-empty string.'); } else if (notificationKey.indexOf(':') !== -1) { // It is possible the developer provides a registration token instead of a notification key // to this method. We can detect some of those cases by checking to see if the string contains // a colon. Not all registration tokens will contain a colon (only newer ones will), but no // notification keys will contain a colon, so we can use it as a rough heuristic. // See b/35394951 for more context. return Promise.reject(new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_RECIPIENT, 'Notification key provided to sendToDeviceGroup() has the format of a registration token. ' + 'You should use sendToDevice() instead.')); } // Validate the types of the payload and options arguments. Since these are common developer // errors, throw an error instead of returning a rejected promise. this.validateMessagingPayloadAndOptionsTypes(payload, options); return Promise.resolve() .then(function () { // Validate the contents of the payload and options objects. Because we are now in a // promise, any thrown error will cause this method to return a rejected promise. var payloadCopy = _this.validateMessagingPayload(payload); var optionsCopy = _this.validateMessagingOptions(options); var request = deep_copy_1.deepCopy(payloadCopy); deep_copy_1.deepExtend(request, optionsCopy); request.to = notificationKey; return _this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, FCM_SEND_PATH, request); }) .then(function (response) { // The sendToDevice() and sendToDeviceGroup() methods both set the `to` query parameter in // the underlying FCM request. If the provided notification key argument has an invalid // format (that is, it is either a registration token or some random string), the response // from the FCM server will default to a devices response (which we detect by looking for // the `multicast_id` property). If that is the case, we either throw an error saying the // provided notification key is invalid (if the message failed to send) or map the response // to a MessagingDevicesResponse (if the message succeeded). // See b/35394951 for more context. if ('multicast_id' in response) { if (response.success === 0) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_RECIPIENT, 'Notification key provided to sendToDeviceGroup() is invalid.'); } else { return mapRawResponseToDevicesResponse(response); } } return mapRawResponseToDeviceGroupResponse(response); }); }; /** * Sends an FCM message to a topic. * * @param {string} topic The name of the topic to which to send the message. * @param {MessagingPayload} payload The message payload. * @param {MessagingOptions} [options = {}] Optional options to alter the message. * * @return {Promise} A Promise fulfilled with the server's response after * the message has been sent. */ Messaging.prototype.sendToTopic = function (topic, payload, options) { var _this = this; if (options === void 0) { options = {}; } // Validate the input argument types. Since these are common developer errors when getting // started, throw an error instead of returning a rejected promise. this.validateTopicType(topic, 'sendToTopic', error_1.MessagingClientErrorCode.INVALID_RECIPIENT); this.validateMessagingPayloadAndOptionsTypes(payload, options); // Prepend the topic with /topics/ if necessary. topic = this.normalizeTopic(topic); return Promise.resolve() .then(function () { // Validate the contents of the payload and options objects. Because we are now in a // promise, any thrown error will cause this method to return a rejected promise. var payloadCopy = _this.validateMessagingPayload(payload); var optionsCopy = _this.validateMessagingOptions(options); _this.validateTopic(topic, 'sendToTopic', error_1.MessagingClientErrorCode.INVALID_RECIPIENT); var request = deep_copy_1.deepCopy(payloadCopy); deep_copy_1.deepExtend(request, optionsCopy); request.to = topic; return _this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, FCM_SEND_PATH, request); }) .then(function (response) { // Rename properties on the server response utils.renameProperties(response, MESSAGING_TOPIC_RESPONSE_KEYS_MAP); return response; }); }; /** * Sends an FCM message to a condition. * * @param {string} condition The condition to which to send the message. * @param {MessagingPayload} payload The message payload. * @param {MessagingOptions} [options = {}] Optional options to alter the message. * * @return {Promise} A Promise fulfilled with the server's response * after the message has been sent. */ Messaging.prototype.sendToCondition = function (condition, payload, options) { var _this = this; if (options === void 0) { options = {}; } if (!validator.isNonEmptyString(condition)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_RECIPIENT, 'Condition provided to sendToCondition() must be a non-empty string.'); } // Validate the types of the payload and options arguments. Since these are common developer // errors, throw an error instead of returning a rejected promise. this.validateMessagingPayloadAndOptionsTypes(payload, options); // The FCM server rejects conditions which are surrounded in single quotes. When the condition // is stringified over the wire, double quotes in it get converted to \" which the FCM server // does not properly handle. We can get around this by replacing internal double quotes with // single quotes. condition = condition.replace(/"/g, '\''); return Promise.resolve() .then(function () { // Validate the contents of the payload and options objects. Because we are now in a // promise, any thrown error will cause this method to return a rejected promise. var payloadCopy = _this.validateMessagingPayload(payload); var optionsCopy = _this.validateMessagingOptions(options); var request = deep_copy_1.deepCopy(payloadCopy); deep_copy_1.deepExtend(request, optionsCopy); request.condition = condition; return _this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, FCM_SEND_PATH, request); }) .then(function (response) { // Rename properties on the server response utils.renameProperties(response, MESSAGING_CONDITION_RESPONSE_KEYS_MAP); return response; }); }; /** * Subscribes a single device or an array of devices to a topic. * * @param {string|string[]} registrationTokenOrTokens The registration token or an array of * registration tokens to subscribe to the topic. * @param {string} topic The topic to which to subscribe. * * @return {Promise} A Promise fulfilled with the parsed FCM * server response. */ Messaging.prototype.subscribeToTopic = function (registrationTokenOrTokens, topic) { return this.sendTopicManagementRequest(registrationTokenOrTokens, topic, 'subscribeToTopic', FCM_TOPIC_MANAGEMENT_ADD_PATH); }; /** * Unsubscribes a single device or an array of devices from a topic. * * @param {string|string[]} registrationTokenOrTokens The registration token or an array of * registration tokens to unsubscribe from the topic. * @param {string} topic The topic to which to subscribe. * * @return {Promise} A Promise fulfilled with the parsed FCM * server response. */ Messaging.prototype.unsubscribeFromTopic = function (registrationTokenOrTokens, topic) { return this.sendTopicManagementRequest(registrationTokenOrTokens, topic, 'unsubscribeFromTopic', FCM_TOPIC_MANAGEMENT_REMOVE_PATH); }; /** * Helper method which sends and handles topic subscription management requests. * * @param {string|string[]} registrationTokenOrTokens The registration token or an array of * registration tokens to unsubscribe from the topic. * @param {string} topic The topic to which to subscribe. * @param {string} methodName The name of the original method called. * @param {string} path The endpoint path to use for the request. * * @return {Promise} A Promise fulfilled with the parsed server * response. */ Messaging.prototype.sendTopicManagementRequest = function (registrationTokenOrTokens, topic, methodName, path) { var _this = this; this.validateRegistrationTokensType(registrationTokenOrTokens, methodName); this.validateTopicType(topic, methodName); // Prepend the topic with /topics/ if necessary. topic = this.normalizeTopic(topic); return Promise.resolve() .then(function () { // Validate the contents of the input arguments. Because we are now in a promise, any thrown // error will cause this method to return a rejected promise. _this.validateRegistrationTokens(registrationTokenOrTokens, methodName); _this.validateTopic(topic, methodName); // Ensure the registration token(s) input argument is an array. var registrationTokensArray = registrationTokenOrTokens; if (validator.isString(registrationTokenOrTokens)) { registrationTokensArray = [registrationTokenOrTokens]; } var request = { to: topic, registration_tokens: registrationTokensArray, }; return _this.messagingRequestHandler.invokeRequestHandler(FCM_TOPIC_MANAGEMENT_HOST, path, request); }) .then(function (response) { return mapRawResponseToTopicManagementResponse(response); }); }; /** * Validates the types of the messaging payload and options. If invalid, an error will be thrown. * * @param {MessagingPayload} payload The messaging payload to validate. * @param {MessagingOptions} options The messaging options to validate. */ Messaging.prototype.validateMessagingPayloadAndOptionsTypes = function (payload, options) { // Validate the payload is an object if (!validator.isNonNullObject(payload)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'Messaging payload must be an object with at least one of the "data" or "notification" properties.'); } // Validate the options argument is an object if (!validator.isNonNullObject(options)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_OPTIONS, 'Messaging options must be an object.'); } }; /** * Validates the messaging payload. If invalid, an error will be thrown. * * @param {MessagingPayload} payload The messaging payload to validate. * * @return {MessagingPayload} A copy of the provided payload with whitelisted properties switched * from camelCase to underscore_case. */ Messaging.prototype.validateMessagingPayload = function (payload) { var payloadCopy = deep_copy_1.deepCopy(payload); var payloadKeys = Object.keys(payloadCopy); var validPayloadKeys = ['data', 'notification']; var containsDataOrNotificationKey = false; payloadKeys.forEach(function (payloadKey) { // Validate the payload does not contain any invalid keys if (validPayloadKeys.indexOf(payloadKey) === -1) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, "Messaging payload contains an invalid \"" + payloadKey + "\" property. Valid properties are " + "\"data\" and \"notification\"."); } else { containsDataOrNotificationKey = true; } }); // Validate the payload contains at least one of the "data" and "notification" keys if (!containsDataOrNotificationKey) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'Messaging payload must contain at least one of the "data" or "notification" properties.'); } payloadKeys.forEach(function (payloadKey) { var value = payloadCopy[payloadKey]; // Validate each top-level key in the payload is an object if (!validator.isNonNullObject(value)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, "Messaging payload contains an invalid value for the \"" + payloadKey + "\" property. " + "Value must be an object."); } Object.keys(value).forEach(function (subKey) { if (!validator.isString(value[subKey])) { // Validate all sub-keys have a string value throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, "Messaging payload contains an invalid value for the \"" + payloadKey + "." + subKey + "\" " + "property. Values must be strings."); } else if (payloadKey === 'data' && /^google\./.test(subKey)) { // Validate the data payload does not contain keys which start with 'google.'. throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, "Messaging payload contains the blacklisted \"data." + subKey + "\" property."); } }); }); // Validate the data payload object does not contain blacklisted properties if ('data' in payloadCopy) { exports.BLACKLISTED_DATA_PAYLOAD_KEYS.forEach(function (blacklistedKey) { if (blacklistedKey in payloadCopy.data) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, "Messaging payload contains the blacklisted \"data." + blacklistedKey + "\" property."); } }); } // Convert whitelisted camelCase keys to underscore_case if ('notification' in payloadCopy) { utils.renameProperties(payloadCopy.notification, CAMELCASED_NOTIFICATION_PAYLOAD_KEYS_MAP); } return payloadCopy; }; /** * Validates the messaging options. If invalid, an error will be thrown. * * @param {MessagingOptions} options The messaging options to validate. * * @return {MessagingOptions} A copy of the provided options with whitelisted properties switched * from camelCase to underscore_case. */ Messaging.prototype.validateMessagingOptions = function (options) { var optionsCopy = deep_copy_1.deepCopy(options); // Validate the options object does not contain blacklisted properties exports.BLACKLISTED_OPTIONS_KEYS.forEach(function (blacklistedKey) { if (blacklistedKey in optionsCopy) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_OPTIONS, "Messaging options contains the blacklisted \"" + blacklistedKey + "\" property."); } }); // Convert whitelisted camelCase keys to underscore_case utils.renameProperties(optionsCopy, CAMELCASE_OPTIONS_KEYS_MAP); // Validate the options object contains valid values for whitelisted properties if ('collapse_key' in optionsCopy && !validator.isNonEmptyString(optionsCopy.collapse_key)) { var keyName = ('collapseKey' in options) ? 'collapseKey' : 'collapse_key'; throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_OPTIONS, "Messaging options contains an invalid value for the \"" + keyName + "\" property. Value must " + 'be a non-empty string.'); } else if ('dry_run' in optionsCopy && !validator.isBoolean(optionsCopy.dry_run)) { var keyName = ('dryRun' in options) ? 'dryRun' : 'dry_run'; throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_OPTIONS, "Messaging options contains an invalid value for the \"" + keyName + "\" property. Value must " + 'be a boolean.'); } else if ('priority' in optionsCopy && !validator.isNonEmptyString(optionsCopy.priority)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_OPTIONS, 'Messaging options contains an invalid value for the "priority" property. Value must ' + 'be a non-empty string.'); } else if ('restricted_package_name' in optionsCopy && !validator.isNonEmptyString(optionsCopy.restricted_package_name)) { var keyName = ('restrictedPackageName' in options) ? 'restrictedPackageName' : 'restricted_package_name'; throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_OPTIONS, "Messaging options contains an invalid value for the \"" + keyName + "\" property. Value must " + 'be a non-empty string.'); } else if ('time_to_live' in optionsCopy && !validator.isNumber(optionsCopy.time_to_live)) { var keyName = ('timeToLive' in options) ? 'timeToLive' : 'time_to_live'; throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_OPTIONS, "Messaging options contains an invalid value for the \"" + keyName + "\" property. Value must " + 'be a number.'); } else if ('content_available' in optionsCopy && !validator.isBoolean(optionsCopy.content_available)) { var keyName = ('contentAvailable' in options) ? 'contentAvailable' : 'content_available'; throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_OPTIONS, "Messaging options contains an invalid value for the \"" + keyName + "\" property. Value must " + 'be a boolean.'); } else if ('mutable_content' in optionsCopy && !validator.isBoolean(optionsCopy.mutable_content)) { var keyName = ('mutableContent' in options) ? 'mutableContent' : 'mutable_content'; throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_OPTIONS, "Messaging options contains an invalid value for the \"" + keyName + "\" property. Value must " + 'be a boolean.'); } return optionsCopy; }; /** * Validates the type of the provided registration token(s). If invalid, an error will be thrown. * * @param {string|string[]} registrationTokenOrTokens The registration token(s) to validate. * @param {string} method The method name to use in error messages. * @param {ErrorInfo?} [errorInfo] The error info to use if the registration tokens are invalid. */ Messaging.prototype.validateRegistrationTokensType = function (registrationTokenOrTokens, methodName, errorInfo) { if (errorInfo === void 0) { errorInfo = error_1.MessagingClientErrorCode.INVALID_ARGUMENT; } if (!validator.isNonEmptyArray(registrationTokenOrTokens) && !validator.isNonEmptyString(registrationTokenOrTokens)) { throw new error_1.FirebaseMessagingError(errorInfo, "Registration token(s) provided to " + methodName + "() must be a non-empty string or a " + 'non-empty array.'); } }; /** * Validates the provided registration tokens. If invalid, an error will be thrown. * * @param {string|string[]} registrationTokenOrTokens The registration token or an array of * registration tokens to validate. * @param {string} method The method name to use in error messages. * @param {errorInfo?} [ErrorInfo] The error info to use if the registration tokens are invalid. */ Messaging.prototype.validateRegistrationTokens = function (registrationTokenOrTokens, methodName, errorInfo) { if (errorInfo === void 0) { errorInfo = error_1.MessagingClientErrorCode.INVALID_ARGUMENT; } if (validator.isArray(registrationTokenOrTokens)) { // Validate the array contains no more than 1,000 registration tokens. if (registrationTokenOrTokens.length > 1000) { throw new error_1.FirebaseMessagingError(errorInfo, "Too many registration tokens provided in a single request to " + methodName + "(). Batch " + 'your requests to contain no more than 1,000 registration tokens per request.'); } // Validate the array contains registration tokens which are non-empty strings. registrationTokenOrTokens.forEach(function (registrationToken, index) { if (!validator.isNonEmptyString(registrationToken)) { throw new error_1.FirebaseMessagingError(errorInfo, "Registration token provided to " + methodName + "() at index " + index + " must be a " + 'non-empty string.'); } }); } }; /** * Validates the type of the provided topic. If invalid, an error will be thrown. * * @param {string} topic The topic to validate. * @param {string} method The method name to use in error messages. * @param {ErrorInfo?} [errorInfo] The error info to use if the topic is invalid. */ Messaging.prototype.validateTopicType = function (topic, methodName, errorInfo) { if (errorInfo === void 0) { errorInfo = error_1.MessagingClientErrorCode.INVALID_ARGUMENT; } if (!validator.isNonEmptyString(topic)) { throw new error_1.FirebaseMessagingError(errorInfo, "Topic provided to " + methodName + "() must be a string which matches the format " + '"/topics/[a-zA-Z0-9-_.~%]+".'); } }; /** * Validates the provided topic. If invalid, an error will be thrown. * * @param {string} topic The topic to validate. * @param {string} method The method name to use in error messages. * @param {ErrorInfo?} [errorInfo] The error info to use if the topic is invalid. */ Messaging.prototype.validateTopic = function (topic, methodName, errorInfo) { if (errorInfo === void 0) { errorInfo = error_1.MessagingClientErrorCode.INVALID_ARGUMENT; } if (!validator.isTopic(topic)) { throw new error_1.FirebaseMessagingError(errorInfo, "Topic provided to " + methodName + "() must be a string which matches the format " + '"/topics/[a-zA-Z0-9-_.~%]+".'); } }; /** * Normalizes the provided topic name by prepending it with '/topics/', if necessary. * * @param {string} topic The topic name to normalize. * * @return {string} The normalized topic name. */ Messaging.prototype.normalizeTopic = function (topic) { if (!/^\/topics\//.test(topic)) { topic = "/topics/" + topic; } return topic; }; return Messaging; }()); exports.Messaging = Messaging;