/* * 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 "FIRMessagingPubSub.h" #import "FIRMessaging.h" #import "FIRMessagingClient.h" #import "FIRMessagingDefines.h" #import "FIRMessagingLogger.h" #import "FIRMessagingPendingTopicsList.h" #import "FIRMessagingUtilities.h" #import "FIRMessaging_Private.h" #import "NSDictionary+FIRMessaging.h" #import "NSError+FIRMessaging.h" static NSString *const kPendingSubscriptionsListKey = @"com.firebase.messaging.pending-subscriptions"; @interface FIRMessagingPubSub () @property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates; @property(nonatomic, readwrite, strong) FIRMessagingClient *client; @end @implementation FIRMessagingPubSub - (instancetype)init { FIRMessagingInvalidateInitializer(); // Need this to disable an Xcode warning. return [self initWithClient:nil]; } - (instancetype)initWithClient:(FIRMessagingClient *)client { self = [super init]; if (self) { _client = client; [self restorePendingTopicsList]; } return self; } - (void)subscribeWithToken:(NSString *)token topic:(NSString *)topic options:(NSDictionary *)options handler:(FIRMessagingTopicOperationCompletion)handler { _FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified"); _FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified"); if (!self.client) { handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]); return; } token = [token copy]; topic = [topic copy]; if (![options count]) { options = @{}; } if (![[self class] isValidTopicWithPrefix:topic]) { FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000, @"Invalid FIRMessaging Pubsub topic %@", topic); handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]); 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.client updateSubscriptionWithToken:token topic:topic options:options shouldDelete:NO handler:^void(NSError *error) { handler(error); }]; } - (void)unsubscribeWithToken:(NSString *)token topic:(NSString *)topic options:(NSDictionary *)options handler:(FIRMessagingTopicOperationCompletion)handler { _FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified"); _FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified"); if (!self.client) { handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]); return; } token = [token copy]; topic = [topic copy]; if (![options count]) { options = @{}; } if (![[self class] isValidTopicWithPrefix:topic]) { FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002, @"Invalid FIRMessaging Pubsub topic %@", topic); handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]); 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.client 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 = [[FIRMessaging messaging] 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 = [[FIRMessaging messaging] 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 = [[FIRMessaging messaging] defaultFcmToken]; return (fcmToken.length > 0); } #pragma mark - Storing Pending Topics - (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSData *pendingData = [NSKeyedArchiver archivedDataWithRootObject:topicsList]; [defaults setObject:pendingData forKey:kPendingSubscriptionsListKey]; [defaults synchronize]; } - (void)restorePendingTopicsList { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey]; FIRMessagingPendingTopicsList *subscriptions; @try { if (pendingData) { subscriptions = [NSKeyedUnarchiver unarchiveObjectWithData:pendingData]; } } @catch (NSException *exception) { // Nothing we can do, just continue as if we don't have pending subscriptions } @finally { 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 occurences of topic names and sender IDs in the sender. * * @param expression 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