/*! firebase-admin v11.0.1 */ "use strict"; /*! * Copyright 2020 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 }); exports.validateMessage = exports.BLACKLISTED_OPTIONS_KEYS = exports.BLACKLISTED_DATA_PAYLOAD_KEYS = void 0; const index_1 = require("../utils/index"); const error_1 = require("../utils/error"); const validator = require("../utils/validator"); // 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 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'); } const 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'); } } const targets = [anyMessage.token, anyMessage.topic, anyMessage.condition]; if (targets.filter((v) => 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); validateFcmOptions(message.fcmOptions); validateNotification(message.notification); } exports.validateMessage = validateMessage; /** * 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((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); validateApnsFcmOptions(config.fcmOptions); } /** * Checks if the given ApnsFcmOptions object is valid. * * @param {ApnsFcmOptions} fcmOptions An object to be validated. */ function validateApnsFcmOptions(fcmOptions) { if (typeof fcmOptions === 'undefined') { return; } else if (!validator.isNonNullObject(fcmOptions)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); } if (typeof fcmOptions.imageUrl !== 'undefined' && !validator.isURL(fcmOptions.imageUrl)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'imageUrl must be a valid URL string'); } if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } const propertyMappings = { imageUrl: 'image', }; Object.keys(propertyMappings).forEach((key) => { if (key in fcmOptions && propertyMappings[key] in fcmOptions) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in ApnsFcmOptions`); } }); (0, index_1.renameProperties)(fcmOptions, propertyMappings); } /** * Checks if the given FcmOptions object is valid. * * @param {FcmOptions} fcmOptions An object to be validated. */ function validateFcmOptions(fcmOptions) { if (typeof fcmOptions === 'undefined') { return; } else if (!validator.isNonNullObject(fcmOptions)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); } if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } } /** * Checks if the given Notification object is valid. * * @param {Notification} notification An object to be validated. */ function validateNotification(notification) { if (typeof notification === 'undefined') { return; } else if (!validator.isNonNullObject(notification)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'notification must be a non-null object'); } if (typeof notification.imageUrl !== 'undefined' && !validator.isURL(notification.imageUrl)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'notification.imageUrl must be a valid URL string'); } const propertyMappings = { imageUrl: 'image', }; Object.keys(propertyMappings).forEach((key) => { if (key in notification && propertyMappings[key] in notification) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in Notification`); } }); (0, index_1.renameProperties)(notification, propertyMappings); } /** * 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); validateApsSound(aps.sound); const propertyMappings = { contentAvailable: 'content-available', mutableContent: 'mutable-content', threadId: 'thread-id', }; Object.keys(propertyMappings).forEach((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`); } }); (0, index_1.renameProperties)(aps, propertyMappings); const contentAvailable = aps['content-available']; if (typeof contentAvailable !== 'undefined' && contentAvailable !== 1) { if (contentAvailable === true) { aps['content-available'] = 1; } else { delete aps['content-available']; } } const mutableContent = aps['mutable-content']; if (typeof mutableContent !== 'undefined' && mutableContent !== 1) { if (mutableContent === true) { aps['mutable-content'] = 1; } else { delete aps['mutable-content']; } } } function validateApsSound(sound) { if (typeof sound === 'undefined' || validator.isNonEmptyString(sound)) { return; } else if (!validator.isNonNullObject(sound)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound must be a non-empty string or a non-null object'); } if (!validator.isNonEmptyString(sound.name)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound.name must be a non-empty string'); } const volume = sound.volume; if (typeof volume !== 'undefined') { if (!validator.isNumber(volume)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound.volume must be a number'); } if (volume < 0 || volume > 1) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound.volume must be in the interval [0, 1]'); } } const soundObject = sound; const key = 'critical'; const critical = soundObject[key]; if (typeof critical !== 'undefined' && critical !== 1) { if (critical === true) { soundObject[key] = 1; } else { delete soundObject[key]; } } } /** * 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'); } const 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'); } if (validator.isNonEmptyArray(apsAlert.subtitleLocArgs) && !validator.isNonEmptyString(apsAlert.subtitleLocKey)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.subtitleLocKey is required when specifying subtitleLocArgs'); } const propertyMappings = { locKey: 'loc-key', locArgs: 'loc-args', titleLocKey: 'title-loc-key', titleLocArgs: 'title-loc-args', subtitleLocKey: 'subtitle-loc-key', subtitleLocArgs: 'subtitle-loc-args', actionLocKey: 'action-loc-key', launchImage: 'launch-image', }; (0, 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 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'); } const duration = (0, index_1.transformMillisecondsToSecondsString)(config.ttl); config.ttl = duration; } validateStringMap(config.data, 'android.data'); validateAndroidNotification(config.notification); validateAndroidFcmOptions(config.fcmOptions); const propertyMappings = { collapseKey: 'collapse_key', restrictedPackageName: 'restricted_package_name', }; (0, 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'); } if (typeof notification.imageUrl !== 'undefined' && !validator.isURL(notification.imageUrl)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.imageUrl must be a valid URL string'); } if (typeof notification.eventTimestamp !== 'undefined') { if (!(notification.eventTimestamp instanceof Date)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.eventTimestamp must be a valid `Date` object'); } // Convert timestamp to RFC3339 UTC "Zulu" format, example "2014-10-02T15:01:23.045123456Z" const zuluTimestamp = notification.eventTimestamp.toISOString(); notification.eventTimestamp = zuluTimestamp; } if (typeof notification.vibrateTimingsMillis !== 'undefined') { if (!validator.isNonEmptyArray(notification.vibrateTimingsMillis)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.vibrateTimingsMillis must be a non-empty array of numbers'); } const vibrateTimings = []; notification.vibrateTimingsMillis.forEach((value) => { if (!validator.isNumber(value) || value < 0) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.vibrateTimingsMillis must be non-negative durations in milliseconds'); } const duration = (0, index_1.transformMillisecondsToSecondsString)(value); vibrateTimings.push(duration); }); notification.vibrateTimingsMillis = vibrateTimings; } if (typeof notification.priority !== 'undefined') { const priority = 'PRIORITY_' + notification.priority.toUpperCase(); notification.priority = priority; } if (typeof notification.visibility !== 'undefined') { const visibility = notification.visibility.toUpperCase(); notification.visibility = visibility; } validateLightSettings(notification.lightSettings); const propertyMappings = { clickAction: 'click_action', bodyLocKey: 'body_loc_key', bodyLocArgs: 'body_loc_args', titleLocKey: 'title_loc_key', titleLocArgs: 'title_loc_args', channelId: 'channel_id', imageUrl: 'image', eventTimestamp: 'event_time', localOnly: 'local_only', priority: 'notification_priority', vibrateTimingsMillis: 'vibrate_timings', defaultVibrateTimings: 'default_vibrate_timings', defaultSound: 'default_sound', lightSettings: 'light_settings', defaultLightSettings: 'default_light_settings', notificationCount: 'notification_count', }; (0, index_1.renameProperties)(notification, propertyMappings); } /** * Checks if the given LightSettings object is valid. The object must have valid color and * light on/off duration parameters. If successful, transforms the input object by renaming * keys to valid Android keys. * * @param {LightSettings} lightSettings An object to be validated. */ function validateLightSettings(lightSettings) { if (typeof lightSettings === 'undefined') { return; } else if (!validator.isNonNullObject(lightSettings)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings must be a non-null object'); } if (!validator.isNumber(lightSettings.lightOnDurationMillis) || lightSettings.lightOnDurationMillis < 0) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds'); } const durationOn = (0, index_1.transformMillisecondsToSecondsString)(lightSettings.lightOnDurationMillis); lightSettings.lightOnDurationMillis = durationOn; if (!validator.isNumber(lightSettings.lightOffDurationMillis) || lightSettings.lightOffDurationMillis < 0) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings.lightOffDurationMillis must be a non-negative duration in milliseconds'); } const durationOff = (0, index_1.transformMillisecondsToSecondsString)(lightSettings.lightOffDurationMillis); lightSettings.lightOffDurationMillis = durationOff; if (!validator.isString(lightSettings.color) || (!/^#[0-9a-fA-F]{6}$/.test(lightSettings.color) && !/^#[0-9a-fA-F]{8}$/.test(lightSettings.color))) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings.color must be in the form #RRGGBB or #RRGGBBAA format'); } const colorString = lightSettings.color.length === 7 ? lightSettings.color + 'FF' : lightSettings.color; const rgb = /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/i.exec(colorString); if (!rgb || rgb.length < 4) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INTERNAL_ERROR, 'regex to extract rgba values from ' + colorString + ' failed.'); } const color = { red: parseInt(rgb[1], 16) / 255.0, green: parseInt(rgb[2], 16) / 255.0, blue: parseInt(rgb[3], 16) / 255.0, alpha: parseInt(rgb[4], 16) / 255.0, }; lightSettings.color = color; const propertyMappings = { lightOnDurationMillis: 'light_on_duration', lightOffDurationMillis: 'light_off_duration', }; (0, index_1.renameProperties)(lightSettings, propertyMappings); } /** * Checks if the given AndroidFcmOptions object is valid. * * @param {AndroidFcmOptions} fcmOptions An object to be validated. */ function validateAndroidFcmOptions(fcmOptions) { if (typeof fcmOptions === 'undefined') { return; } else if (!validator.isNonNullObject(fcmOptions)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); } if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } }