FIRMessagingPubSub.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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. #import "FirebaseMessaging/Sources/FIRMessagingPubSub.h"
  17. #import <GoogleUtilities/GULUserDefaults.h>
  18. #import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
  19. #import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
  20. #import "FirebaseMessaging/Sources/FIRMessagingPendingTopicsList.h"
  21. #import "FirebaseMessaging/Sources/FIRMessagingTopicOperation.h"
  22. #import "FirebaseMessaging/Sources/FIRMessagingTopicsCommon.h"
  23. #import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
  24. #import "FirebaseMessaging/Sources/FIRMessaging_Private.h"
  25. #import "FirebaseMessaging/Sources/NSDictionary+FIRMessaging.h"
  26. #import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
  27. #import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
  28. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenManager.h"
  29. static NSString *const kPendingSubscriptionsListKey =
  30. @"com.firebase.messaging.pending-subscriptions";
  31. @interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>
  32. @property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;
  33. @property(nonatomic, readonly, strong) NSOperationQueue *topicOperations;
  34. // Common errors, instantiated, to avoid generating multiple copies
  35. @property(nonatomic, readwrite, strong) NSError *operationInProgressError;
  36. @property(nonatomic, readwrite, strong) FIRMessagingTokenManager *tokenManager;
  37. @end
  38. @implementation FIRMessagingPubSub
  39. - (instancetype)initWithTokenManager:(FIRMessagingTokenManager *)tokenManager {
  40. self = [super init];
  41. if (self) {
  42. _topicOperations = [[NSOperationQueue alloc] init];
  43. // Do 10 topic operations at a time; it's enough to keep the TCP connection to the host alive,
  44. // saving hundreds of milliseconds on each request (compared to a serial queue).
  45. _topicOperations.maxConcurrentOperationCount = 10;
  46. _tokenManager = tokenManager;
  47. [self restorePendingTopicsList];
  48. }
  49. return self;
  50. }
  51. - (void)subscribeWithToken:(NSString *)token
  52. topic:(NSString *)topic
  53. options:(NSDictionary *)options
  54. handler:(FIRMessagingTopicOperationCompletion)handler {
  55. token = [token copy];
  56. topic = [topic copy];
  57. if (![options count]) {
  58. options = @{};
  59. }
  60. if (![[self class] isValidTopicWithPrefix:topic]) {
  61. NSString *failureReason =
  62. [NSString stringWithFormat:@"Invalid subscription topic :'%@'", topic];
  63. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000, @"%@", failureReason);
  64. handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName
  65. failureReason:failureReason]);
  66. return;
  67. }
  68. if (![self verifyPubSubOptions:options]) {
  69. // we do not want to quit even if options have some invalid values.
  70. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub001,
  71. @"Invalid options passed to FIRMessagingPubSub with non-string keys or "
  72. "values.");
  73. }
  74. // copy the dictionary would trim non-string keys or values if any.
  75. options = [options fcm_trimNonStringValues];
  76. [self updateSubscriptionWithToken:token
  77. topic:topic
  78. options:options
  79. shouldDelete:NO
  80. handler:handler];
  81. }
  82. - (void)dealloc {
  83. [self.topicOperations cancelAllOperations];
  84. }
  85. #pragma mark - FIRMessaging subscribe
  86. - (void)updateSubscriptionWithToken:(NSString *)token
  87. topic:(NSString *)topic
  88. options:(NSDictionary *)options
  89. shouldDelete:(BOOL)shouldDelete
  90. handler:(FIRMessagingTopicOperationCompletion)handler {
  91. if ([_tokenManager hasValidCheckinInfo]) {
  92. FIRMessagingTopicAction action =
  93. shouldDelete ? FIRMessagingTopicActionUnsubscribe : FIRMessagingTopicActionSubscribe;
  94. FIRMessagingTopicOperation *operation = [[FIRMessagingTopicOperation alloc]
  95. initWithTopic:topic
  96. action:action
  97. tokenManager:_tokenManager
  98. options:options
  99. completion:^(NSError *_Nullable error) {
  100. if (error) {
  101. FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001,
  102. @"Failed to subscribe to topic %@", error);
  103. } else {
  104. if (shouldDelete) {
  105. FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,
  106. @"Successfully unsubscribed from topic %@", topic);
  107. } else {
  108. FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,
  109. @"Successfully subscribed to topic %@", topic);
  110. }
  111. }
  112. if (handler) {
  113. handler(error);
  114. }
  115. }];
  116. [self.topicOperations addOperation:operation];
  117. } else {
  118. NSString *failureReason = @"Device ID and checkin info is not found. Will not proceed with "
  119. @"subscription/unsubscription.";
  120. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRegistrar000, @"%@", failureReason);
  121. NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeMissingDeviceID
  122. failureReason:failureReason];
  123. handler(error);
  124. }
  125. }
  126. - (void)unsubscribeWithToken:(NSString *)token
  127. topic:(NSString *)topic
  128. options:(NSDictionary *)options
  129. handler:(FIRMessagingTopicOperationCompletion)handler {
  130. token = [token copy];
  131. topic = [topic copy];
  132. if (![options count]) {
  133. options = @{};
  134. }
  135. if (![[self class] isValidTopicWithPrefix:topic]) {
  136. NSString *failureReason =
  137. [NSString stringWithFormat:@"Invalid topic name : '%@' for unsubscription.", topic];
  138. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002, @"%@", failureReason);
  139. handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName
  140. failureReason:failureReason]);
  141. return;
  142. }
  143. if (![self verifyPubSubOptions:options]) {
  144. // we do not want to quit even if options have some invalid values.
  145. FIRMessagingLoggerError(
  146. kFIRMessagingMessageCodePubSub003,
  147. @"Invalid options passed to FIRMessagingPubSub with non-string keys or values.");
  148. }
  149. // copy the dictionary would trim non-string keys or values if any.
  150. options = [options fcm_trimNonStringValues];
  151. [self updateSubscriptionWithToken:token
  152. topic:topic
  153. options:options
  154. shouldDelete:YES
  155. handler:^void(NSError *error) {
  156. handler(error);
  157. }];
  158. }
  159. - (void)subscribeToTopic:(NSString *)topic
  160. handler:(nullable FIRMessagingTopicOperationCompletion)handler {
  161. [self.pendingTopicUpdates addOperationForTopic:topic
  162. withAction:FIRMessagingTopicActionSubscribe
  163. completion:handler];
  164. }
  165. - (void)unsubscribeFromTopic:(NSString *)topic
  166. handler:(nullable FIRMessagingTopicOperationCompletion)handler {
  167. [self.pendingTopicUpdates addOperationForTopic:topic
  168. withAction:FIRMessagingTopicActionUnsubscribe
  169. completion:handler];
  170. }
  171. - (void)scheduleSync:(BOOL)immediately {
  172. NSString *fcmToken = _tokenManager.defaultFCMToken;
  173. if (fcmToken.length) {
  174. [self.pendingTopicUpdates resumeOperationsIfNeeded];
  175. }
  176. }
  177. #pragma mark - FIRMessagingPendingTopicsListDelegate
  178. - (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
  179. requestedUpdateForTopic:(NSString *)topic
  180. action:(FIRMessagingTopicAction)action
  181. completion:(FIRMessagingTopicOperationCompletion)completion {
  182. NSString *fcmToken = _tokenManager.defaultFCMToken;
  183. if (action == FIRMessagingTopicActionSubscribe) {
  184. [self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];
  185. } else {
  186. [self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];
  187. }
  188. }
  189. - (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
  190. [self archivePendingTopicsList:list];
  191. }
  192. - (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
  193. NSString *fcmToken = _tokenManager.defaultFCMToken;
  194. return (fcmToken.length > 0);
  195. }
  196. #pragma mark - Storing Pending Topics
  197. - (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList {
  198. GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];
  199. NSError *error;
  200. NSData *pendingData = [NSKeyedArchiver archivedDataWithRootObject:topicsList
  201. requiringSecureCoding:YES
  202. error:&error];
  203. if (error) {
  204. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubArchiveError,
  205. @"Failed to archive topic list data %@", error);
  206. return;
  207. }
  208. [defaults setObject:pendingData forKey:kPendingSubscriptionsListKey];
  209. }
  210. - (void)restorePendingTopicsList {
  211. GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];
  212. NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey];
  213. FIRMessagingPendingTopicsList *subscriptions;
  214. if (pendingData) {
  215. NSError *error;
  216. subscriptions = [NSKeyedUnarchiver
  217. unarchivedObjectOfClasses:[NSSet setWithObjects:FIRMessagingPendingTopicsList.class, nil]
  218. fromData:pendingData
  219. error:&error];
  220. if (error) {
  221. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubUnarchiveError,
  222. @"Failed to unarchive topic list data %@", error);
  223. }
  224. }
  225. if (subscriptions) {
  226. self.pendingTopicUpdates = subscriptions;
  227. } else {
  228. self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init];
  229. }
  230. self.pendingTopicUpdates.delegate = self;
  231. }
  232. #pragma mark - Private Helpers
  233. - (BOOL)verifyPubSubOptions:(NSDictionary *)options {
  234. return ![options fcm_hasNonStringKeysOrValues];
  235. }
  236. #pragma mark - Topic Name Helpers
  237. static NSString *const kTopicsPrefix = @"/topics/";
  238. static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)";
  239. + (NSString *)addPrefixToTopic:(NSString *)topic {
  240. if (![self hasTopicsPrefix:topic]) {
  241. return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic];
  242. } else {
  243. return [topic copy];
  244. }
  245. }
  246. + (NSString *)removePrefixFromTopic:(NSString *)topic {
  247. if ([self hasTopicsPrefix:topic]) {
  248. return [topic substringFromIndex:kTopicsPrefix.length];
  249. } else {
  250. return [topic copy];
  251. }
  252. }
  253. + (BOOL)hasTopicsPrefix:(NSString *)topic {
  254. return [topic hasPrefix:kTopicsPrefix];
  255. }
  256. /**
  257. * Returns a regular expression for matching a topic sender.
  258. *
  259. * @return The topic matching regular expression
  260. */
  261. + (NSRegularExpression *)topicRegex {
  262. // Since this is a static regex pattern, we only only need to declare it once.
  263. static NSRegularExpression *topicRegex;
  264. static dispatch_once_t onceToken;
  265. dispatch_once(&onceToken, ^{
  266. NSError *error;
  267. topicRegex =
  268. [NSRegularExpression regularExpressionWithPattern:kTopicRegexPattern
  269. options:NSRegularExpressionAnchorsMatchLines
  270. error:&error];
  271. });
  272. return topicRegex;
  273. }
  274. /**
  275. * Gets the class describing occurrences of topic names and sender IDs in the sender.
  276. *
  277. * @param topic The topic expression used to generate a pubsub topic
  278. *
  279. * @return Representation of captured subexpressions in topic regular expression
  280. */
  281. + (BOOL)isValidTopicWithPrefix:(NSString *)topic {
  282. NSRange topicRange = NSMakeRange(0, topic.length);
  283. NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topic
  284. options:NSMatchingAnchored
  285. range:topicRange];
  286. return NSEqualRanges(topicRange, regexMatchRange);
  287. }
  288. @end