/* * Copyright 2017 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 "FirebaseMessaging/Sources/FIRMessagingPubSub.h" #import #import "FirebaseMessaging/Sources/FIRMessagingDefines.h" #import "FirebaseMessaging/Sources/FIRMessagingLogger.h" #import "FirebaseMessaging/Sources/FIRMessagingPendingTopicsList.h" #import "FirebaseMessaging/Sources/FIRMessagingTopicOperation.h" #import "FirebaseMessaging/Sources/FIRMessagingTopicsCommon.h" #import "FirebaseMessaging/Sources/FIRMessagingUtilities.h" #import "FirebaseMessaging/Sources/FIRMessaging_Private.h" #import "FirebaseMessaging/Sources/NSDictionary+FIRMessaging.h" #import "FirebaseMessaging/Sources/NSError+FIRMessaging.h" #import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h" #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenManager.h" static NSString *const kPendingSubscriptionsListKey = @"com.firebase.messaging.pending-subscriptions"; @interface FIRMessagingPubSub () @property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates; @property(nonatomic, readonly, strong) NSOperationQueue *topicOperations; // Common errors, instantiated, to avoid generating multiple copies @property(nonatomic, readwrite, strong) NSError *operationInProgressError; @property(nonatomic, readwrite, strong) FIRMessagingTokenManager *tokenManager; @end @implementation FIRMessagingPubSub - (instancetype)initWithTokenManager:(FIRMessagingTokenManager *)tokenManager { self = [super init]; if (self) { _topicOperations = [[NSOperationQueue alloc] init]; // Do 10 topic operations at a time; it's enough to keep the TCP connection to the host alive, // saving hundreds of milliseconds on each request (compared to a serial queue). _topicOperations.maxConcurrentOperationCount = 10; _tokenManager = tokenManager; [self restorePendingTopicsList]; } return self; } - (void)subscribeWithToken:(NSString *)token topic:(NSString *)topic options:(NSDictionary *)options handler:(FIRMessagingTopicOperationCompletion)handler { token = [token copy]; topic = [topic copy]; if (![options count]) { options = @{}; } if (![[self class] isValidTopicWithPrefix:topic]) { NSString *failureReason = [NSString stringWithFormat:@"Invalid subscription topic :'%@'", topic]; FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000, @"%@", failureReason); handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName failureReason:failureReason]); return; } if (![self verifyPubSubOptions:options]) { // we do not want to quit even if options have some invalid values. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub001, @"Invalid options passed to FIRMessagingPubSub with non-string keys or " "values."); } // copy the dictionary would trim non-string keys or values if any. options = [options fcm_trimNonStringValues]; [self updateSubscriptionWithToken:token topic:topic options:options shouldDelete:NO handler:handler]; } - (void)dealloc { [self.topicOperations cancelAllOperations]; } #pragma mark - FIRMessaging subscribe - (void)updateSubscriptionWithToken:(NSString *)token topic:(NSString *)topic options:(NSDictionary *)options shouldDelete:(BOOL)shouldDelete handler:(FIRMessagingTopicOperationCompletion)handler { if ([_tokenManager hasValidCheckinInfo]) { FIRMessagingTopicAction action = shouldDelete ? FIRMessagingTopicActionUnsubscribe : FIRMessagingTopicActionSubscribe; FIRMessagingTopicOperation *operation = [[FIRMessagingTopicOperation alloc] initWithTopic:topic action:action tokenManager:_tokenManager options:options completion:^(NSError *_Nullable error) { if (error) { FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001, @"Failed to subscribe to topic %@", error); } else { if (shouldDelete) { FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002, @"Successfully unsubscribed from topic %@", topic); } else { FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003, @"Successfully subscribed to topic %@", topic); } } if (handler) { handler(error); } }]; [self.topicOperations addOperation:operation]; } else { NSString *failureReason = @"Device ID and checkin info is not found. Will not proceed with " @"subscription/unsubscription."; FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRegistrar000, @"%@", failureReason); NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeMissingDeviceID failureReason:failureReason]; handler(error); } } - (void)unsubscribeWithToken:(NSString *)token topic:(NSString *)topic options:(NSDictionary *)options handler:(FIRMessagingTopicOperationCompletion)handler { token = [token copy]; topic = [topic copy]; if (![options count]) { options = @{}; } if (![[self class] isValidTopicWithPrefix:topic]) { NSString *failureReason = [NSString stringWithFormat:@"Invalid topic name : '%@' for unsubscription.", topic]; FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002, @"%@", failureReason); handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName failureReason:failureReason]); return; } if (![self verifyPubSubOptions:options]) { // we do not want to quit even if options have some invalid values. FIRMessagingLoggerError( kFIRMessagingMessageCodePubSub003, @"Invalid options passed to FIRMessagingPubSub with non-string keys or values."); } // copy the dictionary would trim non-string keys or values if any. options = [options fcm_trimNonStringValues]; [self updateSubscriptionWithToken:token topic:topic options:options shouldDelete:YES handler:^void(NSError *error) { handler(error); }]; } - (void)subscribeToTopic:(NSString *)topic handler:(nullable FIRMessagingTopicOperationCompletion)handler { [self.pendingTopicUpdates addOperationForTopic:topic withAction:FIRMessagingTopicActionSubscribe completion:handler]; } - (void)unsubscribeFromTopic:(NSString *)topic handler:(nullable FIRMessagingTopicOperationCompletion)handler { [self.pendingTopicUpdates addOperationForTopic:topic withAction:FIRMessagingTopicActionUnsubscribe completion:handler]; } - (void)scheduleSync:(BOOL)immediately { NSString *fcmToken = _tokenManager.defaultFCMToken; if (fcmToken.length) { [self.pendingTopicUpdates resumeOperationsIfNeeded]; } } #pragma mark - FIRMessagingPendingTopicsListDelegate - (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list requestedUpdateForTopic:(NSString *)topic action:(FIRMessagingTopicAction)action completion:(FIRMessagingTopicOperationCompletion)completion { NSString *fcmToken = _tokenManager.defaultFCMToken; if (action == FIRMessagingTopicActionSubscribe) { [self subscribeWithToken:fcmToken topic:topic options:nil handler:completion]; } else { [self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion]; } } - (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list { [self archivePendingTopicsList:list]; } - (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list { NSString *fcmToken = _tokenManager.defaultFCMToken; return (fcmToken.length > 0); } #pragma mark - Storing Pending Topics - (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList { GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults]; NSError *error; NSData *pendingData = [NSKeyedArchiver archivedDataWithRootObject:topicsList requiringSecureCoding:YES error:&error]; if (error) { FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubArchiveError, @"Failed to archive topic list data %@", error); return; } [defaults setObject:pendingData forKey:kPendingSubscriptionsListKey]; } - (void)restorePendingTopicsList { GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults]; NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey]; FIRMessagingPendingTopicsList *subscriptions; if (pendingData) { NSError *error; subscriptions = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:FIRMessagingPendingTopicsList.class, nil] fromData:pendingData error:&error]; if (error) { FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubUnarchiveError, @"Failed to unarchive topic list data %@", error); } } if (subscriptions) { self.pendingTopicUpdates = subscriptions; } else { self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init]; } self.pendingTopicUpdates.delegate = self; } #pragma mark - Private Helpers - (BOOL)verifyPubSubOptions:(NSDictionary *)options { return ![options fcm_hasNonStringKeysOrValues]; } #pragma mark - Topic Name Helpers static NSString *const kTopicsPrefix = @"/topics/"; static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)"; + (NSString *)addPrefixToTopic:(NSString *)topic { if (![self hasTopicsPrefix:topic]) { return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic]; } else { return [topic copy]; } } + (NSString *)removePrefixFromTopic:(NSString *)topic { if ([self hasTopicsPrefix:topic]) { return [topic substringFromIndex:kTopicsPrefix.length]; } else { return [topic copy]; } } + (BOOL)hasTopicsPrefix:(NSString *)topic { return [topic hasPrefix:kTopicsPrefix]; } /** * Returns a regular expression for matching a topic sender. * * @return The topic matching regular expression */ + (NSRegularExpression *)topicRegex { // Since this is a static regex pattern, we only only need to declare it once. static NSRegularExpression *topicRegex; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSError *error; topicRegex = [NSRegularExpression regularExpressionWithPattern:kTopicRegexPattern options:NSRegularExpressionAnchorsMatchLines error:&error]; }); return topicRegex; } /** * Gets the class describing occurrences of topic names and sender IDs in the sender. * * @param topic The topic expression used to generate a pubsub topic * * @return Representation of captured subexpressions in topic regular expression */ + (BOOL)isValidTopicWithPrefix:(NSString *)topic { NSRange topicRange = NSMakeRange(0, topic.length); NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topic options:NSMatchingAnchored range:topicRange]; return NSEqualRanges(topicRange, regexMatchRange); } @end