FIRMessagingPubSub.m 12 KB

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