2 * Copyright 2017 Google
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #import "FIRMessagingPubSub.h"
19 #import "FIRMessaging.h"
20 #import "FIRMessagingClient.h"
21 #import "FIRMessagingDefines.h"
22 #import "FIRMessagingLogger.h"
23 #import "FIRMessagingPendingTopicsList.h"
24 #import "FIRMessagingUtilities.h"
25 #import "FIRMessaging_Private.h"
26 #import "NSDictionary+FIRMessaging.h"
27 #import "NSError+FIRMessaging.h"
29 static NSString *const kPendingSubscriptionsListKey =
30 @"com.firebase.messaging.pending-subscriptions";
32 @interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>
34 @property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;
35 @property(nonatomic, readwrite, strong) FIRMessagingClient *client;
39 @implementation FIRMessagingPubSub
41 - (instancetype)init {
42 FIRMessagingInvalidateInitializer();
43 // Need this to disable an Xcode warning.
44 return [self initWithClient:nil];
47 - (instancetype)initWithClient:(FIRMessagingClient *)client {
51 [self restorePendingTopicsList];
56 - (void)subscribeWithToken:(NSString *)token
57 topic:(NSString *)topic
58 options:(NSDictionary *)options
59 handler:(FIRMessagingTopicOperationCompletion)handler {
60 _FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified");
61 _FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified");
63 handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
70 if (![options count]) {
74 if (![[self class] isValidTopicWithPrefix:topic]) {
75 FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000,
76 @"Invalid FIRMessaging Pubsub topic %@", topic);
77 handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
81 if (![self verifyPubSubOptions:options]) {
82 // we do not want to quit even if options have some invalid values.
83 FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub001,
84 @"Invalid options passed to FIRMessagingPubSub with non-string keys or "
87 // copy the dictionary would trim non-string keys or values if any.
88 options = [options fcm_trimNonStringValues];
90 [self.client updateSubscriptionWithToken:token
94 handler:^void(NSError *error) {
99 - (void)unsubscribeWithToken:(NSString *)token
100 topic:(NSString *)topic
101 options:(NSDictionary *)options
102 handler:(FIRMessagingTopicOperationCompletion)handler {
103 _FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified");
104 _FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified");
107 handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
111 token = [token copy];
112 topic = [topic copy];
113 if (![options count]) {
117 if (![[self class] isValidTopicWithPrefix:topic]) {
118 FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002,
119 @"Invalid FIRMessaging Pubsub topic %@", topic);
120 handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
123 if (![self verifyPubSubOptions:options]) {
124 // we do not want to quit even if options have some invalid values.
125 FIRMessagingLoggerError(
126 kFIRMessagingMessageCodePubSub003,
127 @"Invalid options passed to FIRMessagingPubSub with non-string keys or values.");
129 // copy the dictionary would trim non-string keys or values if any.
130 options = [options fcm_trimNonStringValues];
132 [self.client updateSubscriptionWithToken:token
136 handler:^void(NSError *error) {
141 - (void)subscribeToTopic:(NSString *)topic
142 handler:(nullable FIRMessagingTopicOperationCompletion)handler {
143 [self.pendingTopicUpdates addOperationForTopic:topic
144 withAction:FIRMessagingTopicActionSubscribe
148 - (void)unsubscribeFromTopic:(NSString *)topic
149 handler:(nullable FIRMessagingTopicOperationCompletion)handler {
150 [self.pendingTopicUpdates addOperationForTopic:topic
151 withAction:FIRMessagingTopicActionUnsubscribe
155 - (void)scheduleSync:(BOOL)immediately {
156 NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
157 if (fcmToken.length) {
158 [self.pendingTopicUpdates resumeOperationsIfNeeded];
162 #pragma mark - FIRMessagingPendingTopicsListDelegate
164 - (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
165 requestedUpdateForTopic:(NSString *)topic
166 action:(FIRMessagingTopicAction)action
167 completion:(FIRMessagingTopicOperationCompletion)completion {
169 NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
170 if (action == FIRMessagingTopicActionSubscribe) {
171 [self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];
173 [self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];
177 - (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
178 [self archivePendingTopicsList:list];
181 - (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
182 NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
183 return (fcmToken.length > 0);
186 #pragma mark - Storing Pending Topics
188 - (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList {
189 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
190 NSData *pendingData = [NSKeyedArchiver archivedDataWithRootObject:topicsList];
191 [defaults setObject:pendingData forKey:kPendingSubscriptionsListKey];
192 [defaults synchronize];
195 - (void)restorePendingTopicsList {
196 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
197 NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey];
198 FIRMessagingPendingTopicsList *subscriptions;
201 subscriptions = [NSKeyedUnarchiver unarchiveObjectWithData:pendingData];
203 } @catch (NSException *exception) {
204 // Nothing we can do, just continue as if we don't have pending subscriptions
207 self.pendingTopicUpdates = subscriptions;
209 self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init];
211 self.pendingTopicUpdates.delegate = self;
215 #pragma mark - Private Helpers
217 - (BOOL)verifyPubSubOptions:(NSDictionary *)options {
218 return ![options fcm_hasNonStringKeysOrValues];
221 #pragma mark - Topic Name Helpers
223 static NSString *const kTopicsPrefix = @"/topics/";
224 static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)";
226 + (NSString *)addPrefixToTopic:(NSString *)topic {
227 if (![self hasTopicsPrefix:topic]) {
228 return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic];
234 + (NSString *)removePrefixFromTopic:(NSString *)topic {
235 if ([self hasTopicsPrefix:topic]) {
236 return [topic substringFromIndex:kTopicsPrefix.length];
242 + (BOOL)hasTopicsPrefix:(NSString *)topic {
243 return [topic hasPrefix:kTopicsPrefix];
247 * Returns a regular expression for matching a topic sender.
249 * @return The topic matching regular expression
251 + (NSRegularExpression *)topicRegex {
252 // Since this is a static regex pattern, we only only need to declare it once.
253 static NSRegularExpression *topicRegex;
254 static dispatch_once_t onceToken;
255 dispatch_once(&onceToken, ^{
258 [NSRegularExpression regularExpressionWithPattern:kTopicRegexPattern
259 options:NSRegularExpressionAnchorsMatchLines
266 * Gets the class describing occurences of topic names and sender IDs in the sender.
268 * @param expression The topic expression used to generate a pubsub topic
270 * @return Representation of captured subexpressions in topic regular expression
272 + (BOOL)isValidTopicWithPrefix:(NSString *)topic {
273 NSRange topicRange = NSMakeRange(0, topic.length);
274 NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topic
275 options:NSMatchingAnchored
277 return NSEqualRanges(topicRange, regexMatchRange);