Update UniversalLink class to parse path and query params

This commit is contained in:
ismailgulek 2022-06-07 16:05:47 +03:00
parent a9beeac55a
commit 36ebbc6b9c
No known key found for this signature in database
GPG key ID: E96336D42D9470A9
4 changed files with 247 additions and 8 deletions

View file

@ -52,6 +52,7 @@
#import "BubbleRoomTimelineCellProvider.h"
#import "RoomSelectedStickerBubbleCell.h"
#import "MXRoom+Riot.h"
#import "UniversalLink.h"
// MatrixKit common imports, shared with all targets
#import "MatrixKit-Bridging-Header.h"

View file

@ -18,17 +18,27 @@
NS_ASSUME_NONNULL_BEGIN
@interface UniversalLink : NSObject
@interface UniversalLink : NSObject <NSCopying>
/// Original url
@property (nonatomic, copy, readonly) NSURL *url;
/// Path params from the link.
@property (nonatomic, copy, readonly) NSArray<NSString*> *pathParams;
@property (nonatomic, copy, readonly) NSDictionary<NSString*, NSString*> *queryParams;
/// Query params from the link. Does not conform to RFC 1808. Designed for simplicity.
@property (nonatomic, copy, readonly) NSDictionary<NSString*, id> *queryParams;
- (id)initWithUrl:(NSURL *)url
pathParams:(NSArray<NSString*> *)pathParams
queryParams:(NSDictionary<NSString*, NSString*> *)queryParams;
/// Homeserver url in the link if any
@property (nonatomic, copy, readonly, nullable) NSString *homeserverUrl;
/// Identity server url in the link if any
@property (nonatomic, copy, readonly, nullable) NSString *identityServerUrl;
/// via parameters url in the link if any
@property (nonatomic, copy, readonly) NSArray<NSString*> *via;
/// Initializer
/// @param url original url
- (id)initWithUrl:(NSURL *)url;
@end

View file

