AutorÃa | Ultima modificación | Ver Log |
/** Copyright 2017 Google** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/#import "FirebaseMessaging/Sources/FIRMessagingPubSub.h"#import <GoogleUtilities/GULSecureCoding.h>#import <GoogleUtilities/GULUserDefaults.h>#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"#import "FirebaseMessaging/Sources/FIRMessagingPendingTopicsList.h"#import "FirebaseMessaging/Sources/FIRMessagingTopicOperation.h"#import "FirebaseMessaging/Sources/FIRMessagingTopicsCommon.h"#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"#import "FirebaseMessaging/Sources/FIRMessaging_Private.h"#import "FirebaseMessaging/Sources/NSDictionary+FIRMessaging.h"#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenManager.h"static NSString *const kPendingSubscriptionsListKey =@"com.firebase.messaging.pending-subscriptions";@interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>@property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;@property(nonatomic, readonly, strong) NSOperationQueue *topicOperations;// Common errors, instantiated, to avoid generating multiple copies@property(nonatomic, readwrite, strong) NSError *operationInProgressError;@property(nonatomic, readwrite, strong) FIRMessagingTokenManager *tokenManager;@end@implementation FIRMessagingPubSub- (instancetype)initWithTokenManager:(FIRMessagingTokenManager *)tokenManager {self = [super init];if (self) {_topicOperations = [[NSOperationQueue alloc] init];// Do 10 topic operations at a time; it's enough to keep the TCP connection to the host alive,// saving hundreds of milliseconds on each request (compared to a serial queue)._topicOperations.maxConcurrentOperationCount = 10;_tokenManager = tokenManager;[self restorePendingTopicsList];}return self;}- (void)subscribeWithToken:(NSString *)tokentopic:(NSString *)topicoptions:(NSDictionary *)optionshandler:(FIRMessagingTopicOperationCompletion)handler {token = [token copy];topic = [topic copy];if (![options count]) {options = @{};}if (![[self class] isValidTopicWithPrefix:topic]) {NSString *failureReason =[NSString stringWithFormat:@"Invalid subscription topic :'%@'", topic];FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000, @"%@", failureReason);handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicNamefailureReason:failureReason]);return;}if (![self verifyPubSubOptions:options]) {// we do not want to quit even if options have some invalid values.FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub001,@"Invalid options passed to FIRMessagingPubSub with non-string keys or ""values.");}// copy the dictionary would trim non-string keys or values if any.options = [options fcm_trimNonStringValues];[self updateSubscriptionWithToken:tokentopic:topicoptions:optionsshouldDelete:NOhandler:handler];}- (void)dealloc {[self.topicOperations cancelAllOperations];}#pragma mark - FIRMessaging subscribe- (void)updateSubscriptionWithToken:(NSString *)tokentopic:(NSString *)topicoptions:(NSDictionary *)optionsshouldDelete:(BOOL)shouldDeletehandler:(FIRMessagingTopicOperationCompletion)handler {if ([_tokenManager hasValidCheckinInfo]) {FIRMessagingTopicAction action =shouldDelete ? FIRMessagingTopicActionUnsubscribe : FIRMessagingTopicActionSubscribe;FIRMessagingTopicOperation *operation = [[FIRMessagingTopicOperation alloc]initWithTopic:topicaction:actiontokenManager:_tokenManageroptions:optionscompletion:^(NSError *_Nullable error) {if (error) {FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001,@"Failed to subscribe to topic %@", error);} else {if (shouldDelete) {FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,@"Successfully unsubscribed from topic %@", topic);} else {FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,@"Successfully subscribed to topic %@", topic);}}if (handler) {handler(error);}}];[self.topicOperations addOperation:operation];} else {NSString *failureReason = @"Device ID and checkin info is not found. Will not proceed with "@"subscription/unsubscription.";FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRegistrar000, @"%@", failureReason);NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeMissingDeviceIDfailureReason:failureReason];handler(error);}}- (void)unsubscribeWithToken:(NSString *)tokentopic:(NSString *)topicoptions:(NSDictionary *)optionshandler:(FIRMessagingTopicOperationCompletion)handler {token = [token copy];topic = [topic copy];if (![options count]) {options = @{};}if (![[self class] isValidTopicWithPrefix:topic]) {NSString *failureReason =[NSString stringWithFormat:@"Invalid topic name : '%@' for unsubscription.", topic];FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002, @"%@", failureReason);handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicNamefailureReason:failureReason]);return;}if (![self verifyPubSubOptions:options]) {// we do not want to quit even if options have some invalid values.FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub003,@"Invalid options passed to FIRMessagingPubSub with non-string keys or values.");}// copy the dictionary would trim non-string keys or values if any.options = [options fcm_trimNonStringValues];[self updateSubscriptionWithToken:tokentopic:topicoptions:optionsshouldDelete:YEShandler:^void(NSError *error) {handler(error);}];}- (void)subscribeToTopic:(NSString *)topichandler:(nullable FIRMessagingTopicOperationCompletion)handler {[self.pendingTopicUpdates addOperationForTopic:topicwithAction:FIRMessagingTopicActionSubscribecompletion:handler];}- (void)unsubscribeFromTopic:(NSString *)topichandler:(nullable FIRMessagingTopicOperationCompletion)handler {[self.pendingTopicUpdates addOperationForTopic:topicwithAction:FIRMessagingTopicActionUnsubscribecompletion:handler];}- (void)scheduleSync:(BOOL)immediately {NSString *fcmToken = _tokenManager.defaultFCMToken;if (fcmToken.length) {[self.pendingTopicUpdates resumeOperationsIfNeeded];}}#pragma mark - FIRMessagingPendingTopicsListDelegate- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)listrequestedUpdateForTopic:(NSString *)topicaction:(FIRMessagingTopicAction)actioncompletion:(FIRMessagingTopicOperationCompletion)completion {NSString *fcmToken = _tokenManager.defaultFCMToken;if (action == FIRMessagingTopicActionSubscribe) {[self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];} else {[self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];}}- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {[self archivePendingTopicsList:list];}- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {NSString *fcmToken = _tokenManager.defaultFCMToken;return (fcmToken.length > 0);}#pragma mark - Storing Pending Topics- (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList {GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];NSError *error;NSData *pendingData = [GULSecureCoding archivedDataWithRootObject:topicsList error:&error];if (error) {FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubArchiveError,@"Failed to archive topic list data %@", error);return;}[defaults setObject:pendingData forKey:kPendingSubscriptionsListKey];[defaults synchronize];}- (void)restorePendingTopicsList {GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey];FIRMessagingPendingTopicsList *subscriptions;if (pendingData) {NSError *error;subscriptions = [GULSecureCodingunarchivedObjectOfClasses:[NSSet setWithObjects:FIRMessagingPendingTopicsList.class, nil]fromData:pendingDataerror:&error];if (error) {FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubUnarchiveError,@"Failed to unarchive topic list data %@", error);}}if (subscriptions) {self.pendingTopicUpdates = subscriptions;} else {self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init];}self.pendingTopicUpdates.delegate = self;}#pragma mark - Private Helpers- (BOOL)verifyPubSubOptions:(NSDictionary *)options {return ![options fcm_hasNonStringKeysOrValues];}#pragma mark - Topic Name Helpersstatic NSString *const kTopicsPrefix = @"/topics/";static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)";+ (NSString *)addPrefixToTopic:(NSString *)topic {if (![self hasTopicsPrefix:topic]) {return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic];} else {return [topic copy];}}+ (NSString *)removePrefixFromTopic:(NSString *)topic {if ([self hasTopicsPrefix:topic]) {return [topic substringFromIndex:kTopicsPrefix.length];} else {return [topic copy];}}+ (BOOL)hasTopicsPrefix:(NSString *)topic {return [topic hasPrefix:kTopicsPrefix];}/*** Returns a regular expression for matching a topic sender.** @return The topic matching regular expression*/+ (NSRegularExpression *)topicRegex {// Since this is a static regex pattern, we only only need to declare it once.static NSRegularExpression *topicRegex;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{NSError *error;topicRegex =[NSRegularExpression regularExpressionWithPattern:kTopicRegexPatternoptions:NSRegularExpressionAnchorsMatchLineserror:&error];});return topicRegex;}/*** Gets the class describing occurences of topic names and sender IDs in the sender.** @param topic The topic expression used to generate a pubsub topic** @return Representation of captured subexpressions in topic regular expression*/+ (BOOL)isValidTopicWithPrefix:(NSString *)topic {NSRange topicRange = NSMakeRange(0, topic.length);NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topicoptions:NSMatchingAnchoredrange:topicRange];return NSEqualRanges(topicRange, regexMatchRange);}@end