FIRMessagingPubSub.m 13 KB

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