Index: src/OFMutableURL.h ================================================================== --- src/OFMutableURL.h +++ src/OFMutableURL.h @@ -124,10 +124,24 @@ * than necessary are URL-encoded, it is kept this way. */ @property OF_NULLABLE_PROPERTY (readwrite, copy, nonatomic) OFString *URLEncodedQuery; +/** + * @brief The query part of the URL as a dictionary. + * + * For example, a query like `key1=value1&key2=value2` would correspond to the + * following dictionary: + * + * @{ + * @"key1": "value1", + * @"key2": "value2" + * } + */ +@property OF_NULLABLE_PROPERTY (readwrite, copy, nonatomic) + OFDictionary OF_GENERIC(OFString *, OFString *) *queryDictionary; + /** * @brief The fragment part of the URL. */ @property OF_NULLABLE_PROPERTY (readwrite, copy, nonatomic) OFString *fragment; Index: src/OFMutableURL.m ================================================================== --- src/OFMutableURL.m +++ src/OFMutableURL.m @@ -30,11 +30,12 @@ extern void of_url_verify_escaped(OFString *, OFCharacterSet *); @implementation OFMutableURL @dynamic scheme, URLEncodedScheme, host, URLEncodedHost, port, user; @dynamic URLEncodedUser, password, URLEncodedPassword, path, URLEncodedPath; -@dynamic pathComponents, query, URLEncodedQuery, fragment, URLEncodedFragment; +@dynamic pathComponents, query, URLEncodedQuery, queryDictionary, fragment; +@dynamic URLEncodedFragment; + (instancetype)URL { return [[[self alloc] init] autorelease]; } @@ -230,10 +231,41 @@ old = _URLEncodedQuery; _URLEncodedQuery = [URLEncodedQuery copy]; [old release]; } + +- (void)setQueryDictionary: + (OFDictionary OF_GENERIC(OFString *, OFString *) *)dictionary +{ + void *pool = objc_autoreleasePoolPush(); + OFMutableString *URLEncodedQuery = [OFMutableString string]; + OFEnumerator *keyEnumerator = [dictionary keyEnumerator]; + OFEnumerator *objectEnumerator = [dictionary objectEnumerator]; + OFCharacterSet *characterSet = + [OFCharacterSet URLQueryKeyValueAllowedCharacterSet]; + OFString *key, *object, *old; + + while ((key = [keyEnumerator nextObject]) != nil && + (object = [objectEnumerator nextObject]) != nil) { + key = [key + stringByURLEncodingWithAllowedCharacters: characterSet]; + object = [object + stringByURLEncodingWithAllowedCharacters: characterSet]; + + if (URLEncodedQuery.length > 0) + [URLEncodedQuery appendString: @"&"]; + + [URLEncodedQuery appendFormat: @"%@=%@", key, object]; + } + + old = _URLEncodedQuery; + _URLEncodedQuery = [URLEncodedQuery copy]; + [old release]; + + objc_autoreleasePoolPop(pool); +} - (void)setFragment: (OFString *)fragment { void *pool = objc_autoreleasePoolPush(); OFString *old = _URLEncodedFragment; Index: src/OFURL.h ================================================================== --- src/OFURL.h +++ src/OFURL.h @@ -20,10 +20,11 @@ #import "OFSerialization.h" OF_ASSUME_NONNULL_BEGIN @class OFArray OF_GENERIC(ObjectType); +@class OFDictionary OF_GENERIC(KeyType, ObjectType); @class OFNumber; @class OFString; /** * @class OFURL OFURL.h ObjFW/OFURL.h @@ -125,10 +126,24 @@ * @brief The query part of the URL in URL-encoded form. */ @property OF_NULLABLE_PROPERTY (readonly, copy, nonatomic) OFString *URLEncodedQuery; +/** + * @brief The query part of the URL as a dictionary. + * + * For example, a query like `key1=value1&key2=value2` would correspond to the + * following dictionary: + * + * @{ + * @"key1": "value1", + * @"key2": "value2" + * } + */ +@property OF_NULLABLE_PROPERTY (readonly, copy, nonatomic) + OFDictionary OF_GENERIC(OFString *, OFString *) *queryDictionary; + /** * @brief The fragment part of the URL. */ @property OF_NULLABLE_PROPERTY (readonly, copy, nonatomic) OFString *fragment; @@ -288,10 +303,12 @@ @property (class, readonly, nonatomic) OFCharacterSet *URLPathAllowedCharacterSet; @property (class, readonly, nonatomic) OFCharacterSet *URLQueryAllowedCharacterSet; @property (class, readonly, nonatomic) + OFCharacterSet *URLQueryKeyValueAllowedCharacterSet; +@property (class, readonly, nonatomic) OFCharacterSet *URLFragmentAllowedCharacterSet; #endif /** * @brief Returns the characters allowed in the scheme part of a URL. @@ -333,10 +350,18 @@ * * @return The characters allowed in the query part of a URL. */ + (OFCharacterSet *)URLQueryAllowedCharacterSet; +/** + * @brief Returns the characters allowed in a key/value in the query part of a + * URL. + * + * @return The characters allowed in a key/value in the query part of a URL. + */ ++ (OFCharacterSet *)URLQueryKeyValueAllowedCharacterSet; + /** * @brief Returns the characters allowed in the fragment part of a URL. * * @return The characters allowed in the fragment part of a URL. */ Index: src/OFURL.m ================================================================== --- src/OFURL.m +++ src/OFURL.m @@ -49,15 +49,19 @@ @interface OFURLPathAllowedCharacterSet: OFURLAllowedCharacterSetBase @end @interface OFURLQueryOrFragmentAllowedCharacterSet: OFURLAllowedCharacterSetBase @end + +@interface OFURLQueryKeyValueAllowedCharacterSet: OFURLAllowedCharacterSetBase +@end static OFCharacterSet *URLAllowedCharacterSet = nil; static OFCharacterSet *URLSchemeAllowedCharacterSet = nil; static OFCharacterSet *URLPathAllowedCharacterSet = nil; static OFCharacterSet *URLQueryOrFragmentAllowedCharacterSet = nil; +static OFCharacterSet *URLQueryKeyValueAllowedCharacterSet = nil; static of_once_t URLAllowedCharacterSetOnce = OF_ONCE_INIT; static of_once_t URLQueryOrFragmentAllowedCharacterSetOnce = OF_ONCE_INIT; static void @@ -84,10 +88,17 @@ initURLQueryOrFragmentAllowedCharacterSet(void) { URLQueryOrFragmentAllowedCharacterSet = [[OFURLQueryOrFragmentAllowedCharacterSet alloc] init]; } + +static void +initURLQueryKeyValueAllowedCharacterSet(void) +{ + URLQueryKeyValueAllowedCharacterSet = + [[OFURLQueryKeyValueAllowedCharacterSet alloc] init]; +} OF_DIRECT_MEMBERS @interface OFInvertedCharacterSetWithoutPercent: OFCharacterSet { OFCharacterSet *_characterSet; @@ -247,10 +258,41 @@ default: return false; } } @end + +@implementation OFURLQueryKeyValueAllowedCharacterSet +- (bool)characterIsMember: (of_unichar_t)character +{ + if (character < CHAR_MAX && of_ascii_isalnum(character)) + return true; + + switch (character) { + case '-': + case '.': + case '_': + case '~': + case '!': + case '$': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case ':': + case '@': + case '/': + case '?': + return true; + default: + return false; + } +} +@end @implementation OFInvertedCharacterSetWithoutPercent - (instancetype)initWithCharacterSet: (OFCharacterSet *)characterSet { self = [super init]; @@ -339,10 +381,18 @@ of_once(&URLQueryOrFragmentAllowedCharacterSetOnce, initURLQueryOrFragmentAllowedCharacterSet); return URLQueryOrFragmentAllowedCharacterSet; } + ++ (OFCharacterSet *)URLQueryKeyValueAllowedCharacterSet +{ + static of_once_t onceControl = OF_ONCE_INIT; + of_once(&onceControl, initURLQueryKeyValueAllowedCharacterSet); + + return URLQueryKeyValueAllowedCharacterSet; +} + (OFCharacterSet *)URLFragmentAllowedCharacterSet { of_once(&URLQueryOrFragmentAllowedCharacterSetOnce, initURLQueryOrFragmentAllowedCharacterSet); @@ -983,10 +1033,35 @@ - (OFString *)URLEncodedQuery { return _URLEncodedQuery; } + +- (OFDictionary OF_GENERIC(OFString *, OFString *) *)queryDictionary +{ + void *pool = objc_autoreleasePoolPush(); + OFArray *pairs = [_URLEncodedQuery componentsSeparatedByString: @"&"]; + OFMutableDictionary *ret = [OFMutableDictionary + dictionaryWithCapacity: pairs.count]; + + for (OFString *pair in pairs) { + OFArray *parts = [pair componentsSeparatedByString: @"="]; + + if (parts.count != 2) + @throw [OFInvalidFormatException exception]; + + [ret setObject: [[parts objectAtIndex: 1] stringByURLDecoding] + forKey: [[parts objectAtIndex: 0] stringByURLDecoding]]; + } + + [ret makeImmutable]; + [ret retain]; + + objc_autoreleasePoolPop(pool); + + return [ret autorelease]; +} - (OFString *)fragment { return _URLEncodedFragment.stringByURLDecoding; } Index: tests/OFURLTests.m ================================================================== --- tests/OFURLTests.m +++ tests/OFURLTests.m @@ -19,11 +19,11 @@ #import "TestsAppDelegate.h" static OFString *module = @"OFURL"; static OFString *url_str = @"ht%3atp://us%3Aer:p%40w@ho%3Ast:1234/" - @"pa%3Fth?que%23ry#frag%23ment"; + @"pa%3Fth?que%23ry=1&f%26oo=b%3dar#frag%23ment"; @implementation TestsAppDelegate (OFURLTests) - (void)URLTests { void *pool = objc_autoreleasePoolPush(); @@ -176,11 +176,15 @@ lastPathComponent] isEqual: @"foo"] && [[[OFURL URLWithString: @"http://host/"] lastPathComponent] isEqual: @"/"] && [u5.lastPathComponent isEqual: @"foo/bar"]) TEST(@"-[query]", - [u1.query isEqual: @"que#ry"] && u4.query == nil) + [u1.query isEqual: @"que#ry=1&f&oo=b=ar"] && u4.query == nil) + TEST(@"-[queryDictionary]", + [u1.queryDictionary isEqual: + [OFDictionary dictionaryWithKeysAndObjects: + @"que#ry", @"1", @"f&oo", @"b=ar", nil]]); TEST(@"-[fragment]", [u1.fragment isEqual: @"frag#ment"] && u4.fragment == nil) TEST(@"-[copy]", R(u4 = [[u1 copy] autorelease])) @@ -267,10 +271,16 @@ EXPECT_EXCEPTION( @"-[setURLEncodedQuery:] with invalid characters fails", OFInvalidFormatException, mu.URLEncodedQuery = @"`") + TEST(@"-[setQueryDictionary:]", + (mu.queryDictionary = [OFDictionary dictionaryWithKeysAndObjects: + @"foo&bar", @"baz=qux", @"f=oobar", @"b&azqux", nil]) && + [mu.URLEncodedQuery isEqual: + @"foo%26bar=baz%3Dqux&f%3Doobar=b%26azqux"]) + TEST(@"-[setFragment:]", (mu.fragment = @"frag/ment?#") && [mu.URLEncodedFragment isEqual: @"frag/ment?%23"]) TEST(@"-[setURLEncodedFragment:]",