added iOS source code
[wl-app.git] / iOS / Pods / FirebaseMessaging / Firebase / Messaging / FIRMessagingTopicOperation.m
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
17 #import "FIRMessagingTopicOperation.h"
18
19 #import "FIRMessagingCheckinService.h"
20 #import "FIRMessagingDefines.h"
21 #import "FIRMessagingLogger.h"
22 #import "FIRMessagingUtilities.h"
23 #import "NSError+FIRMessaging.h"
24
25 #define DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS 0
26
27 static NSString *const kFIRMessagingSubscribeServerHost =
28     @"https://iid.googleapis.com/iid/register";
29
30 NSString *FIRMessagingSubscriptionsServer() {
31   static NSString *serverHost = nil;
32   static dispatch_once_t onceToken;
33   dispatch_once(&onceToken, ^{
34     NSDictionary *environment = [[NSProcessInfo processInfo] environment];
35     NSString *customServerHost = environment[@"FCM_SERVER_ENDPOINT"];
36     if (customServerHost.length) {
37       serverHost = customServerHost;
38     } else {
39       serverHost = kFIRMessagingSubscribeServerHost;
40     }
41   });
42   return serverHost;
43 }
44
45 @interface FIRMessagingTopicOperation () {
46   BOOL _isFinished;
47   BOOL _isExecuting;
48 }
49
50 @property(nonatomic, readwrite, copy) NSString *topic;
51 @property(nonatomic, readwrite, assign) FIRMessagingTopicAction action;
52 @property(nonatomic, readwrite, copy) NSString *token;
53 @property(nonatomic, readwrite, copy) NSDictionary *options;
54 @property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
55 @property(nonatomic, readwrite, copy) FIRMessagingTopicOperationCompletion completion;
56
57 @property(atomic, strong) NSURLSessionDataTask *dataTask;
58
59 @end
60
61 @implementation FIRMessagingTopicOperation
62
63 + (NSURLSession *)sharedSession {
64   static NSURLSession *subscriptionOperationSharedSession;
65   static dispatch_once_t onceToken;
66   dispatch_once(&onceToken, ^{
67     NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
68     config.timeoutIntervalForResource = 60.0f;  // 1 minute
69     subscriptionOperationSharedSession = [NSURLSession sessionWithConfiguration:config];
70     subscriptionOperationSharedSession.sessionDescription = @"com.google.fcm.topics.session";
71   });
72   return subscriptionOperationSharedSession;
73 }
74
75 - (instancetype)initWithTopic:(NSString *)topic
76                        action:(FIRMessagingTopicAction)action
77                         token:(NSString *)token
78                       options:(NSDictionary *)options
79                checkinService:(FIRMessagingCheckinService *)checkinService
80                    completion:(FIRMessagingTopicOperationCompletion)completion {
81   if (self = [super init]) {
82     _topic = topic;
83     _action = action;
84     _token = token;
85     _options = options;
86     _checkinService = checkinService;
87     _completion = completion;
88
89     _isExecuting = NO;
90     _isFinished = NO;
91   }
92   return self;
93 }
94
95 - (void)dealloc {
96   _topic = nil;
97   _token = nil;
98   _checkinService = nil;
99   _completion = nil;
100 }
101
102 - (BOOL)isAsynchronous {
103   return YES;
104 }
105
106 - (BOOL)isExecuting {
107   return _isExecuting;
108 }
109
110 - (void)setExecuting:(BOOL)executing {
111   [self willChangeValueForKey:@"isExecuting"];
112   _isExecuting = executing;
113   [self didChangeValueForKey:@"isExecuting"];
114 }
115
116 - (BOOL)isFinished {
117   return _isFinished;
118 }
119
120 - (void)setFinished:(BOOL)finished {
121   [self willChangeValueForKey:@"isFinished"];
122   _isFinished = finished;
123   [self didChangeValueForKey:@"isFinished"];
124 }
125
126 - (void)start {
127   if (self.isCancelled) {
128     NSError *error =
129         [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubOperationIsCancelled];
130     [self finishWithError:error];
131     return;
132   }
133
134   [self setExecuting:YES];
135
136   [self performSubscriptionChange];
137 }
138
139 - (void)finishWithError:(NSError *)error {
140   // Add a check to prevent this finish from being called more than once.
141   if (self.isFinished) {
142     return;
143   }
144   self.dataTask = nil;
145   if (self.completion) {
146     self.completion(error);
147   }
148
149   [self setExecuting:NO];
150   [self setFinished:YES];
151 }
152
153 - (void)cancel {
154   [super cancel];
155   [self.dataTask cancel];
156   NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubOperationIsCancelled];
157   [self finishWithError:error];
158 }
159
160 - (void)performSubscriptionChange {
161
162   NSURL *url = [NSURL URLWithString:FIRMessagingSubscriptionsServer()];
163   NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
164   NSString *appIdentifier = FIRMessagingAppIdentifier();
165   NSString *deviceAuthID = self.checkinService.deviceAuthID;
166   NSString *secretToken = self.checkinService.secretToken;
167   NSString *authString = [NSString stringWithFormat:@"AidLogin %@:%@", deviceAuthID, secretToken];
168   [request setValue:authString forHTTPHeaderField:@"Authorization"];
169   [request setValue:appIdentifier forHTTPHeaderField:@"app"];
170   [request setValue:self.checkinService.versionInfo forHTTPHeaderField:@"info"];
171
172   // Topic can contain special characters (like `%`) so encode the value.
173   NSCharacterSet *characterSet = [NSCharacterSet URLQueryAllowedCharacterSet];
174   NSString *encodedTopic =
175       [self.topic stringByAddingPercentEncodingWithAllowedCharacters:characterSet];
176   if (encodedTopic == nil) {
177     // The transformation was somehow not possible, so use the original topic.
178     FIRMessagingLoggerWarn(kFIRMessagingMessageCodeTopicOptionTopicEncodingFailed,
179                            @"Unable to encode the topic '%@' during topic subscription change. "
180                            @"Please ensure that the topic name contains only valid characters.",
181                            self.topic);
182     encodedTopic = self.topic;
183   }
184
185   NSMutableString *content = [NSMutableString stringWithFormat:
186                               @"sender=%@&app=%@&device=%@&"
187                               @"app_ver=%@&X-gcm.topic=%@&X-scope=%@",
188                               self.token,
189                               appIdentifier,
190                               deviceAuthID,
191                               FIRMessagingCurrentAppVersion(),
192                               encodedTopic,
193                               encodedTopic];
194
195   if (self.action == FIRMessagingTopicActionUnsubscribe) {
196     [content appendString:@"&delete=true"];
197   }
198
199   FIRMessagingLoggerInfo(kFIRMessagingMessageCodeTopicOption000, @"Topic subscription request: %@",
200                          content);
201
202   request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
203   [request setHTTPMethod:@"POST"];
204
205 #if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
206   NSDate *start = [NSDate date];
207 #endif
208
209   FIRMessaging_WEAKIFY(self)
210   void(^requestHandler)(NSData *, NSURLResponse *, NSError *) =
211       ^(NSData *data, NSURLResponse *URLResponse, NSError *error) {
212         FIRMessaging_STRONGIFY(self)
213     if (error) {
214       // Our operation could have been cancelled, which would result in our data task's error being
215       // NSURLErrorCancelled
216       if (error.code == NSURLErrorCancelled) {
217         // We would only have been cancelled in the -cancel method, which will call finish for us
218         // so just return and do nothing.
219         return;
220       }
221       FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption001,
222                               @"Device registration HTTP fetch error. Error Code: %ld",
223                               _FIRMessaging_L(error.code));
224       [self finishWithError:error];
225       return;
226     }
227     NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
228     if (response.length == 0) {
229       FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOperationEmptyResponse,
230                               @"Invalid registration response - zero length.");
231       [self finishWithError:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
232       return;
233     }
234     NSArray *parts = [response componentsSeparatedByString:@"="];
235     _FIRMessagingDevAssert(parts.count, @"Invalid registration response");
236     if (![parts[0] isEqualToString:@"token"] || parts.count <= 1) {
237       FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption002,
238                               @"Invalid registration response %@", response);
239       [self finishWithError:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
240       return;
241     }
242 #if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
243     NSTimeInterval duration = -[start timeIntervalSinceNow];
244     FIRMessagingLoggerDebug(@"%@ change took %.2fs", self.topic, duration);
245 #endif
246     [self finishWithError:nil];
247
248   };
249
250   NSURLSession *urlSession = [FIRMessagingTopicOperation sharedSession];
251
252   self.dataTask = [urlSession dataTaskWithRequest:request completionHandler:requestHandler];
253   NSString *description;
254   if (_action == FIRMessagingTopicActionSubscribe) {
255     description = [NSString stringWithFormat:@"com.google.fcm.topics.subscribe: %@", _topic];
256   } else {
257     description = [NSString stringWithFormat:@"com.google.fcm.topics.unsubscribe: %@", _topic];
258   }
259   self.dataTask.taskDescription = description;
260   [self.dataTask resume];
261 }
262
263 @end