FIRMessagingPubSub.m 12 KB

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