Skip to content

Commit df27538

Browse files
committed
Fix #3 by encoding correctly the query parameters into callback urls
1 parent 909a765 commit df27538

File tree

16 files changed

+266
-222
lines changed

16 files changed

+266
-222
lines changed

CallbackURLKit.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Pod::Spec.new do |s|
22

33
# ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
44
s.name = "CallbackURLKit"
5-
s.version = "0.1.2"
5+
s.version = "0.2.0"
66
s.summary = "Implemenation of x-callback-url in swift"
77
s.homepage = "https://github.com/phimage/CallbackURLKit"
88

CallbackURLKit/CallbackURLKit.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ extension FailureCallbackErrorType {
6969
return [kXCUErrorCode: "\(self.code)", kXCUErrorMessage: self.message]
7070
}
7171
public var XCUErrorQuery: String {
72-
return XCUErrorParameters.query
72+
return XCUErrorParameters.queryString
7373
}
7474
}
7575

CallbackURLKit/Extensions.swift

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,58 @@ SOFTWARE.
2323

2424
import Foundation
2525

26+
// MARK: String
2627
extension String {
27-
var parseURLParams: [String : String] {
28+
29+
var toQueryDictionary: [String : String] {
2830
var result: [String : String] = [String : String]()
2931
let pairs: [String] = self.componentsSeparatedByString("&")
3032
for pair in pairs {
3133
var comps: [String] = pair.componentsSeparatedByString("=")
3234
if comps.count >= 2 {
3335
let key = comps[0]
3436
let value = comps.dropFirst().joinWithSeparator("=")
35-
result[key] = value.stringByRemovingPercentEncoding
37+
result[key.queryDecode] = value.queryDecode
3638
}
3739
}
3840
return result
3941
}
4042

43+
var queryEncodeRFC3986: String {
44+
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
45+
let subDelimitersToEncode = "!$&'()*+,;="
46+
47+
let allowedCharacterSet = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy() as! NSMutableCharacterSet
48+
allowedCharacterSet.removeCharactersInString(generalDelimitersToEncode + subDelimitersToEncode)
49+
50+
return self.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? self
51+
}
52+
53+
var queryEncode: String {
54+
return self.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) ?? self
55+
}
56+
57+
var queryDecode: String {
58+
return self.stringByRemovingPercentEncoding ?? self
59+
}
60+
4161
}
4262

