mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Update UniversalLink class to parse path and query params
This commit is contained in:
parent
a9beeac55a
commit
36ebbc6b9c
4 changed files with 247 additions and 8 deletions
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
98
RiotTests/UniversalLinkTests.swift
Normal file
98
RiotTests/UniversalLinkTests.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue