Skip to content

Commit

Permalink
Fix #3 by encoding correctly the query parameters into callback urls
Browse files Browse the repository at this point in the history
  • Loading branch information
phimage committed Aug 20, 2016
1 parent 909a765 commit df27538
Show file tree
Hide file tree
Showing 16 changed files with 266 additions and 222 deletions.
2 changes: 1 addition & 1 deletion CallbackURLKit.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Pod::Spec.new do |s|

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

Expand Down
2 changes: 1 addition & 1 deletion CallbackURLKit/CallbackURLKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ extension FailureCallbackErrorType {
return [kXCUErrorCode: "\(self.code)", kXCUErrorMessage: self.message]
}
public var XCUErrorQuery: String {
return XCUErrorParameters.query
return XCUErrorParameters.queryString
}
}

Expand Down
61 changes: 50 additions & 11 deletions CallbackURLKit/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,58 @@ SOFTWARE.

import Foundation

// MARK: String
extension String {
var parseURLParams: [String : String] {

var toQueryDictionary: [String : String] {
var result: [String : String] = [String : String]()
let pairs: [String] = self.componentsSeparatedByString("&")
for pair in pairs {
var comps: [String] = pair.componentsSeparatedByString("=")
if comps.count >= 2 {
let key = comps[0]
let value = comps.dropFirst().joinWithSeparator("=")
result[key] = value.stringByRemovingPercentEncoding
result[key.queryDecode] = value.queryDecode
}
}
return result
}

var queryEncodeRFC3986: String {
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="

let allowedCharacterSet = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy() as! NSMutableCharacterSet
allowedCharacterSet.removeCharactersInString(generalDelimitersToEncode + subDelimitersToEncode)

return self.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? self
}

var queryEncode: String {
return self.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) ?? self
}

var queryDecode: String {
return self.stringByRemovingPercentEncoding ?? self
}

}

