/* * Copyright 2019 Google * * 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. */ #import #import #import #import #import #import "FirebaseMessaging/Sources/FIRMessagingCode.h" #import "FirebaseMessaging/Sources/FIRMessagingConstants.h" #import "FirebaseMessaging/Sources/FIRMessagingLogger.h" #import "FirebaseMessaging/Sources/Protogen/nanopb/me.nanopb.h" #import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessagingExtensionHelper.h" static NSString *const kPayloadOptionsName = @"fcm_options"; static NSString *const kPayloadOptionsImageURLName = @"image"; static NSString *const kNoExtension = @""; static NSString *const kImagePathPrefix = @"image/"; #pragma mark - nanopb helper functions /** Callocs a pb_bytes_array and copies the given NSData bytes into the bytes array. * * @note Memory needs to be free manually, through pb_free or pb_release. * @param data The data to copy into the new bytes array. */ pb_bytes_array_t *FIRMessagingEncodeData(NSData *data) { pb_bytes_array_t *pbBytesArray = calloc(1, PB_BYTES_ARRAY_T_ALLOCSIZE(data.length)); if (pbBytesArray != NULL) { [data getBytes:pbBytesArray->bytes length:data.length]; pbBytesArray->size = (pb_size_t)data.length; } return pbBytesArray; } /** Callocs a pb_bytes_array and copies the given NSString's bytes into the bytes array. * * @note Memory needs to be free manually, through pb_free or pb_release. * @param string The string to encode as pb_bytes. */ pb_bytes_array_t *FIRMessagingEncodeString(NSString *string) { NSData *stringBytes = [string dataUsingEncoding:NSUTF8StringEncoding]; return FIRMessagingEncodeData(stringBytes); } @interface FIRMessagingMetricsLog : NSObject @property(nonatomic) fm_MessagingClientEventExtension eventExtension; @end @implementation FIRMessagingMetricsLog - (instancetype)initWithEventExtension:(fm_MessagingClientEventExtension)eventExtension { self = [super init]; if (self) { _eventExtension = eventExtension; } return self; } - (NSData *)transportBytes { pb_ostream_t sizestream = PB_OSTREAM_SIZING; // Encode 1 time to determine the size. if (!pb_encode(&sizestream, fm_MessagingClientEventExtension_fields, &_eventExtension)) { FIRMessagingLoggerError(kFIRMessagingServiceExtensionTransportBytesError, @"Error in nanopb encoding for size: %s", PB_GET_ERROR(&sizestream)); } // Encode a 2nd time to actually get the bytes from it. size_t bufferSize = sizestream.bytes_written; CFMutableDataRef dataRef = CFDataCreateMutable(CFAllocatorGetDefault(), bufferSize); CFDataSetLength(dataRef, bufferSize); pb_ostream_t ostream = pb_ostream_from_buffer((void *)CFDataGetBytePtr(dataRef), bufferSize); if (!pb_encode(&ostream, fm_MessagingClientEventExtension_fields, &_eventExtension)) { FIRMessagingLoggerError(kFIRMessagingServiceExtensionTransportBytesError, @"Error in nanopb encoding for bytes: %s", PB_GET_ERROR(&ostream)); } CFDataSetLength(dataRef, ostream.bytes_written); return CFBridgingRelease(dataRef); } @end @interface FIRMessagingExtensionHelper () @property(nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver); @property(nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; @end @implementation FIRMessagingExtensionHelper - (void)populateNotificationContent:(UNMutableNotificationContent *)content withContentHandler:(void (^)(UNNotificationContent *_Nonnull))contentHandler { self.contentHandler = [contentHandler copy]; self.bestAttemptContent = content; // The `userInfo` property isn't available on newer versions of tvOS. #if !TARGET_OS_TV NSObject *currentImageURL = content.userInfo[kPayloadOptionsName][kPayloadOptionsImageURLName]; if (!currentImageURL || currentImageURL == [NSNull null]) { [self deliverNotification]; return; } NSURL *attachmentURL = [NSURL URLWithString:(NSString *)currentImageURL]; if (attachmentURL) { [self loadAttachmentForURL:attachmentURL completionHandler:^(UNNotificationAttachment *attachment) { if (attachment != nil) { self.bestAttemptContent.attachments = @[ attachment ]; } [self deliverNotification]; }]; } else { FIRMessagingLoggerError(kFIRMessagingServiceExtensionImageInvalidURL, @"The Image URL provided is invalid %@.", currentImageURL); [self deliverNotification]; } #else // !TARGET_OS_TV [self deliverNotification]; #endif // !TARGET_OS_TV } #if !TARGET_OS_TV - (NSString *)fileExtensionForResponse:(NSURLResponse *)response { NSString *suggestedPathExtension = [response.suggestedFilename pathExtension]; if (suggestedPathExtension.length > 0) { return [NSString stringWithFormat:@".%@", suggestedPathExtension]; } if ([response.MIMEType containsString:kImagePathPrefix]) { return [response.MIMEType stringByReplacingOccurrencesOfString:kImagePathPrefix withString:@"."]; } return kNoExtension; } - (void)loadAttachmentForURL:(NSURL *)attachmentURL completionHandler:(void (^)(UNNotificationAttachment *))completionHandler { __block UNNotificationAttachment *attachment = nil; NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; [[session downloadTaskWithURL:attachmentURL completionHandler:^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) { if (error != nil) { FIRMessagingLoggerError(kFIRMessagingServiceExtensionImageNotDownloaded, @"Failed to download image given URL %@, error: %@\n", attachmentURL, error); completionHandler(attachment); return; } NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *fileExtension = [self fileExtensionForResponse:response]; NSURL *localURL = [NSURL fileURLWithPath:[temporaryFileLocation.path stringByAppendingString:fileExtension]]; [fileManager moveItemAtURL:temporaryFileLocation toURL:localURL error:&error]; if (error) { FIRMessagingLoggerError( kFIRMessagingServiceExtensionLocalFileNotCreated, @"Failed to move the image file to local location: %@, error: %@\n", localURL, error); completionHandler(attachment); return; } attachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:localURL options:nil error:&error]; if (error) { FIRMessagingLoggerError(kFIRMessagingServiceExtensionImageNotAttached, @"Failed to create attachment with URL %@, error: %@\n", localURL, error); completionHandler(attachment); return; } completionHandler(attachment); }] resume]; } #endif // !TARGET_OS_TV - (void)deliverNotification { if (self.contentHandler) { self.contentHandler(self.bestAttemptContent); } } - (void)exportDeliveryMetricsToBigQueryWithMessageInfo:(NSDictionary *)info { GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"1249" transformers:nil target:kGDTCORTargetCCT]; fm_MessagingClientEventExtension eventExtension = fm_MessagingClientEventExtension_init_default; fm_MessagingClientEvent clientEvent = fm_MessagingClientEvent_init_default; if (!info[kFIRMessagingSenderID]) { FIRMessagingLoggerError(kFIRMessagingServiceExtensionInvalidProjectID, @"Delivery logging failed: Invalid project ID"); return; } clientEvent.project_number = (int64_t)[info[kFIRMessagingSenderID] longLongValue]; if (!info[kFIRMessagingMessageIDKey] || ![info[kFIRMessagingMessageIDKey] isKindOfClass:NSString.class]) { FIRMessagingLoggerWarn(kFIRMessagingServiceExtensionInvalidMessageID, @"Delivery logging failed: Invalid Message ID"); return; } clientEvent.message_id = FIRMessagingEncodeString(info[kFIRMessagingMessageIDKey]); if (!info[kFIRMessagingFID] || ![info[kFIRMessagingFID] isKindOfClass:NSString.class]) { FIRMessagingLoggerWarn(kFIRMessagingServiceExtensionInvalidInstanceID, @"Delivery logging failed: Invalid Instance ID"); return; } clientEvent.instance_id = FIRMessagingEncodeString(info[kFIRMessagingFID]); if ([info[@"aps"][kFIRMessagingMessageAPNSContentAvailableKey] intValue] == 1 && ![GULAppEnvironmentUtil isAppExtension]) { clientEvent.message_type = fm_MessagingClientEvent_MessageType_DATA_MESSAGE; } else { clientEvent.message_type = fm_MessagingClientEvent_MessageType_DISPLAY_NOTIFICATION; } clientEvent.sdk_platform = fm_MessagingClientEvent_SDKPlatform_IOS; NSString *bundleID = [NSBundle mainBundle].bundleIdentifier; if ([GULAppEnvironmentUtil isAppExtension]) { bundleID = [[self class] bundleIdentifierByRemovingLastPartFrom:bundleID]; } if (bundleID) { clientEvent.package_name = FIRMessagingEncodeString(bundleID); } clientEvent.event = fm_MessagingClientEvent_Event_MESSAGE_DELIVERED; if (info[kFIRMessagingAnalyticsMessageLabel]) { clientEvent.analytics_label = FIRMessagingEncodeString(info[kFIRMessagingAnalyticsMessageLabel]); } if (info[kFIRMessagingAnalyticsComposerIdentifier]) { clientEvent.campaign_id = (int64_t)[info[kFIRMessagingAnalyticsComposerIdentifier] longLongValue]; } if (info[kFIRMessagingAnalyticsComposerLabel]) { clientEvent.composer_label = FIRMessagingEncodeString(info[kFIRMessagingAnalyticsComposerLabel]); } eventExtension.messaging_client_event = &clientEvent; FIRMessagingMetricsLog *log = [[FIRMessagingMetricsLog alloc] initWithEventExtension:eventExtension]; GDTCOREvent *event; if (info[kFIRMessagingProductID]) { int32_t productID = [info[kFIRMessagingProductID] intValue]; GDTCORProductData *productData = [[GDTCORProductData alloc] initWithProductID:productID]; event = [transport eventForTransportWithProductData:productData]; } else { event = [transport eventForTransport]; } event.dataObject = log; event.qosTier = GDTCOREventQoSFast; // Use this API for SDK service data events. [transport sendDataEvent:event]; } + (NSString *)bundleIdentifierByRemovingLastPartFrom:(NSString *)bundleIdentifier { NSString *bundleIDComponentsSeparator = @"."; NSMutableArray *bundleIDComponents = [[bundleIdentifier componentsSeparatedByString:bundleIDComponentsSeparator] mutableCopy]; [bundleIDComponents removeLastObject]; return [bundleIDComponents componentsJoinedByString:bundleIDComponentsSeparator]; } @end