FIRMessagingPubSub.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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 <FirebaseMessaging/FIRMessaging.h>
  18. #import <GoogleUtilities/GULSecureCoding.h>
  19. #import <GoogleUtilities/GULUserDefaults.h>
  20. #import "FirebaseMessaging/Sources/FIRMessagingClient.h"
  21. #import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
  22. #import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
  23. #import "FirebaseMessaging/Sources/FIRMessagingPendingTopicsList.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. static NSString *const kPendingSubscriptionsListKey =
  29. @"com.firebase.messaging.pending-subscriptions";
  30. @interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>
  31. @property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;
  32. @property(nonatomic, readwrite, strong) FIRMessagingClient *client;
  33. @end
  34. @implementation FIRMessagingPubSub
  35. - (instancetype)init {
  36. FIRMessagingInvalidateInitializer();
  37. // Need this to disable an Xcode warning.
  38. return [self initWithClient:nil];
  39. }
  40. - (instancetype)initWithClient:(FIRMessagingClient *)client {
  41. self = [super init];
  42. if (self) {
  43. _client = client;
  44. [self restorePendingTopicsList];
  45. }
  46. return self;
  47. }
  48. - (void)subscribeWithToken:(NSString *)token
  49. topic:(NSString *)topic
  50. options:(NSDictionary *)options
  51. handler:(FIRMessagingTopicOperationCompletion)handler {
  52. if (!self.client) {
  53. handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
  54. return;
  55. }
  56. token = [token copy];
  57. topic = [topic copy];
  58. if (![options count]) {
  59. options = @{};
  60. }
  61. if (![[self class] isValidTopicWithPrefix:topic]) {
  62. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000,
  63. @"Invalid FIRMessaging Pubsub topic %@", topic);
  64. handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
  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.client updateSubscriptionWithToken:token
  76. topic:topic
  77. options:options
  78. shouldDelete:NO
  79. handler:^void(NSError *error) {
  80. handler(error);
  81. }];
  82. }
  83. - (void)unsubscribeWithToken:(NSString *)token
  84. topic:(NSString *)topic
  85. options:(NSDictionary *)options
  86. handler:(FIRMessagingTopicOperationCompletion)handler {
  87. if (!self.client) {
  88. handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
  89. return;
  90. }
  91. token = [token copy];
  92. topic = [topic copy];
  93. if (![options count]) {
  94. options = @{};
  95. }
  96. if (![[self class] isValidTopicWithPrefix:topic]) {
  97. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002,
  98. @"Invalid FIRMessaging Pubsub topic %@", topic);
  99. handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
  100. return;
  101. }
  102. if (![self verifyPubSubOptions:options]) {
  103. // we do not want to quit even if options have some invalid values.
  104. FIRMessagingLoggerError(
  105. kFIRMessagingMessageCodePubSub003,
  106. @"Invalid options passed to FIRMessagingPubSub with non-string keys or values.");
  107. }
  108. // copy the dictionary would trim non-string keys or values if any.
  109. options = [options fcm_trimNonStringValues];
  110. [self.client updateSubscriptionWithToken:token
  111. topic:topic
  112. options:options
  113. shouldDelete:YES
  114. handler:^void(NSError *error) {
  115. handler(error);
  116. }];
  117. }
  118. - (void)subscribeToTopic:(NSString *)topic
  119. handler:(nullable FIRMessagingTopicOperationCompletion)handler {
  120. [self.pendingTopicUpdates addOperationForTopic:topic
  121. withAction:FIRMessagingTopicActionSubscribe
  122. completion:handler];
  123. }
  124. - (void)unsubscribeFromTopic:(NSString *)topic
  125. handler:(nullable FIRMessagingTopicOperationCompletion)handler {
  126. [self.pendingTopicUpdates addOperationForTopic:topic
  127. withAction:FIRMessagingTopicActionUnsubscribe
  128. completion:handler];
  129. }
  130. - (void)scheduleSync:(BOOL)immediately {
  131. NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
  132. if (fcmToken.length) {
  133. [self.pendingTopicUpdates resumeOperationsIfNeeded];
  134. }
  135. }
  136. #pragma mark - FIRMessagingPendingTopicsListDelegate
  137. - (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
  138. requestedUpdateForTopic:(NSString *)topic
  139. action:(FIRMessagingTopicAction)action
  140. completion:(FIRMessagingTopicOperationCompletion)completion {
  141. NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
  142. if (action == FIRMessagingTopicActionSubscribe) {
  143. [self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];
  144. } else {
  145. [self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];
  146. }
  147. }
  148. - (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
  149. [self archivePendingTopicsList:list];
  150. }
  151. - (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
  152. NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
  153. return (fcmToken.length > 0);
  154. }
  155. #pragma mark - Storing Pending Topics
  156. - (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList {
  157. GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];
  158. NSError *error;
  159. NSData *pendingData = [GULSecureCoding archivedDataWithRootObject:topicsList error:&error];
  160. if (error) {
  161. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubArchiveError,
  162. @"Failed to archive topic list data %@", error);
  163. return;
  164. }
  165. [defaults setObject:pendingData forKey:kPendingSubscriptionsListKey];
  166. [defaults synchronize];
  167. }
  168. - (void)restorePendingTopicsList {
  169. GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];
  170. NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey];
  171. FIRMessagingPendingTopicsList *subscriptions;
  172. if (pendingData) {
  173. NSError *error;
  174. subscriptions = [GULSecureCoding
  175. unarchivedObjectOfClasses:[NSSet setWithObjects:FIRMessagingPendingTopicsList.class, nil]
  176. fromData:pendingData
  177. error:&error];
  178. if (error) {
  179. FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubUnarchiveError,
  180. @"Failed to unarchive topic list data %@", error);
  181. }
  182. }
  183. if (subscriptions) {
  184. self.pendingTopicUpdates = subscriptions;
  185. } else {
  186. self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init];
  187. }
  188. self.pendingTopicUpdates.delegate = self;
  189. }
  190. #pragma mark - Private Helpers
  191. - (BOOL)verifyPubSubOptions:(NSDictionary *)options {
  192. return ![options fcm_hasNonStringKeysOrValues];
  193. }
  194. #pragma mark - Topic Name Helpers
  195. static NSString *const kTopicsPrefix = @"/topics/";
  196. static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)";
  197. + (NSString *)addPrefixToTopic:(NSString *)topic {
  198. if (![self hasTopicsPrefix:topic]) {
  199. return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic];
  200. } else {
  201. return [topic copy];
  202. }
  203. }
  204. + (NSString *)removePrefixFromTopic:(NSString *)topic {
  205. if ([self hasTopicsPrefix:topic]) {
  206. return [topic substringFromIndex:kTopicsPrefix.length];
  207. } else {
  208. return [topic copy];
  209. }
  210. }
  211. + (BOOL)hasTopicsPrefix:(NSString *)topic {
  212. return [topic hasPrefix:kTopicsPrefix];
  213. }
  214. /**
  215. * Returns a regular expression for matching a topic sender.
  216. *
  217. * @return The topic matching regular expression
  218. */
  219. + (NSRegularExpression *)topicRegex {
  220. // Since this is a static regex pattern, we only only need to declare it once.
  221. static NSRegularExpression *topicRegex;
  222. static dispatch_once_t onceToken;
  223. dispatch_once(&onceToken, ^{
  224. NSError *error;
  225. topicRegex =
  226. [NSRegularExpression regularExpressionWithPattern:kTopicRegexPattern
  227. options:NSRegularExpressionAnchorsMatchLines
  228. error:&error];
  229. });
  230. return topicRegex;
  231. }
  232. /**
  233. * Gets the class describing occurences of topic names and sender IDs in the sender.
  234. *
  235. * @param expression The topic expression used to generate a pubsub topic
  236. *
  237. * @return Representation of captured subexpressions in topic regular expression
  238. */
  239. + (BOOL)isValidTopicWithPrefix:(NSString *)topic {
  240. NSRange topicRange = NSMakeRange(0, topic.length);
  241. NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topic
  242. options:NSMatchingAnchored
  243. range:topicRange];
  244. return NSEqualRanges(topicRange, regexMatchRange);
  245. }
  246. @end