// MARK: Dictionary
extension Dictionary {

var query: String {
var queryString: String {
var parts = [String]()
for (key, value) in self {
let keyString = "\(key)".stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
let valueString = "\(value)".stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
let keyString = "\(key)".queryEncodeRFC3986
let valueString = "\(value)".queryEncodeRFC3986
let query = "\(keyString)=\(valueString)"
parts.append(query)
}
return parts.joinWithSeparator("&") as String
}

func join(other: Dictionary) -> Dictionary {
private func join(other: Dictionary) -> Dictionary {
var joinedDictionary = Dictionary()

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


// MARK: NSURLComponents
extension NSURLComponents {
func addToQuery(add: String) {
if let query = self.query {
self.query = query + "&" + add

var queryDictionary: [String: String] {
get {
guard let query = self.query else {
return [:]
}
return query.toQueryDictionary
}
set {
if newValue.isEmpty {
self.query = nil
} else {
self.percentEncodedQuery = newValue.queryString
}
}
}

private func addToQuery(add: String) {
if let query = self.percentEncodedQuery {
self.percentEncodedQuery = query + "&" + add
} else {
self.query = add
self.percentEncodedQuery = add
}
}

}

func &= (left: NSURLComponents, right: String) { left.addToQuery(right) }
func &= (left: NSURLComponents, right: String) { left.addToQuery(right) }

62 changes: 28 additions & 34 deletions CallbackURLKit/Manager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public class Manager {

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

let parameters = url.query?.parseURLParams ?? [:]
let parameters = url.query?.toQueryDictionary ?? [:]
let actionParameters = Manager.actionParameters(parameters)

// is a reponse?
Expand All @@ -93,31 +93,20 @@ public class Manager {
return false
}
else if let actionHandler = actions[action] { // handle the action
let successCallback = { (returnParams: Parameters?) in
if let urlString = parameters[kXCUSuccess], url = NSURL(string: urlString) {
// returnParams
let comp = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)!
if let query = returnParams?.query {
let successCallback: SuccessCallback = { [weak self] returnParams in
self?.openCallback(parameters, type: .success) { comp in
if let query = returnParams?.queryString {
comp &= query
}
if let newURL = comp.URL {
Manager.openURL(newURL)
}
}
}
let failureCallback = { (error: FailureCallbackErrorType) in
if let urlString = parameters[kXCUError], url = NSURL(string: urlString) {
let comp = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)!
let failureCallback: FailureCallback = { [weak self] error in
self?.openCallback(parameters, type: .error) { comp in
comp &= error.XCUErrorQuery
if let newURL = comp.URL {
Manager.openURL(newURL)
}
}
}
let cancelCallback = {
if let urlString = parameters[kXCUCancel], url = NSURL(string: urlString) {
Manager.openURL(url)
}
let cancelCallback: CancelCallback = { [weak self] in
self?.openCallback(parameters, type: .cancel)
}

actionHandler(actionParameters, successCallback, failureCallback, cancelCallback)
Expand All @@ -138,6 +127,16 @@ public class Manager {
}
return false
}

private func openCallback(parameters: [String : String], type: ResponseType, handler: ((NSURLComponents) -> Void)? = nil ) {
if let urlString = parameters[type.key], url = NSURL(string: urlString),
comp = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) {
handler?(comp)
if let newURL = comp.URL {
Manager.openURL(newURL)
}
}
}

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

// MARK: internal


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

let xcuParams: Parameters = [kRequestID: request.ID]

if request.successCallback != nil {
xcuComponents.query = (xcuParams + [kResponseType: ResponseType.success.rawValue]).query
query[kXCUSuccess] = xcuComponents.URL?.absoluteString ?? ""

xcuComponents.query = (xcuParams + [kResponseType: ResponseType.cancel.rawValue]).query
query[kXCUCancel] = xcuComponents.URL?.absoluteString ?? ""
}
if request.failureCallback != nil {
xcuComponents.query = (xcuParams + [kResponseType: ResponseType.error.rawValue]).query
query[kXCUError] = xcuComponents.URL?.absoluteString ?? ""

for reponseType in request.responseTypes {
xcuComponents.queryDictionary = xcuParams + [kResponseType: reponseType.rawValue]
if let urlString = xcuComponents.URL?.absoluteString {
query[reponseType.key] = urlString
}
}

if request.hasCallback {
Expand Down Expand Up @@ -248,7 +243,7 @@ public class Manager {
}

static var appName: String {
if let appName = NSBundle.mainBundle().localizedInfoDictionary?["CFBundleDisplayName"] as? String{
if let appName = NSBundle.mainBundle().localizedInfoDictionary?["CFBundleDisplayName"] as? String {
return appName
}
return NSBundle.mainBundle().infoDictionary?["CFBundleDisplayName"] as? String ?? "CallbackURLKit"
Expand All @@ -261,11 +256,10 @@ public class Manager {
extension Manager {

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

public func handleGetURLEvent(event: NSAppleEventDescriptor!, withReplyEvent replyEvent: NSAppleEventDescriptor!)
{
@objc public func handleURLEvent(event: NSAppleEventDescriptor, withReply replyEvent: NSAppleEventDescriptor) {
if let urlString = event.paramDescriptorForKeyword(AEKeyword(keyDirectObject))?.stringValue, url = NSURL(string: urlString) {
handleOpenURL(url)
}
Expand Down
32 changes: 29 additions & 3 deletions CallbackURLKit/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,44 @@ struct Request {
components.scheme = self.client.URLScheme
components.host = kXCUHost
components.path = "/\(self.action)"
components.query = (parameters + query).query
components.queryDictionary = (parameters + query)
return components
}

var hasCallback: Bool {
return successCallback != nil || failureCallback != nil || cancelCallback != nil
}

}
var responseTypes: [ResponseType] {
var responseTypes = [ResponseType]()
if self.successCallback != nil {
responseTypes.append(.success)
}
if self.cancelCallback != nil {
responseTypes.append(.cancel)
}
if self.failureCallback != nil {
responseTypes.append(.error)
}
return responseTypes
}

}
// Callback response type
enum ResponseType: String {
case success
case error
case cancel
}

var key: String {
switch self {
case .success:
return kXCUSuccess
case .error:
return kXCUError
case .cancel:
return kXCUCancel
}
}
}

17 changes: 2 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,8 @@ func application(application: UIApplication, openURL url: NSURL, sourceApplicati
return true
}
```
On OSX
```swift
func applicationDidFinishLaunching(aNotification: NSNotification) {
...
NSAppleEventManager.sharedAppleEventManager().setEventHandler(self, andSelector:"handleGetURLEvent:withReplyEvent:", forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
}
func handleGetURLEvent(event: NSAppleEventDescriptor!, withReplyEvent: NSAppleEventDescriptor!) {
if let urlString = event.paramDescriptorForKeyword(AEKeyword(keyDirectObject))?.stringValue, url = NSURL(string: urlString) {
manager.handleOpenURL(url)
}
}
```
Or in OSX if you have no other need with URL events you can let manager do all the job by calling into `applicationDidFinishLaunching`
the method `Manager.registerToURLEvent()`

On OSX if you have no other need with URL events you can let manager do all the job by calling into `applicationDidFinishLaunching`
the method `Manager.instance.registerToURLEvent()`

#### Add new action
The client application will interact with your application using the following URL Structure.
Expand Down
24 changes: 10 additions & 14 deletions SampleApp/CallbackURLKitDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
/* Begin PBXBuildFile section */
2D82E9831C46ED3DB8D898C8 /* Pods_CallbackURLKitDemoOSX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEFE74F8F5010842B34724E9 /* Pods_CallbackURLKitDemoOSX.framework */; };
574DDA2DC910102F338C8BDB /* Pods_CallbackURLKitDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 899A43CB2504DC3678E479C9 /* Pods_CallbackURLKitDemo.framework */; };
C46E5DA31C399EBF0061E818 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E5DA21C399EBF0061E818 /* Shared.swift */; };
C46E5DA41C399EBF0061E818 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E5DA21C399EBF0061E818 /* Shared.swift */; };
C4A4AA871C399A1500932E7D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A4AA861C399A1500932E7D /* AppDelegate.swift */; };
C4A4AA891C399A1500932E7D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A4AA881C399A1500932E7D /* ViewController.swift */; };
C46E5DA31C399EBF0061E818 /* CallbackURLKitDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E5DA21C399EBF0061E818 /* CallbackURLKitDemo.swift */; };
C46E5DA41C399EBF0061E818 /* CallbackURLKitDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E5DA21C399EBF0061E818 /* CallbackURLKitDemo.swift */; };
C47767001D68B01E00A66C25 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E32D6B1C3999AE005CD033 /* AppDelegate.swift */; };
C47767011D68B08000A66C25 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E32D6D1C3999AE005CD033 /* ViewController.swift */; };
C4A4AA8E1C399A1500932E7D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C4A4AA8C1C399A1500932E7D /* Main.storyboard */; };
C4E32D6C1C3999AE005CD033 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E32D6B1C3999AE005CD033 /* AppDelegate.swift */; };
C4E32D6E1C3999AE005CD033 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E32D6D1C3999AE005CD033 /* ViewController.swift */; };
Expand All @@ -26,10 +26,8 @@
899A43CB2504DC3678E479C9 /* Pods_CallbackURLKitDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CallbackURLKitDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
BEFE74F8F5010842B34724E9 /* Pods_CallbackURLKitDemoOSX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CallbackURLKitDemoOSX.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C46E5DA21C399EBF0061E818 /* Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = "<group>"; };
C46E5DA21C399EBF0061E818 /* CallbackURLKitDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallbackURLKitDemo.swift; sourceTree = "<group>"; };
C4A4AA841C399A1500932E7D /* CallbackURLKitDemoOSX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CallbackURLKitDemoOSX.app; sourceTree = BUILT_PRODUCTS_DIR; };
C4A4AA861C399A1500932E7D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C4A4AA881C399A1500932E7D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
C4A4AA8D1C399A1500932E7D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
C4A4AA8F1C399A1500932E7D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C4E32D681C3999AE005CD033 /* CallbackURLKitDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CallbackURLKitDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -84,8 +82,6 @@
C4A4AA851C399A1500932E7D /* CallbackURLKitDemoOSX */ = {
isa = PBXGroup;
children = (
C4A4AA861C399A1500932E7D /* AppDelegate.swift */,
C4A4AA881C399A1500932E7D /* ViewController.swift */,
C4A4AA8C1C399A1500932E7D /* Main.storyboard */,
C4A4AA8F1C399A1500932E7D /* Info.plist */,
);
Expand Down Expand Up @@ -115,12 +111,12 @@
C4E32D6A1C3999AE005CD033 /* CallbackURLKitDemo */ = {
isa = PBXGroup;
children = (
C46E5DA21C399EBF0061E818 /* CallbackURLKitDemo.swift */,
C4E32D6B1C3999AE005CD033 /* AppDelegate.swift */,
C4E32D6D1C3999AE005CD033 /* ViewController.swift */,
C4E32D6F1C3999AE005CD033 /* Main.storyboard */,
C4E32D741C3999AE005CD033 /* LaunchScreen.storyboard */,
C4E32D771C3999AE005CD033 /* Info.plist */,
C46E5DA21C399EBF0061E818 /* Shared.swift */,
);
path = CallbackURLKitDemo;
sourceTree = "<group>";
Expand Down Expand Up @@ -323,17 +319,17 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C46E5DA41C399EBF0061E818 /* Shared.swift in Sources */,
C4A4AA891C399A1500932E7D /* ViewController.swift in Sources */,
C4A4AA871C399A1500932E7D /* AppDelegate.swift in Sources */,
C47767001D68B01E00A66C25 /* AppDelegate.swift in Sources */,
C46E5DA41C399EBF0061E818 /* CallbackURLKitDemo.swift in Sources */,
C47767011D68B08000A66C25 /* ViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C4E32D641C3999AE005CD033 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C46E5DA31C399EBF0061E818 /* Shared.swift in Sources */,
C46E5DA31C399EBF0061E818 /* CallbackURLKitDemo.swift in Sources */,
C4E32D6E1C3999AE005CD033 /* ViewController.swift in Sources */,
C4E32D6C1C3999AE005CD033 /* AppDelegate.swift in Sources */,
);
Expand Down
Loading

0 comments on commit df27538

Please sign in to comment.