FIRMessagingPubSub.m 12 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/GULSecureCoding.h>
  18. #import <GoogleUtilities/GULUserDefaults.h>
  19. #import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
  20. #import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
  21. #import "FirebaseMessaging/Sources/FIRMessagingPendingTopicsList.h"
  22. #import "FirebaseMessaging/Sources/FIRMessagingTopicOperation.h"
  23. #import "FirebaseMessaging/Sources/FIRMessagingTopicsCommon.h"
  24. #import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
  25. #import "FirebaseMessaging/Sources/FIRMessaging_Private.h"
  26. #import "FirebaseMessaging/Sources/NSDictionary+FIRMessaging.h"
  27. #import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
  28. #import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
  29. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenManager.h"
  30. static NSString *const kPendingSubscriptionsListKey =
  31. @"com.firebase.messaging.pending-subscriptions";
  32. @interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>
  33. @property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;
  34. @property(nonatomic, readonly, strong) NSOperationQueue *topicOperations;
  35. // Common errors, instantiated, to avoid generating multiple copies
  36. @property(nonatomic, readwrite, strong) NSError *operationInProgressError;
  37. @property(nonatomic, readwrite, strong) FIRMessagingTokenManager *tokenManager;
  38. @end
  39. @implementation FIRMessagingPubSub
  40. - (instancetype)initWithTokenManager:(FIRMessagingTokenManager *)tokenManager {
  41. self = [super init];
  42. if (self) {
  43. _topicOperations = [[NSOperationQueue alloc] init];
  44. // Do 10 topic operations at a time; it's enough to keep the TCP connection to the host alive,
  45. // saving hundreds of milliseconds on each request (compared to a serial queue).
  46. _topicOperations.maxConcurrentOperationCount = 10;
  47. _tokenManager = tokenManager;
  48. [self restorePendingTopicsList];
  49. }
  50. return self;
  51. }
  52. - (void)subscribeWithToken:(NSString *)token
  53. topic:(NSString *)topic
  54. options:(NSDictionary *)options
  55. handler:(FIRMessagingTopicOperationCompletion)handler {
  56. token = [token copy];
  57. topic = [topic copy];
  58. if (![options count]) {
  59. options = @{};
  60. }
  61. if (![[self class] isValidTopicWithPrefix:topic]) {
  62. NSString *failureReason =
  63. [NSString stringWithFormat:@"Invalid subscription topic :'%@'", topic];
  64. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000, @"%@", failureReason);
  65. handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName
  66. failureReason:failureReason]);
  67. return;
  68. }
  69. if (![self verifyPubSubOptions:options]) {
  70. // we do not want to quit even if options have some invalid values.
  71. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub001,
  72. @"Invalid options passed to FIRMessagingPubSub with non-string keys or "
  73. "values.");
  74. }
  75. // copy the dictionary would trim non-string keys or values if any.
  76. options = [options fcm_trimNonStringValues];
  77. [self updateSubscriptionWithToken:token
  78. topic:topic
  79. options:options
  80. shouldDelete:NO
  81. handler:handler];
  82. }
  83. - (void)dealloc {
  84. [self.topicOperations cancelAllOperations];
  85. }
  86. #pragma mark - FIRMessaging subscribe
  87. - (void)updateSubscriptionWithToken:(NSString *)token
  88. topic:(NSString *)topic
  89. options:(NSDictionary *)options
  90. shouldDelete:(BOOL)shouldDelete
  91. handler:(FIRMessagingTopicOperationCompletion)handler {
  92. if ([_tokenManager hasValidCheckinInfo]) {
  93. FIRMessagingTopicAction action =
  94. shouldDelete ? FIRMessagingTopicActionUnsubscribe : FIRMessagingTopicActionSubscribe;
  95. FIRMessagingTopicOperation *operation = [[FIRMessagingTopicOperation alloc]
  96. initWithTopic:topic
  97. action:action
  98. tokenManager:_tokenManager
  99. options:options
  100. completion:^(NSError *_Nullable error) {
  101. if (error) {
  102. FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001,
  103. @"Failed to subscribe to topic %@", error);
  104. } else {
  105. if (shouldDelete) {
  106. FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,
  107. @"Successfully unsubscribed from topic %@", topic);
  108. } else {
  109. FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,
  110. @"Successfully subscribed to topic %@", topic);
  111. }
  112. }
  113. if (handler) {
  114. handler(error);
  115. }
  116. }];
  117. [self.topicOperations addOperation:operation];
  118. } else {
  119. NSString *failureReason = @"Device ID and checkin info is not found. Will not proceed with "
  120. @"subscription/unsubscription.";
  121. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRegistrar000, @"%@", failureReason);
  122. NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeMissingDeviceID
  123. failureReason:failureReason];
  124. handler(error);
  125. }
  126. }
  127. - (void)unsubscribeWithToken:(NSString *)token
  128. topic:(NSString *)topic
  129. options:(NSDictionary *)options
  130. handler:(FIRMessagingTopicOperationCompletion)handler {
  131. token = [token copy];
  132. topic = [topic copy];
  133. if (![options count]) {
  134. options = @{};
  135. }
  136. if (![[self class] isValidTopicWithPrefix:topic]) {
  137. NSString *failureReason =
  138. [NSString stringWithFormat:@"Invalid topic name : '%@' for unsubscription.", topic];
  139. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002, @"%@", failureReason);
  140. handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName
  141. failureReason:failureReason]);
  142. return;
  143. }
  144. if (![self verifyPubSubOptions:options]) {
  145. // we do not want to quit even if options have some invalid values.
  146. FIRMessagingLoggerError(
  147. kFIRMessagingMessageCodePubSub003,
  148. @"Invalid options passed to FIRMessagingPubSub with non-string keys or values.");
  149. }
  150. // copy the dictionary would trim non-string keys or values if any.
  151. options = [options fcm_trimNonStringValues];
  152. [self updateSubscriptionWithToken:token
  153. topic:topic
  154. options:options
  155. shouldDelete:YES
  156. handler:^void(NSError *error) {
  157. handler(error);
  158. }];
  159. }
  160. - (void)subscribeToTopic:(NSString *)topic
  161. handler:(nullable FIRMessagingTopicOperationCompletion)handler {
  162. [self.pendingTopicUpdates addOperationForTopic:topic
  163. withAction:FIRMessagingTopicActionSubscribe
  164. completion:handler];
  165. }
  166. - (void)unsubscribeFromTopic:(NSString *)topic
  167. handler:(nullable FIRMessagingTopicOperationCompletion)handler {
  168. [self.pendingTopicUpdates addOperationForTopic:topic
  169. withAction:FIRMessagingTopicActionUnsubscribe
  170. completion:handler];
  171. }
  172. - (void)scheduleSync:(BOOL)immediately {
  173. NSString *fcmToken = _tokenManager.defaultFCMToken;
  174. if (fcmToken.length) {
  175. [self.pendingTopicUpdates resumeOperationsIfNeeded];
  176. }
  177. }
  178. #pragma mark - FIRMessagingPendingTopicsListDelegate
  179. - (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
  180. requestedUpdateForTopic:(NSString *)topic
  181. action:(FIRMessagingTopicAction)action
  182. completion:(FIRMessagingTopicOperationCompletion)completion {
  183. NSString *fcmToken = _tokenManager.defaultFCMToken;
  184. if (action == FIRMessagingTopicActionSubscribe) {
  185. [self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];
  186. } else {
  187. [self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];
  188. }
  189. }
  190. - (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
  191. [self archivePendingTopicsList:list];
  192. }
  193. - (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
  194. NSString *fcmToken = _tokenManager.defaultFCMToken;
  195. return (fcmToken.length > 0);
  196. }
  197. #pragma mark - Storing Pending Topics
  198. - (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList {
  199. GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];
  200. NSError *error;
  201. NSData *pendingData = [GULSecureCoding archivedDataWithRootObject:topicsList error:&error];
  202. if (error) {
  203. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubArchiveError,
  204. @"Failed to archive topic list data %@", error);
  205. return;
  206. }
  207. [defaults setObject:pendingData forKey:kPendingSubscriptionsListKey];
  208. [defaults synchronize];
  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 = [GULSecureCoding
  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 occurences 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