63+
// MARK: Dictionary
4364
extension Dictionary {
4465

45-
var query: String {
66+
var queryString: String {
4667
var parts = [String]()
4768
for (key, value) in self {
48-
let keyString = "\(key)".stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
49-
let valueString = "\(value)".stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
69+
let keyString = "\(key)".queryEncodeRFC3986
70+
let valueString = "\(value)".queryEncodeRFC3986
5071
let query = "\(keyString)=\(valueString)"
5172
parts.append(query)
5273
}
5374
return parts.joinWithSeparator("&") as String
5475
}
5576

56-
func join(other: Dictionary) -> Dictionary {
77+
private func join(other: Dictionary) -> Dictionary {
5778
var joinedDictionary = Dictionary()
5879

5980
for (key, value) in self {
@@ -78,16 +99,34 @@ extension Dictionary {
7899
func +<K, V> (left: [K : V], right: [K : V]) -> [K : V] { return left.join(right) }
79100

80101

102+
// MARK: NSURLComponents
81103
extension NSURLComponents {
82-
func addToQuery(add: String) {
83-
if let query = self.query {
84-
self.query = query + "&" + add
104+
105+
var queryDictionary: [String: String] {
106+
get {
107+
guard let query = self.query else {
108+
return [:]
109+
}
110+
return query.toQueryDictionary
111+
}
112+
set {
113+
if newValue.isEmpty {
114+
self.query = nil
115+
} else {
116+
self.percentEncodedQuery = newValue.queryString
117+
}
118+
}
119+
}
120+
121+
private func addToQuery(add: String) {
122+
if let query = self.percentEncodedQuery {
123+
self.percentEncodedQuery = query + "&" + add
85124
} else {
86-
self.query = add
125+
self.percentEncodedQuery = add
87126
}
88127
}
89128

90129
}
91130

92-
func &= (left: NSURLComponents, right: String) { left.addToQuery(right) }
131+
func &= (left: NSURLComponents, right: String) { left.addToQuery(right) }
93132

CallbackURLKit/Manager.swift

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public class Manager {
6969

7070
let action = String(path.characters.dropFirst()) // remove /
7171

72-
let parameters = url.query?.parseURLParams ?? [:]
72+
let parameters = url.query?.toQueryDictionary ?? [:]
7373
let actionParameters = Manager.actionParameters(parameters)
7474

7575
// is a reponse?
@@ -93,31 +93,20 @@ public class Manager {
9393
return false
9494
}
9595
else if let actionHandler = actions[action] { // handle the action
96-
let successCallback = { (returnParams: Parameters?) in
97-
if let urlString = parameters[kXCUSuccess], url = NSURL(string: urlString) {
98-
// returnParams
99-
let comp = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)!
100-
if let query = returnParams?.query {
96+
let successCallback: SuccessCallback = { [weak self] returnParams in
97+
self?.openCallback(parameters, type: .success) { comp in
98+
if let query = returnParams?.queryString {
10199
comp &= query
102100
}
103-
if let newURL = comp.URL {
104-
Manager.openURL(newURL)
105-
}
106101
}
107102
}
108-
let failureCallback = { (error: FailureCallbackErrorType) in
109-
if let urlString = parameters[kXCUError], url = NSURL(string: urlString) {
110-
let comp = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)!
103+
let failureCallback: FailureCallback = { [weak self] error in
104+
self?.openCallback(parameters, type: .error) { comp in
111105
comp &= error.XCUErrorQuery
112-
if let newURL = comp.URL {
113-
Manager.openURL(newURL)
114-
}
115106
}
116107
}
117-
let cancelCallback = {
118-
if let urlString = parameters[kXCUCancel], url = NSURL(string: urlString) {
119-
Manager.openURL(url)
120-
}
108+
let cancelCallback: CancelCallback = { [weak self] in
109+
self?.openCallback(parameters, type: .cancel)
121110
}
122111

123112
actionHandler(actionParameters, successCallback, failureCallback, cancelCallback)
@@ -138,6 +127,16 @@ public class Manager {
138127
}
139128
return false
140129
}
130+
131+
private func openCallback(parameters: [String : String], type: ResponseType, handler: ((NSURLComponents) -> Void)? = nil ) {
132+
if let urlString = parameters[type.key], url = NSURL(string: urlString),
133+
comp = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) {
134+
handler?(comp)
135+
if let newURL = comp.URL {
136+
Manager.openURL(newURL)
137+
}
138+
}
139+
}
141140

142141
// Handle url with manager shared instance
143142
public static func handleOpenURL(url: NSURL) -> Bool {
@@ -183,6 +182,7 @@ public class Manager {
183182

184183
// MARK: internal
185184

185+
186186
func sendRequest(request: Request) throws {
187187
if !request.client.appInstalled {
188188
throw CallbackURLKitError.AppWithSchemeNotInstalled(scheme: request.client.URLScheme)
@@ -199,17 +199,12 @@ public class Manager {
199199
xcuComponents.path = "/" + kResponse
200200

201201
let xcuParams: Parameters = [kRequestID: request.ID]
202-
203-
if request.successCallback != nil {
204-
xcuComponents.query = (xcuParams + [kResponseType: ResponseType.success.rawValue]).query
205-
query[kXCUSuccess] = xcuComponents.URL?.absoluteString ?? ""
206-
207-
xcuComponents.query = (xcuParams + [kResponseType: ResponseType.cancel.rawValue]).query
208-
query[kXCUCancel] = xcuComponents.URL?.absoluteString ?? ""
209-
}
210-
if request.failureCallback != nil {
211-
xcuComponents.query = (xcuParams + [kResponseType: ResponseType.error.rawValue]).query
212-
query[kXCUError] = xcuComponents.URL?.absoluteString ?? ""
202+
203+
for reponseType in request.responseTypes {
204+
xcuComponents.queryDictionary = xcuParams + [kResponseType: reponseType.rawValue]
205+
if let urlString = xcuComponents.URL?.absoluteString {
206+
query[reponseType.key] = urlString
207+
}
213208
}
214209

215210
if request.hasCallback {
@@ -248,7 +243,7 @@ public class Manager {
248243
}
249244

250245
static var appName: String {
251-
if let appName = NSBundle.mainBundle().localizedInfoDictionary?["CFBundleDisplayName"] as? String{
246+
if let appName = NSBundle.mainBundle().localizedInfoDictionary?["CFBundleDisplayName"] as? String {
252247
return appName
253248
}
254249
return NSBundle.mainBundle().infoDictionary?["CFBundleDisplayName"] as? String ?? "CallbackURLKit"
@@ -261,11 +256,10 @@ public class Manager {
261256
extension Manager {
262257

263258
public func registerToURLEvent() {
264-
NSAppleEventManager.sharedAppleEventManager().setEventHandler(self, andSelector:"handleGetURLEvent:withReplyEvent:", forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
259+
NSAppleEventManager.sharedAppleEventManager().setEventHandler(self, andSelector: #selector(Manager.handleURLEvent(_:withReply:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
265260
}
266261

267-
public func handleGetURLEvent(event: NSAppleEventDescriptor!, withReplyEvent replyEvent: NSAppleEventDescriptor!)
268-
{
262+
@objc public func handleURLEvent(event: NSAppleEventDescriptor, withReply replyEvent: NSAppleEventDescriptor) {
269263
if let urlString = event.paramDescriptorForKeyword(AEKeyword(keyDirectObject))?.stringValue, url = NSURL(string: urlString) {
270264
handleOpenURL(url)
271265
}

CallbackURLKit/Request.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,44 @@ struct Request {
4141
components.scheme = self.client.URLScheme
4242
components.host = kXCUHost
4343
components.path = "/\(self.action)"
44-
components.query = (parameters + query).query
44+
components.queryDictionary = (parameters + query)
4545
return components
4646
}
4747

4848
var hasCallback: Bool {
4949
return successCallback != nil || failureCallback != nil || cancelCallback != nil
5050
}
5151

52-
}
52+
var responseTypes: [ResponseType] {
53+
var responseTypes = [ResponseType]()
54+
if self.successCallback != nil {
55+
responseTypes.append(.success)
56+
}
57+
if self.cancelCallback != nil {
58+
responseTypes.append(.cancel)
59+
}
60+
if self.failureCallback != nil {
61+
responseTypes.append(.error)
62+
}
63+
return responseTypes
64+
}
5365

66+
}
67+
// Callback response type
5468
enum ResponseType: String {
5569
case success
5670
case error
5771
case cancel
58-
}
72+
73+
var key: String {
74+
switch self {
75+
case .success:
76+
return kXCUSuccess
77+
case .error:
78+
return kXCUError
79+
case .cancel:
80+
return kXCUCancel
81+
}
82+
}
83+
}
84+

README.md

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,21 +113,8 @@ func application(application: UIApplication, openURL url: NSURL, sourceApplicati
113113
return true
114114
}
115115
```
116-
On OSX
117-
```swift
118-
func applicationDidFinishLaunching(aNotification: NSNotification) {
119-
...
120-
NSAppleEventManager.sharedAppleEventManager().setEventHandler(self, andSelector:"handleGetURLEvent:withReplyEvent:", forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
121-
}
122-
func handleGetURLEvent(event: NSAppleEventDescriptor!, withReplyEvent: NSAppleEventDescriptor!) {
123-
if let urlString = event.paramDescriptorForKeyword(AEKeyword(keyDirectObject))?.stringValue, url = NSURL(string: urlString) {
124-
manager.handleOpenURL(url)
125-
}
126-
}
127-
```
128-
Or in OSX if you have no other need with URL events you can let manager do all the job by calling into `applicationDidFinishLaunching`
129-
the method `Manager.registerToURLEvent()`
130-
116+
On OSX if you have no other need with URL events you can let manager do all the job by calling into `applicationDidFinishLaunching`
117+
the method `Manager.instance.registerToURLEvent()`
131118

132119
#### Add new action
133120
The client application will interact with your application using the following URL Structure.

SampleApp/CallbackURLKitDemo.xcodeproj/project.pbxproj

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
/* Begin PBXBuildFile section */
1010
2D82E9831C46ED3DB8D898C8 /* Pods_CallbackURLKitDemoOSX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEFE74F8F5010842B34724E9 /* Pods_CallbackURLKitDemoOSX.framework */; };
1111
574DDA2DC910102F338C8BDB /* Pods_CallbackURLKitDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 899A43CB2504DC3678E479C9 /* Pods_CallbackURLKitDemo.framework */; };
12-
C46E5DA31C399EBF0061E818 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E5DA21C399EBF0061E818 /* Shared.swift */; };
13-
C46E5DA41C399EBF0061E818 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E5DA21C399EBF0061E818 /* Shared.swift */; };
14-
C4A4AA871C399A1500932E7D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A4AA861C399A1500932E7D /* AppDelegate.swift */; };
15-
C4A4AA891C399A1500932E7D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A4AA881C399A1500932E7D /* ViewController.swift */; };
12+
C46E5DA31C399EBF0061E818 /* CallbackURLKitDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E5DA21C399EBF0061E818 /* CallbackURLKitDemo.swift */; };
13+
C46E5DA41C399EBF0061E818 /* CallbackURLKitDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E5DA21C399EBF0061E818 /* CallbackURLKitDemo.swift */; };
14+
C47767001D68B01E00A66C25 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E32D6B1C3999AE005CD033 /* AppDelegate.swift */; };
15+
C47767011D68B08000A66C25 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E32D6D1C3999AE005CD033 /* ViewController.swift */; };
1616
C4A4AA8E1C399A1500932E7D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C4A4AA8C1C399A1500932E7D /* Main.storyboard */; };
1717
C4E32D6C1C3999AE005CD033 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E32D6B1C3999AE005CD033 /* AppDelegate.swift */; };
1818
C4E32D6E1C3999AE005CD033 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E32D6D1C3999AE005CD033 /* ViewController.swift */; };
@@ -26,10 +26,8 @@
2626
899A43CB2504DC3678E479C9 /* Pods_CallbackURLKitDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CallbackURLKitDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2727
94ACDE1068C6A2C95BE32518 /* Pods-CallbackURLKitDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CallbackURLKitDemo.release.xcconfig"; path = "Pods/Target Support Files/Pods-CallbackURLKitDemo/Pods-CallbackURLKitDemo.release.xcconfig"; sourceTree = "<group>"; };
2828
BEFE74F8F5010842B34724E9 /* Pods_CallbackURLKitDemoOSX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CallbackURLKitDemoOSX.framework; sourceTree = BUILT_PRODUCTS_DIR; };
29-
C46E5DA21C399EBF0061E818 /* Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = "<group>"; };
29+
C46E5DA21C399EBF0061E818 /* CallbackURLKitDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallbackURLKitDemo.swift; sourceTree = "<group>"; };
3030
C4A4AA841C399A1500932E7D /* CallbackURLKitDemoOSX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CallbackURLKitDemoOSX.app; sourceTree = BUILT_PRODUCTS_DIR; };
31-
C4A4AA861C399A1500932E7D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
32-
C4A4AA881C399A1500932E7D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
3331
C4A4AA8D1C399A1500932E7D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
3432
C4A4AA8F1C399A1500932E7D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3533
C4E32D681C3999AE005CD033 /* CallbackURLKitDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CallbackURLKitDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -84,8 +82,6 @@
8482
C4A4AA851C399A1500932E7D /* CallbackURLKitDemoOSX */ = {
8583
isa = PBXGroup;
8684
children = (
87-
C4A4AA861C399A1500932E7D /* AppDelegate.swift */,
88-
C4A4AA881C399A1500932E7D /* ViewController.swift */,
8985
C4A4AA8C1C399A1500932E7D /* Main.storyboard */,
9086
C4A4AA8F1C399A1500932E7D /* Info.plist */,
9187
);
@@ -115,12 +111,12 @@
115111
C4E32D6A1C3999AE005CD033 /* CallbackURLKitDemo */ = {
116112
isa = PBXGroup;
117113
children = (
114+
C46E5DA21C399EBF0061E818 /* CallbackURLKitDemo.swift */,
118115
C4E32D6B1C3999AE005CD033 /* AppDelegate.swift */,
119116
C4E32D6D1C3999AE005CD033 /* ViewController.swift */,
120117
C4E32D6F1C3999AE005CD033 /* Main.storyboard */,
121118
C4E32D741C3999AE005CD033 /* LaunchScreen.storyboard */,
122119
C4E32D771C3999AE005CD033 /* Info.plist */,
123-
C46E5DA21C399EBF0061E818 /* Shared.swift */,
124120
);
125121
path = CallbackURLKitDemo;
126122
sourceTree = "<group>";
@@ -323,17 +319,17 @@
323319
isa = PBXSourcesBuildPhase;
324320
buildActionMask = 2147483647;
325321
files = (
326-
C46E5DA41C399EBF0061E818 /* Shared.swift in Sources */,
327-
C4A4AA891C399A1500932E7D /* ViewController.swift in Sources */,
328-
C4A4AA871C399A1500932E7D /* AppDelegate.swift in Sources */,
322+
C47767001D68B01E00A66C25 /* AppDelegate.swift in Sources */,
323+
C46E5DA41C399EBF0061E818 /* CallbackURLKitDemo.swift in Sources */,
324+
C47767011D68B08000A66C25 /* ViewController.swift in Sources */,
329325
);
330326
runOnlyForDeploymentPostprocessing = 0;
331327
};
332328
C4E32D641C3999AE005CD033 /* Sources */ = {
333329
isa = PBXSourcesBuildPhase;
334330
buildActionMask = 2147483647;
335331
files = (
336-
C46E5DA31C399EBF0061E818 /* Shared.swift in Sources */,
332+
C46E5DA31C399EBF0061E818 /* CallbackURLKitDemo.swift in Sources */,
337333
C4E32D6E1C3999AE005CD033 /* ViewController.swift in Sources */,
338334
C4E32D6C1C3999AE005CD033 /* AppDelegate.swift in Sources */,
339335
);

0 commit comments

Comments
 (0)