UNPKG

23.1 kBJavaScriptView Raw
1/*! firebase-admin v12.0.0 */
2"use strict";
3/*!
4 * Copyright 2020 Google Inc.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18Object.defineProperty(exports, "__esModule", { value: true });
19exports.validateMessage = exports.BLACKLISTED_OPTIONS_KEYS = exports.BLACKLISTED_DATA_PAYLOAD_KEYS = void 0;
20const index_1 = require("../utils/index");
21const error_1 = require("../utils/error");
22const validator = require("../utils/validator");
23// Keys which are not allowed in the messaging data payload object.
24exports.BLACKLISTED_DATA_PAYLOAD_KEYS = ['from'];
25// Keys which are not allowed in the messaging options object.
26exports.BLACKLISTED_OPTIONS_KEYS = [
27 'condition', 'data', 'notification', 'registrationIds', 'registration_ids', 'to',
28];
29/**
30 * Checks if the given Message object is valid. Recursively validates all the child objects
31 * included in the message (android, apns, data etc.). If successful, transforms the message
32 * in place by renaming the keys to what's expected by the remote FCM service.
33 *
34 * @param {Message} Message An object to be validated.
35 */
36function validateMessage(message) {
37 if (!validator.isNonNullObject(message)) {
38 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'Message must be a non-null object');
39 }
40 const anyMessage = message;
41 if (anyMessage.topic) {
42 // If the topic name is prefixed, remove it.
43 if (anyMessage.topic.startsWith('/topics/')) {
44 anyMessage.topic = anyMessage.topic.replace(/^\/topics\//, '');
45 }
46 // Checks for illegal characters and empty string.
47 if (!/^[a-zA-Z0-9-_.~%]+$/.test(anyMessage.topic)) {
48 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'Malformed topic name');
49 }
50 }
51 const targets = [anyMessage.token, anyMessage.topic, anyMessage.condition];
52 if (targets.filter((v) => validator.isNonEmptyString(v)).length !== 1) {
53 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'Exactly one of topic, token or condition is required');
54 }
55 validateStringMap(message.data, 'data');
56 validateAndroidConfig(message.android);
57 validateWebpushConfig(message.webpush);
58 validateApnsConfig(message.apns);
59 validateFcmOptions(message.fcmOptions);
60 validateNotification(message.notification);
61}
62exports.validateMessage = validateMessage;
63/**
64 * Checks if the given object only contains strings as child values.
65 *
66 * @param {object} map An object to be validated.
67 * @param {string} label A label to be included in the errors thrown.
68 */
69function validateStringMap(map, label) {
70 if (typeof map === 'undefined') {
71 return;
72 }
73 else if (!validator.isNonNullObject(map)) {
74 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, `${label} must be a non-null object`);
75 }
76 Object.keys(map).forEach((key) => {
77 if (!validator.isString(map[key])) {
78 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, `${label} must only contain string values`);
79 }
80 });
81}
82/**
83 * Checks if the given WebpushConfig object is valid. The object must have valid headers and data.
84 *
85 * @param {WebpushConfig} config An object to be validated.
86 */
87function validateWebpushConfig(config) {
88 if (typeof config === 'undefined') {
89 return;
90 }
91 else if (!validator.isNonNullObject(config)) {
92 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'webpush must be a non-null object');
93 }
94 validateStringMap(config.headers, 'webpush.headers');
95 validateStringMap(config.data, 'webpush.data');
96}
97/**
98 * Checks if the given ApnsConfig object is valid. The object must have valid headers and a
99 * payload.
100 *
101 * @param {ApnsConfig} config An object to be validated.
102 */
103function validateApnsConfig(config) {
104 if (typeof config === 'undefined') {
105 return;
106 }
107 else if (!validator.isNonNullObject(config)) {
108 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns must be a non-null object');
109 }
110 validateStringMap(config.headers, 'apns.headers');
111 validateApnsPayload(config.payload);
112 validateApnsFcmOptions(config.fcmOptions);
113}
114/**
115 * Checks if the given ApnsFcmOptions object is valid.
116 *
117 * @param {ApnsFcmOptions} fcmOptions An object to be validated.
118 */
119function validateApnsFcmOptions(fcmOptions) {
120 if (typeof fcmOptions === 'undefined') {
121 return;
122 }
123 else if (!validator.isNonNullObject(fcmOptions)) {
124 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object');
125 }
126 if (typeof fcmOptions.imageUrl !== 'undefined' &&
127 !validator.isURL(fcmOptions.imageUrl)) {
128 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'imageUrl must be a valid URL string');
129 }
130 if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) {
131 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value');
132 }
133 const propertyMappings = {
134 imageUrl: 'image',
135 };
136 Object.keys(propertyMappings).forEach((key) => {
137 if (key in fcmOptions && propertyMappings[key] in fcmOptions) {
138 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in ApnsFcmOptions`);
139 }
140 });
141 (0, index_1.renameProperties)(fcmOptions, propertyMappings);
142}
143/**
144 * Checks if the given FcmOptions object is valid.
145 *
146 * @param {FcmOptions} fcmOptions An object to be validated.
147 */
148function validateFcmOptions(fcmOptions) {
149 if (typeof fcmOptions === 'undefined') {
150 return;
151 }
152 else if (!validator.isNonNullObject(fcmOptions)) {
153 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object');
154 }
155 if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) {
156 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value');
157 }
158}
159/**
160 * Checks if the given Notification object is valid.
161 *
162 * @param {Notification} notification An object to be validated.
163 */
164function validateNotification(notification) {
165 if (typeof notification === 'undefined') {
166 return;
167 }
168 else if (!validator.isNonNullObject(notification)) {
169 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'notification must be a non-null object');
170 }
171 if (typeof notification.imageUrl !== 'undefined' && !validator.isURL(notification.imageUrl)) {
172 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'notification.imageUrl must be a valid URL string');
173 }
174 const propertyMappings = {
175 imageUrl: 'image',
176 };
177 Object.keys(propertyMappings).forEach((key) => {
178 if (key in notification && propertyMappings[key] in notification) {
179 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in Notification`);
180 }
181 });
182 (0, index_1.renameProperties)(notification, propertyMappings);
183}
184/**
185 * Checks if the given ApnsPayload object is valid. The object must have a valid aps value.
186 *
187 * @param {ApnsPayload} payload An object to be validated.
188 */
189function validateApnsPayload(payload) {
190 if (typeof payload === 'undefined') {
191 return;
192 }
193 else if (!validator.isNonNullObject(payload)) {
194 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload must be a non-null object');
195 }
196 validateAps(payload.aps);
197}
198/**
199 * Checks if the given Aps object is valid. The object must have a valid alert. If the validation
200 * is successful, transforms the input object by renaming the keys to valid APNS payload keys.
201 *
202 * @param {Aps} aps An object to be validated.
203 */
204function validateAps(aps) {
205 if (typeof aps === 'undefined') {
206 return;
207 }
208 else if (!validator.isNonNullObject(aps)) {
209 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps must be a non-null object');
210 }
211 validateApsAlert(aps.alert);
212 validateApsSound(aps.sound);
213 const propertyMappings = {
214 contentAvailable: 'content-available',
215 mutableContent: 'mutable-content',
216 threadId: 'thread-id',
217 };
218 Object.keys(propertyMappings).forEach((key) => {
219 if (key in aps && propertyMappings[key] in aps) {
220 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in Aps`);
221 }
222 });
223 (0, index_1.renameProperties)(aps, propertyMappings);
224 const contentAvailable = aps['content-available'];
225 if (typeof contentAvailable !== 'undefined' && contentAvailable !== 1) {
226 if (contentAvailable === true) {
227 aps['content-available'] = 1;
228 }
229 else {
230 delete aps['content-available'];
231 }
232 }
233 const mutableContent = aps['mutable-content'];
234 if (typeof mutableContent !== 'undefined' && mutableContent !== 1) {
235 if (mutableContent === true) {
236 aps['mutable-content'] = 1;
237 }
238 else {
239 delete aps['mutable-content'];
240 }
241 }
242}
243function validateApsSound(sound) {
244 if (typeof sound === 'undefined' || validator.isNonEmptyString(sound)) {
245 return;
246 }
247 else if (!validator.isNonNullObject(sound)) {
248 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');
249 }
250 if (!validator.isNonEmptyString(sound.name)) {
251 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound.name must be a non-empty string');
252 }
253 const volume = sound.volume;
254 if (typeof volume !== 'undefined') {
255 if (!validator.isNumber(volume)) {
256 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound.volume must be a number');
257 }
258 if (volume < 0 || volume > 1) {
259 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound.volume must be in the interval [0, 1]');
260 }
261 }
262 const soundObject = sound;
263 const key = 'critical';
264 const critical = soundObject[key];
265 if (typeof critical !== 'undefined' && critical !== 1) {
266 if (critical === true) {
267 soundObject[key] = 1;
268 }
269 else {
270 delete soundObject[key];
271 }
272 }
273}
274/**
275 * Checks if the given alert object is valid. Alert could be a string or a complex object.
276 * If specified as an object, it must have valid localization parameters. If successful, transforms
277 * the input object by renaming the keys to valid APNS payload keys.
278 *
279 * @param {string | ApsAlert} alert An alert string or an object to be validated.
280 */
281function validateApsAlert(alert) {
282 if (typeof alert === 'undefined' || validator.isString(alert)) {
283 return;
284 }
285 else if (!validator.isNonNullObject(alert)) {
286 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert must be a string or a non-null object');
287 }
288 const apsAlert = alert;
289 if (validator.isNonEmptyArray(apsAlert.locArgs) &&
290 !validator.isNonEmptyString(apsAlert.locKey)) {
291 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.locKey is required when specifying locArgs');
292 }
293 if (validator.isNonEmptyArray(apsAlert.titleLocArgs) &&
294 !validator.isNonEmptyString(apsAlert.titleLocKey)) {
295 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.titleLocKey is required when specifying titleLocArgs');
296 }
297 if (validator.isNonEmptyArray(apsAlert.subtitleLocArgs) &&
298 !validator.isNonEmptyString(apsAlert.subtitleLocKey)) {
299 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.subtitleLocKey is required when specifying subtitleLocArgs');
300 }
301 const propertyMappings = {
302 locKey: 'loc-key',
303 locArgs: 'loc-args',
304 titleLocKey: 'title-loc-key',
305 titleLocArgs: 'title-loc-args',
306 subtitleLocKey: 'subtitle-loc-key',
307 subtitleLocArgs: 'subtitle-loc-args',
308 actionLocKey: 'action-loc-key',
309 launchImage: 'launch-image',
310 };
311 (0, index_1.renameProperties)(apsAlert, propertyMappings);
312}
313/**
314 * Checks if the given AndroidConfig object is valid. The object must have valid ttl, data,
315 * and notification fields. If successful, transforms the input object by renaming keys to valid
316 * Android keys. Also transforms the ttl value to the format expected by FCM service.
317 *
318 * @param config - An object to be validated.
319 */
320function validateAndroidConfig(config) {
321 if (typeof config === 'undefined') {
322 return;
323 }
324 else if (!validator.isNonNullObject(config)) {
325 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android must be a non-null object');
326 }
327 if (typeof config.ttl !== 'undefined') {
328 if (!validator.isNumber(config.ttl) || config.ttl < 0) {
329 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'TTL must be a non-negative duration in milliseconds');
330 }
331 const duration = (0, index_1.transformMillisecondsToSecondsString)(config.ttl);
332 config.ttl = duration;
333 }
334 validateStringMap(config.data, 'android.data');
335 validateAndroidNotification(config.notification);
336 validateAndroidFcmOptions(config.fcmOptions);
337 const propertyMappings = {
338 collapseKey: 'collapse_key',
339 restrictedPackageName: 'restricted_package_name',
340 };
341 (0, index_1.renameProperties)(config, propertyMappings);
342}
343/**
344 * Checks if the given AndroidNotification object is valid. The object must have valid color and
345 * localization parameters. If successful, transforms the input object by renaming keys to valid
346 * Android keys.
347 *
348 * @param {AndroidNotification} notification An object to be validated.
349 */
350function validateAndroidNotification(notification) {
351 if (typeof notification === 'undefined') {
352 return;
353 }
354 else if (!validator.isNonNullObject(notification)) {
355 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification must be a non-null object');
356 }
357 if (typeof notification.color !== 'undefined' && !/^#[0-9a-fA-F]{6}$/.test(notification.color)) {
358 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.color must be in the form #RRGGBB');
359 }
360 if (validator.isNonEmptyArray(notification.bodyLocArgs) &&
361 !validator.isNonEmptyString(notification.bodyLocKey)) {
362 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.bodyLocKey is required when specifying bodyLocArgs');
363 }
364 if (validator.isNonEmptyArray(notification.titleLocArgs) &&
365 !validator.isNonEmptyString(notification.titleLocKey)) {
366 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.titleLocKey is required when specifying titleLocArgs');
367 }
368 if (typeof notification.imageUrl !== 'undefined' &&
369 !validator.isURL(notification.imageUrl)) {
370 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.imageUrl must be a valid URL string');
371 }
372 if (typeof notification.eventTimestamp !== 'undefined') {
373 if (!(notification.eventTimestamp instanceof Date)) {
374 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.eventTimestamp must be a valid `Date` object');
375 }
376 // Convert timestamp to RFC3339 UTC "Zulu" format, example "2014-10-02T15:01:23.045123456Z"
377 const zuluTimestamp = notification.eventTimestamp.toISOString();
378 notification.eventTimestamp = zuluTimestamp;
379 }
380 if (typeof notification.vibrateTimingsMillis !== 'undefined') {
381 if (!validator.isNonEmptyArray(notification.vibrateTimingsMillis)) {
382 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.vibrateTimingsMillis must be a non-empty array of numbers');
383 }
384 const vibrateTimings = [];
385 notification.vibrateTimingsMillis.forEach((value) => {
386 if (!validator.isNumber(value) || value < 0) {
387 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.vibrateTimingsMillis must be non-negative durations in milliseconds');
388 }
389 const duration = (0, index_1.transformMillisecondsToSecondsString)(value);
390 vibrateTimings.push(duration);
391 });
392 notification.vibrateTimingsMillis = vibrateTimings;
393 }
394 if (typeof notification.priority !== 'undefined') {
395 const priority = 'PRIORITY_' + notification.priority.toUpperCase();
396 notification.priority = priority;
397 }
398 if (typeof notification.visibility !== 'undefined') {
399 const visibility = notification.visibility.toUpperCase();
400 notification.visibility = visibility;
401 }
402 validateLightSettings(notification.lightSettings);
403 const propertyMappings = {
404 clickAction: 'click_action',
405 bodyLocKey: 'body_loc_key',
406 bodyLocArgs: 'body_loc_args',
407 titleLocKey: 'title_loc_key',
408 titleLocArgs: 'title_loc_args',
409 channelId: 'channel_id',
410 imageUrl: 'image',
411 eventTimestamp: 'event_time',
412 localOnly: 'local_only',
413 priority: 'notification_priority',
414 vibrateTimingsMillis: 'vibrate_timings',
415 defaultVibrateTimings: 'default_vibrate_timings',
416 defaultSound: 'default_sound',
417 lightSettings: 'light_settings',
418 defaultLightSettings: 'default_light_settings',
419 notificationCount: 'notification_count',
420 };
421 (0, index_1.renameProperties)(notification, propertyMappings);
422}
423/**
424 * Checks if the given LightSettings object is valid. The object must have valid color and
425 * light on/off duration parameters. If successful, transforms the input object by renaming
426 * keys to valid Android keys.
427 *
428 * @param {LightSettings} lightSettings An object to be validated.
429 */
430function validateLightSettings(lightSettings) {
431 if (typeof lightSettings === 'undefined') {
432 return;
433 }
434 else if (!validator.isNonNullObject(lightSettings)) {
435 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings must be a non-null object');
436 }
437 if (!validator.isNumber(lightSettings.lightOnDurationMillis) || lightSettings.lightOnDurationMillis < 0) {
438 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds');
439 }
440 const durationOn = (0, index_1.transformMillisecondsToSecondsString)(lightSettings.lightOnDurationMillis);
441 lightSettings.lightOnDurationMillis = durationOn;
442 if (!validator.isNumber(lightSettings.lightOffDurationMillis) || lightSettings.lightOffDurationMillis < 0) {
443 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings.lightOffDurationMillis must be a non-negative duration in milliseconds');
444 }
445 const durationOff = (0, index_1.transformMillisecondsToSecondsString)(lightSettings.lightOffDurationMillis);
446 lightSettings.lightOffDurationMillis = durationOff;
447 if (!validator.isString(lightSettings.color) ||
448 (!/^#[0-9a-fA-F]{6}$/.test(lightSettings.color) && !/^#[0-9a-fA-F]{8}$/.test(lightSettings.color))) {
449 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings.color must be in the form #RRGGBB or #RRGGBBAA format');
450 }
451 const colorString = lightSettings.color.length === 7 ? lightSettings.color + 'FF' : lightSettings.color;
452 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);
453 if (!rgb || rgb.length < 4) {
454 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INTERNAL_ERROR, 'regex to extract rgba values from ' + colorString + ' failed.');
455 }
456 const color = {
457 red: parseInt(rgb[1], 16) / 255.0,
458 green: parseInt(rgb[2], 16) / 255.0,
459 blue: parseInt(rgb[3], 16) / 255.0,
460 alpha: parseInt(rgb[4], 16) / 255.0,
461 };
462 lightSettings.color = color;
463 const propertyMappings = {
464 lightOnDurationMillis: 'light_on_duration',
465 lightOffDurationMillis: 'light_off_duration',
466 };
467 (0, index_1.renameProperties)(lightSettings, propertyMappings);
468}
469/**
470 * Checks if the given AndroidFcmOptions object is valid.
471 *
472 * @param {AndroidFcmOptions} fcmOptions An object to be validated.
473 */
474function validateAndroidFcmOptions(fcmOptions) {
475 if (typeof fcmOptions === 'undefined') {
476 return;
477 }
478 else if (!validator.isNonNullObject(fcmOptions)) {
479 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object');
480 }
481 if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) {
482 throw new error_1.FirebaseMessagingError(error_1.MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value');
483 }
484}