diff --git a/CHANGES.rst b/CHANGES.rst index a6bade52f..dc536476e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ Changes in 0.8.3 (2019-xx-xx) =============================================== +Bug fix: + * Widgets: Attempt to re-register for a scalar token if ours is invalid (#2326). + * Widgets: Pass scalar_token only when required. Changes in 0.8.2 (2019-03-11) diff --git a/Riot/Assets/Riot-Defaults.plist b/Riot/Assets/Riot-Defaults.plist index 9c18e135d..7ad072f6b 100644 --- a/Riot/Assets/Riot-Defaults.plist +++ b/Riot/Assets/Riot-Defaults.plist @@ -28,6 +28,11 @@ https://scalar-staging.riot.im/scalar-web/ integrationsRestUrl https://scalar-staging.riot.im/scalar/api + integrationsWidgetsUrls + + https://scalar-staging.riot.im/scalar/api + https://scalar.vector.im/api + piwik url diff --git a/Riot/Generated/RiotDefaults.swift b/Riot/Generated/RiotDefaults.swift index 5c063181b..51897ad61 100644 --- a/Riot/Generated/RiotDefaults.swift +++ b/Riot/Generated/RiotDefaults.swift @@ -21,6 +21,7 @@ internal enum RiotDefaults { internal static let identityserverurl: String = _document["identityserverurl"] internal static let integrationsRestUrl: String = _document["integrationsRestUrl"] internal static let integrationsUiUrl: String = _document["integrationsUiUrl"] + internal static let integrationsWidgetsUrls: [String] = _document["integrationsWidgetsUrls"] internal static let matrixApps: Bool = _document["matrixApps"] internal static let maxAllowedMediaCacheSize: Int = _document["maxAllowedMediaCacheSize"] internal static let pinRoomsWithMissedNotif: Bool = _document["pinRoomsWithMissedNotif"] diff --git a/Riot/Managers/Widgets/Widget.m b/Riot/Managers/Widgets/Widget.m index 3831d9a02..482f54e9c 100644 --- a/Riot/Managers/Widgets/Widget.m +++ b/Riot/Managers/Widgets/Widget.m @@ -49,68 +49,73 @@ - (MXHTTPOperation *)widgetUrl:(void (^)(NSString * _Nonnull))success failure:(void (^)(NSError * _Nonnull))failure { - // Format the url string with user data (including their scalar token) - __weak typeof(self) weakSelf = self; - return [[WidgetManager sharedManager] getScalarTokenForMXSession:_mxSession success:^(NSString *scalarToken) { + __block NSString *widgetUrl = _url; - if (weakSelf) + // Format the url string with user data + NSString *userId = self.mxSession.myUser.userId; + NSString *displayName = self.mxSession.myUser.displayname ? self.mxSession.myUser.displayname : self.mxSession.myUser.userId; + NSString *avatarUrl = self.mxSession.myUser.avatarUrl ? self.mxSession.myUser.avatarUrl : @""; + + // Escape everything to build a valid URL string + // We can't know where the values escaped here will be inserted in the URL, so the alphanumeric charset is used + userId = [MXTools encodeURIComponent:userId]; + displayName = [MXTools encodeURIComponent:displayName]; + avatarUrl = [MXTools encodeURIComponent:avatarUrl]; + + widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:@"$matrix_user_id" withString:userId]; + widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:@"$matrix_display_name" withString:displayName]; + widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:@"$matrix_avatar_url" withString:avatarUrl]; + + // Integrate widget data into widget url + for (NSString *key in _data) + { + NSString *paramKey = [NSString stringWithFormat:@"$%@", key]; + + NSString *dataString; + MXJSONModelSetString(dataString, _data[key]); + + // Fix number data instead of expected string data + if (!dataString && [_data[key] isKindOfClass:NSNumber.class]) { - typeof(self) self = weakSelf; - NSString *userId = self.mxSession.myUser.userId; - NSString *displayName = self.mxSession.myUser.displayname ? self.mxSession.myUser.displayname : self.mxSession.myUser.userId; - NSString *avatarUrl = self.mxSession.myUser.avatarUrl ? self.mxSession.myUser.avatarUrl : @""; - - // Escape everything to build a valid URL string - // We can't know where the values escaped here will be inserted in the URL, so the alphanumeric charset is used - userId = [MXTools encodeURIComponent:userId]; - displayName = [MXTools encodeURIComponent:displayName]; - avatarUrl = [MXTools encodeURIComponent:avatarUrl]; - - NSString *widgetUrl = _url; - widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:@"$matrix_user_id" withString:userId]; - widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:@"$matrix_display_name" withString:displayName]; - widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:@"$matrix_avatar_url" withString:avatarUrl]; - - // Integrate widget data into widget url - for (NSString *key in _data) - { - NSString *paramKey = [NSString stringWithFormat:@"$%@", key]; - - NSString *dataString; - MXJSONModelSetString(dataString, _data[key]); - - // Fix number data instead of expected string data - if (!dataString && [_data[key] isKindOfClass:NSNumber.class]) - { - dataString = [((NSNumber*)_data[key]) stringValue]; - } - - if (dataString) - { - // same question as above - NSString *value = [MXTools encodeURIComponent:dataString]; - - widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:paramKey - withString:value]; - } - else - { - NSLog(@"[Widget] Error: Invalid data field value in %@ for key %@ in data %@", self, key, _data); - } - } - - // Add the user scalar token - widgetUrl = [widgetUrl stringByAppendingString:[NSString stringWithFormat:@"%@scalar_token=%@", - [widgetUrl containsString:@"?"] ? @"&" : @"?", - scalarToken]]; - - // Add the widget id - widgetUrl = [widgetUrl stringByAppendingString:[NSString stringWithFormat:@"&widgetId=%@", _widgetId]]; - - success(widgetUrl); + dataString = [((NSNumber*)_data[key]) stringValue]; } - } failure:failure]; + if (dataString) + { + // same question as above + NSString *value = [MXTools encodeURIComponent:dataString]; + + widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:paramKey + withString:value]; + } + else + { + NSLog(@"[Widget] Error: Invalid data field value in %@ for key %@ in data %@", self, key, _data); + } + } + + // Add the widget id + widgetUrl = [widgetUrl stringByAppendingString:[NSString stringWithFormat:@"%@widgetId=%@", + [widgetUrl containsString:@"?"] ? @"&" : @"?", + _widgetId]]; + + // Check if their scalar token must added + if ([WidgetManager isScalarUrl:widgetUrl]) + { + return [[WidgetManager sharedManager] getScalarTokenForMXSession:_mxSession validate:NO success:^(NSString *scalarToken) { + // Add the user scalar token + widgetUrl = [widgetUrl stringByAppendingString:[NSString stringWithFormat:@"&scalar_token=%@", + scalarToken]]; + + success(widgetUrl); + } failure:failure]; + } + else + { + success(widgetUrl); + } + + return nil; } - (BOOL)isActive diff --git a/Riot/Managers/Widgets/WidgetManager.h b/Riot/Managers/Widgets/WidgetManager.h index a95ba83e1..83b41ac2f 100644 --- a/Riot/Managers/Widgets/WidgetManager.h +++ b/Riot/Managers/Widgets/WidgetManager.h @@ -187,12 +187,22 @@ WidgetManagerErrorCode; to get one. @param mxSession the session to check. + @param validate if it is cached, check its validity on the scalar server. @param success A block object called when the operation succeeds. @param failure A block object called when the operation fails. */ - (MXHTTPOperation *)getScalarTokenForMXSession:(MXSession*)mxSession + validate:(BOOL)validate success:(void (^)(NSString *scalarToken))success failure:(void (^)(NSError *error))failure; +/** + Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api + + @param urlString the URL to check. + @return YES if specified URL is a scalar URL. + */ ++ (BOOL)isScalarUrl:(NSString*)urlString; + @end diff --git a/Riot/Managers/Widgets/WidgetManager.m b/Riot/Managers/Widgets/WidgetManager.m index c225e140e..8f761bf08 100644 --- a/Riot/Managers/Widgets/WidgetManager.m +++ b/Riot/Managers/Widgets/WidgetManager.m @@ -442,6 +442,7 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; #pragma mark - Modular interface - (MXHTTPOperation *)getScalarTokenForMXSession:(MXSession*)mxSession + validate:(BOOL)validate success:(void (^)(NSString *scalarToken))success failure:(void (^)(NSError *error))failure; { @@ -450,65 +451,156 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; __block NSString *scalarToken = [self scalarTokenForMXSession:mxSession]; if (scalarToken) { - success(scalarToken); + if (!validate) + { + success(scalarToken); + } + else + { + operation = [self validateScalarToken:scalarToken forMXSession:mxSession complete:^(BOOL valid) { + + if (valid) + { + success(scalarToken); + } + else + { + NSLog(@"[WidgetManager] getScalarTokenForMXSession: Invalid stored token. Need to register for a new token"); + MXHTTPOperation *operation2 = [self registerForScalarToken:mxSession success:success failure:failure]; + [operation mutateTo:operation2]; + } + + } failure:failure]; + } } else { - NSLog(@"[WidgetManager] getScalarTokenForMXSession: Need to register to get a token"); - - __weak __typeof__(self) weakSelf = self; - operation = [mxSession.matrixRestClient openIdToken:^(MXOpenIdToken *tokenObject) { - - typeof(self) self = weakSelf; - - if (self) - { - // Exchange the token for a scalar token - NSString *modularRestUrl = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsRestUrl"]; - - MXHTTPClient *httpClient = [[MXHTTPClient alloc] initWithBaseURL:modularRestUrl andOnUnrecognizedCertificateBlock:nil]; - - MXHTTPOperation *operation2 = [httpClient requestWithMethod:@"POST" - path:@"register" - parameters:tokenObject.JSONDictionary - success:^(NSDictionary *JSONResponse) { - - MXJSONModelSetString(scalarToken, JSONResponse[@"scalar_token"]) - self->scalarTokens[mxSession.myUser.userId] = scalarToken; - - [self save]; - - if (success) - { - success(scalarToken); - } - - } failure:^(NSError *error) { - NSLog(@"[WidgetManager] getScalarTokenForMXSession. Error in modular/register request"); - - if (failure) - { - failure(error); - } - }]; - - [operation mutateTo:operation2]; - - } - - } failure:^(NSError *error) { - NSLog(@"[WidgetManager] getScalarTokenForMXSession. Error in openIdToken request"); - - if (failure) - { - failure(error); - } - }]; + NSLog(@"[WidgetManager] getScalarTokenForMXSession: Need to register for a token"); + operation = [self registerForScalarToken:mxSession success:success failure:failure]; } return operation; } +- (MXHTTPOperation *)registerForScalarToken:(MXSession*)mxSession + success:(void (^)(NSString *scalarToken))success + failure:(void (^)(NSError *error))failure +{ + MXHTTPOperation *operation; + + MXWeakify(self); + operation = [mxSession.matrixRestClient openIdToken:^(MXOpenIdToken *tokenObject) { + MXStrongifyAndReturnIfNil(self); + + // Exchange the token for a scalar token + NSString *modularRestUrl = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsRestUrl"]; + + MXHTTPClient *httpClient = [[MXHTTPClient alloc] initWithBaseURL:modularRestUrl andOnUnrecognizedCertificateBlock:nil]; + + MXHTTPOperation *operation2 = + [httpClient requestWithMethod:@"POST" + path:@"register?v=1.1" + parameters:tokenObject.JSONDictionary + success:^(NSDictionary *JSONResponse) + { + + NSString *scalarToken; + MXJSONModelSetString(scalarToken, JSONResponse[@"scalar_token"]) + self->scalarTokens[mxSession.myUser.userId] = scalarToken; + + [self save]; + + if (success) + { + success(scalarToken); + } + + } failure:^(NSError *error) { + NSLog(@"[WidgetManager] registerForScalarToken. Error in modular/register request"); + + if (failure) + { + failure(error); + } + }]; + + [operation mutateTo:operation2]; + + } failure:^(NSError *error) { + NSLog(@"[WidgetManager] registerForScalarToken. Error in openIdToken request"); + + if (failure) + { + failure(error); + } + }]; + + return operation; +} + +- (MXHTTPOperation *)validateScalarToken:(NSString*)scalarToken forMXSession:(MXSession*)mxSession + complete:(void (^)(BOOL valid))complete + failure:(void (^)(NSError *error))failure +{ + NSString *modularRestUrl = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsRestUrl"]; + MXHTTPClient *httpClient = [[MXHTTPClient alloc] initWithBaseURL:modularRestUrl andOnUnrecognizedCertificateBlock:nil]; + + return [httpClient requestWithMethod:@"GET" + path:[NSString stringWithFormat:@"account?v=1.1&scalar_token=%@", scalarToken] + parameters:nil + success:^(NSDictionary *JSONResponse) { + + NSString *userId; + MXJSONModelSetString(userId, JSONResponse[@"user_id"]) + + if ([userId isEqualToString:mxSession.myUser.userId]) + { + complete(YES); + } + else + { + NSLog(@"[WidgetManager] validateScalarToken. Unexpected modular/account response: %@", JSONResponse); + complete(NO); + } + + } failure:^(NSError *error) { + NSHTTPURLResponse *urlResponse = [MXHTTPOperation urlResponseFromError:error]; + + NSLog(@"[WidgetManager] validateScalarToken. Error in modular/account request. statusCode: %@", @(urlResponse.statusCode)); + + if (urlResponse && urlResponse.statusCode / 100 != 2) + { + complete(NO); + } + else if (failure) + { + failure(error); + } + }]; +} + ++ (BOOL)isScalarUrl:(NSString *)urlString +{ + BOOL isScalarUrl = NO; + + NSArray *scalarUrlStrings = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsWidgetsUrls"]; + if (scalarUrlStrings.count == 0) + { + scalarUrlStrings = @[[[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsRestUrl"]]; + } + + for (NSString *scalarUrlString in scalarUrlStrings) + { + if ([urlString hasPrefix:scalarUrlString]) + { + isScalarUrl = YES; + break; + } + } + + return isScalarUrl; +} + #pragma mark - Private methods - (NSString *)scalarTokenForMXSession:(MXSession *)mxSession diff --git a/Riot/Modules/Integrations/IntegrationManagerViewController.m b/Riot/Modules/Integrations/IntegrationManagerViewController.m index b8dd28448..37bab65fa 100644 --- a/Riot/Modules/Integrations/IntegrationManagerViewController.m +++ b/Riot/Modules/Integrations/IntegrationManagerViewController.m @@ -73,7 +73,7 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; // Make sure we have a scalar token MXWeakify(self); - operation = [[WidgetManager sharedManager] getScalarTokenForMXSession:mxSession success:^(NSString *theScalarToken) { + operation = [[WidgetManager sharedManager] getScalarTokenForMXSession:mxSession validate:YES success:^(NSString *theScalarToken) { MXStrongifyAndReturnIfNil(self); self->operation = nil; diff --git a/Riot/Modules/Integrations/Widgets/WidgetViewController.m b/Riot/Modules/Integrations/Widgets/WidgetViewController.m index 64e9be433..f67456292 100644 --- a/Riot/Modules/Integrations/Widgets/WidgetViewController.m +++ b/Riot/Modules/Integrations/Widgets/WidgetViewController.m @@ -172,12 +172,7 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse { // Filter out the users's scalar token NSString *errorDescription = error.description; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"scalar_token=\\w*" - options:NSRegularExpressionCaseInsensitive error:nil]; - errorDescription = [regex stringByReplacingMatchesInString:errorDescription - options:0 - range:NSMakeRange(0, errorDescription.length) - withTemplate:@"scalar_token=..."]; + errorDescription = [self stringByReplacingScalarTokenInString:errorDescription byScalarToken:@"..."]; NSLog(@"[WidgetVC] didFailLoadWithError: %@", errorDescription); @@ -185,6 +180,24 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse [self showErrorAsAlert:error]; } +- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { + + if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) + { + NSHTTPURLResponse * response = (NSHTTPURLResponse *)navigationResponse.response; + if (response.statusCode != 200) + { + NSLog(@"[WidgetVC] decidePolicyForNavigationResponse: statusCode: %@", @(response.statusCode)); + } + + if (response.statusCode == 403 && [WidgetManager isScalarUrl:self.URL]) + { + [self fixScalarToken]; + } + } + decisionHandler(WKNavigationResponsePolicyAllow); +} + #pragma mark - postMessage API - (void)onPostMessageRequest:(NSString*)requestId data:(NSDictionary*)requestData @@ -315,4 +328,50 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse [self sendError:NSLocalizedStringFromTable(errorKey, @"Vector", nil) toRequest:requestId]; } + +#pragma mark - Private methods + +- (NSString *)stringByReplacingScalarTokenInString:(NSString*)string byScalarToken:(NSString*)scalarToken +{ + if (!string) + { + return nil; + } + + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"scalar_token=\\w*" + options:NSRegularExpressionCaseInsensitive error:nil]; + return [regex stringByReplacingMatchesInString:string + options:0 + range:NSMakeRange(0, string.length) + withTemplate:[NSString stringWithFormat:@"scalar_token=%@", scalarToken]]; +} + +/** + Reset the scalar token used in the webview URL. + */ +- (void)fixScalarToken +{ + NSLog(@"[WidgetVC] fixScalarToken"); + + self->webView.hidden = YES; + + // Get a fresh new scalar token + [WidgetManager.sharedManager deleteDataForUser:widget.mxSession.myUser.userId]; + + MXWeakify(self); + [WidgetManager.sharedManager getScalarTokenForMXSession:widget.mxSession validate:NO success:^(NSString *scalarToken) { + MXStrongifyAndReturnIfNil(self); + + NSLog(@"[WidgetVC] fixScalarToken: DONE"); + + self.URL = [self stringByReplacingScalarTokenInString:self.URL byScalarToken:scalarToken]; + + self->webView.hidden = NO; + + } failure:^(NSError *error) { + NSLog(@"[WidgetVC] fixScalarToken: Error: %@", error); + [self showErrorAsAlert:error]; + }]; +} + @end