added iOS source code
[wl-app.git] / iOS / Pods / FirebaseMessaging / Firebase / Messaging / FIRMessagingPubSub.m
1 /*
2  * Copyright 2017 Google
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 #import "FIRMessagingPubSub.h"
18
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"
28
29 static NSString *const kPendingSubscriptionsListKey =
30     @"com.firebase.messaging.pending-subscriptions";
31
32 @interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>
33
34 @property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;
35 @property(nonatomic, readwrite, strong) FIRMessagingClient *client;
36
37 @end
38
39 @implementation FIRMessagingPubSub
40
41 - (instancetype)init {
42   FIRMessagingInvalidateInitializer();
43   // Need this to disable an Xcode warning.
44   return [self initWithClient:nil];
45 }
46
47 - (instancetype)initWithClient:(FIRMessagingClient *)client {
48   self = [super init];
49   if (self) {
50     _client = client;
51     [self restorePendingTopicsList];
52   }
53   return self;
54 }
55
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");
62   if (!self.client) {
63     handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
64     return;
65   }
66
67   token = [token copy];
68   topic = [topic copy];
69
70   if (![options count]) {
71     options = @{};
72   }
73
74   if (![[self class] isValidTopicWithPrefix:topic]) {
75     FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000,
76                             @"Invalid FIRMessaging Pubsub topic %@", topic);
77     handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
78     return;
79   }
80
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 "
85                              "values.");
86   }
87   // copy the dictionary would trim non-string keys or values if any.
88   options = [options fcm_trimNonStringValues];
89
90   [self.client updateSubscriptionWithToken:token
91                                      topic:topic
92                                    options:options
93                               shouldDelete:NO
94                                    handler:^void(NSError *error) {
95                                      handler(error);
96                                    }];
97 }
98
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");
105
106   if (!self.client) {
107     handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
108     return;
109   }
110
111   token = [token copy];
112   topic = [topic copy];
113   if (![options count]) {
114     options = @{};
115   }
116
117   if (![[self class] isValidTopicWithPrefix:topic]) {
118     FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002,
119                             @"Invalid FIRMessaging Pubsub topic %@", topic);
120     handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
121     return;
122   }
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.");
128   }
129   // copy the dictionary would trim non-string keys or values if any.
130   options = [options fcm_trimNonStringValues];
131
132   [self.client updateSubscriptionWithToken:token
133                                      topic:topic
134                                    options:options
135                               shouldDelete:YES
136                                    handler:^void(NSError *error) {
137                                      handler(error);
138                                    }];
139 }
140
141 - (void)subscribeToTopic:(NSString *)topic
142                  handler:(nullable FIRMessagingTopicOperationCompletion)handler {
143   [self.pendingTopicUpdates addOperationForTopic:topic
144                                       withAction:FIRMessagingTopicActionSubscribe
145                                       completion:handler];
146 }
147
148 - (void)unsubscribeFromTopic:(NSString *)topic
149                      handler:(nullable FIRMessagingTopicOperationCompletion)handler {
150   [self.pendingTopicUpdates addOperationForTopic:topic
151                                       withAction:FIRMessagingTopicActionUnsubscribe
152                                       completion:handler];
153 }
154
155 - (void)scheduleSync:(BOOL)immediately {
156   NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
157   if (fcmToken.length) {
158     [self.pendingTopicUpdates resumeOperationsIfNeeded];
159   }
160 }
161
162 #pragma mark - FIRMessagingPendingTopicsListDelegate
163
164 - (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
165   requestedUpdateForTopic:(NSString *)topic
166                    action:(FIRMessagingTopicAction)action
167                completion:(FIRMessagingTopicOperationCompletion)completion {
168
169   NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
170   if (action == FIRMessagingTopicActionSubscribe) {
171     [self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];
172   } else {
173     [self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];
174   }
175 }
176
177 - (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
178   [self archivePendingTopicsList:list];
179 }
180
181 - (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
182   NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
183   return (fcmToken.length > 0);
184 }
185
186 #pragma mark - Storing Pending Topics
187
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];
193 }
194
195 - (void)restorePendingTopicsList {
196   NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
197   NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey];
198   FIRMessagingPendingTopicsList *subscriptions;
199   @try {
200     if (pendingData) {
201       subscriptions = [NSKeyedUnarchiver unarchiveObjectWithData:pendingData];
202     }
203   } @catch (NSException *exception) {
204     // Nothing we can do, just continue as if we don't have pending subscriptions
205   } @finally {
206     if (subscriptions) {
207       self.pendingTopicUpdates = subscriptions;
208     } else {
209       self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init];
210     }
211     self.pendingTopicUpdates.delegate = self;
212   }
213 }
214
215 #pragma mark - Private Helpers
216
217 - (BOOL)verifyPubSubOptions:(NSDictionary *)options {
218   return ![options fcm_hasNonStringKeysOrValues];
219 }
220
221 #pragma mark - Topic Name Helpers
222
223 static NSString *const kTopicsPrefix = @"/topics/";
224 static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)";
225
226 + (NSString *)addPrefixToTopic:(NSString *)topic {
227   if (![self hasTopicsPrefix:topic]) {
228     return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic];
229   } else {
230     return [topic copy];
231   }
232 }
233
234 + (NSString *)removePrefixFromTopic:(NSString *)topic {
235   if ([self hasTopicsPrefix:topic]) {
236     return [topic substringFromIndex:kTopicsPrefix.length];
237   } else {
238     return [topic copy];
239   }
240 }
241
242 + (BOOL)hasTopicsPrefix:(NSString *)topic {
243   return [topic hasPrefix:kTopicsPrefix];
244 }
245
246 /**
247  *  Returns a regular expression for matching a topic sender.
248  *
249  *  @return The topic matching regular expression
250  */
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, ^{
256     NSError *error;
257     topicRegex =
258         [NSRegularExpression regularExpressionWithPattern:kTopicRegexPattern
259                                                   options:NSRegularExpressionAnchorsMatchLines
260                                                     error:&error];
261   });
262   return topicRegex;
263 }
264
265 /**
266  *  Gets the class describing occurences of topic names and sender IDs in the sender.
267  *
268  *  @param expression The topic expression used to generate a pubsub topic
269  *
270  *  @return Representation of captured subexpressions in topic regular expression
271  */
272 + (BOOL)isValidTopicWithPrefix:(NSString *)topic {
273   NSRange topicRange = NSMakeRange(0, topic.length);
274   NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topic
275                                                                  options:NSMatchingAnchored
276                                                                    range:topicRange];
277   return NSEqualRanges(topicRange, regexMatchRange);
278 }
279
280 @end