@ -15,21 +15,134 @@
*/
#import "UniversalLink.h"
#import "NSArray+Element.h"
@implementation UniversalLink
- (id)initWithUrl:(NSURL *)url pathParams:(NSArray<NSString *> *)pathParams queryParams:(NSDictionary<NSString *,NSString *> *)queryParams
- (id)initWithUrl:(NSURL *)url
{
self = [super init];
if (self)
{
_url = url;
_pathParams = pathParams;
_queryParams = queryParams;
// Extract required parameters from the link
[self parsePathAndQueryParams];
}
return self;
}
/**
Extract params from the URL fragment part (after '#') of a vector.im Universal link:
The fragment can contain a '?'. So there are two kinds of parameters: path params and query params.
It is in the form of /[pathParam1]/[pathParam2]?[queryParam1Key]=[queryParam1Value]&[queryParam2Key]=[queryParam2Value]
*/
- (void)parsePathAndQueryParams
{
NSArray<NSString*> *pathParams;
NSMutableDictionary *queryParams = [NSMutableDictionary dictionary];
NSArray<NSString*> *fragments = [_url.fragment componentsSeparatedByString:@"?"];
// Extract path params
pathParams = [fragments[0] componentsSeparatedByString:@"/"];
// Remove the first empty path param string
pathParams = [pathParams filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"length > 0"]];
// URL decode each path param
pathParams = [pathParams vc_map:^id _Nonnull(NSString * _Nonnull item) {
return [item stringByRemovingPercentEncoding];
}];
// Extract query params
NSURLComponents *components = [NSURLComponents componentsWithURL:_url resolvingAgainstBaseURL:NO];
for (NSURLQueryItem *item in components.queryItems)
{
if (item.value)
{
NSString *key = item.name;
NSString *value = item.value;
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
value = [value stringByRemovingPercentEncoding];
if ([key isEqualToString:@"via"])
{
// Special case the via parameter
// As we can have several of them, store each value into an array
if (!queryParams[key])
{
queryParams[key] = [NSMutableArray array];
}
[queryParams[key] addObject:value];
}
else
{
queryParams[key] = value;
}
}
}
// Query params are in the form [queryParam1Key]=[queryParam1Value], so the
// presence of at least one '=' character is mandatory
if (fragments.count == 2 && (NSNotFound != [fragments[1] rangeOfString:@"="].location))
{
for (NSString *keyValue in [fragments[1] componentsSeparatedByString:@"&"])
{
// Get the parameter name
NSString *key = [keyValue componentsSeparatedByString:@"="][0];
// Get the parameter value
NSString *value = [keyValue componentsSeparatedByString:@"="][1];
if (value.length)
{
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
value = [value stringByRemovingPercentEncoding];
if ([key isEqualToString:@"via"])
{
// Special case the via parameter
// As we can have several of them, store each value into an array
if (!queryParams[key])
{
queryParams[key] = [NSMutableArray array];
}
[queryParams[key] addObject:value];
}
else
{
queryParams[key] = value;
}
}
}
}
_pathParams = pathParams;
_queryParams = queryParams;
}
- (NSString *)homeserverUrl
{
return _queryParams[@"hs_url"];
}
- (NSString *)identityServerUrl
{
return _queryParams[@"is_url"];
}
- (NSArray<NSString *> *)via
{
NSArray<NSString *> *result = _queryParams[@"via"];
if (!result)
{
return @[];
}
return result;
}
- (BOOL)isEqual:(id)other
{
if (other == self)
@ -57,4 +170,21 @@
return result;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<UniversalLink: %@>", _url.absoluteString];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone
{
UniversalLink *link = [[self.class allocWithZone:zone] init];
link->_url = [_url copyWithZone:zone];
link->_pathParams = [_pathParams copyWithZone:zone];
link->_queryParams = [_queryParams copyWithZone:zone];
return link;
}
@end

View file

@ -0,0 +1,98 @@
//
// Copyright 2022 New Vector Ltd
//
// 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 XCTest
@testable import Riot
class UniversalLinkTests: XCTestCase {
enum UniversalLinkTestError: Error {
case invalidURL
}
func testInitialization() throws {
guard let url = URL(string: "https://example.com") else {
throw UniversalLinkTestError.invalidURL
}
let universalLink = UniversalLink(url: url)
XCTAssertEqual(universalLink.url, url)
XCTAssertTrue(universalLink.pathParams.isEmpty)
XCTAssertTrue(universalLink.queryParams.isEmpty)
}
func testRegistrationLink() throws {
guard let url = URL(string: "https://app.element.io/#/register/?hs_url=matrix.example.com&is_url=identity.example.com") else {
throw UniversalLinkTestError.invalidURL
}
let universalLink = UniversalLink(url: url)
XCTAssertEqual(universalLink.url, url)
XCTAssertEqual(universalLink.pathParams.count, 1)
XCTAssertEqual(universalLink.pathParams.first, "register")
XCTAssertEqual(universalLink.queryParams.count, 2)
XCTAssertEqual(universalLink.homeserverUrl, "matrix.example.com")
XCTAssertEqual(universalLink.identityServerUrl, "identity.example.com")
}
func testLoginLink() throws {
guard let url = URL(string: "https://mobile.element.io/?hs_url=matrix.example.com&is_url=identity.example.com") else {
throw UniversalLinkTestError.invalidURL
}
let universalLink = UniversalLink(url: url)
XCTAssertEqual(universalLink.url, url)
XCTAssertTrue(universalLink.pathParams.isEmpty)
XCTAssertEqual(universalLink.queryParams.count, 2)
XCTAssertEqual(universalLink.homeserverUrl, "matrix.example.com")
XCTAssertEqual(universalLink.identityServerUrl, "identity.example.com")
}
func testPathParams() throws {
guard let url = URL(string: "https://mobile.element.io/#/param1/param2/param3?hs_url=matrix.example.com&is_url=identity.example.com") else {
throw UniversalLinkTestError.invalidURL
}
let universalLink = UniversalLink(url: url)
XCTAssertEqual(universalLink.url, url)
XCTAssertEqual(universalLink.pathParams.count, 3)
XCTAssertEqual(universalLink.pathParams[0], "param1")
XCTAssertEqual(universalLink.pathParams[1], "param2")
XCTAssertEqual(universalLink.pathParams[2], "param3")
XCTAssertEqual(universalLink.queryParams.count, 2)
XCTAssertEqual(universalLink.homeserverUrl, "matrix.example.com")
XCTAssertEqual(universalLink.identityServerUrl, "identity.example.com")
}
func testVia() throws {
guard let url = URL(string: "https://mobile.element.io/?hs_url=matrix.example.com&is_url=identity.example.com&via=param1&via=param2") else {
throw UniversalLinkTestError.invalidURL
}
let universalLink = UniversalLink(url: url)
XCTAssertEqual(universalLink.url, url)
XCTAssertEqual(universalLink.queryParams.count, 3)
XCTAssertEqual(universalLink.homeserverUrl, "matrix.example.com")
XCTAssertEqual(universalLink.identityServerUrl, "identity.example.com")
XCTAssertEqual(universalLink.via, ["param1", "param2"])
}
func testDescription() throws {
let str = "https://mobile.element.io/?hs_url=matrix.example.com&is_url=identity.example.com&via=param1&via=param2"
guard let url = URL(string: str) else {
throw UniversalLinkTestError.invalidURL
}
let universalLink = UniversalLink(url: url)
let desc = String(format: "<UniversalLink: %@>", str)
XCTAssertEqual(universalLink.description, desc)
}
}