├── .gitignore ├── .travis.yml ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Bridging.swift ├── CKAcceptSharesOperation.swift ├── CKAcceptSharesURLRequest.swift ├── CKAccount.swift ├── CKAccountStatus.swift ├── CKAsset.swift ├── CKCodable.swift ├── CKContainer.swift ├── CKContainerConfig.swift ├── CKContainerInfo.swift ├── CKDatabase.swift ├── CKDatabaseNotification.swift ├── CKDictionaryValue.swift ├── CKDiscoverAllUserIdentitiesOperation.swift ├── CKDiscoverUserIdentitiesOperation.swift ├── CKDownloadAssetsOperation.swift ├── CKError.swift ├── CKFetchErrorDictionary.swift ├── CKFetchRecordZonesOperation.swift ├── CKFetchRecordsOperation.swift ├── CKFetchSubscriptionsOperation.swift ├── CKLocation.swift ├── CKLocationSortDescriptor.swift ├── CKModifyRecordZonesOperation.swift ├── CKModifyRecordsOperation.swift ├── CKModifyRecordsURLRequest.swift ├── CKModifySubscriptionsOperation.swift ├── CKModifySubscriptionsURLRequest.swift ├── CKNotification.swift ├── CKOperation.swift ├── CKOperationInfo.swift ├── CKOperationMetrics.swift ├── CKOperationResult.swift ├── CKPersonNameComponents.swift ├── CKPredicate.swift ├── CKPredicateReader.swift ├── CKPrettyError.swift ├── CKPublishAssetsOperation.swift ├── CKPushConnection.swift ├── CKQuery.swift ├── CKQueryCursor.swift ├── CKQueryFilter.swift ├── CKQueryNotification.swift ├── CKQueryOperation.swift ├── CKQueryURLRequest.swift ├── CKRecord.swift ├── CKRecordID.swift ├── CKRecordZone.swift ├── CKRecordZoneNotification.swift ├── CKReference.swift ├── CKRegisterTokenOperation.swift ├── CKRequest.swift ├── CKServerRequestAuth.swift ├── CKServerType.swift ├── CKShare.swift ├── CKShareMetadata.swift ├── CKShareParticipant.swift ├── CKSubscription.swift ├── CKTokenCreateURLRequest.swift ├── CKTokenRegistrationURLRequest.swift ├── CKURLRequest.swift ├── CKUserIdentity.swift ├── CKUserIdentityLookupInfo.swift ├── CKWebRequest.swift ├── CLLocation+OpenCloudKit.swift ├── EVPDigestSign.swift ├── OpenCloudKit.swift └── SortDescriptor.swift └── Tests ├── LinuxMain.swift └── OpenCloudKitTests ├── CKConfigTests.swift ├── CKPredicateTests.swift ├── CKRecordTests.swift ├── CKShareMetadataTests.swift ├── CKTokenRegistrationURLRequestTests.swift ├── CKURLRequestTests.swift ├── OpenCloudKitTests.swift └── Supporting ├── config.json ├── eckey.pub ├── sharemetadata.json ├── signed.bin ├── test.txt └── testeckey.pem /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | language: generic 5 | sudo: required 6 | dist: trusty 7 | osx_image: xcode10 8 | script: 9 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 10 | DIR="$(pwd)"; 11 | cd ..; 12 | export SWIFT_VERSION=4.1; 13 | wget https://swift.org/builds/swift-${SWIFT_VERSION}-release/ubuntu1404/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu14.04.tar.gz; 14 | tar xzf swift-${SWIFT_VERSION}-RELEASE-ubuntu14.04.tar.gz; 15 | export PATH="${PWD}/swift-${SWIFT_VERSION}-RELEASE-ubuntu14.04/usr/bin:${PATH}"; 16 | cd "$DIR"; 17 | fi 18 | - swift package update 19 | - swift build 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Benjamin Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CLibreSSL", 6 | "repositoryURL": "https://github.com/vapor/clibressl.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "23ddb296981d17a8ee6c7418742a40cad5d2f9d0", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | { 14 | "package": "CryptoSwift", 15 | "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "3f6869c469abf3e2ebefe3b6baef4bfac3aac305", 19 | "version": "0.11.0" 20 | } 21 | }, 22 | { 23 | "package": "Jay", 24 | "repositoryURL": "https://github.com/DanToml/Jay.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "833125251b2d026bee6afa0aff2967a90eb63cd0", 28 | "version": "1.0.1" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "OpenCloudKit", 5 | dependencies: [ 6 | .Package(url: "https://github.com/vapor/clibressl.git", majorVersion: 1), 7 | .Package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", majorVersion: 0), 8 | .Package(url: "https://github.com/DanToml/Jay.git", majorVersion: 1) 9 | ] 10 | ) 11 | 12 | #if os(Linux) 13 | package.exclude.append("Sources/CLLocation+OpenCloudKit.swift") 14 | #endif 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenCloudKit 2 | An open source framework for CloudKit written in Swift and inspired by Apple's CloudKit framework. 3 | OpenCloudKit is backed by CloudKit Web Services and is designed to allow easy CloudKit integration into Swift Servers whilst being familiar for developers who have experience with CloudKit on Apple's platforms. 4 | 5 | ## Features 6 | 7 | - [x] Fully featured wrapper around CloudKit Web Services 8 | - [x] Same API as Apple's CloudKit 9 | - [x] Supports all major operations supported by Apple’s CloudKit Framework 10 | - [x] Server-to-Server Key Auth Support 11 | 12 | # Installation 13 | 14 | ## Swift Package Manager 15 | Add the following to dependencies in your `Package.swift`. 16 | ```swift 17 | .Package(url: "https://github.com/BennyKJohnson/OpenCloudKit.git", majorVersion: 0, minor: 5) 18 | ``` 19 | Or create the 'Package.swift' file for your project and add the following: 20 | ```swift 21 | import PackageDescription 22 | 23 | let package = Package( 24 | dependencies: [ 25 | .Package(url: "https://github.com/BennyKJohnson/OpenCloudKit.git", majorVersion: 0, minor: 5), 26 | ] 27 | ) 28 | ``` 29 | 30 | # Getting Started 31 | ## Configure OpenCloudKit 32 | Configuring OpenCloudKit is similar to configuring CloudKitJS. Use the `CloudKit.shared.configure(with: CKConfig)` method to config OpenCloudKit with a `CKConfig` instance. 33 | ### JSON configuration file 34 | You can store CloudKit configuration in a JSON file 35 | ```javascript 36 | // API Token Config 37 | { 38 | "containers": [{ 39 | "containerIdentifier": "[insert your container ID here]", 40 | "apiTokenAuth": { 41 | "apiToken": "[insert your API token and other authentication properties here]" 42 | }, 43 | "environment": "development" 44 | }] 45 | } 46 | 47 | // Server-to-Server Config 48 | { 49 | "containers": [{ 50 | "containerIdentifier": "[Container ID]", 51 | "serverToServerKeyAuth": { 52 | "keyID": "[Key ID]", 53 | "privateKeyFile":"eckey.pem" 54 | }, 55 | "environment": "development" 56 | }] 57 | } 58 | ``` 59 | Initialize a CKConfig from JSON file, OpenCloudKit supports the same structure as [CloudKit JS](https://developer.apple.com/library/ios/documentation/CloudKitJS/Reference/CloudKitJSTypesReference/index.html#//apple_ref/javascript/struct/CloudKit.CloudKitConfig) 60 | ```swift 61 | let config = CKConfig(contentsOfFile: "config.json") 62 | ``` 63 | ### Manual Configuration 64 | Below is an example of building a CKConfig manually 65 | ```swift 66 | let serverKeyAuth = CKServerToServerKeyAuth(keyID: "[KEY ID]",privateKeyFile: "eckey.pem") 67 | let defaultContainerConfig = CKContainerConfig(containerIdentifier: "[CONTAINER ID]", environment: .development, serverToServerKeyAuth: serverKeyAuth) 68 | let config = CKConfig(containers: [defaultContainerConfig]) 69 | 70 | CloudKit.shared.configure(with: config) 71 | ``` 72 | ## Working with OpenCloudKit 73 | Get the database in your app’s default container 74 | ```swift 75 | let container = CKContainer.default() 76 | let database = container.publicCloudDatabase 77 | ``` 78 | 79 | ### Creating a record 80 | ```swift 81 | let movieRecord = CKRecord(recordType: "Movie") 82 | movieRecord["title"] = "Finding Dory" 83 | movieRecord["directors"] = NSArray(array: ["Andrew Stanton", "Angus MacLane"]) 84 | ``` 85 | ### Saving a record 86 | ```swift 87 | database.save(record: movieRecord) { (movieRecord, error) in 88 | if let savedRecord = movieRecord { 89 | // Insert Successfully saved record code 90 | 91 | } else if let error = error { 92 | // Insert error handling 93 | } 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /Sources/Bridging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bridging.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 20/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol _OCKBridgable { 12 | associatedtype ObjectType 13 | func bridge() -> ObjectType 14 | } 15 | 16 | public protocol CKNumberValueType: CKRecordValue {} 17 | extension CKNumberValueType where Self: _OCKBridgable, Self.ObjectType == NSNumber { 18 | public var recordFieldDictionary: [String: Any] { 19 | return ["value": self.bridge()] 20 | } 21 | } 22 | 23 | extension String: _OCKBridgable { 24 | public typealias ObjectType = NSString 25 | 26 | public func bridge() -> NSString { 27 | return NSString(string: self) 28 | } 29 | } 30 | 31 | extension Int: _OCKBridgable { 32 | public typealias ObjectType = NSNumber 33 | 34 | public func bridge() -> NSNumber { 35 | return NSNumber(value: self) 36 | } 37 | } 38 | 39 | extension UInt: _OCKBridgable { 40 | public typealias ObjectType = NSNumber 41 | 42 | public func bridge() -> NSNumber { 43 | return NSNumber(value: self) 44 | } 45 | } 46 | 47 | extension Float: _OCKBridgable { 48 | public typealias ObjectType = NSNumber 49 | 50 | public func bridge() -> NSNumber { 51 | return NSNumber(value: self) 52 | } 53 | } 54 | 55 | extension Double: _OCKBridgable { 56 | public typealias ObjectType = NSNumber 57 | 58 | public func bridge() -> NSNumber { 59 | return NSNumber(value: self) 60 | } 61 | } 62 | 63 | extension Dictionary { 64 | func bridge() -> NSDictionary { 65 | var newDictionary: [NSString: Any] = [:] 66 | 67 | for (key,value) in self { 68 | if let stringKey = key as? String { 69 | newDictionary[stringKey.bridge()] = value 70 | } else if let nsstringKey = key as? NSString { 71 | newDictionary[nsstringKey] = value 72 | } 73 | } 74 | return newDictionary._bridgeToObjectiveC() 75 | } 76 | } 77 | 78 | 79 | #if !os(Linux) 80 | 81 | typealias NSErrorUserInfoType = [AnyHashable: Any] 82 | 83 | public extension NSString { 84 | func bridge() -> String { 85 | return self as String 86 | } 87 | } 88 | 89 | extension NSArray { 90 | func bridge() -> Array { 91 | return self as! Array 92 | } 93 | } 94 | 95 | extension NSDictionary { 96 | public func bridge() -> [NSObject: Any] { 97 | return self as [NSObject: AnyObject] 98 | } 99 | } 100 | 101 | extension Array { 102 | func bridge() -> NSArray { 103 | return self as NSArray 104 | } 105 | } 106 | 107 | extension Date { 108 | func bridge() -> NSDate { 109 | return self as NSDate 110 | } 111 | } 112 | 113 | extension NSDate { 114 | func bridge() -> Date { 115 | return self as Date 116 | } 117 | } 118 | 119 | extension NSData { 120 | func bridge() -> Data { 121 | return self as Data 122 | } 123 | } 124 | 125 | 126 | 127 | #elseif os(Linux) 128 | 129 | typealias NSErrorUserInfoType = [String: Any] 130 | 131 | public extension NSString { 132 | func bridge() -> String { 133 | return self._bridgeToSwift() 134 | } 135 | } 136 | 137 | extension NSArray { 138 | public func bridge() -> Array { 139 | return self._bridgeToSwift() 140 | } 141 | } 142 | 143 | extension NSDictionary { 144 | public func bridge() -> [AnyHashable: Any] { 145 | return self._bridgeToSwift() 146 | } 147 | } 148 | 149 | 150 | extension Array { 151 | public func bridge() -> NSArray { 152 | return self._bridgeToObjectiveC() 153 | } 154 | } 155 | 156 | extension NSData { 157 | public func bridge() -> Data { 158 | return self._bridgeToSwift() 159 | } 160 | } 161 | 162 | extension Date { 163 | public func bridge() -> NSDate { 164 | return self._bridgeToObjectiveC() 165 | } 166 | } 167 | 168 | extension NSDate { 169 | public func bridge() -> Date { 170 | return self._bridgeToSwift() 171 | } 172 | } 173 | 174 | #endif 175 | 176 | extension NSError { 177 | public convenience init(error: Error) { 178 | 179 | var userInfo: [String : Any] = [:] 180 | var code: Int = 0 181 | 182 | // Retrieve custom userInfo information. 183 | if let customUserInfoError = error as? CustomNSError { 184 | userInfo = customUserInfoError.errorUserInfo 185 | code = customUserInfoError.errorCode 186 | } 187 | 188 | if let localizedError = error as? LocalizedError { 189 | if let description = localizedError.errorDescription { 190 | userInfo[NSLocalizedDescriptionKey] = description 191 | } 192 | 193 | if let reason = localizedError.failureReason { 194 | userInfo[NSLocalizedFailureReasonErrorKey] = reason 195 | } 196 | 197 | if let suggestion = localizedError.recoverySuggestion { 198 | userInfo[NSLocalizedRecoverySuggestionErrorKey] = suggestion 199 | } 200 | 201 | if let helpAnchor = localizedError.helpAnchor { 202 | userInfo[NSHelpAnchorErrorKey] = helpAnchor 203 | } 204 | 205 | } 206 | 207 | if let recoverableError = error as? RecoverableError { 208 | userInfo[NSLocalizedRecoveryOptionsErrorKey] = recoverableError.recoveryOptions 209 | // userInfo[NSRecoveryAttempterErrorKey] = recoverableError 210 | 211 | } 212 | 213 | self.init(domain: "OpenCloudKit", code: code, userInfo: userInfo) 214 | 215 | } 216 | 217 | 218 | 219 | } 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /Sources/CKAcceptSharesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKAcceptSharesOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 16/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKAcceptSharesOperation: CKOperation { 12 | 13 | var shortGUIDs: [CKShortGUID] 14 | 15 | public var acceptSharesCompletionBlock: ((Error?) -> Void)? 16 | 17 | public var perShareCompletionBlock: ((CKShareMetadata, CKShare?, Error?) -> Void)? 18 | 19 | public override init() { 20 | shortGUIDs = [] 21 | super.init() 22 | } 23 | 24 | public convenience init(shortGUIDs: [CKShortGUID]) { 25 | self.init() 26 | self.shortGUIDs = shortGUIDs 27 | } 28 | 29 | override func finishOnCallbackQueue(error: Error?) { 30 | self.acceptSharesCompletionBlock?(error) 31 | 32 | super.finishOnCallbackQueue(error: error) 33 | } 34 | 35 | func perShare(shareMetadata: CKShareMetadata, acceptedShare: CKShare?, error: Error?){ 36 | callbackQueue.async { 37 | self.perShareCompletionBlock?(shareMetadata, nil, nil) 38 | } 39 | } 40 | 41 | override func performCKOperation() { 42 | 43 | let operationURLRequest = CKAcceptSharesURLRequest(shortGUIDs: shortGUIDs) 44 | 45 | operationURLRequest.accountInfoProvider = CloudKit.shared.defaultAccount 46 | 47 | operationURLRequest.completionBlock = { [weak self] (result) in 48 | guard let strongSelf = self, !strongSelf.isCancelled else { 49 | return 50 | } 51 | switch result { 52 | case .success(let dictionary): 53 | 54 | // Process Records 55 | if let resultsDictionary = dictionary["results"] as? [[String: Any]] { 56 | // Parse JSON into CKRecords 57 | for resultDictionary in resultsDictionary { 58 | if let shareMetadata = CKShareMetadata(dictionary: resultDictionary) { 59 | strongSelf.perShare(shareMetadata: shareMetadata, acceptedShare: nil, error: nil) 60 | } 61 | } 62 | } 63 | 64 | strongSelf.finish(error: nil) 65 | 66 | case .error(let error): 67 | strongSelf.finish(error: error.error) 68 | } 69 | } 70 | 71 | operationURLRequest.performRequest() 72 | } 73 | 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/CKAcceptSharesURLRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKAcceptSharesURLRequest.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 16/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class CKAcceptSharesURLRequest: CKURLRequest { 12 | 13 | // let shareMetadatasToAccept: [CKShareMetadata] 14 | 15 | 16 | init(shortGUIDs: [CKShortGUID]) { 17 | super.init() 18 | 19 | self.path = "accept" 20 | self.operationType = CKOperationRequestType.records 21 | 22 | var parameters: [String: Any] = [:] 23 | 24 | parameters["shortGUIDs"] = shortGUIDs.map({ (guid) -> NSDictionary in 25 | return guid.dictionary.bridge() 26 | }).bridge() 27 | 28 | requestProperties = parameters 29 | accountInfoProvider = CloudKit.shared.defaultAccount 30 | 31 | } 32 | 33 | convenience init(shareMetadatasToAccept: [CKShareMetadata]) { 34 | 35 | self.init(shortGUIDs: []) 36 | 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /Sources/CKAccount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKAccount.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 27/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum CKAccountType { 12 | case primary 13 | case anoymous 14 | case server 15 | } 16 | 17 | public class CKAccount: CKAccountInfoProvider { 18 | 19 | let accountType: CKAccountType 20 | 21 | var isAnonymousAccount: Bool { 22 | return accountType == .anoymous 23 | } 24 | 25 | var isServerAccount: Bool { 26 | return accountType == .server 27 | } 28 | 29 | var containerInfo: CKContainerInfo 30 | 31 | public var iCloudAuthToken: String? 32 | 33 | let cloudKitAuthToken: String? 34 | 35 | init(type: CKAccountType, containerInfo: CKContainerInfo, cloudKitAuthToken: String?) { 36 | self.accountType = type 37 | self.containerInfo = containerInfo 38 | self.cloudKitAuthToken = cloudKitAuthToken 39 | } 40 | 41 | init(containerInfo: CKContainerInfo,cloudKitAuthToken: String, iCloudAuthToken: String) { 42 | self.accountType = .primary 43 | self.containerInfo = containerInfo 44 | self.iCloudAuthToken = iCloudAuthToken 45 | self.cloudKitAuthToken = cloudKitAuthToken 46 | } 47 | 48 | func baseURL(forServerType serverType: CKServerType) -> URL { 49 | var baseURL = URL(string: CKServerInfo.path)! 50 | switch serverType { 51 | case .database: 52 | baseURL.appendPathComponent("database") 53 | case .device: 54 | baseURL.appendPathComponent("device") 55 | default: 56 | baseURL.appendPathComponent("database") 57 | } 58 | 59 | // Append version 60 | 61 | return baseURL.appendingPathComponent("\(CKServerInfo.version)") 62 | 63 | } 64 | 65 | } 66 | 67 | public class CKServerAccount: CKAccount { 68 | 69 | let serverToServerAuth: CKServerToServerKeyAuth 70 | 71 | init(containerInfo: CKContainerInfo, serverToServerAuth: CKServerToServerKeyAuth) { 72 | self.serverToServerAuth = serverToServerAuth 73 | super.init(type: .server, containerInfo: containerInfo, cloudKitAuthToken: nil) 74 | } 75 | 76 | convenience init(containerInfo: CKContainerInfo, keyID: String, privateKeyFile: String, passPhrase: String? = nil) { 77 | 78 | let keyAuth = CKServerToServerKeyAuth(keyID: keyID, privateKeyFile: privateKeyFile, privateKeyPassPhrase: passPhrase) 79 | 80 | self.init(containerInfo: containerInfo, serverToServerAuth: keyAuth) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/CKAccountStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKAccountStatus.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 17/10/16. 6 | // 7 | // 8 | 9 | enum CKAccountStatus : Int { 10 | case couldNotDetermine 11 | case available 12 | case restricted 13 | case noAccount 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CKAsset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKAsset.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 16/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKAsset: NSObject { 12 | 13 | public var fileURL : NSURL 14 | 15 | var recordKey: String? 16 | 17 | var uploaded: Bool = false 18 | 19 | var downloaded: Bool = false 20 | 21 | var recordID: CKRecordID? 22 | 23 | var downloadBaseURL: String? 24 | 25 | var downloadURL: URL? { 26 | get { 27 | if let downloadBaseURL = downloadBaseURL { 28 | return URL(string: downloadBaseURL)! 29 | } else { 30 | return nil 31 | } 32 | } 33 | } 34 | 35 | var size: UInt? 36 | 37 | var hasSize: Bool { 38 | return size != nil 39 | } 40 | 41 | var uploadReceipt: String? 42 | 43 | public init(fileURL: NSURL) { 44 | self.fileURL = fileURL 45 | } 46 | 47 | init?(dictionary: [String: Any]) { 48 | 49 | guard 50 | let downloadURL = dictionary["downloadURL"] as? String, 51 | let size = dictionary["size"] as? NSNumber 52 | else { 53 | return nil 54 | } 55 | 56 | let downloadURLString = downloadURL.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)! 57 | 58 | fileURL = NSURL(string: downloadURLString)! 59 | self.downloadBaseURL = downloadURL 60 | self.size = size.uintValue 61 | downloaded = false 62 | 63 | } 64 | } 65 | 66 | extension CKAsset: CustomDictionaryConvertible { 67 | public var dictionary: [String: Any] { 68 | var fieldDictionary: [String: Any] = [:] 69 | if let recordID = recordID, let recordKey = recordKey { 70 | fieldDictionary["recordName"] = recordID.recordName.bridge() 71 | // fieldDictionary["recordType"] = "Items".bridge() 72 | fieldDictionary["fieldName"] = recordKey.bridge() 73 | } 74 | 75 | return fieldDictionary 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/CKCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKCodable.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 26/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CKCodable { 12 | var dictionary: [String: Any] { get } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CKContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKContainer.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 6/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public class CKContainer { 13 | 14 | let convenienceOperationQueue = OperationQueue() 15 | 16 | public let containerIdentifier: String 17 | 18 | public init(containerIdentifier: String) { 19 | self.containerIdentifier = containerIdentifier 20 | } 21 | 22 | public class func `default`() -> CKContainer { 23 | // Get Default Container 24 | return CKContainer(containerIdentifier: CloudKit.shared.containers.first!.containerIdentifier) 25 | } 26 | 27 | public lazy var publicCloudDatabase: CKDatabase = { 28 | return CKDatabase(container: self, scope: .public) 29 | }() 30 | 31 | public lazy var privateCloudDatabase: CKDatabase = { 32 | return CKDatabase(container: self, scope: .private) 33 | }() 34 | 35 | public lazy var sharedCloudDatabase: CKDatabase = { 36 | return CKDatabase(container: self, scope: .shared) 37 | }() 38 | 39 | var isRegisteredForNotifications: Bool { 40 | return false 41 | } 42 | 43 | 44 | func registerForNotifications() {} 45 | 46 | func accountStatus(completionHandler: @escaping (CKAccountStatus, Error?) -> Void) { 47 | 48 | guard let _ = CloudKit.shared.defaultAccount.iCloudAuthToken else { 49 | completionHandler(.noAccount, nil) 50 | return 51 | } 52 | 53 | // Verify the account is valid 54 | completionHandler(.available, nil) 55 | } 56 | 57 | func schedule(convenienceOperation: CKOperation) { 58 | convenienceOperation.queuePriority = .veryHigh 59 | convenienceOperation.qualityOfService = .utility 60 | 61 | add(convenienceOperation) 62 | } 63 | 64 | public func add(_ operation: CKOperation) { 65 | if !(operation is CKDatabaseOperation) { 66 | operation.container = self 67 | convenienceOperationQueue.addOperation(operation) 68 | } else { 69 | fatalError("CKDatabaseOperations must be submitted to a CKDatabase") 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/CKContainerConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKContainerConfig.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 14/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | enum CKConfigError: Error { 12 | case FailedInit 13 | case InvalidJSON 14 | } 15 | 16 | public struct CKConfig { 17 | 18 | let containers: [CKContainerConfig] 19 | 20 | public init(containers: [CKContainerConfig]) { 21 | self.containers = containers 22 | } 23 | 24 | public init(container: CKContainerConfig) { 25 | self.containers = [container] 26 | } 27 | 28 | init?(dictionary: [String: Any], workingDirectory: String?) { 29 | guard let containerDictionaries = dictionary["containers"] as? [[String: Any]] else { 30 | return nil 31 | } 32 | 33 | let containers = containerDictionaries.flatMap { (containerDictionary) -> CKContainerConfig? in 34 | var containerConfig = CKContainerConfig(dictionary: containerDictionary) 35 | if let workingDirectory = workingDirectory, let privateKeyFile = containerConfig?.serverToServerKeyAuth?.privateKeyFile { 36 | containerConfig?.serverToServerKeyAuth?.privateKeyFile = "\(workingDirectory)/\(privateKeyFile)" 37 | } 38 | return containerConfig 39 | } 40 | 41 | if containers.count > 0 { 42 | self.containers = containers 43 | } else { 44 | return nil 45 | } 46 | } 47 | 48 | public init(contentsOfFile path: String) throws { 49 | 50 | let url = URL(fileURLWithPath: path) 51 | 52 | let directory = url.deletingLastPathComponent() 53 | 54 | let jsonData = try NSData(contentsOfFile: path, options: []) 55 | 56 | if let dictionary = try JSONSerialization.jsonObject(with: jsonData.bridge(), options: []) as? [String: Any] { 57 | self.init(dictionary: dictionary, workingDirectory: directory.path)! 58 | } else { 59 | throw CKConfigError.InvalidJSON 60 | } 61 | 62 | } 63 | } 64 | 65 | public struct CKContainerConfig { 66 | public let containerIdentifier: String 67 | public let environment: CKEnvironment 68 | public let apnsEnvironment: CKEnvironment 69 | public let apiTokenAuth: String? 70 | public var serverToServerKeyAuth: CKServerToServerKeyAuth? 71 | 72 | public init(containerIdentifier: String, environment: CKEnvironment,apiTokenAuth: String, apnsEnvironment: CKEnvironment? = nil) { 73 | self.containerIdentifier = containerIdentifier 74 | self.environment = environment 75 | if let apnsEnvironment = apnsEnvironment { 76 | self.apnsEnvironment = apnsEnvironment 77 | } else { 78 | self.apnsEnvironment = environment 79 | } 80 | 81 | self.apiTokenAuth = apiTokenAuth 82 | self.serverToServerKeyAuth = nil 83 | } 84 | 85 | public init(containerIdentifier: String, environment: CKEnvironment, serverToServerKeyAuth: CKServerToServerKeyAuth, apnsEnvironment: CKEnvironment? = nil) { 86 | self.containerIdentifier = containerIdentifier 87 | self.environment = environment 88 | if let apnsEnvironment = apnsEnvironment { 89 | self.apnsEnvironment = apnsEnvironment 90 | } else { 91 | self.apnsEnvironment = environment 92 | } 93 | self.apiTokenAuth = nil 94 | self.serverToServerKeyAuth = serverToServerKeyAuth 95 | } 96 | 97 | init?(dictionary: [String: Any]) { 98 | guard let containerIdentifier = dictionary["containerIdentifier"] as? String, let environmentValue = dictionary["environment"] as? String, 99 | let environment = CKEnvironment(rawValue: environmentValue) else { 100 | return nil 101 | } 102 | 103 | let apnsEnvironment = CKEnvironment(rawValue: dictionary["apnsEnvironment"] as? String ?? "") 104 | 105 | if let apiTokenAuthDictionary = dictionary["apiTokenAuth"] as? [String: Any] { 106 | 107 | if let apiToken = apiTokenAuthDictionary["apiToken"] as? String { 108 | self.init(containerIdentifier: containerIdentifier, environment: environment, apiTokenAuth: apiToken, apnsEnvironment: apnsEnvironment) 109 | } else { 110 | return nil 111 | } 112 | 113 | } else if let serverToServerKeyAuthDictionary = dictionary["serverToServerKeyAuth"] as? [String: Any] { 114 | guard let keyID = serverToServerKeyAuthDictionary["keyID"] as? String, let privateKeyFile = serverToServerKeyAuthDictionary["privateKeyFile"] as? String else { 115 | return nil 116 | } 117 | 118 | let privateKeyPassPhrase = serverToServerKeyAuthDictionary["privateKeyPassPhrase"] as? String 119 | let auth = CKServerToServerKeyAuth(keyID: keyID, privateKeyFile: privateKeyFile, privateKeyPassPhrase: privateKeyPassPhrase) 120 | 121 | self.init(containerIdentifier: containerIdentifier, environment: environment, serverToServerKeyAuth: auth, apnsEnvironment: apnsEnvironment) 122 | 123 | } else { 124 | return nil 125 | } 126 | } 127 | } 128 | 129 | extension CKContainerConfig { 130 | var containerInfo: CKContainerInfo { 131 | return CKContainerInfo(containerID: containerIdentifier, environment: environment) 132 | } 133 | } 134 | 135 | public struct CKServerToServerKeyAuth { 136 | // A unique identifier for the key generated using CloudKit Dashboard. To create this key, read 137 | public let keyID: String 138 | // The path to the PEM encoded key file. 139 | public var privateKeyFile: String 140 | 141 | //The pass phrase for the key. 142 | public let privateKeyPassPhrase: String? 143 | 144 | public init(keyID: String, privateKeyFile: String, privateKeyPassPhrase: String? = nil) { 145 | self.keyID = keyID 146 | self.privateKeyFile = privateKeyFile 147 | self.privateKeyPassPhrase = privateKeyPassPhrase 148 | } 149 | } 150 | extension CKServerToServerKeyAuth:Equatable {} 151 | 152 | public func ==(lhs: CKServerToServerKeyAuth, rhs: CKServerToServerKeyAuth) -> Bool { 153 | return lhs.keyID == rhs.keyID && lhs.privateKeyFile == rhs.privateKeyFile && lhs.privateKeyPassPhrase == rhs.privateKeyPassPhrase 154 | } 155 | -------------------------------------------------------------------------------- /Sources/CKContainerInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKContainerInfo.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 25/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | struct CKContainerInfo { 12 | 13 | let environment: CKEnvironment 14 | 15 | let containerID: String 16 | 17 | func publicCloudDBURL(databaseScope: CKDatabaseScope) -> URL { 18 | let baseURL = "\(CKServerInfo.path)/database/\(CKServerInfo.version)/\(containerID)/\(environment)/\(databaseScope)" 19 | return URL(string: baseURL)! 20 | } 21 | 22 | 23 | init(containerID: String, environment: CKEnvironment) { 24 | self.containerID = containerID 25 | self.environment = environment 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/CKDatabaseNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKDatabaseNotification.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 20/1/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKDatabaseNotification : CKNotification { 12 | 13 | public var databaseScope: CKDatabaseScope = .public 14 | 15 | override init(fromRemoteNotificationDictionary notificationDictionary: [AnyHashable : Any]) { 16 | 17 | super.init(fromRemoteNotificationDictionary: notificationDictionary) 18 | 19 | notificationType = .database 20 | 21 | if let ckDictionary = notificationDictionary["ck"] as? [String: Any] { 22 | if let metDictionary = ckDictionary["met"] as? [String: Any] { 23 | 24 | // Set database scope 25 | if let dbs = metDictionary["dbs"] as? NSNumber, let scope = CKDatabaseScope(rawValue: dbs.intValue) { 26 | databaseScope = scope 27 | } 28 | 29 | // Set Subscription ID 30 | if let sid = metDictionary["sid"] as? String { 31 | subscriptionID = sid 32 | } 33 | 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/CKDictionaryValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKDictionaryValue.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 22/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | /* 11 | protocol CKDictionaryValue { 12 | func toObject() -> Any? 13 | } 14 | 15 | extension String: CKDictionaryValue { 16 | func toObject() -> Any? { 17 | return self.bridge() 18 | } 19 | } 20 | 21 | extension Int: CKDictionaryValue { 22 | func toObject() -> Any? { 23 | return NSNumber(value: self) 24 | } 25 | } 26 | extension Double: CKDictionaryValue { 27 | func toObject() -> Any? { 28 | return NSNumber(value: self) 29 | } 30 | } 31 | 32 | extension Float: CKDictionaryValue { 33 | func toObject() -> Any? { 34 | return NSNumber(value: self) 35 | } 36 | } 37 | 38 | extension Bool: CKDictionaryValue { 39 | func toObject() -> Any? { 40 | return NSNumber(value: self) 41 | } 42 | } 43 | 44 | extension Array: CKDictionaryValue { 45 | func toObject() -> Any? { 46 | return self.bridge() as? Any 47 | } 48 | } 49 | 50 | extension Dictionary where Key: StringLiteralConvertible, Value: CKDictionaryValue { 51 | func toObject() -> Any? { 52 | var dictionary: [String: Any] = [:] 53 | 54 | for (key, value) in dictionary { 55 | dictionary[key] = value.toObject() 56 | } 57 | 58 | return dictionary 59 | } 60 | } 61 | */ 62 | -------------------------------------------------------------------------------- /Sources/CKDiscoverAllUserIdentitiesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKDiscoverAllUserIdentitiesOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 14/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKDiscoverAllUserIdentitiesOperation : CKOperation { 12 | 13 | var discoveredIdentities: [CKUserIdentity] = [] 14 | 15 | public override init() { 16 | super.init() 17 | } 18 | 19 | 20 | public var userIdentityDiscoveredBlock: ((CKUserIdentity) -> Swift.Void)? 21 | 22 | public var discoverAllUserIdentitiesCompletionBlock: ((Error?) -> Swift.Void)? 23 | 24 | override func finishOnCallbackQueue(error: Error?) { 25 | 26 | self.discoverAllUserIdentitiesCompletionBlock?(error) 27 | 28 | super.finishOnCallbackQueue(error: error) 29 | } 30 | 31 | func discovered(userIdentity: CKUserIdentity){ 32 | callbackQueue.async { 33 | self.userIdentityDiscoveredBlock?(userIdentity) 34 | } 35 | } 36 | 37 | override func performCKOperation() { 38 | 39 | let url = "\(databaseURL)/public/users/discover" 40 | 41 | urlSessionTask = CKWebRequest(container: operationContainer).request(withURL: url) { [weak self] (dictionary, error) in 42 | 43 | guard let strongSelf = self, !strongSelf.isCancelled else { 44 | return 45 | } 46 | 47 | var returnError = error 48 | defer { 49 | strongSelf.finish(error: returnError) 50 | } 51 | 52 | guard let dictionary = dictionary, 53 | let userDictionaries = dictionary["users"] as? [[String: Any]], 54 | error == nil else { 55 | return 56 | } 57 | 58 | // Process Records 59 | // Parse JSON into CKRecords 60 | for userDictionary in userDictionaries { 61 | 62 | if let userIdentity = CKUserIdentity(dictionary: userDictionary) { 63 | strongSelf.discoveredIdentities.append(userIdentity) 64 | 65 | // Call discovered callback 66 | strongSelf.discovered(userIdentity: userIdentity) 67 | 68 | } else { 69 | 70 | // Create Error 71 | returnError = NSError(domain: CKErrorDomain, code: CKErrorCode.PartialFailure.rawValue, userInfo: [NSLocalizedDescriptionKey: "Failed to parse record from server"]) 72 | return 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/CKDiscoverUserIdentitiesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKDiscoverUserIdentitiesOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 14/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKDiscoverUserIdentitiesOperation : CKOperation { 12 | 13 | 14 | public override init() { 15 | userIdentityLookupInfos = [] 16 | super.init() 17 | } 18 | 19 | public convenience init(userIdentityLookupInfos: [CKUserIdentityLookupInfo]) { 20 | self.init() 21 | self.userIdentityLookupInfos = userIdentityLookupInfos 22 | } 23 | 24 | public var userIdentityLookupInfos: [CKUserIdentityLookupInfo] 25 | 26 | public var userIdentityDiscoveredBlock: ((CKUserIdentity, CKUserIdentityLookupInfo) -> Swift.Void)? 27 | 28 | public var discoverUserIdentitiesCompletionBlock: ((Error?) -> Swift.Void)? 29 | 30 | override func finishOnCallbackQueue(error: Error?) { 31 | self.discoverUserIdentitiesCompletionBlock?(error) 32 | super.finishOnCallbackQueue(error: error) 33 | } 34 | 35 | func discovered(userIdentity: CKUserIdentity, lookupInfo: CKUserIdentityLookupInfo){ 36 | callbackQueue.async { 37 | self.userIdentityDiscoveredBlock?(userIdentity, lookupInfo) 38 | } 39 | } 40 | 41 | override func performCKOperation() { 42 | 43 | let url = "\(databaseURL)/public/users/discover" 44 | let lookUpInfos = userIdentityLookupInfos.map { (lookupInfo) -> [String: Any] in 45 | return lookupInfo.dictionary 46 | } 47 | 48 | let request: [String: Any] = ["lookupInfos": lookUpInfos.bridge() as Any] 49 | 50 | urlSessionTask = CKWebRequest(container: operationContainer).request(withURL: url, parameters: request) { [weak self] (dictionary, error) in 51 | 52 | guard let strongSelf = self, !strongSelf.isCancelled else { 53 | return 54 | } 55 | 56 | var returnError = error 57 | defer { 58 | strongSelf.finish(error: returnError) 59 | } 60 | 61 | guard let dictionary = dictionary, 62 | let userDictionaries = dictionary["users"] as? [[String: Any]], 63 | error == nil else { 64 | return 65 | } 66 | 67 | // Process Records 68 | // Parse JSON into CKRecords 69 | for userDictionary in userDictionaries { 70 | 71 | if let userIdenity = CKUserIdentity(dictionary: userDictionary) { 72 | 73 | // Call RecordCallback 74 | strongSelf.discovered(userIdentity: userIdenity, lookupInfo: userIdenity.lookupInfo!) 75 | 76 | } else { 77 | 78 | // Create Error 79 | returnError = NSError(domain: CKErrorDomain, code: CKErrorCode.PartialFailure.rawValue, userInfo: [NSLocalizedDescriptionKey: "Failed to parse record from server"]) 80 | // Call RecordCallback 81 | return 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/CKDownloadAssetsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKDownloadAssetsOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 16/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKDownloadAssetsOperation: CKDatabaseOperation { 12 | 13 | let assetsToDownload: [CKAsset] 14 | 15 | var assetsByDownloadTask:[URLSessionDownloadTask: CKAsset] = [:] 16 | 17 | public var perAssetProgressBlock: ((CKAsset, Double) -> Swift.Void)? 18 | 19 | /* Called on success or failure for each record. */ 20 | public var perAssetCompletionBlock: ((CKAsset, Error?) -> Swift.Void)? 21 | 22 | public var downloadAssetsCompletionBlock: (([CKAsset], Error?) -> Swift.Void)? 23 | 24 | public var downloadedAssets: [CKAsset] = [] 25 | 26 | var downloadSession: URLSession? 27 | 28 | public init(assetsToDownload: [CKAsset]) { 29 | 30 | self.assetsToDownload = assetsToDownload 31 | super.init() 32 | 33 | } 34 | 35 | func download() { 36 | for downloadTask in assetsByDownloadTask.keys { 37 | downloadTask.resume() 38 | } 39 | } 40 | 41 | func prepareForDownload() { 42 | #if !os(Linux) 43 | downloadSession = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil) 44 | #endif 45 | 46 | if let downloadSession = downloadSession { 47 | 48 | // Create URLSessionDownloadTasks 49 | for asset in assetsToDownload { 50 | if let downloadURL = asset.downloadURL { 51 | CloudKit.debugPrint("URL: \(downloadURL.absoluteString)") 52 | // Create request for download URL 53 | let downloadRequest = URLRequest(url: downloadURL) 54 | // Create download task 55 | let downloadTask = downloadSession.downloadTask(with: downloadRequest) 56 | 57 | // Add to dictionary 58 | assetsByDownloadTask[downloadTask] = asset 59 | 60 | } 61 | } 62 | 63 | } 64 | } 65 | 66 | override public func cancel() { 67 | super.cancel() 68 | 69 | downloadSession?.invalidateAndCancel() 70 | } 71 | 72 | override func performCKOperation() { 73 | prepareForDownload() 74 | 75 | download() 76 | } 77 | 78 | override func finishOnCallbackQueue(error: Error?) { 79 | 80 | if(error == nil){ 81 | // todo create partial error from assetErrors array, see modify records 82 | } 83 | 84 | downloadAssetsCompletionBlock?(assetsToDownload, error) 85 | 86 | super.finishOnCallbackQueue(error: error) 87 | } 88 | 89 | func progressed(asset: CKAsset, progress: Double){ 90 | callbackQueue.async { 91 | self.perAssetProgressBlock?(asset, progress) 92 | } 93 | } 94 | 95 | func completed(asset: CKAsset, error: Error?){ 96 | callbackQueue.async { 97 | self.perAssetCompletionBlock?(asset, error) 98 | } 99 | } 100 | } 101 | 102 | extension CKDownloadAssetsOperation { 103 | public convenience init(records: [CKRecord]) { 104 | 105 | var assets: [CKAsset] = [] 106 | for record in records { 107 | for key in record.allKeys() { 108 | if let asset = record[key] as? CKAsset { 109 | assets.append(asset) 110 | } 111 | } 112 | } 113 | 114 | self.init(assetsToDownload: assets) 115 | } 116 | } 117 | 118 | #if !os(Linux) 119 | extension CKDownloadAssetsOperation: URLSessionDownloadDelegate { 120 | 121 | 122 | public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64){ 123 | 124 | guard let currentAsset = assetsByDownloadTask[downloadTask] else { 125 | fatalError("Asset should belong to completed download task") 126 | } 127 | 128 | let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) 129 | 130 | // Call Progress Block 131 | progressed(asset: currentAsset, progress: progress) 132 | } 133 | 134 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 135 | let downloadTask = task as! URLSessionDownloadTask 136 | 137 | CloudKit.debugPrint(task.currentRequest?.url?.absoluteString as Any) 138 | guard let currentAsset = assetsByDownloadTask[downloadTask] else { 139 | fatalError("Asset should belong to completed download task") 140 | } 141 | 142 | if let error = error { 143 | completed(asset: currentAsset, error: error) 144 | 145 | // todo add to assetErrors array 146 | } 147 | 148 | assetsByDownloadTask[downloadTask] = nil 149 | 150 | self.downloadedAssets.append(currentAsset) 151 | 152 | // If all task complete 153 | if assetsByDownloadTask.count == 0 { 154 | finish(error: nil) 155 | } 156 | } 157 | 158 | public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { 159 | 160 | guard let currentAsset = assetsByDownloadTask[downloadTask] else { 161 | fatalError("Asset should belong to completed download task") 162 | } 163 | 164 | // Save file to temporary Assets location 165 | let temporaryDirectory = NSTemporaryDirectory() 166 | let filename = NSUUID().uuidString 167 | let destinationURL = URL(fileURLWithPath: "\(temporaryDirectory)\(filename)") 168 | 169 | CloudKit.debugPrint(destinationURL) 170 | 171 | let fileManager = FileManager.default 172 | do { 173 | 174 | try fileManager.removeItem(at: destinationURL) 175 | } catch {} 176 | 177 | do { 178 | try fileManager.copyItem(at: location, to: destinationURL) 179 | } catch let error { 180 | CloudKit.debugPrint("Could not copy downloaded asset file to disk: \(error.localizedDescription)") 181 | completed(asset: currentAsset, error: error) 182 | return 183 | } 184 | 185 | // Modifiy the CKAsset file URL 186 | currentAsset.fileURL = destinationURL as NSURL 187 | 188 | // Call perAssetCompleteBlock 189 | completed(asset: currentAsset, error: nil) 190 | } 191 | } 192 | #endif 193 | -------------------------------------------------------------------------------- /Sources/CKError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKErrorCode.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 7/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | let CKErrorDomain: String = "CKErrorDomain" 12 | 13 | enum CKErrorCode : Int { 14 | case InternalError 15 | case PartialFailure 16 | case NetworkUnavailable 17 | case NetworkFailure 18 | case BadContainer 19 | case ServiceUnavailable 20 | case RequestRateLimited 21 | case MissingEntitlement 22 | case NotAuthenticated 23 | case PermissionFailure 24 | case UnknownItem 25 | case InvalidArguments 26 | case ResultsTruncated 27 | case ServerRecordChanged 28 | case ServerRejectedRequest 29 | case AssetFileNotFound 30 | case AssetFileModified 31 | case IncompatibleVersion 32 | case ConstraintViolation 33 | case OperationCancelled 34 | case ChangeTokenExpired 35 | case BatchRequestFailed 36 | case ZoneBusy 37 | case BadDatabase 38 | case QuotaExceeded 39 | case ZoneNotFound 40 | case LimitExceeded 41 | case UserDeletedZone 42 | case TooManyParticipants 43 | case AlreadyShared 44 | case ReferenceViolation 45 | case ManagedAccountRestricted 46 | case ParticipantMayNeedVerification 47 | 48 | } 49 | 50 | extension CKErrorCode { 51 | static func errorCode(serverError: String) -> CKErrorCode? { 52 | 53 | switch(serverError) { 54 | case "ACCESS_DENIED": 55 | return CKErrorCode.NotAuthenticated 56 | case "ATOMIC_ERROR": 57 | return CKErrorCode.BatchRequestFailed 58 | case "AUTHENTICATION_FAILED": 59 | return CKErrorCode.NotAuthenticated 60 | case "AUTHENTICATION_REQUIRED": 61 | return CKErrorCode.PermissionFailure 62 | case "BAD_REQUEST": 63 | return CKErrorCode.ServerRejectedRequest 64 | case "CONFLICT": 65 | return CKErrorCode.ChangeTokenExpired 66 | case "EXISTS": 67 | return CKErrorCode.ConstraintViolation 68 | case "INTERNAL_ERROR": 69 | return CKErrorCode.InternalError 70 | case "NOT_FOUND": 71 | return CKErrorCode.UnknownItem 72 | case "QUOTA_EXCEEDED": 73 | return CKErrorCode.QuotaExceeded 74 | case "THROTTLED": 75 | return CKErrorCode.RequestRateLimited 76 | case "TRY_AGAIN_LATER": 77 | return CKErrorCode.InternalError 78 | case "VALIDATING_REFERENCE_ERROR": 79 | return CKErrorCode.ConstraintViolation 80 | case "ZONE_NOT_FOUND": 81 | return CKErrorCode.ZoneNotFound 82 | default: 83 | fatalError("Unknown Server Error: \(serverError)") 84 | } 85 | } 86 | } 87 | 88 | extension CKErrorCode: CustomStringConvertible { 89 | var description: String { 90 | switch self { 91 | case .InternalError: 92 | return "Internal Error" 93 | case .PartialFailure: 94 | return "Partial Failure" 95 | case .NetworkUnavailable: 96 | return "Network Unavailable" 97 | case .NetworkFailure: 98 | return "Network Failure" 99 | case .BadContainer: 100 | return "Bad Container" 101 | case .ServiceUnavailable: 102 | return "Service Unavailable" 103 | case .RequestRateLimited: 104 | return "Request Rate Limited" 105 | case .MissingEntitlement: 106 | return "Missing Entitlement" 107 | case .NotAuthenticated: 108 | return "Not Authenticated" 109 | case .PermissionFailure: 110 | return "Permission Failure" 111 | case .UnknownItem: 112 | return "Unknown Item" 113 | case .InvalidArguments: 114 | return "Invalid Arguments" 115 | case .ResultsTruncated: 116 | return "Results Truncated" 117 | case .ServerRecordChanged: 118 | return "Server Record Changed" 119 | case .ServerRejectedRequest: 120 | return "Server Rejected Request" 121 | case .AssetFileNotFound: 122 | return "Asset File Not Found" 123 | case .AssetFileModified: 124 | return "Asset File Modified" 125 | case .IncompatibleVersion: 126 | return "Incompatible Version" 127 | case .ConstraintViolation: 128 | return "Constraint Violation" 129 | case .OperationCancelled: 130 | return "Operation Cancelled" 131 | case .ChangeTokenExpired: 132 | return "Change Token Expired" 133 | case .BatchRequestFailed: 134 | return "Batch Request Failed" 135 | case .ZoneBusy: 136 | return "Zone Busy" 137 | case .BadDatabase: 138 | return "Invalid Database For Operation" 139 | case .QuotaExceeded: 140 | return "Quota Exceeded" 141 | case .ZoneNotFound: 142 | return "Zone Not Found" 143 | case .LimitExceeded: 144 | return "Limit Exceeded" 145 | case .UserDeletedZone: 146 | return "User Deleted Zone" 147 | case .TooManyParticipants: 148 | return "Too Many Participants" 149 | case .AlreadyShared: 150 | return "Already Shared" 151 | case .ReferenceViolation: 152 | return "Reference Violation" 153 | case .ManagedAccountRestricted: 154 | return "Managed Account Restricted" 155 | case .ParticipantMayNeedVerification: 156 | return "Participant May Need Verification" 157 | } 158 | } 159 | } 160 | 161 | -------------------------------------------------------------------------------- /Sources/CKFetchErrorDictionary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKFetchErrorDictionary.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 15/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CKFetchErrorDictionaryIdentifier { 12 | init?(dictionary: [String: Any]) 13 | 14 | static var identifierKey: String { get } 15 | } 16 | 17 | extension CKRecordZoneID: CKFetchErrorDictionaryIdentifier { 18 | 19 | @nonobjc static let identifierKey = "zoneID" 20 | 21 | } 22 | 23 | // TODO: Fix error handling 24 | struct CKErrorDictionary { 25 | 26 | let reason: String 27 | let serverErrorCode: String 28 | let retryAfter: NSNumber? 29 | let redirectURL: String? 30 | let uuid: String 31 | 32 | init?(dictionary: [String: Any]) { 33 | 34 | guard 35 | let uuid = dictionary["uuid"] as? String, 36 | let reason = dictionary[CKRecordFetchErrorDictionary.reasonKey] as? String, 37 | let serverErrorCode = dictionary[CKRecordFetchErrorDictionary.serverErrorCodeKey] as? String 38 | else { 39 | return nil 40 | } 41 | 42 | self.uuid = uuid 43 | self.reason = reason 44 | self.serverErrorCode = serverErrorCode 45 | 46 | self.retryAfter = (dictionary[CKRecordFetchErrorDictionary.retryAfterKey] as? NSNumber) 47 | self.redirectURL = dictionary[CKRecordFetchErrorDictionary.redirectURLKey] as? String 48 | 49 | 50 | } 51 | 52 | func error() -> NSError { 53 | 54 | let errorCode = CKErrorCode.errorCode(serverError: serverErrorCode)! 55 | 56 | var userInfo: NSErrorUserInfoType = [NSLocalizedDescriptionKey: reason.bridge() as Any, "serverErrorCode": serverErrorCode.bridge() as Any] 57 | if let redirectURL = redirectURL { 58 | userInfo[CKErrorRedirectURLKey] = redirectURL.bridge() 59 | } 60 | if let retryAfter = retryAfter { 61 | userInfo[CKErrorRetryAfterKey] = retryAfter as NSNumber 62 | } 63 | 64 | return NSError(domain: CKErrorDomain, code: errorCode.rawValue, userInfo: userInfo) 65 | 66 | } 67 | } 68 | 69 | struct CKFetchErrorDictionary { 70 | 71 | let identifier: T 72 | let reason: String 73 | let serverErrorCode: String 74 | let retryAfter: NSNumber? 75 | let redirectURL: String? 76 | 77 | init?(dictionary: [String: Any]) { 78 | 79 | guard 80 | let identifier = T(dictionary: dictionary[T.identifierKey] as? [String: Any] ?? [:]), 81 | let reason = dictionary[CKRecordFetchErrorDictionary.reasonKey] as? String, 82 | let serverErrorCode = dictionary[CKRecordFetchErrorDictionary.serverErrorCodeKey] as? String 83 | else { 84 | return nil 85 | } 86 | 87 | self.identifier = identifier 88 | self.reason = reason 89 | self.serverErrorCode = serverErrorCode 90 | 91 | self.retryAfter = (dictionary[CKRecordFetchErrorDictionary.retryAfterKey] as? NSNumber) 92 | self.redirectURL = dictionary[CKRecordFetchErrorDictionary.redirectURLKey] as? String 93 | 94 | 95 | } 96 | 97 | func error() -> NSError { 98 | 99 | let errorCode = CKErrorCode.errorCode(serverError: serverErrorCode)! 100 | 101 | var userInfo: NSErrorUserInfoType = [NSLocalizedDescriptionKey: reason.bridge() as Any, "serverErrorCode": serverErrorCode.bridge() as Any] 102 | if let redirectURL = redirectURL { 103 | userInfo[CKErrorRedirectURLKey] = redirectURL.bridge() 104 | } 105 | if let retryAfter = retryAfter { 106 | userInfo[CKErrorRetryAfterKey] = retryAfter as NSNumber 107 | } 108 | 109 | return NSError(domain: CKErrorDomain, code: errorCode.rawValue, userInfo: userInfo) 110 | 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/CKFetchRecordZonesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKFetchRecordZonesOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 15/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKFetchRecordZonesOperation : CKDatabaseOperation { 12 | 13 | 14 | public class func fetchAllRecordZonesOperation() -> Self { 15 | return self.init() 16 | } 17 | 18 | public override required init() { 19 | self.recordZoneIDs = nil 20 | super.init() 21 | 22 | } 23 | 24 | public init(recordZoneIDs zoneIDs: [CKRecordZoneID]) { 25 | self.recordZoneIDs = zoneIDs 26 | super.init() 27 | } 28 | 29 | var isFetchAllRecordZonesOperation: Bool = false 30 | 31 | var recordZoneIDs : [CKRecordZoneID]? 32 | 33 | var recordZoneErrors: [CKRecordZoneID: NSError] = [:] 34 | 35 | public var recordZoneByZoneID: [CKRecordZoneID: CKRecordZone] = [:] 36 | 37 | /* This block is called when the operation completes. 38 | The [NSOperation completionBlock] will also be called if both are set. 39 | If the error is CKErrorPartialFailure, the error's userInfo dictionary contains 40 | a dictionary of zoneIDs to errors keyed off of CKPartialErrorsByItemIDKey. 41 | */ 42 | public var fetchRecordZonesCompletionBlock: (([CKRecordZoneID : CKRecordZone]?, Error?) -> Swift.Void)? 43 | 44 | override func finishOnCallbackQueue(error: Error?) { 45 | var error = error 46 | if(error == nil){ 47 | if self.recordZoneErrors.count > 0 { 48 | error = NSError(domain: CKErrorDomain, code: CKErrorCode.PartialFailure.rawValue, userInfo: [CKPartialErrorsByItemIDKey: self.recordZoneErrors]) 49 | } 50 | } 51 | // Call the final completionBlock 52 | self.fetchRecordZonesCompletionBlock?(self.recordZoneByZoneID, error) 53 | 54 | super.finishOnCallbackQueue(error: error) 55 | } 56 | 57 | override func performCKOperation() { 58 | let url: String 59 | let request: [String: Any]? 60 | 61 | if let recordZoneIDs = recordZoneIDs { 62 | 63 | url = "\(databaseURL)/zones/lookup" 64 | let zones = recordZoneIDs.map({ (zoneID) -> [String: Any] in 65 | return zoneID.dictionary 66 | }) 67 | 68 | request = ["zones": zones.bridge()] 69 | } else { 70 | url = "\(databaseURL)/zones/list" 71 | request = nil 72 | } 73 | 74 | urlSessionTask = CKWebRequest(container: operationContainer).request(withURL: url, parameters: request) { [weak self] (dictionary, error) in 75 | 76 | guard let strongSelf = self, !strongSelf.isCancelled else { 77 | return 78 | } 79 | 80 | defer { 81 | strongSelf.finish(error: error) 82 | } 83 | 84 | guard let dictionary = dictionary, 85 | let zoneDictionaries = dictionary["zones"] as? [[String: Any]], 86 | error == nil else { 87 | return 88 | } 89 | 90 | 91 | // Parse JSON into CKRecords 92 | for zoneDictionary in zoneDictionaries { 93 | 94 | if let zone = CKRecordZone(dictionary: zoneDictionary) { 95 | strongSelf.recordZoneByZoneID[zone.zoneID] = zone 96 | } else if let fetchError = CKFetchErrorDictionary(dictionary: zoneDictionary) { 97 | 98 | // Append Error 99 | strongSelf.recordZoneErrors[fetchError.identifier] = fetchError.error() 100 | } 101 | } 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Sources/CKFetchRecordsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKFetchRecordsOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 7/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKFetchRecordsOperation: CKDatabaseOperation { 12 | 13 | var isFetchCurrentUserOperation = false 14 | 15 | var recordErrors: [String: Any] = [:] // todo use this for partial errors 16 | 17 | var shouldFetchAssetContent: Bool = false 18 | 19 | var recordIDsToRecords: [CKRecordID: CKRecord] = [:] 20 | 21 | /* Called repeatedly during transfer. */ 22 | public var perRecordProgressBlock: ((CKRecordID, Double) -> Void)? 23 | 24 | /* Called on success or failure for each record. */ 25 | public var perRecordCompletionBlock: ((CKRecord?, CKRecordID?, Error?) -> Void)? 26 | 27 | 28 | /* This block is called when the operation completes. 29 | The [NSOperation completionBlock] will also be called if both are set. 30 | If the error is CKErrorPartialFailure, the error's userInfo dictionary contains 31 | a dictionary of recordIDs to errors keyed off of CKPartialErrorsByItemIDKey. 32 | */ 33 | public var fetchRecordsCompletionBlock: (([CKRecordID : CKRecord]?, Error?) -> Void)? 34 | 35 | public class func fetchCurrentUserRecord() -> Self { 36 | let operation = self.init() 37 | operation.isFetchCurrentUserOperation = true 38 | 39 | return operation 40 | } 41 | 42 | public override required init() { 43 | super.init() 44 | } 45 | 46 | public var recordIDs: [CKRecordID]? 47 | 48 | public var desiredKeys: [String]? 49 | 50 | public convenience init(recordIDs: [CKRecordID]) { 51 | self.init() 52 | self.recordIDs = recordIDs 53 | } 54 | 55 | override func finishOnCallbackQueue(error: Error?) { 56 | if(error == nil){ 57 | // todo build partial error using recordErrors 58 | } 59 | self.fetchRecordsCompletionBlock?(self.recordIDsToRecords, error) 60 | 61 | super.finishOnCallbackQueue(error: error) 62 | } 63 | 64 | func completed(record: CKRecord?, recordID: CKRecordID?, error: Error?){ 65 | callbackQueue.async { 66 | self.perRecordCompletionBlock?(record, recordID, error) 67 | } 68 | } 69 | 70 | func progressed(recordID: CKRecordID, progress: Double){ 71 | callbackQueue.async { 72 | self.perRecordProgressBlock?(recordID, progress) 73 | } 74 | } 75 | 76 | override func performCKOperation() { 77 | 78 | // Generate the CKOperation Web Service URL 79 | let url = "\(operationURL)/records/\(CKRecordOperation.lookup)" 80 | 81 | var request: [String: Any] = [:] 82 | let lookupRecords = recordIDs?.map { (recordID) -> [String: Any] in 83 | return ["recordName": recordID.recordName.bridge()] 84 | } 85 | 86 | request["records"] = lookupRecords?.bridge() 87 | 88 | urlSessionTask = CKWebRequest(container: operationContainer).request(withURL: url, parameters: request) { [weak self] (dictionary, error) in 89 | 90 | guard let strongSelf = self, !strongSelf.isCancelled else { 91 | return 92 | } 93 | 94 | defer { 95 | strongSelf.finish(error: error) 96 | } 97 | 98 | guard let dictionary = dictionary, 99 | let recordsDictionary = dictionary["records"] as? [[String: Any]], 100 | error == nil else { 101 | return 102 | } 103 | 104 | // Process Records 105 | // Parse JSON into CKRecords 106 | for (index,recordDictionary) in recordsDictionary.enumerated() { 107 | 108 | // Call Progress Block, this is hacky support and not the callbacks intented purpose 109 | let progress = Double(index + 1) / Double((strongSelf.recordIDs!.count)) 110 | let recordID = strongSelf.recordIDs![index] 111 | strongSelf.progressed(recordID: recordID, progress: progress) 112 | 113 | if let record = CKRecord(recordDictionary: recordDictionary) { 114 | strongSelf.recordIDsToRecords[record.recordID] = record 115 | 116 | // Call per record callback, not to be confused with finished 117 | strongSelf.completed(record: record, recordID: record.recordID, error: nil) 118 | 119 | } else { 120 | 121 | // Create Error 122 | let error = NSError(domain: CKErrorDomain, code: CKErrorCode.PartialFailure.rawValue, userInfo: [NSLocalizedDescriptionKey: "Failed to parse record from server".bridge()]) 123 | 124 | // Call per record callback, not to be confused with finished 125 | strongSelf.completed(record: nil, recordID: nil, error: error) 126 | 127 | // todo add to recordErrors array 128 | 129 | } 130 | } 131 | } 132 | } 133 | 134 | 135 | 136 | 137 | } 138 | -------------------------------------------------------------------------------- /Sources/CKFetchSubscriptionsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKFetchSubscriptionsOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 13/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKFetchSubscriptionsOperation : CKDatabaseOperation { 12 | 13 | public var subscriptionErrors : [String : NSError] = [:] 14 | 15 | public var subscriptionsIDToSubscriptions: [String: CKSubscription] = [:] 16 | 17 | public override required init() { 18 | super.init() 19 | } 20 | 21 | public class func fetchAllSubscriptionsOperation() -> Self { 22 | let operation = self.init() 23 | return operation 24 | } 25 | 26 | public convenience init(subscriptionIDs: [String]) { 27 | self.init() 28 | self.subscriptionIDs = subscriptionIDs 29 | } 30 | 31 | public var subscriptionIDs: [String]? 32 | 33 | /* This block is called when the operation completes. 34 | The [NSOperation completionBlock] will also be called if both are set. 35 | If the error is CKErrorPartialFailure, the error's userInfo dictionary contains 36 | a dictionary of subscriptionID to errors keyed off of CKPartialErrorsByItemIDKey. 37 | */ 38 | public var fetchSubscriptionCompletionBlock: (([String : CKSubscription]?, Error?) -> Void)? 39 | 40 | override func finishOnCallbackQueue(error: Error?) { 41 | var error = error 42 | if(error == nil){ 43 | if (subscriptionErrors.count > 0) { 44 | error = NSError(domain: CKErrorDomain, code: CKErrorCode.PartialFailure.rawValue, userInfo: 45 | [NSLocalizedDescriptionKey: "Partial Error", 46 | CKPartialErrorsByItemIDKey: subscriptionErrors]) 47 | } 48 | } 49 | self.fetchSubscriptionCompletionBlock?(subscriptionsIDToSubscriptions, error) 50 | 51 | super.finishOnCallbackQueue(error: error) 52 | } 53 | 54 | 55 | override func performCKOperation() { 56 | 57 | let url = "\(operationURL)/subscriptions/lookup" 58 | 59 | var request: [String: Any] = [:] 60 | if let subscriptionIDs = subscriptionIDs { 61 | 62 | request["subscriptions"] = subscriptionIDs.bridge() 63 | } 64 | 65 | 66 | urlSessionTask = CKWebRequest(container: operationContainer).request(withURL: url, parameters: request) { [weak self] (dictionary, networkError) in 67 | 68 | guard let strongSelf = self, !strongSelf.isCancelled else { 69 | return 70 | } 71 | 72 | defer { 73 | strongSelf.finish(error: networkError) 74 | } 75 | 76 | guard let dictionary = dictionary, 77 | let subscriptionsDictionary = dictionary["subscriptions"] as? [[String: Any]], 78 | networkError == nil else { 79 | return 80 | } 81 | 82 | // Parse JSON into CKRecords 83 | for subscriptionDictionary in subscriptionsDictionary { 84 | 85 | if let subscription = CKSubscription(dictionary: subscriptionDictionary) { 86 | // Append Record 87 | strongSelf.subscriptionsIDToSubscriptions[subscription.subscriptionID] = subscription 88 | 89 | } else if let subscriptionFetchError = CKSubscriptionFetchErrorDictionary(dictionary: subscriptionDictionary) { 90 | 91 | let errorCode = CKErrorCode.errorCode(serverError: subscriptionFetchError.serverErrorCode)! 92 | let error = NSError(domain: CKErrorDomain, code: errorCode.rawValue, userInfo: [NSLocalizedDescriptionKey: subscriptionFetchError.reason]) 93 | 94 | strongSelf.subscriptionErrors[subscriptionFetchError.subscriptionID] = error 95 | 96 | } else { 97 | fatalError("Couldn't resolve record or record fetch error dictionary") 98 | } 99 | } 100 | } 101 | 102 | urlSessionTask?.resume() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/CKLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKLocation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 19/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol CKLocationType: CustomDictionaryConvertible, CKRecordFieldProvider { 12 | 13 | var coordinateType: CKLocationCoordinate2DType { get } 14 | 15 | var altitude: CKLocationDistance { get } 16 | 17 | var horizontalAccuracy: CKLocationAccuracy { get } 18 | 19 | var verticalAccuracy: CKLocationAccuracy { get } 20 | 21 | var course: CKLocationDirection { get } 22 | 23 | var speed: CKLocationSpeed { get } 24 | 25 | var timestamp: Date { get } 26 | } 27 | 28 | public protocol CKLocationCoordinate2DType { 29 | 30 | var latitude: CKLocationDegrees { get } 31 | 32 | var longitude: CKLocationDegrees { get } 33 | 34 | } 35 | 36 | public typealias CKLocationDegrees = Double 37 | 38 | public typealias CKLocationDistance = Double 39 | 40 | public typealias CKLocationAccuracy = Double 41 | 42 | public typealias CKLocationSpeed = Double 43 | 44 | public typealias CKLocationDirection = Double 45 | 46 | public struct CKLocationCoordinate2D: Equatable, CKLocationCoordinate2DType { 47 | 48 | public var latitude: CKLocationDegrees 49 | 50 | public var longitude: CKLocationDegrees 51 | 52 | public init() { 53 | latitude = 0 54 | longitude = 0 55 | } 56 | 57 | public init(latitude: CKLocationDegrees, longitude: CKLocationDegrees) { 58 | self.latitude = latitude 59 | self.longitude = longitude 60 | } 61 | } 62 | 63 | public func ==(lhs: CKLocationCoordinate2D, rhs: CKLocationCoordinate2D) -> Bool { 64 | return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 65 | } 66 | 67 | 68 | public class CKLocation: NSObject { 69 | 70 | public init(latitude: CKLocationDegrees, longitude: CKLocationDegrees) { 71 | 72 | self.coordinate = CKLocationCoordinate2D(latitude: latitude, longitude: longitude) 73 | self.altitude = 0 74 | self.horizontalAccuracy = 0 75 | self.verticalAccuracy = 0 76 | self.timestamp = Date() 77 | self.speed = -1 78 | self.course = -1 79 | 80 | } 81 | 82 | public init(coordinate: CKLocationCoordinate2D, altitude: CKLocationDistance, horizontalAccuracy hAccuracy: CKLocationAccuracy, verticalAccuracy vAccuracy: CKLocationAccuracy, timestamp: Date) { 83 | 84 | self.coordinate = coordinate 85 | self.altitude = altitude 86 | self.horizontalAccuracy = hAccuracy 87 | self.verticalAccuracy = vAccuracy 88 | self.timestamp = timestamp 89 | 90 | self.speed = -1 91 | self.course = -1 92 | } 93 | 94 | public init(coordinate: CKLocationCoordinate2D, altitude: CKLocationDistance, horizontalAccuracy hAccuracy: CKLocationAccuracy, verticalAccuracy vAccuracy: CKLocationAccuracy, course: CKLocationDirection, speed: CKLocationSpeed, timestamp: Date) { 95 | 96 | self.coordinate = coordinate 97 | self.altitude = altitude 98 | self.horizontalAccuracy = hAccuracy 99 | self.verticalAccuracy = vAccuracy 100 | self.course = course 101 | self.speed = speed 102 | self.timestamp = timestamp 103 | } 104 | 105 | 106 | public let coordinate: CKLocationCoordinate2D 107 | 108 | public let altitude: CKLocationDistance 109 | 110 | public let horizontalAccuracy: CKLocationAccuracy 111 | 112 | public let verticalAccuracy: CKLocationAccuracy 113 | 114 | public let course: CKLocationDirection 115 | 116 | public let speed: CKLocationSpeed 117 | 118 | public let timestamp: Date 119 | 120 | public override var description: String { 121 | return "<\(coordinate.latitude),\(coordinate.longitude)> +/- \(horizontalAccuracy)m (speed \(speed) mps / course \(course))" 122 | } 123 | 124 | override public func isEqual(_ object: Any?) -> Bool { 125 | if let location = object as? CKLocation { 126 | 127 | 128 | return location.coordinate == self.coordinate 129 | } 130 | return false 131 | 132 | } 133 | } 134 | 135 | extension CKLocation: CKLocationType { 136 | public var coordinateType: CKLocationCoordinate2DType { 137 | return coordinate 138 | } 139 | } 140 | 141 | extension CKLocationType { 142 | 143 | public var dictionary: [String: Any] { 144 | return [ 145 | "latitude": NSNumber(value: coordinateType.latitude), 146 | "longitude": NSNumber(value: coordinateType.longitude), 147 | "horizontalAccuracy": NSNumber(value: horizontalAccuracy), 148 | "verticalAccuracy": NSNumber(value: verticalAccuracy), 149 | "altitude": NSNumber(value: altitude), 150 | "speed": NSNumber(value: speed), 151 | "course": NSNumber(value: course), 152 | // CKWebServicesReference doesn't say if this should be seconds or milliseconds, assuming millis since thats what TIMESTAMP uses. 153 | "timestamp": NSNumber(value: UInt64(timestamp.timeIntervalSince1970 * 1000)) 154 | ] 155 | } 156 | } 157 | 158 | extension CKLocation { 159 | 160 | } 161 | 162 | -------------------------------------------------------------------------------- /Sources/CKLocationSortDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKLocationSortDescriptor.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 20/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKLocationSortDescriptor: NSSortDescriptor { 12 | 13 | public init(key: String, relativeLocation: CKLocationType) { 14 | self.relativeLocation = relativeLocation 15 | super.init(key: key, ascending: true) 16 | } 17 | 18 | required public init?(coder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | public var relativeLocation: CKLocationType 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CKModifyRecordZonesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKModifyRecordZonesOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 15/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKModifyRecordZonesOperation : CKDatabaseOperation { 12 | 13 | 14 | public override init() { 15 | super.init() 16 | } 17 | 18 | public convenience init(recordZonesToSave: [CKRecordZone]?, recordZoneIDsToDelete: [CKRecordZoneID]?) { 19 | self.init() 20 | self.recordZonesToSave = recordZonesToSave 21 | self.recordZoneIDsToDelete = recordZoneIDsToDelete 22 | } 23 | 24 | 25 | public var recordZonesToSave: [CKRecordZone]? 26 | 27 | public var recordZoneIDsToDelete: [CKRecordZoneID]? 28 | 29 | var recordZoneErrors: [CKRecordZoneID: NSError] = [:] 30 | 31 | var recordZonesByZoneIDs:[CKRecordZoneID: CKRecordZone] = [:] 32 | 33 | /* This block is called when the operation completes. 34 | The [NSOperation completionBlock] will also be called if both are set. 35 | If the error is CKErrorPartialFailure, the error's userInfo dictionary contains 36 | a dictionary of recordZoneIDs to errors keyed off of CKPartialErrorsByItemIDKey. 37 | This call happens as soon as the server has 38 | seen all record changes, and may be invoked while the server is processing the side effects 39 | of those changes. 40 | */ 41 | public var modifyRecordZonesCompletionBlock: (([CKRecordZone]?, [CKRecordZoneID]?, Error?) -> Swift.Void)? 42 | 43 | func zoneOperations() -> [[String: Any]] { 44 | 45 | var operationDictionaries: [[String: Any]] = [] 46 | if let recordZonesToSave = recordZonesToSave { 47 | let saveOperations = recordZonesToSave.map({ (zone) -> [String: Any] in 48 | 49 | let operation: [String: Any] = [ 50 | "operationType": "create".bridge(), 51 | "zone": ["zoneID".bridge(): zone.zoneID.dictionary].bridge() as Any 52 | ] 53 | 54 | return operation 55 | }) 56 | 57 | operationDictionaries.append(contentsOf: saveOperations) 58 | } 59 | 60 | if let recordZoneIDsToDelete = recordZoneIDsToDelete { 61 | let deleteOperations = recordZoneIDsToDelete.map({ (zoneID) -> [String: Any] in 62 | 63 | let operation: [String: Any] = [ 64 | "operationType": "delete".bridge(), 65 | "zone": ["zoneID".bridge(): zoneID.dictionary.bridge()].bridge() as Any 66 | ] 67 | 68 | return operation 69 | }) 70 | 71 | operationDictionaries.append(contentsOf: deleteOperations) 72 | } 73 | 74 | return operationDictionaries 75 | } 76 | 77 | override func finishOnCallbackQueue(error: Error?) { 78 | var error = error 79 | if(error == nil){ 80 | if self.recordZoneErrors.count > 0 { 81 | error = CKPrettyError(code: CKErrorCode.PartialFailure, userInfo: [CKPartialErrorsByItemIDKey: recordZoneErrors], description: "Failed to modify some zones") 82 | } 83 | } 84 | 85 | // Call the final completionBlock 86 | self.modifyRecordZonesCompletionBlock?(Array(self.recordZonesByZoneIDs.values), self.recordZoneIDsToDelete, error) 87 | 88 | super.finishOnCallbackQueue(error: error) 89 | } 90 | 91 | override func performCKOperation() { 92 | 93 | let url = "\(databaseURL)/zones/modify" 94 | let zoneOperations = self.zoneOperations().bridge() 95 | 96 | let request: [String: Any] = ["operations": zoneOperations] 97 | 98 | urlSessionTask = CKWebRequest(container: operationContainer).request(withURL: url, parameters: request) { [weak self] (dictionary, error) in 99 | 100 | guard let strongSelf = self, !strongSelf.isCancelled else { 101 | return 102 | } 103 | 104 | defer { 105 | strongSelf.finish(error: error) 106 | } 107 | 108 | guard let dictionary = dictionary, 109 | let zoneDictionaries = dictionary["zones"] as? [[String: Any]], 110 | error == nil else { 111 | return 112 | } 113 | 114 | // Parse JSON into CKRecords 115 | for zoneDictionary in zoneDictionaries { 116 | if let zone = CKRecordZone(dictionary: zoneDictionary) { 117 | strongSelf.recordZonesByZoneIDs[zone.zoneID] = zone 118 | } else if let fetchError = CKFetchErrorDictionary(dictionary: zoneDictionary) { 119 | 120 | // Append Error 121 | strongSelf.recordZoneErrors[fetchError.identifier] = fetchError.error() 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/CKModifyRecordsURLRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKModifyRecordsURLRequest.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 27/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class CKModifyRecordsURLRequest: CKURLRequest { 12 | 13 | var recordsToSave: [CKRecord]? 14 | 15 | var recordIDsToDelete: [CKRecordID]? 16 | 17 | var recordsByRecordIDs: [CKRecordID: CKRecord] = [:] 18 | 19 | var atomic: Bool = true 20 | 21 | 22 | // var sendAllFields: Bool 23 | 24 | var savePolicy: CKRecordSavePolicy 25 | 26 | init(recordsToSave: [CKRecord]?, recordIDsToDelete: [CKRecordID]?, isAtomic: Bool, database: CKDatabase, savePolicy: CKRecordSavePolicy, zoneID: CKRecordZoneID?) { 27 | 28 | self.recordsToSave = recordsToSave 29 | self.recordIDsToDelete = recordIDsToDelete 30 | self.atomic = isAtomic 31 | self.savePolicy = savePolicy 32 | 33 | 34 | super.init() 35 | 36 | self.databaseScope = database.scope 37 | 38 | self.path = "modify" 39 | self.operationType = CKOperationRequestType.records 40 | 41 | // Setup Body Properties 42 | var parameters: [String: Any] = [:] 43 | 44 | if database.scope == .public { 45 | parameters["atomic"] = NSNumber(value: false) 46 | } else { 47 | parameters["atomic"] = NSNumber(value: isAtomic) 48 | } 49 | 50 | #if os(Linux) 51 | parameters["operations"] = operationsDictionary().bridge() 52 | #else 53 | parameters["operations"] = operationsDictionary() as NSArray 54 | #endif 55 | 56 | if let zoneID = zoneID { 57 | parameters["zoneID"] = zoneID.dictionary.bridge() 58 | } 59 | 60 | 61 | requestProperties = parameters 62 | } 63 | 64 | 65 | func operationsDictionary() -> [[String: Any]] { 66 | var operationsDictionaryArray: [[String: Any]] = [] 67 | 68 | if let recordIDsToDelete = recordIDsToDelete { 69 | let deleteOperations = recordIDsToDelete.map({ (recordID) -> [String: Any] in 70 | let operationDictionary: [String: Any] = [ 71 | "operationType": "forceDelete".bridge(), 72 | "record":(["recordName":recordID.recordName.bridge()] as [String: Any]).bridge() as Any 73 | ] 74 | 75 | return operationDictionary 76 | }) 77 | 78 | operationsDictionaryArray.append(contentsOf: deleteOperations) 79 | } 80 | if let recordsToSave = recordsToSave { 81 | let saveOperations = recordsToSave.map({ (record) -> [String: Any] in 82 | 83 | let operationType: String 84 | let fieldsDictionary: [String: Any] 85 | 86 | //record.dictionary 87 | var recordDictionary: [String: Any] = ["recordType": record.recordType.bridge(), "recordName": record.recordID.recordName.bridge()] 88 | if let recordChangeTag = record.recordChangeTag { 89 | 90 | if savePolicy == .IfServerRecordUnchanged { 91 | operationType = "update" 92 | } else { 93 | operationType = "forceUpdate" 94 | } 95 | 96 | // Set Operation Type to Replace 97 | if savePolicy == .AllKeys { 98 | fieldsDictionary = record.fieldsDictionary(forKeys: record.allKeys()) 99 | } else { 100 | fieldsDictionary = record.fieldsDictionary(forKeys: record.changedKeys()) 101 | } 102 | 103 | recordDictionary["recordChangeTag"] = recordChangeTag.bridge() 104 | 105 | } else { 106 | // Create new record 107 | fieldsDictionary = record.fieldsDictionary(forKeys: record.allKeys()) 108 | operationType = "create" 109 | } 110 | 111 | 112 | recordDictionary["fields"] = fieldsDictionary.bridge() as NSDictionary 113 | if let parent = record.parent { 114 | recordDictionary["createShortGUID"] = NSNumber(value: 1) 115 | recordDictionary["parent"] = ["recordName": parent.recordID.recordName.bridge()].bridge() 116 | } 117 | 118 | 119 | let operationDictionary: [String: Any] = ["operationType": operationType.bridge(), "record": recordDictionary.bridge() as NSDictionary] 120 | 121 | 122 | 123 | return operationDictionary 124 | }) 125 | 126 | operationsDictionaryArray.append(contentsOf: saveOperations) 127 | } 128 | 129 | return operationsDictionaryArray 130 | } 131 | 132 | 133 | 134 | 135 | 136 | 137 | } 138 | -------------------------------------------------------------------------------- /Sources/CKModifySubscriptionsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKModifySubscriptionsOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 12/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKModifySubscriptionsOperation : CKDatabaseOperation { 12 | 13 | public init(subscriptionsToSave: [CKSubscription]?, subscriptionIDsToDelete: [String]?) { 14 | super.init() 15 | 16 | self.subscriptionsToSave = subscriptionsToSave 17 | self.subscriptionIDsToDelete = subscriptionIDsToDelete 18 | } 19 | 20 | public var subscriptionsToSave: [CKSubscription]? 21 | public var subscriptionIDsToDelete: [String]? 22 | 23 | var subscriptions: [CKSubscription] = [] 24 | var deletedSubscriptionIDs: [String] = [] 25 | var subscriptionErrors: [String: NSError] = [:] // todo needs filled up with the errors 26 | 27 | /* This block is called when the operation completes. 28 | The [NSOperation completionBlock] will also be called if both are set. 29 | If the error is CKErrorPartialFailure, the error's userInfo dictionary contains 30 | a dictionary of subscriptionIDs to errors keyed off of CKPartialErrorsByItemIDKey. 31 | */ 32 | public var modifySubscriptionsCompletionBlock: (([CKSubscription]?, [String]?, Error?) -> Void)? 33 | 34 | override func finishOnCallbackQueue(error: Error?) { 35 | var error = error 36 | if(error == nil){ 37 | if subscriptionErrors.count > 0 { 38 | error = CKPrettyError(code: CKErrorCode.PartialFailure, userInfo: [CKPartialErrorsByItemIDKey : subscriptionErrors], description: "Errors modifying subscriptions") 39 | } 40 | } 41 | self.modifySubscriptionsCompletionBlock?(subscriptions, deletedSubscriptionIDs, error) 42 | 43 | super.finishOnCallbackQueue(error: error) 44 | } 45 | 46 | override func performCKOperation() { 47 | 48 | let subscriptionURLRequest = CKModifySubscriptionsURLRequest(subscriptionsToSave: subscriptionsToSave, subscriptionIDsToDelete: subscriptionIDsToDelete) 49 | subscriptionURLRequest.completionBlock = { [weak self] (result) in 50 | guard let strongSelf = self, !strongSelf.isCancelled else { 51 | return 52 | } 53 | switch result { 54 | case .success(let dictionary): 55 | 56 | if let subscriptionsDictionary = dictionary["subscriptions"] as? [[String: Any]] { 57 | // Parse JSON into CKRecords 58 | 59 | 60 | for subscriptionDictionary in subscriptionsDictionary { 61 | 62 | if let subscription = CKSubscription(dictionary: subscriptionDictionary) { 63 | // Append Record 64 | strongSelf.subscriptions.append(subscription) 65 | 66 | } else if let subscriptionID = subscriptionDictionary["subscriptionID"] as? String { 67 | strongSelf.deletedSubscriptionIDs.append(subscriptionID) 68 | 69 | } else if let subscriptionFetchError = CKSubscriptionFetchErrorDictionary(dictionary: subscriptionDictionary) { 70 | 71 | // Create Error 72 | let _ = NSError(domain: CKErrorDomain, code: CKErrorCode.PartialFailure.rawValue, userInfo: [NSLocalizedDescriptionKey: subscriptionFetchError.reason]) 73 | 74 | // todo add to errors 75 | //subscriptionErrors["id"] = error 76 | 77 | } else { 78 | fatalError("Couldn't resolve record or record fetch error dictionary") 79 | } 80 | } 81 | } 82 | 83 | strongSelf.finish(error: nil) 84 | case .error(let error): 85 | strongSelf.finish(error: error.error) 86 | } 87 | } 88 | 89 | subscriptionURLRequest.performRequest() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/CKModifySubscriptionsURLRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKModifySubscriptionsURLRequest.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 20/1/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class CKModifySubscriptionsURLRequest: CKURLRequest { 12 | 13 | var subscriptionsToSave: [CKSubscription]? 14 | 15 | var subscriptionIDsToDelete: [String]? 16 | 17 | var zoneID: CKRecordZoneID? 18 | 19 | func operationsDictionary() -> [[String: Any]] { 20 | var operations: [[String: Any]] = [] 21 | 22 | if let subscriptionsToSave = subscriptionsToSave { 23 | 24 | for subscription in subscriptionsToSave { 25 | 26 | let operation: [String: Any] = [ 27 | "operationType": "create".bridge(), 28 | "subscription": subscription.subscriptionDictionary.bridge() as Any 29 | ] 30 | 31 | operations.append(operation) 32 | } 33 | } 34 | 35 | if let subscriptionIDsToDelete = subscriptionIDsToDelete { 36 | for subscriptionID in subscriptionIDsToDelete { 37 | 38 | let operation: [String: Any] = [ 39 | "operationType": "create".bridge(), 40 | "subscription": (["subscriptionID": subscriptionID.bridge()] as [String: Any]).bridge() as Any 41 | ] 42 | 43 | operations.append(operation) 44 | } 45 | } 46 | 47 | return operations 48 | } 49 | 50 | init(subscriptionsToSave: [CKSubscription]?, subscriptionIDsToDelete: [String]?) { 51 | 52 | self.subscriptionsToSave = subscriptionsToSave 53 | self.subscriptionIDsToDelete = subscriptionIDsToDelete 54 | 55 | super.init() 56 | 57 | let properties: [String: Any] = ["operations": operationsDictionary().bridge() as Any] 58 | self.operationType = .subscriptions 59 | self.path = "modify" 60 | self.requestProperties = properties 61 | 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/CKNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKNotification.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 16/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public struct CKNotificationID { 12 | let notificationUUID: String 13 | } 14 | 15 | public enum CKNotificationType : Int { 16 | case query 17 | case recordZone 18 | case readNotification 19 | case database 20 | } 21 | 22 | let CKNotificationAPSAlertBodyKey = "body" 23 | let CKNotificationCKKey = "ck" 24 | let CKNotificiationAPSAlertLaunchImageKey = "launch-image" 25 | let CKNotificationAPSAlertBadgeKey = "badge" 26 | let CKNotificationQueryNotificationKey = "qry" 27 | let CKNotificationZoneNotificationKey = "fet" 28 | let CKNotificationDatabaseNotificationKey = "met" 29 | let CKNotificationContainerIDKey = "cid" 30 | 31 | 32 | public class CKNotification : NSObject { 33 | 34 | class func notification(fromRemoteNotificationDictionary notificationDictionary: [AnyHashable : Any]) -> CKNotification? { 35 | 36 | if let cloudKitDictionary = notificationDictionary[CKNotificationCKKey] as? [String: Any] { 37 | 38 | if cloudKitDictionary[CKNotificationQueryNotificationKey] as? [String: Any] != nil { 39 | return CKQueryNotification(fromRemoteNotificationDictionary: notificationDictionary) 40 | } else if cloudKitDictionary[CKNotificationZoneNotificationKey] as? [String: Any] != nil { 41 | return CKRecordZoneNotification(fromRemoteNotificationDictionary: notificationDictionary) 42 | } else if cloudKitDictionary[CKNotificationDatabaseNotificationKey] as? [String: Any] != nil { 43 | return CKDatabaseNotification(fromRemoteNotificationDictionary: notificationDictionary) 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | 50 | 51 | 52 | init(fromRemoteNotificationDictionary notificationDictionary: [AnyHashable : Any]) 53 | { 54 | super.init() 55 | 56 | notificationType = .database 57 | 58 | // Check that this notification is a CloudKit notification, if not return nil 59 | if let ckDictionary = notificationDictionary[CKNotificationCKKey] as? [String: Any] { 60 | 61 | // Get the container ID from dictionary 62 | if let containerID = ckDictionary[CKNotificationContainerIDKey] as? String { 63 | containerIdentifier = containerID 64 | } 65 | 66 | // Get the notification ID from dictionary 67 | if let nID = ckDictionary["nid"] as? String { 68 | let id = CKNotificationID(notificationUUID: nID) 69 | notificationID = id 70 | } 71 | 72 | // Set isPruned from dictionary 73 | if ckDictionary["p"] != nil { 74 | isPruned = true 75 | } 76 | 77 | } 78 | 79 | if let apsDictionary = notificationDictionary["aps"] as? [String: Any] { 80 | if let alertDictionary = apsDictionary["alert"] as? [String: Any] { 81 | 82 | // Set body 83 | if let body = alertDictionary[CKNotificationAPSAlertBodyKey] as? String { 84 | alertBody = body 85 | } 86 | 87 | // Set Alert Localization Key 88 | if let locKey = alertDictionary["loc-key"] as? String { 89 | alertLocalizationKey = locKey 90 | } 91 | 92 | // Set Alert LocalizationArgs 93 | if let locArgs = alertDictionary["loc-args"] as? [String] { 94 | self.alertLocalizationArgs = locArgs 95 | } 96 | 97 | // Set Action Localization Key 98 | if let actionLocKey = alertDictionary["action-loc-key"] as? String { 99 | alertActionLocalizationKey = actionLocKey 100 | } 101 | 102 | // Set Launch Image 103 | if let launchImage = alertDictionary[CKNotificiationAPSAlertLaunchImageKey] as? String { 104 | alertLaunchImage = launchImage 105 | } 106 | 107 | // Set Badge 108 | if let badgeVale = alertDictionary[CKNotificationAPSAlertBadgeKey] as? NSNumber { 109 | badge = badgeVale 110 | } 111 | 112 | // Set Sound Name 113 | if let sound = alertDictionary["sound"] as? String { 114 | soundName = sound 115 | } 116 | 117 | // Set Category 118 | if let cat = alertDictionary["category"] as? String { 119 | category = cat 120 | } 121 | } 122 | } 123 | } 124 | 125 | public var notificationType: CKNotificationType = .database 126 | public var notificationID: CKNotificationID? 127 | public var containerIdentifier: String? 128 | 129 | 130 | /* push notifications have a limited size. In some cases, CloudKit servers may not be able to send you a full 131 | CKNotification's worth of info in one push. In those cases, isPruned returns YES. The order in which we'll 132 | drop properties is defined in each CKNotification subclass below. 133 | The CKNotification can be obtained in full via a CKFetchNotificationChangesOperation */ 134 | public var isPruned: Bool = false 135 | 136 | /* These keys are parsed out of the 'aps' payload from a remote notification dictionary. 137 | On tvOS, alerts, badges, sounds, and categories are not handled in push notifications. */ 138 | 139 | /* Optional alert string to display in a push notification. */ 140 | public var alertBody: String? 141 | 142 | /* Instead of a raw alert string, you may optionally specify a key for a localized string in your app's Localizable.strings file. */ 143 | public var alertLocalizationKey: String? 144 | 145 | /* A list of field names to take from the matching record that is used as substitution variables in a formatted alert string. */ 146 | public var alertLocalizationArgs: [String]? 147 | 148 | /* A key for a localized string to be used as the alert action in a modal style notification. */ 149 | public var alertActionLocalizationKey: String? 150 | 151 | /* The name of an image in your app bundle to be used as the launch image when launching in response to the notification. */ 152 | public var alertLaunchImage: String? 153 | 154 | /* The number to display as the badge of the application icon */ 155 | @NSCopying public var badge: NSNumber? 156 | 157 | /* The name of a sound file in your app bundle to play upon receiving the notification. */ 158 | public var soundName: String? 159 | 160 | /* The ID of the subscription that caused this notification to fire */ 161 | public var subscriptionID: String? 162 | 163 | /* The category for user-initiated actions in the notification */ 164 | public var category: String? 165 | } 166 | 167 | public enum CKQueryNotificationReason : Int { 168 | 169 | case recordCreated 170 | 171 | case recordUpdated 172 | 173 | case recordDeleted 174 | } 175 | 176 | 177 | -------------------------------------------------------------------------------- /Sources/CKOperationInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKOperationInfo.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 29/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class CKOperationInfo { 12 | 13 | } 14 | 15 | class CKDatabaseOperationInfo: CKOperationInfo { 16 | let databaseScope: CKDatabaseScope 17 | 18 | init(databaseScope: CKDatabaseScope) { 19 | self.databaseScope = databaseScope 20 | super.init() 21 | } 22 | 23 | } 24 | 25 | extension CKOperationInfo: CKCodable { 26 | var dictionary: [String : Any] { 27 | return [:] 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /Sources/CKOperationMetrics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKOperationMetrics.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 26/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CKURLRequestMetricsDelegate { 12 | func requestDidFinish(withMetrics metrics:CKOperationMetrics) 13 | } 14 | 15 | struct CKOperationMetrics { 16 | 17 | var bytesDownloaded: UInt = 0 18 | 19 | var bytesUploaded: UInt = 0 20 | 21 | var duration: TimeInterval = 0 22 | 23 | var startDate: Date 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/CKOperationResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKOperationResult.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 8/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | /* 11 | class CKURLRequest: NSObject, URLSessionDataDelegate { 12 | 13 | var url: NSURL! 14 | 15 | var request: NSURLRequest? 16 | 17 | var urlSessionTask: NSURLSessionTask? 18 | 19 | var cancelled: Bool = false 20 | 21 | func urlSession(_ session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) { 22 | 23 | } 24 | 25 | func urlSession(_ session: NSURLSession, task: NSURLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { 26 | 27 | } 28 | 29 | func urlSession(_ session: NSURLSession, dataTask: NSURLSessionDataTask, didReceive response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Swift.Void) { 30 | 31 | } 32 | 33 | func performRequest() { 34 | 35 | } 36 | 37 | func setupPublicDatabaseURL() { 38 | 39 | } 40 | } 41 | */ 42 | -------------------------------------------------------------------------------- /Sources/CKPersonNameComponents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKPersonNameComponents.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 19/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public protocol CKPersonNameComponentsType { 13 | 14 | var namePrefix: String? { get set } 15 | 16 | /* Name bestowed upon an individual by one's parents, e.g. Johnathan */ 17 | var givenName: String? { get set } 18 | 19 | /* Secondary given name chosen to differentiate those with the same first name, e.g. Maple */ 20 | var middleName: String? { get set } 21 | 22 | /* Name passed from one generation to another to indicate lineage, e.g. Appleseed */ 23 | var familyName: String? { get set } 24 | 25 | /* Post-nominal letters denoting degree, accreditation, or other honor, e.g. Esq., Jr., Ph.D. */ 26 | var nameSuffix: String? { get set } 27 | 28 | /* Name substituted for the purposes of familiarity, e.g. "Johnny"*/ 29 | var nickname: String? { get set } 30 | 31 | init?(dictionary: [String: Any]) 32 | } 33 | 34 | public struct CKPersonNameComponents { 35 | 36 | /* Pre-nominal letters denoting title, salutation, or honorific, e.g. Dr., Mr. */ 37 | public var namePrefix: String? 38 | 39 | /* Name bestowed upon an individual by one's parents, e.g. Johnathan */ 40 | public var givenName: String? 41 | 42 | /* Secondary given name chosen to differentiate those with the same first name, e.g. Maple */ 43 | public var middleName: String? 44 | 45 | /* Name passed from one generation to another to indicate lineage, e.g. Appleseed */ 46 | public var familyName: String? 47 | 48 | /* Post-nominal letters denoting degree, accreditation, or other honor, e.g. Esq., Jr., Ph.D. */ 49 | public var nameSuffix: String? 50 | 51 | /* Name substituted for the purposes of familiarity, e.g. "Johnny"*/ 52 | public var nickname: String? 53 | 54 | /* Each element of the phoneticRepresentation should correspond to an element of the original PersonNameComponents instance. 55 | The phoneticRepresentation of the phoneticRepresentation object itself will be ignored. nil by default, must be instantiated. 56 | */ 57 | } 58 | 59 | extension CKPersonNameComponents: CKPersonNameComponentsType { 60 | public init?(dictionary: [String: Any]) { 61 | 62 | namePrefix = dictionary["namePrefix"] as? String 63 | givenName = dictionary["givenName"] as? String 64 | familyName = dictionary["familyName"] as? String 65 | nickname = dictionary["nickname"] as? String 66 | nameSuffix = dictionary["nameSuffix"] as? String 67 | middleName = dictionary["middleName"] as? String 68 | // phoneticRepresentation 69 | } 70 | } 71 | 72 | /* 73 | @available(OSX 10.11, *) 74 | extension PersonNameComponents: CKPersonNameComponentsType { 75 | public convenience init?(dictionary: [String: Any]) { 76 | self.init() 77 | 78 | namePrefix = dictionary["namePrefix"] as? String 79 | givenName = dictionary["givenName"] as? String 80 | familyName = dictionary["familyName"] as? String 81 | nickname = dictionary["nickname"] as? String 82 | nameSuffix = dictionary["nameSuffix"] as? String 83 | middleName = dictionary["middleName"] as? String 84 | // phoneticRepresentation 85 | } 86 | } 87 | */ 88 | -------------------------------------------------------------------------------- /Sources/CKPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKPredicate.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 19/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum CKCompatorType: String { 12 | case equals = "EQUALS" 13 | case notEquals = "NOT_EQUALS" 14 | case lessThan = "LESS_THAN" 15 | case lessThanOrEquals = "LESS_THAN_OR_EQUALS" 16 | case greaterThan = "GREATER_THAN" 17 | case greaterThanOrEquals = "GREATER_THAN_OR_EQUALS" 18 | case near = "NEAR" 19 | case containsAllTokens = "CONTAINS_ALL_TOKENS" 20 | case `in` = "IN" 21 | case notIn = "NOT_IN" 22 | case containsAnyTokens = "CONTAINS_ANY_TOKENS" 23 | case listContains = "LIST_CONTAINS" 24 | case notListContains = "NOT_LIST_CONTAINS" 25 | case notListContainsAny = "NOT_LIST_CONTAINS_ANY" 26 | case beginsWith = "BEGINS_WITH" 27 | case notBeginsWith = "NOT_BEGINS_WITH" 28 | case listMemberBeginsWith = "LIST_MEMBER_BEGINS_WITH" 29 | case notListMemberBeginsWith = "NOT_LIST_MEMBER_BEGINS_WITH" 30 | case listContainsAll = "LIST_CONTAINS_ALL" 31 | case notListContainsAll = "NOT_LIST_CONTAINS_ALL" 32 | 33 | init?(expression: String) { 34 | switch expression { 35 | case "==": 36 | self = .equals 37 | case "!=": 38 | self = .notEquals 39 | case "<": 40 | self = .lessThan 41 | case "<=": 42 | self = .lessThanOrEquals 43 | case ">": 44 | self = .greaterThan 45 | case ">=": 46 | self = .greaterThanOrEquals 47 | default: 48 | return nil 49 | } 50 | } 51 | } 52 | 53 | 54 | struct CKPredicate { 55 | 56 | struct TokenPosition { 57 | var index: Int 58 | var length: Int 59 | 60 | init(index: Int) { 61 | self.index = index 62 | self.length = 0 63 | } 64 | mutating func advance() { 65 | length += 1 66 | } 67 | } 68 | 69 | let predicate: NSPredicate 70 | 71 | let numberFormatter = NumberFormatter() 72 | 73 | init(predicate: NSPredicate) { 74 | self.predicate = predicate 75 | } 76 | 77 | func compoundPredicates(with format: String) -> [String] { 78 | let compoundPredicateComponents = format.components(separatedBy: " AND ") 79 | return compoundPredicateComponents 80 | } 81 | 82 | func filters() -> [CKQueryFilter] { 83 | var filterDictionaries: [CKQueryFilter] = [] 84 | let compoundPredicates = self.compoundPredicates(with: predicate.predicateFormat) 85 | for predicate in compoundPredicates { 86 | let components = self.components(for: predicate) 87 | if let filterDictionary = try! filterPredicate(components: components) { 88 | filterDictionaries.append(filterDictionary) 89 | } 90 | } 91 | 92 | if filterDictionaries.count == compoundPredicates.count { 93 | return filterDictionaries 94 | } else { 95 | return [] 96 | } 97 | } 98 | 99 | func components(for string: String) -> [String] { 100 | 101 | let reader = CKPredicateReader(string: string) 102 | return try! reader.parse(0) 103 | } 104 | 105 | func filterPredicate(components: [String]) throws -> CKQueryFilter? { 106 | if components.count == 3 { 107 | let lhs = components[0] 108 | var fieldName = lhs 109 | let comparatorValue = components[1] 110 | let rhs = value(forString: components[2]) 111 | var fieldValue = rhs 112 | 113 | guard let comparator = CKCompatorType(expression: comparatorValue) else { 114 | return nil 115 | } 116 | 117 | if lhs.hasPrefix("distanceToLocation:fromLocation:") { 118 | // Parse Location Function 119 | let locationReader = CKPredicateReader(string: fieldName) 120 | if let locationExpression = try! locationReader.parseLocationExpression(0) { 121 | fieldName = locationExpression.fieldName 122 | let coordinate = locationExpression.coordinate 123 | fieldValue = CKLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) 124 | 125 | if let distance = (rhs as? NSNumber)?.doubleValue { 126 | return CKQueryFilter(fieldName: fieldName, comparator: comparator, fieldValue: fieldValue, distance: distance) 127 | } 128 | } 129 | } 130 | 131 | return CKQueryFilter(fieldName: fieldName, comparator: comparator, fieldValue: fieldValue) 132 | } else if components.count == 1 { 133 | if components[0] == "TRUEPREDICATE" { 134 | return nil 135 | } 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func parseFunction(string: String) -> CKPredicateFunction? { 142 | let functionReader = CKPredicateReader(string: string) 143 | do { 144 | let parsedFunction = try functionReader.parseFunction(0) 145 | return CKPredicateFunction(name: parsedFunction.0, parameters: parsedFunction.parameters) 146 | } catch { 147 | return nil 148 | } 149 | } 150 | 151 | func value(forCastParameters parameters: [Any]) -> CKRecordValue? { 152 | let type = parameters.last as! String 153 | switch type { 154 | case "NSDate": 155 | let interval = parameters.first as! NSNumber 156 | return NSDate(timeIntervalSinceReferenceDate: interval.doubleValue) 157 | 158 | default: 159 | return nil 160 | } 161 | } 162 | 163 | func value(forString string: String) -> CKRecordValue { 164 | 165 | 166 | let numberFromString = numberFormatter.number(from: string) 167 | 168 | if let number = numberFromString { 169 | return number 170 | } else { 171 | if string.hasPrefix("CAST") { 172 | // Parse Function 173 | let parseFunction = self.parseFunction(string: string) 174 | return value(forCastParameters: parseFunction!.parameters)! 175 | 176 | } else { 177 | return string 178 | } 179 | } 180 | } 181 | } 182 | 183 | public struct CKPredicateFunction { 184 | let name: String 185 | let parameters: [Any] 186 | } 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /Sources/CKPrettyError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKPrettyError.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 29/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | enum CKError { 13 | case network(Error) 14 | case server([String: Any]) 15 | case parse(Error) 16 | 17 | var error: NSError { 18 | switch self { 19 | case .network(let networkError): 20 | return ckError(forNetworkError: NSError(error: networkError)) 21 | case .server(let dictionary): 22 | return ckError(forServerResponseDictionary: dictionary) 23 | case .parse(let parseError): 24 | let error = NSError(error: parseError) 25 | return NSError(domain: CKErrorDomain, code: CKErrorCode.InternalError.rawValue, userInfo: error.userInfo ) 26 | } 27 | } 28 | 29 | func ckError(forNetworkError networkError: NSError) -> NSError { 30 | let userInfo = networkError.userInfo 31 | let errorCode: CKErrorCode 32 | 33 | switch networkError.code { 34 | case NSURLErrorNotConnectedToInternet: 35 | errorCode = .NetworkUnavailable 36 | case NSURLErrorCannotFindHost, NSURLErrorCannotConnectToHost: 37 | errorCode = .ServiceUnavailable 38 | default: 39 | errorCode = .NetworkFailure 40 | } 41 | 42 | let error = NSError(domain: CKErrorDomain, code: errorCode.rawValue, userInfo: userInfo) 43 | return error 44 | } 45 | 46 | func ckError(forServerResponseDictionary dictionary: [String: Any]) -> NSError { 47 | if let recordFetchError = CKRecordFetchErrorDictionary(dictionary: dictionary) { 48 | 49 | let errorCode = CKErrorCode.errorCode(serverError: recordFetchError.serverErrorCode)! 50 | 51 | var userInfo: NSErrorUserInfoType = [:] 52 | 53 | userInfo["redirectURL"] = recordFetchError.redirectURL 54 | userInfo[NSLocalizedDescriptionKey] = recordFetchError.reason 55 | 56 | userInfo[CKErrorRetryAfterKey] = recordFetchError.retryAfter 57 | userInfo["uuid"] = recordFetchError.uuid 58 | 59 | return NSError(domain: CKErrorDomain, code: errorCode.rawValue, userInfo: userInfo) 60 | } else { 61 | 62 | let userInfo = [:] as NSErrorUserInfoType 63 | return NSError(domain: CKErrorDomain, code: CKErrorCode.InternalError.rawValue, userInfo: userInfo) 64 | } 65 | } 66 | } 67 | 68 | class CKPrettyError: NSError { 69 | 70 | /* 71 | 72 | CVarArgs not automatically supported in Swift Linux 73 | 74 | convenience init(code: CKErrorCode, format: String, _ args: CVarArg...){ 75 | let description = String(format: format, arguments: args) 76 | self.init(code, userInfo: nil, error: nil, path: nil, URL: nil, description: description) 77 | } 78 | 79 | convenience init(code: CKErrorCode, userInfo: NSErrorUserInfoType, format: String, _ args: CVarArg...){ 80 | let description = String(format: format, arguments: args) 81 | self.init(code: code, userInfo: userInfo, description: description) 82 | } 83 | */ 84 | 85 | convenience init(code: CKErrorCode, description: String) { 86 | self.init(code, userInfo: nil, error: nil, path: nil, URL: nil, description: description) 87 | } 88 | 89 | convenience init(code: CKErrorCode, userInfo: NSErrorUserInfoType, description: String) { 90 | self.init(code, userInfo: userInfo, error: nil, path: nil, URL: nil, description: description) 91 | } 92 | 93 | init(_ code: CKErrorCode, userInfo: NSErrorUserInfoType?, error: Error?, path: String?, URL: URL?, description: String?){ 94 | var userInfo = userInfo 95 | 96 | if(description != nil){ 97 | if(userInfo == nil){ 98 | userInfo = NSErrorUserInfoType() 99 | } 100 | userInfo?[NSLocalizedDescriptionKey] = description; 101 | userInfo?["CKErrorDescription"] = description; 102 | } 103 | 104 | super.init(domain: CKErrorDomain, code: code.rawValue, userInfo: userInfo) 105 | } 106 | 107 | required init?(coder aDecoder: NSCoder) { 108 | super.init(coder: aDecoder) 109 | } 110 | 111 | 112 | //override var description: String{ 113 | // was cancelled before it started"> 114 | //} 115 | 116 | override public var description: String { 117 | // \(withUnsafePointer(to: self)) 118 | let errorDescription = CKErrorCode(rawValue: self.code)?.description ?? "" 119 | return " Void)? 18 | 19 | override func finishOnCallbackQueue(error: Error?) { 20 | if(error == nil){ 21 | // build partial error 22 | } 23 | 24 | // call assets completion block 25 | 26 | super.finishOnCallbackQueue(error: error) 27 | } 28 | 29 | 30 | override func performCKOperation() { 31 | //self.finish(error: error or nil) 32 | } 33 | 34 | init(assets: [CKAsset]) { 35 | self.assets = assets 36 | 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Sources/CKPushConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKPushConnection.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 15/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class CKPushConnection: NSObject, URLSessionDataDelegate { 12 | 13 | var longPollingTask: URLSessionDataTask? 14 | 15 | var callBack: ((CKNotification) -> Void)? 16 | 17 | init(url: URL) { 18 | 19 | super.init() 20 | 21 | let urlRequest = URLRequest(url: url) 22 | 23 | let configuration = URLSessionConfiguration.default 24 | 25 | configuration.timeoutIntervalForRequest = Double.greatestFiniteMagnitude 26 | 27 | let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) 28 | 29 | longPollingTask = session.dataTask(with: urlRequest) 30 | 31 | longPollingTask?.resume() 32 | 33 | } 34 | 35 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 36 | 37 | // Serialize JSON 38 | do { 39 | if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { 40 | CloudKit.debugPrint(json) 41 | // Create Notification 42 | if let notification = CKNotification.notification(fromRemoteNotificationDictionary: json) { 43 | callBack?(notification) 44 | } 45 | /* 46 | if let notification = CKNotification(fromRemoteNotificationDictionary: json) { 47 | callBack?(notification) 48 | } 49 | */ 50 | } 51 | } catch { 52 | 53 | } 54 | 55 | 56 | } 57 | 58 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 59 | if let error = error { 60 | CloudKit.debugPrint(error) 61 | } 62 | 63 | // Restart Task 64 | longPollingTask?.resume() 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Sources/CKQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKQuery.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 6/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | struct CKQueryDictionary { 12 | static let recordType = "recordType" 13 | static let filterBy = "filterBy" 14 | static let sortBy = "sortBy" 15 | } 16 | 17 | struct CKSortDescriptorDictionary { 18 | static let fieldName = "fieldName" 19 | static let ascending = "ascending" 20 | static let relativeLocation = "relativeLocation" 21 | } 22 | 23 | public class CKQuery: CKCodable { 24 | 25 | public var recordType: String 26 | 27 | public var predicate: NSPredicate 28 | 29 | let filters: [CKQueryFilter] 30 | 31 | public init(recordType: String, predicate: NSPredicate) { 32 | self.recordType = recordType 33 | self.predicate = predicate 34 | self.filters = CKPredicate(predicate: predicate).filters() 35 | } 36 | 37 | public init(recordType: String, filters: [CKQueryFilter]) { 38 | self.recordType = recordType 39 | self.filters = filters 40 | self.predicate = NSPredicate(value: true) 41 | } 42 | 43 | public var sortDescriptors: [NSSortDescriptor] = [] 44 | 45 | // Returns a Dictionary Representation of a Query Dictionary 46 | var dictionary: [String: Any] { 47 | 48 | var queryDictionary: [String: Any] = ["recordType": recordType.bridge()] 49 | 50 | queryDictionary["filterBy"] = filters.map({ (filter) -> [String: Any] in 51 | return filter.dictionary 52 | }).bridge() 53 | 54 | // Create Sort Descriptor Dictionaries 55 | queryDictionary["sortBy"] = sortDescriptors.flatMap { (sortDescriptor) -> [String: Any]? in 56 | 57 | if let fieldName = sortDescriptor.key { 58 | var sortDescriptionDictionary: [String: Any] = [CKSortDescriptorDictionary.fieldName: fieldName.bridge(), 59 | CKSortDescriptorDictionary.ascending: NSNumber(value: sortDescriptor.ascending)] 60 | if let locationSortDescriptor = sortDescriptor as? CKLocationSortDescriptor { 61 | sortDescriptionDictionary[CKSortDescriptorDictionary.relativeLocation] = locationSortDescriptor.relativeLocation.recordFieldDictionary.bridge() 62 | } 63 | 64 | return sortDescriptionDictionary 65 | 66 | } else { 67 | return nil 68 | } 69 | }.bridge() 70 | 71 | return queryDictionary 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/CKQueryCursor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKQueryCursor.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 7/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKQueryCursor: NSObject { 12 | 13 | var data: NSData 14 | 15 | var zoneID: CKRecordZoneID 16 | 17 | init(data: NSData, zoneID: CKRecordZoneID) { 18 | 19 | self.data = data 20 | self.zoneID = zoneID 21 | 22 | super.init() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CKQueryFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKQueryFilter.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 26/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public struct CKLocationBound { 12 | let radius: CKLocationDistance 13 | } 14 | 15 | public struct CKQueryFilter: Equatable { 16 | let fieldName: String 17 | let type: CKCompatorType 18 | let fieldValue: CKRecordValue 19 | let bounds: CKLocationBound? 20 | 21 | public init(fieldName: String, comparator: CKCompatorType, fieldValue: CKRecordValue, distance: CKLocationDistance? = nil) { 22 | self.fieldName = fieldName 23 | self.type = comparator 24 | self.fieldValue = fieldValue 25 | if let distance = distance { 26 | self.bounds = CKLocationBound(radius: distance) 27 | } else { 28 | self.bounds = nil 29 | } 30 | } 31 | } 32 | 33 | extension CKQueryFilter: CKCodable { 34 | 35 | public var dictionary: [String: Any] { 36 | var filterDictionary: [String: Any] = [ 37 | "comparator": type.rawValue.bridge(), 38 | "fieldName": fieldName.bridge(), 39 | "fieldValue": fieldValue.recordFieldDictionary.bridge() 40 | ] 41 | 42 | if let bounds = bounds { 43 | filterDictionary["distance"] = NSNumber(value: bounds.radius) 44 | } 45 | 46 | return filterDictionary 47 | } 48 | } 49 | 50 | public func ==(lhs: CKQueryFilter, rhs: CKQueryFilter) -> Bool { 51 | 52 | return lhs.fieldName == rhs.fieldName && lhs.type == rhs.type 53 | } 54 | -------------------------------------------------------------------------------- /Sources/CKQueryNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKQueryNotification.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 20/1/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKQueryNotification : CKNotification { 12 | 13 | 14 | public override init(fromRemoteNotificationDictionary notificationDictionary: [AnyHashable : Any]) { 15 | 16 | super.init(fromRemoteNotificationDictionary: notificationDictionary) 17 | 18 | guard let cloudDictionary = notificationDictionary[CKNotificationCKKey] as? [String: Any] else { 19 | return 20 | } 21 | 22 | if let queryDictionary = cloudDictionary[CKNotificationQueryNotificationKey] as? [String: Any] { 23 | 24 | // Set recordID 25 | if 26 | let zoneName = queryDictionary["zid"] as? String, 27 | let ownerName = queryDictionary["zoid"] as? String, 28 | let recordName = queryDictionary["rid"] as? String 29 | { 30 | let zoneID = CKRecordZoneID(zoneName: zoneName, ownerName: ownerName) 31 | recordID = CKRecordID(recordName: recordName, zoneID: zoneID) 32 | } 33 | 34 | // Set database scope 35 | if let dbs = queryDictionary["dbs"] as? NSNumber, let scope = CKDatabaseScope(rawValue: dbs.intValue) { 36 | databaseScope = scope 37 | } 38 | 39 | // Set notification reason 40 | if let fo = queryDictionary["fo"] as? NSNumber, let reason = CKQueryNotificationReason(rawValue: fo.intValue) { 41 | queryNotificationReason = reason 42 | } else { 43 | queryNotificationReason = .recordCreated 44 | } 45 | 46 | // Set Subscription ID 47 | if let sid = queryDictionary["sid"] as? String { 48 | subscriptionID = sid 49 | } 50 | } 51 | } 52 | 53 | public var queryNotificationReason: CKQueryNotificationReason = .recordCreated 54 | 55 | /* A set of key->value pairs for creates and updates. You request the server fill out this property via the 56 | "desiredKeys" property of CKNotificationInfo */ 57 | public var recordFields: [String : Any]? 58 | 59 | public var recordID: CKRecordID? 60 | 61 | public var isPublicDatabase: Bool { 62 | return databaseScope == .public 63 | } 64 | 65 | public var databaseScope: CKDatabaseScope = .public 66 | } 67 | -------------------------------------------------------------------------------- /Sources/CKQueryOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKQueryOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 7/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | let CKQueryOperationMaximumResults = 0 12 | 13 | public class CKQueryOperation: CKDatabaseOperation { 14 | 15 | public override init() { 16 | super.init() 17 | } 18 | 19 | public convenience init(query: CKQuery) { 20 | self.init() 21 | self.query = query 22 | 23 | } 24 | 25 | public convenience init(cursor: CKQueryCursor) { 26 | self.init() 27 | self.cursor = cursor 28 | } 29 | 30 | public var shouldFetchAssetContent = true 31 | 32 | public var query: CKQuery? 33 | 34 | public var cursor: CKQueryCursor? 35 | 36 | public var resultsCursor: CKQueryCursor? 37 | 38 | var isFinishing: Bool = false 39 | 40 | public var zoneID: CKRecordZoneID? 41 | 42 | public var resultsLimit: Int = CKQueryOperationMaximumResults 43 | 44 | public var desiredKeys: [String]? 45 | 46 | public var recordFetchedBlock: ((CKRecord) -> Swift.Void)? 47 | 48 | public var queryCompletionBlock: ((CKQueryCursor?, Error?) -> Swift.Void)? 49 | 50 | override func CKOperationShouldRun() throws { 51 | // "Warn: There's no point in running a query if there are no progress or completion blocks set. Bailing early." 52 | 53 | if(query == nil && cursor == nil){ 54 | throw CKPrettyError(code: CKErrorCode.InvalidArguments, description: "either a query or query cursor must be provided for \(self)") 55 | } 56 | } 57 | 58 | override func finishOnCallbackQueue(error: Error?) { 59 | // log "Operation %@ has completed. Query cursor is %@.%@%@" 60 | self.queryCompletionBlock?(self.resultsCursor, error) 61 | 62 | super.finishOnCallbackQueue(error: error) 63 | } 64 | 65 | func fetched(record: CKRecord){ 66 | callbackQueue.async { 67 | self.recordFetchedBlock?(record) 68 | } 69 | } 70 | 71 | override func performCKOperation() { 72 | 73 | let queryOperationURLRequest = CKQueryURLRequest(query: query!, cursor: cursor?.data.bridge(), limit: resultsLimit, requestedFields: desiredKeys, zoneID: zoneID) 74 | queryOperationURLRequest.accountInfoProvider = CloudKit.shared.defaultAccount 75 | queryOperationURLRequest.databaseScope = database?.scope ?? .public 76 | 77 | queryOperationURLRequest.completionBlock = { [weak self] (result) in 78 | guard let strongSelf = self, !strongSelf.isCancelled else { 79 | return 80 | } 81 | switch result { 82 | case .success(let dictionary): 83 | 84 | // Process cursor 85 | if let continuationMarker = dictionary["continuationMarker"] as? String { 86 | 87 | #if os(Linux) 88 | let data = NSData(base64Encoded: continuationMarker, options: []) 89 | #else 90 | let data = NSData(base64Encoded: continuationMarker) 91 | #endif 92 | 93 | if let data = data { 94 | strongSelf.resultsCursor = CKQueryCursor(data: data, zoneID: CKRecordZoneID(zoneName: "_defaultZone", ownerName: "")) 95 | } 96 | } 97 | 98 | 99 | // Process Records 100 | if let recordsDictionary = dictionary["records"] as? [[String: Any]] { 101 | // Parse JSON into CKRecords 102 | for recordDictionary in recordsDictionary { 103 | 104 | if let record = CKRecord(recordDictionary: recordDictionary) { 105 | // Call RecordCallback 106 | strongSelf.fetched(record: record) 107 | } else { 108 | // Create Error 109 | // Invalid state to be in, this operation normally doesnt provide partial errors 110 | let error = NSError(domain: CKErrorDomain, code: CKErrorCode.PartialFailure.rawValue, userInfo: [NSLocalizedDescriptionKey: "Failed to parse record from server"]) 111 | strongSelf.finish(error: error) 112 | return 113 | } 114 | } 115 | } 116 | strongSelf.finish(error: nil) 117 | case .error(let error): 118 | strongSelf.finish(error: error.error) 119 | } 120 | } 121 | 122 | queryOperationURLRequest.performRequest() 123 | } 124 | } 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /Sources/CKQueryURLRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKQueryURLRequest.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 26/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class CKQueryURLRequest: CKURLRequest { 12 | 13 | var cursor: Data? 14 | 15 | var limit: Int 16 | 17 | let query: CKQuery 18 | 19 | var queryResponses: [[String: Any]] = [] 20 | 21 | var requestedFields: [String]? 22 | 23 | var resultsCursor: Data? 24 | 25 | var zoneID: CKRecordZoneID? 26 | 27 | init(query: CKQuery, cursor: Data?, limit: Int, requestedFields: [String]?, zoneID: CKRecordZoneID?) { 28 | 29 | self.query = query 30 | self.cursor = cursor 31 | self.limit = limit 32 | self.requestedFields = requestedFields 33 | self.zoneID = zoneID 34 | 35 | super.init() 36 | 37 | self.path = "query" 38 | self.operationType = CKOperationRequestType.records 39 | 40 | // Setup Body Properties 41 | var parameters: [String: Any] = [:] 42 | 43 | let isZoneWide = false 44 | if let zoneID = zoneID , zoneID.zoneName != CKRecordZoneDefaultName { 45 | // Add ZoneID Dictionary to parameters 46 | parameters["zoneID"] = zoneID.dictionary.bridge() 47 | } 48 | 49 | parameters["zoneWide"] = NSNumber(value: isZoneWide) 50 | parameters["query"] = query.dictionary.bridge() as NSDictionary 51 | 52 | if let cursor = cursor { 53 | 54 | parameters["continuationMarker"] = cursor.base64EncodedString(options: []).bridge() 55 | } 56 | accountInfoProvider = CloudKit.shared.defaultAccount 57 | requestProperties = parameters 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/CKRecordID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKRecordID.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 6/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKRecordID: NSObject { 12 | 13 | public convenience init(recordName: String) { 14 | let defaultZone = CKRecordZoneID(zoneName: "_defaultZone", ownerName: "_defaultOwner") 15 | self.init(recordName: recordName, zoneID: defaultZone) 16 | } 17 | 18 | public init(recordName: String, zoneID: CKRecordZoneID) { 19 | 20 | self.recordName = recordName 21 | self.zoneID = zoneID 22 | 23 | } 24 | 25 | public let recordName: String 26 | 27 | public var zoneID: CKRecordZoneID 28 | 29 | } 30 | 31 | extension CKRecordID { 32 | 33 | convenience init?(recordDictionary: [String: Any]) { 34 | 35 | guard let recordName = recordDictionary[CKRecordDictionary.recordName] as? String, 36 | let zoneIDDictionary = recordDictionary[CKRecordDictionary.zoneID] as? [String: Any] 37 | else { 38 | return nil 39 | } 40 | 41 | // Parse ZoneID Dictionary into CKRecordZoneID 42 | let zoneID = CKRecordZoneID(dictionary: zoneIDDictionary)! 43 | self.init(recordName: recordName, zoneID: zoneID) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/CKRecordZone.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKRecordZone.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 15/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public struct CKRecordZoneCapabilities : OptionSet { 12 | 13 | public let rawValue: UInt 14 | 15 | public init(rawValue: UInt) { 16 | self.rawValue = rawValue 17 | } 18 | 19 | 20 | /* This zone supports CKFetchRecordChangesOperation */ 21 | public static var fetchChanges: CKRecordZoneCapabilities = CKRecordZoneCapabilities(rawValue: 1) 22 | 23 | /* Batched changes to this zone happen atomically */ 24 | public static var atomic: CKRecordZoneCapabilities = CKRecordZoneCapabilities(rawValue: 2) 25 | 26 | /* Records in this zone can be shared */ 27 | public static var sharing: CKRecordZoneCapabilities = CKRecordZoneCapabilities(rawValue: 4) 28 | } 29 | 30 | /* The default zone has no capabilities */ 31 | public let CKRecordZoneDefaultName: String = "_defaultZone" 32 | 33 | public let CKRecordZoneIDDefaultOwnerName = "__defaultOwner__" 34 | 35 | public class CKRecordZone : NSObject { 36 | 37 | 38 | public class func `default`() -> CKRecordZone { 39 | return CKRecordZone(zoneName: CKRecordZoneDefaultName) 40 | } 41 | 42 | public convenience init(zoneName: String) { 43 | let zoneID = CKRecordZoneID(zoneName: zoneName, ownerName: CKRecordZoneIDDefaultOwnerName) 44 | self.init(zoneID: zoneID) 45 | } 46 | 47 | public init(zoneID: CKRecordZoneID) { 48 | self.zoneID = zoneID 49 | super.init() 50 | } 51 | 52 | 53 | public let zoneID: CKRecordZoneID 54 | 55 | /* Capabilities are not set until a record zone is saved */ 56 | public var capabilities: CKRecordZoneCapabilities = CKRecordZoneCapabilities(rawValue: 0) 57 | } 58 | 59 | extension CKRecordZone { 60 | convenience init?(dictionary: [String: Any]) { 61 | 62 | guard let zoneIDDictionary = dictionary["zoneID"] as? [String: Any], let zoneID = CKRecordZoneID(dictionary: zoneIDDictionary) else { 63 | return nil 64 | } 65 | 66 | self.init(zoneID: zoneID) 67 | 68 | if let isAtomic = dictionary["atomic"] as? Bool , isAtomic { 69 | capabilities = CKRecordZoneCapabilities.atomic 70 | } 71 | } 72 | 73 | var dictionary: [String: Any] { 74 | return ["zoneID": zoneID.dictionary.bridge() as Any] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/CKRecordZoneNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKRecordZoneNotification.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 20/1/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKRecordZoneNotification : CKNotification { 12 | 13 | public var recordZoneID: CKRecordZoneID? 14 | 15 | public var databaseScope: CKDatabaseScope = .public 16 | 17 | override init(fromRemoteNotificationDictionary notificationDictionary: [AnyHashable : Any]) { 18 | super.init(fromRemoteNotificationDictionary: notificationDictionary) 19 | 20 | notificationType = CKNotificationType.recordZone 21 | 22 | if let cloudDictionary = notificationDictionary["ck"] as? [String: Any] { 23 | 24 | if let zoneDictionary = cloudDictionary["fet"] as? [String: Any] { 25 | 26 | // Set RecordZoneID 27 | if let zoneName = zoneDictionary["zid"] as? String { 28 | let zoneID = CKRecordZoneID(zoneName: zoneName, ownerName: "__defaultOwner__") 29 | recordZoneID = zoneID 30 | } 31 | 32 | // Set Database Scope 33 | if let dbs = zoneDictionary["dbs"] as? NSNumber, let scope = CKDatabaseScope(rawValue: dbs.intValue) { 34 | databaseScope = scope 35 | } 36 | 37 | // Set Subscription ID 38 | if let sid = zoneDictionary["sid"] as? String { 39 | subscriptionID = sid 40 | } 41 | 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/CKReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKReference.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 27/08/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum CKReferenceAction : UInt { 12 | case none 13 | case deleteSelf 14 | 15 | public init?(value: String) { 16 | switch value { 17 | case "NONE", "VALIDATE": 18 | self = .none 19 | case "DELETE_SELF": 20 | self = .deleteSelf 21 | default: 22 | return nil 23 | } 24 | } 25 | } 26 | 27 | extension CKReferenceAction: CustomStringConvertible { 28 | public var description: String { 29 | switch self { 30 | case .none: 31 | return "NONE" 32 | case .deleteSelf: 33 | return "DELETE_SELF" 34 | } 35 | } 36 | } 37 | 38 | open class CKReference: NSObject { 39 | 40 | 41 | /* It is acceptable to relate two records that have not yet been uploaded to the server, but those records must be uploaded to the server in the same operation. 42 | If a record references a record that does not exist on the server and is not in the current save operation it will result in an error. */ 43 | public init(recordID: CKRecordID, action: CKReferenceAction) { 44 | self.recordID = recordID 45 | self.referenceAction = action 46 | } 47 | 48 | public convenience init(record: CKRecord, action: CKReferenceAction) { 49 | self.init(recordID: record.recordID, action: action) 50 | } 51 | 52 | public let referenceAction: CKReferenceAction 53 | 54 | public let recordID: CKRecordID 55 | } 56 | 57 | extension CKReference { 58 | 59 | convenience init?(dictionary: [String: Any]) { 60 | 61 | guard 62 | let recordName = dictionary["recordName"] as? String, 63 | let actionValue = dictionary["action"] as? String, 64 | let action = CKReferenceAction(value: actionValue) 65 | else { 66 | return nil 67 | } 68 | 69 | let recordID: CKRecordID 70 | if let zoneDictionary = dictionary["zoneID"] as? [String: Any], 71 | let zoneID = CKRecordZoneID(dictionary: zoneDictionary) { 72 | recordID = CKRecordID(recordName: recordName, zoneID: zoneID) 73 | } else { 74 | recordID = CKRecordID(recordName: recordName) 75 | } 76 | 77 | self.init(recordID: recordID, action: action) 78 | } 79 | 80 | var dictionary: [String: Any] { 81 | let dict: [String: Any] = ["recordName": recordID.recordName.bridge(), "zoneID": recordID.zoneID.dictionary.bridge(), "action": referenceAction.description.bridge()] 82 | 83 | return dict 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /Sources/CKRegisterTokenOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKRegisterTokenOperation.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Ben Johnson on 15/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class CKRegisterTokenOperation : CKOperation { 12 | 13 | let apnsEnvironment: CKEnvironment 14 | 15 | let apnsToken: Data 16 | 17 | var tokenInfo: CKPushTokenInfo? 18 | 19 | public var registerTokenCompletionBlock: ((CKPushTokenInfo?, Error?) -> Void)? 20 | 21 | init(apnsEnvironment:CKEnvironment, apnsToken: Data) { 22 | 23 | self.apnsEnvironment = apnsEnvironment 24 | 25 | self.apnsToken = apnsToken 26 | 27 | super.init() 28 | 29 | } 30 | 31 | override func finishOnCallbackQueue(error: Error?) { 32 | registerTokenCompletionBlock?(tokenInfo, error) 33 | 34 | super.finishOnCallbackQueue(error: error) 35 | } 36 | 37 | override func performCKOperation() { 38 | 39 | let request = CKTokenRegistrationURLRequest(token: apnsToken, apnsEnvironment: "\(apnsEnvironment)") 40 | request.completionBlock = { [weak self] (result) in 41 | guard let strongSelf = self, !strongSelf.isCancelled else { 42 | return 43 | } 44 | switch result { 45 | case .success(let dictionary): 46 | strongSelf.tokenInfo = CKPushTokenInfo(dictionaryRepresentation: dictionary) 47 | CloudKit.debugPrint(dictionary) 48 | strongSelf.finish(error: nil) 49 | case .error(let error): 50 | strongSelf.finish(error: error.error) 51 | } 52 | } 53 | 54 | request.performRequest() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/CKRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKRequest.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 19/1/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | struct CKRequestOptions { 12 | 13 | var serverType: String? 14 | 15 | init(serverType: String) { 16 | self.serverType = serverType 17 | } 18 | 19 | init() {} 20 | 21 | } 22 | 23 | class CKRequest { 24 | 25 | static func options() -> CKRequestOptions { 26 | return CKRequestOptions() 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Sources/CKServerRequestAuth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKServerRequestAuth.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 11/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CryptoSwift 11 | 12 | struct CKServerRequestAuth { 13 | 14 | static let ISO8601DateFormatter: DateFormatter = { 15 | let dateFormatter = DateFormatter() 16 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 17 | dateFormatter.timeZone = TimeZone(abbreviation: "GMT") 18 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 19 | 20 | return dateFormatter 21 | }() 22 | 23 | static let CKRequestKeyIDHeaderKey = "X-Apple-CloudKit-Request-KeyID" 24 | 25 | static let CKRequestDateHeaderKey = "X-Apple-CloudKit-Request-ISO8601Date" 26 | 27 | static let CKRequestSignatureHeaderKey = "X-Apple-CloudKit-Request-SignatureV1" 28 | 29 | 30 | let requestDate: String 31 | 32 | let signature: String 33 | 34 | init?(requestBody: NSData, urlPath: String, privateKeyPath: String) { 35 | 36 | self.requestDate = CKServerRequestAuth.ISO8601DateFormatter.string(from: Date()) 37 | 38 | if let signature = CKServerRequestAuth.signature(requestDate: requestDate,requestBody: requestBody, urlSubpath: urlPath, privateKeyPath: privateKeyPath) { 39 | 40 | self.signature = signature 41 | 42 | } else { 43 | return nil 44 | } 45 | } 46 | 47 | static func sign(data: NSData, privateKeyPath: String) -> NSData? { 48 | do { 49 | 50 | let ecsda = try! MessageDigest("sha256WithRSAEncryption") 51 | let digestContext = try! MessageDigestContext(ecsda) 52 | 53 | try digestContext.update(data) 54 | 55 | return try digestContext.sign(privateKeyURL: privateKeyPath) 56 | 57 | 58 | } catch { 59 | if let messageError = error as? MessageDigestContextError { 60 | switch messageError { 61 | case .privateKeyNotFound: 62 | fatalError("Private Key at \(privateKeyPath) not found") 63 | default: 64 | fatalError("Error occured while signing \(error)") 65 | } 66 | } else { 67 | CloudKit.debugPrint(error) 68 | return nil 69 | } 70 | } 71 | } 72 | 73 | static func rawPayload(withRequestDate requestDate: String, requestBody: NSData, urlSubpath: String) -> String { 74 | 75 | let bodyHash = requestBody.bridge().sha256() 76 | let hashedBody = bodyHash.base64EncodedString(options: []) 77 | return "\(requestDate):\(hashedBody):\(urlSubpath)" 78 | } 79 | 80 | static func signature(requestDate: String, requestBody: NSData, urlSubpath: String, privateKeyPath: String) -> String? { 81 | 82 | let rawPayloadString = rawPayload(withRequestDate: requestDate, requestBody: requestBody, urlSubpath: urlSubpath) 83 | 84 | let requestData = rawPayloadString.data(using: String.Encoding.utf8)! 85 | 86 | 87 | let signedData = sign(data: NSData(data: requestData), privateKeyPath: privateKeyPath) 88 | 89 | return signedData?.base64EncodedString(options: []) 90 | } 91 | 92 | static func authenicateServer(forRequest request: URLRequest, withServerToServerKeyAuth auth: CKServerToServerKeyAuth) -> URLRequest? { 93 | return authenticateServer(forRequest: request, serverKeyID: auth.keyID, privateKeyPath: auth.privateKeyFile) 94 | } 95 | 96 | static func authenticateServer(forRequest request: URLRequest, serverKeyID: String, privateKeyPath: String) -> URLRequest? { 97 | var request = request 98 | guard let requestBody = request.httpBody, let path = request.url?.path, let auth = CKServerRequestAuth(requestBody: NSData(data: requestBody), urlPath: path, privateKeyPath: privateKeyPath) else { 99 | return nil 100 | } 101 | 102 | request.setValue(serverKeyID, forHTTPHeaderField: CKRequestKeyIDHeaderKey) 103 | request.setValue(auth.requestDate, forHTTPHeaderField: CKRequestDateHeaderKey) 104 | request.setValue(auth.signature, forHTTPHeaderField: CKRequestSignatureHeaderKey) 105 | 106 | 107 | return request 108 | } 109 | } 110 | 111 | -------------------------------------------------------------------------------- /Sources/CKServerType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKServerType.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 19/1/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | enum CKServerType: String { 12 | 13 | case database = "CKDatabaseService" 14 | 15 | case share = "CKShareService" 16 | 17 | case device = "CKDeviceService" 18 | 19 | case codeService = "CKCodeService" 20 | 21 | var urlComponent: String { 22 | switch self { 23 | case .database: 24 | return "database" 25 | case .share: 26 | return "database" 27 | case .device: 28 | return "device" 29 | default: 30 | fatalError() 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/CKShare.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKShare.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 16/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public let CKShareRecordType = "cloudkit.share" 12 | 13 | public class CKShare : CKRecord { 14 | 15 | let shortGUID: String? 16 | 17 | /* When saving a newly created CKShare, you must save the share and its rootRecord in the same CKModifyRecordsOperation batch. */ 18 | public convenience init(rootRecord: CKRecord) { 19 | self.init(rootRecord: rootRecord, share: CKRecordID(recordName: "Share-\(rootRecord.recordID)")) 20 | } 21 | 22 | public init(rootRecord: CKRecord, share shareID: CKRecordID) { 23 | shortGUID = nil 24 | 25 | super.init(recordType: CKShareRecordType, recordID: shareID) 26 | 27 | } 28 | 29 | public init?(dictionary: [String: Any]) { 30 | 31 | shortGUID = dictionary["shortGUID"] as? String 32 | 33 | super.init(recordDictionary: dictionary) 34 | 35 | if let rawPublicPermission = dictionary["publicPermission"] as? String, let permission = CKShareParticipantPermission(string: rawPublicPermission) { 36 | publicPermission = permission 37 | 38 | 39 | } 40 | 41 | if let rawPerticipants = dictionary["participants"] as? [[String: Any]] { 42 | for rawParticipant in rawPerticipants { 43 | if let participant = CKShareParticipant(dictionary: rawParticipant) { 44 | participants.append(participant) 45 | } 46 | } 47 | } 48 | 49 | } 50 | 51 | 52 | 53 | /* 54 | Shares with publicPermission more permissive than CKShareParticipantPermissionNone can be joined by any user with access to the share's shareURL. 55 | This property defines what permission those users will have. 56 | By default, public permission is CKShareParticipantPermissionNone. 57 | Changing the public permission to CKShareParticipantPermissionReadOnly or CKShareParticipantPermissionReadWrite will result in all pending participants being removed. Already-accepted participants will remain on the share. 58 | Changing the public permission to CKShareParticipantPermissionNone will result in all participants being removed from the share. You may subsequently choose to call addParticipant: before saving the share, those participants will be added to the share. */ 59 | public var publicPermission: CKShareParticipantPermission = .none 60 | 61 | 62 | /* A URL that can be used to invite participants to this share. Only available after share record has been saved to the server. This url is stable, and is tied to the rootRecord. That is, if you share a rootRecord, delete the share, and re-share the same rootRecord via a newly created share, that newly created share's url will be identical to the prior share's url */ 63 | public var url: URL? { 64 | if let shortGUID = shortGUID { 65 | 66 | let CKShareBaseURL = URL(string: "https://www.icloud.com/share/")! 67 | return CKShareBaseURL.appendingPathComponent("\(shortGUID)#\(recordID.zoneID.zoneName)") 68 | 69 | } else { 70 | return nil 71 | } 72 | 73 | } 74 | 75 | 76 | /* The participants array will contain all participants on the share that the current user has permissions to see. 77 | At the minimum that will include the owner and the current user. */ 78 | public var participants: [CKShareParticipant] = [] 79 | 80 | 81 | /* Convenience methods for fetching special users from the participant array */ 82 | public var owner: CKShareParticipant { 83 | return participants.first { (participant) -> Bool in 84 | return participant.type == .owner 85 | }! 86 | } 87 | 88 | public var currentUserParticipant: CKShareParticipant? { 89 | return nil 90 | } 91 | 92 | 93 | /* 94 | If a participant with a matching userIdentity already exists, then that existing participant's properties will be updated; no new participant will be added. 95 | In order to modify the list of participants, a share must have publicPermission set to CKShareParticipantPermissionNone. That is, you cannot mix-and-match private users and public users in the same share. 96 | Only certain participant types may be added via this API, see the comments around CKShareParticipantType 97 | */ 98 | public func addParticipant(_ participant: CKShareParticipant) { 99 | 100 | let existing = participants.first { (current) -> Bool in 101 | return current.userIdentity == participant.userIdentity 102 | } 103 | 104 | if let existing = existing { 105 | // Update info 106 | existing.acceptanceStatus = participant.acceptanceStatus 107 | existing.permission = participant.permission 108 | existing.type = participant.type 109 | existing.userIdentity = participant.userIdentity 110 | } else if publicPermission == .none { 111 | participants.append(participant) 112 | } 113 | } 114 | 115 | public func removeParticipant(_ participant: CKShareParticipant) { 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/CKShareMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKShareMetadata.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 16/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension Bool { 12 | var number: NSNumber { 13 | if self == true { 14 | return NSNumber(value: 1) 15 | } else { 16 | return NSNumber(value: 0) 17 | 18 | } 19 | } 20 | } 21 | 22 | public struct CKShortGUID { 23 | 24 | public let value: String 25 | 26 | public let shouldFetchRootRecord: Bool 27 | 28 | public let rootRecordDesiredKeys: [String]? 29 | 30 | public var dictionary: [String: Any] { 31 | let dict:[String: Any] = ["value": value.bridge(), 32 | "shouldFetchRootRecord": shouldFetchRootRecord.number] 33 | return dict 34 | } 35 | 36 | public init(value: String, shouldFetchRootRecord: Bool, rootRecordDesiredKeys: [String]? = nil) { 37 | self.value = value 38 | self.shouldFetchRootRecord = shouldFetchRootRecord 39 | self.rootRecordDesiredKeys = rootRecordDesiredKeys 40 | } 41 | 42 | } 43 | 44 | open class CKShareMetadata { 45 | 46 | init() { 47 | 48 | containerIdentifier = "" 49 | 50 | } 51 | 52 | open var containerIdentifier: String 53 | 54 | open var share: CKShare? 55 | 56 | open var rootRecordID: CKRecordID? 57 | 58 | /* These properties reflect the participant properties of the user invoking CKFetchShareMetadataOperation */ 59 | open var participantType: CKShareParticipantType = .unknown 60 | 61 | open var participantStatus: CKShareParticipantAcceptanceStatus = .unknown 62 | 63 | open var participantPermission: CKShareParticipantPermission = CKShareParticipantPermission.unknown 64 | 65 | 66 | open var ownerIdentity: CKUserIdentity? 67 | 68 | 69 | /* This is only present if the share metadata was returned from a CKFetchShareMetadataOperation with shouldFetchRootRecord set to YES */ 70 | open var rootRecord: CKRecord? 71 | 72 | init?(dictionary:[String: Any]) { 73 | /* 74 | if let dictionary = CKFetchErrorDictionary(dictionary: dictionary) { 75 | return nil 76 | } 77 | */ 78 | 79 | containerIdentifier = dictionary["containerIdentifier"] as! String 80 | 81 | let rootRecordName = dictionary["rootRecordName"] as! String 82 | 83 | let zoneID = CKRecordZoneID(dictionary: dictionary["zoneID"] as! [String:Any])! 84 | 85 | rootRecordID = CKRecordID(recordName: rootRecordName, zoneID: zoneID) 86 | 87 | // Set participant type 88 | let rawParticipantType = dictionary["participantType"] as! String 89 | participantType = CKShareParticipantType(string: rawParticipantType)! 90 | 91 | // Set participant permission 92 | if let rawParticipantPermission = dictionary["participantPermission"] as? String, let permission = CKShareParticipantPermission(string: rawParticipantPermission) { 93 | participantPermission = permission 94 | } 95 | 96 | 97 | // Set status 98 | if let rawParticipantStatus = dictionary["participantStatus"] as? String, let status = CKShareParticipantAcceptanceStatus(string: rawParticipantStatus) { 99 | participantStatus = status 100 | } 101 | 102 | 103 | if let ownerIdentityDictionary = dictionary["ownerIdentity"] as? [String: Any] { 104 | ownerIdentity = CKUserIdentity(dictionary: ownerIdentityDictionary) 105 | } 106 | 107 | // Set root record if available 108 | if let rootRecordDictionary = dictionary["rootRecord"] as? [String: Any] { 109 | rootRecord = CKRecord(recordDictionary: rootRecordDictionary) 110 | } 111 | 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/CKShareParticipant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKShareParticipant.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 16/10/16. 6 | // 7 | // 8 | 9 | public enum CKShareParticipantAcceptanceStatus : Int { 10 | 11 | 12 | case unknown 13 | 14 | case pending 15 | 16 | case accepted 17 | 18 | case removed 19 | 20 | init?(string: String) { 21 | switch string { 22 | case "UNKNOWN": 23 | self = .unknown 24 | case "PENDING": 25 | self = .pending 26 | case "ACCEPTED": 27 | self = .accepted 28 | case "REMOVED": 29 | self = .removed 30 | default: 31 | return nil 32 | } 33 | } 34 | } 35 | 36 | public enum CKShareParticipantPermission : Int { 37 | 38 | 39 | case unknown 40 | 41 | case none 42 | 43 | case readOnly 44 | 45 | case readWrite 46 | 47 | init?(string: String) { 48 | switch string { 49 | case "READ_WRITE": 50 | self = .readWrite 51 | case "NONE": 52 | self = .none 53 | case "READ_ONLY": 54 | self = .readOnly 55 | case "UNKNOWN": 56 | self = .unknown 57 | default: 58 | return nil 59 | } 60 | } 61 | } 62 | 63 | public enum CKShareParticipantType : Int { 64 | 65 | 66 | case unknown 67 | 68 | case owner 69 | 70 | case privateUser 71 | 72 | case publicUser 73 | 74 | init?(string: String) { 75 | switch string { 76 | case "OWNER": 77 | self = .owner 78 | case "USER": 79 | self = .privateUser 80 | case "PUBLIC_USER": 81 | self = .publicUser 82 | case "UNKNOWN": 83 | self = .unknown 84 | default: 85 | fatalError("Unknown type \(string)") 86 | } 87 | } 88 | } 89 | 90 | open class CKShareParticipant { 91 | 92 | open var userIdentity: CKUserIdentity 93 | 94 | 95 | /* The default participant type is CKShareParticipantTypePrivateUser. */ 96 | open var type: CKShareParticipantType = .privateUser 97 | 98 | open var acceptanceStatus: CKShareParticipantAcceptanceStatus = .unknown 99 | 100 | /* The default permission for a new participant is CKShareParticipantPermissionReadOnly. */ 101 | open var permission: CKShareParticipantPermission = .readOnly 102 | 103 | init(userIdentity: CKUserIdentity) { 104 | self.userIdentity = userIdentity 105 | } 106 | 107 | convenience init?(dictionary: [String: Any]) { 108 | 109 | guard let userIdentityDictionary = dictionary["userIdentity"] as? [String: Any], let identity = CKUserIdentity(dictionary: userIdentityDictionary) else { 110 | return nil 111 | } 112 | 113 | self.init(userIdentity: identity) 114 | 115 | if let rawType = dictionary["type"] as? String, let userType = CKShareParticipantType(string: rawType) { 116 | type = userType 117 | } 118 | 119 | if let rawAcceptanceStatus = dictionary["acceptanceStatus"] as? String, let status = CKShareParticipantAcceptanceStatus(string: rawAcceptanceStatus) { 120 | acceptanceStatus = status 121 | } 122 | 123 | if let rawPermission = dictionary["permission"] as? String, let permission = CKShareParticipantPermission(string: rawPermission) { 124 | self.permission = permission 125 | } 126 | 127 | 128 | 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/CKTokenCreateURLRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKTokenCreateURLRequest.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 19/1/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class CKTokenCreateURLRequest: CKURLRequest { 12 | 13 | let apnsEnvironment: CKEnvironment 14 | 15 | override var serverType: CKServerType { 16 | return .device 17 | } 18 | 19 | init(apnsEnvironment: CKEnvironment) { 20 | self.apnsEnvironment = apnsEnvironment 21 | 22 | super.init() 23 | self.operationType = .tokens 24 | path = "create" 25 | // self.serverType = .device 26 | } 27 | } 28 | 29 | public struct CKPushTokenInfo { 30 | 31 | public let apnsToken: Data 32 | 33 | public let apnsEnvironment: CKEnvironment 34 | 35 | public let webcourierURL: URL 36 | 37 | init?(dictionaryRepresentation dictionary: [String: Any]) { 38 | guard 39 | let apnsEnvironmentString = dictionary["apnsEnvironment"] as? String, 40 | let apnsToken = dictionary["apnsToken"] as? String, 41 | let webcourierURLString = dictionary["webcourierURL"] as? String, 42 | let environment = CKEnvironment(rawValue: apnsEnvironmentString), 43 | let url = URL(string: webcourierURLString), 44 | let data = Data(base64Encoded: apnsToken) else { 45 | return nil 46 | } 47 | 48 | self.apnsToken = data 49 | self.apnsEnvironment = environment 50 | self.webcourierURL = url 51 | } 52 | 53 | } 54 | 55 | class CKTokenCreateOperation: CKOperation { 56 | 57 | let apnsEnvironment: CKEnvironment 58 | 59 | init(apnsEnvironment: CKEnvironment) { 60 | self.apnsEnvironment = apnsEnvironment 61 | } 62 | 63 | var createTokenCompletionBlock: ((CKPushTokenInfo?, Error?) -> ())? 64 | 65 | var info : CKPushTokenInfo? 66 | 67 | var bodyDictionaryRepresentation: [String: Any] { 68 | return ["apnsEnvironment": "\(apnsEnvironment)"] 69 | } 70 | 71 | override func finishOnCallbackQueue(error: Error?) { 72 | createTokenCompletionBlock?(info, error) 73 | 74 | super.finishOnCallbackQueue(error: error) 75 | } 76 | 77 | override func performCKOperation() { 78 | 79 | let request = CKTokenCreateURLRequest(apnsEnvironment: apnsEnvironment) 80 | request.accountInfoProvider = CloudKit.shared.defaultAccount 81 | request.requestProperties = bodyDictionaryRepresentation 82 | 83 | request.completionBlock = { result in 84 | if(self.isCancelled){ 85 | return 86 | } 87 | switch result { 88 | case .success(let dictionary): 89 | self.info = CKPushTokenInfo(dictionaryRepresentation: dictionary)! 90 | self.finish(error:nil) 91 | 92 | case .error(let error): 93 | self.finish(error:error.error) 94 | } 95 | } 96 | request.performRequest() 97 | 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/CKTokenRegistrationURLRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKTokenRegistrationURLRequest.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 19/1/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | class PBCodable { 12 | func dictionaryRepresentation() -> [String: Any] { 13 | return [:] 14 | } 15 | } 16 | 17 | class CKTokenRegistrationBody: PBCodable { 18 | 19 | let apnsEnv: String 20 | 21 | let token: Data 22 | 23 | init(apnsEnv: String, token: Data) { 24 | self.apnsEnv = apnsEnv 25 | self.token = token 26 | } 27 | 28 | override func dictionaryRepresentation() -> [String : Any] { 29 | return ["apnsEnvironment": apnsEnv, "apnsToken": token.base64EncodedString()] 30 | } 31 | } 32 | 33 | class CKTokenRegistrationURLRequest: CKURLRequest { 34 | 35 | var tokenRegistrationBody: CKTokenRegistrationBody? 36 | 37 | var hasTokenRegistrationBody: Bool { 38 | return tokenRegistrationBody != nil 39 | } 40 | 41 | let apsEnvironmentString: String 42 | 43 | override var serverType: CKServerType { 44 | return .device 45 | } 46 | 47 | let token: Data 48 | 49 | init(token: Data, apnsEnvironment: String) { 50 | 51 | self.token = token 52 | self.apsEnvironmentString = apnsEnvironment 53 | 54 | super.init() 55 | 56 | self.operationType = .tokens 57 | path = "register" 58 | 59 | let body = CKTokenRegistrationBody(apnsEnv: apnsEnvironment, token: token) 60 | requestProperties = body.dictionaryRepresentation() 61 | } 62 | 63 | override var requiresTokenRegistration: Bool { 64 | return false 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Sources/CKUserIdentity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKUserIdentity.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 14/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKUserIdentity : NSObject { 12 | 13 | 14 | // This is the lookupInfo you passed in to CKDiscoverUserIdentitiesOperation or CKFetchShareParticipantsOperation 15 | public let lookupInfo: CKUserIdentityLookupInfo? 16 | 17 | public let nameComponents: CKPersonNameComponentsType? 18 | 19 | public let userRecordID: CKRecordID? 20 | 21 | public let hasiCloudAccount: Bool 22 | 23 | var firstName: String? 24 | 25 | var lastName: String? 26 | 27 | public init(userRecordID: CKRecordID) { 28 | 29 | self.userRecordID = userRecordID 30 | 31 | self.lookupInfo = nil 32 | 33 | hasiCloudAccount = false 34 | 35 | nameComponents = nil 36 | 37 | super.init() 38 | } 39 | 40 | 41 | init?(dictionary: [String: Any]) { 42 | 43 | if let lookUpInfoDictionary = dictionary["lookupInfo"] as? [String: Any],let lookupInfo = CKUserIdentityLookupInfo(dictionary: lookUpInfoDictionary) { 44 | self.lookupInfo = lookupInfo 45 | } else { 46 | self.lookupInfo = nil 47 | } 48 | 49 | if let userRecordName = dictionary["userRecordName"] as? String { 50 | self.userRecordID = CKRecordID(recordName: userRecordName) 51 | } else { 52 | self.userRecordID = nil 53 | } 54 | 55 | if let nameComponentsDictionary = dictionary["nameComponents"] as? [String: Any] { 56 | if #available(OSX 10.11, *) { 57 | self.nameComponents = CKPersonNameComponents(dictionary: nameComponentsDictionary) 58 | } else { 59 | // Fallback on earlier versions 60 | self.nameComponents = CKPersonNameComponents(dictionary: nameComponentsDictionary) 61 | } 62 | 63 | // self.firstName = nameComponents?.givenName 64 | // self.lastName = nameComponents?.familyName 65 | } else { 66 | self.nameComponents = nil 67 | } 68 | 69 | self.hasiCloudAccount = false 70 | 71 | super.init() 72 | 73 | } 74 | 75 | } 76 | /* 77 | extension PersonNameComponents { 78 | init?(dictionary: [String: Any]) { 79 | self.init() 80 | 81 | namePrefix = dictionary["namePrefix"] as? String 82 | givenName = dictionary["givenName"] as? String 83 | familyName = dictionary["familyName"] as? String 84 | nickname = dictionary["nickname"] as? String 85 | nameSuffix = dictionary["nameSuffix"] as? String 86 | middleName = dictionary["middleName"] as? String 87 | // phoneticRepresentation 88 | } 89 | } 90 | */ 91 | -------------------------------------------------------------------------------- /Sources/CKUserIdentityLookupInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKUserIdentityLookupInfo.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 14/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class CKUserIdentityLookupInfo : NSObject { 12 | 13 | public init(emailAddress: String) { 14 | self.emailAddress = emailAddress 15 | self.phoneNumber = nil 16 | self.userRecordID = nil 17 | } 18 | 19 | public init(phoneNumber: String) { 20 | self.emailAddress = nil 21 | self.phoneNumber = phoneNumber 22 | self.userRecordID = nil 23 | } 24 | 25 | public init(userRecordID: CKRecordID) { 26 | self.emailAddress = nil 27 | self.phoneNumber = nil 28 | self.userRecordID = userRecordID 29 | } 30 | 31 | public init(emailAddress: String, phoneNumber: String, userRecordID: CKRecordID) { 32 | self.emailAddress = emailAddress 33 | self.phoneNumber = phoneNumber 34 | self.userRecordID = userRecordID 35 | } 36 | 37 | public class func lookupInfos(withEmails emails: [String]) -> [CKUserIdentityLookupInfo] { 38 | return emails.map({ (email) -> CKUserIdentityLookupInfo in 39 | return CKUserIdentityLookupInfo(emailAddress: email) 40 | }) 41 | } 42 | 43 | public class func lookupInfos(withPhoneNumbers phoneNumbers: [String]) -> [CKUserIdentityLookupInfo] { 44 | return phoneNumbers.map({ (phoneNumber) -> CKUserIdentityLookupInfo in 45 | return CKUserIdentityLookupInfo(phoneNumber: phoneNumber) 46 | }) 47 | } 48 | 49 | public class func lookupInfos(with recordIDs: [CKRecordID]) -> [CKUserIdentityLookupInfo] { 50 | return recordIDs.map({ (recordID) -> CKUserIdentityLookupInfo in 51 | return CKUserIdentityLookupInfo(userRecordID: recordID) 52 | }) 53 | } 54 | 55 | public let emailAddress: String? 56 | 57 | public let phoneNumber: String? 58 | 59 | public let userRecordID: CKRecordID? 60 | } 61 | 62 | extension CKUserIdentityLookupInfo: CKCodable { 63 | convenience init?(dictionary: [String: Any]) { 64 | 65 | guard let emailAddress = dictionary["emailAddress"] as? String, 66 | let phoneNumber = dictionary["phoneNumber"] as? String, 67 | let userRecordName = dictionary["userRecordName"] as? String else { 68 | return nil 69 | } 70 | 71 | self.init(emailAddress: emailAddress, phoneNumber: phoneNumber, userRecordID: CKRecordID(recordName: userRecordName)) 72 | } 73 | 74 | var dictionary: [String: Any] { 75 | 76 | var lookupInfo: [String: Any] = [:] 77 | lookupInfo["emailAddress"] = emailAddress?.bridge() 78 | lookupInfo["phoneNumber"] = phoneNumber?.bridge() 79 | lookupInfo["userRecordName"] = userRecordID?.recordName.bridge() 80 | 81 | return lookupInfo 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/CLLocation+OpenCloudKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocation+OpenCloudKit.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 20/07/2016. 6 | // 7 | // 8 | 9 | import CoreLocation 10 | 11 | extension CLLocationCoordinate2D: CKLocationCoordinate2DType {} 12 | 13 | extension CLLocation: CKLocationType { 14 | public var coordinateType: CKLocationCoordinate2DType { 15 | return coordinate 16 | } 17 | } 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/EVPDigestSign.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EVPDigestSign.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 10/07/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CLibreSSL 11 | 12 | public enum MessageDigestError: Error { 13 | case unknownDigest 14 | } 15 | 16 | public final class MessageDigest { 17 | static var addedAllDigests = false 18 | let messageDigest: UnsafeMutablePointer 19 | 20 | 21 | public init(_ messageDigest: String) throws { 22 | if !MessageDigest.addedAllDigests { 23 | OpenSSL_add_all_digests() 24 | MessageDigest.addedAllDigests = true 25 | } 26 | 27 | guard let messageDigest = messageDigest.withCString({EVP_get_digestbyname($0)}) else { 28 | throw MessageDigestError.unknownDigest 29 | } 30 | 31 | self.messageDigest = UnsafeMutablePointer(mutating: messageDigest) 32 | } 33 | } 34 | 35 | public enum MessageDigestContextError: Error { 36 | case initializationFailed 37 | case updateFailed 38 | case signFailed 39 | case privateKeyLoadFailed 40 | case privateKeyNotFound 41 | } 42 | 43 | public enum EVPKeyType { 44 | case Public 45 | case Private 46 | } 47 | 48 | public final class EVPKey { 49 | 50 | let pkey: UnsafeMutablePointer! 51 | let type: EVPKeyType 52 | 53 | deinit { 54 | EVP_PKEY_free(pkey) 55 | } 56 | 57 | public init(contentsOfFile path: String, type: EVPKeyType) throws { 58 | // Load Private Key 59 | let filePointer = BIO_new_file(path, "r") 60 | guard let file = filePointer else { 61 | throw MessageDigestContextError.privateKeyNotFound 62 | } 63 | 64 | self.type = type 65 | 66 | switch type { 67 | case .Private: 68 | guard let privateKey = PEM_read_bio_PrivateKey(file, nil, nil, nil) else { 69 | throw MessageDigestContextError.privateKeyLoadFailed 70 | } 71 | pkey = privateKey 72 | 73 | case .Public: 74 | guard let publicKey = PEM_read_bio_PUBKEY(file, nil, nil, nil) else { 75 | throw MessageDigestContextError.privateKeyLoadFailed 76 | } 77 | pkey = publicKey 78 | } 79 | 80 | BIO_free_all(file) 81 | } 82 | } 83 | 84 | public final class MessageVerifyContext { 85 | 86 | let context: UnsafeMutablePointer 87 | 88 | deinit { 89 | EVP_MD_CTX_destroy(context) 90 | } 91 | 92 | public init(_ messageDigest: MessageDigest, withKey key: EVPKey) throws { 93 | 94 | let context: UnsafeMutablePointer! = EVP_MD_CTX_create() 95 | 96 | if EVP_DigestVerifyInit(context, nil, messageDigest.messageDigest, nil, key.pkey) == 0 { 97 | throw MessageDigestContextError.initializationFailed 98 | } 99 | 100 | guard let c = context else { 101 | throw MessageDigestContextError.initializationFailed 102 | } 103 | 104 | self.context = c 105 | } 106 | 107 | // Message 108 | func update(data: NSData) throws { 109 | 110 | if EVP_DigestUpdate(context, data.bytes, data.length) == 0 { 111 | throw MessageDigestContextError.updateFailed 112 | } 113 | 114 | } 115 | 116 | // Signature 117 | func verify(signature: NSData) -> Bool { 118 | 119 | let typedPointer = signature.bytes.bindMemory(to: UInt8.self, capacity: signature.length) 120 | var bytes = Array(UnsafeBufferPointer(start: typedPointer, count: signature.length)) 121 | 122 | return EVP_DigestVerifyFinal(context, &bytes, bytes.count) == 1 123 | } 124 | } 125 | 126 | public final class MessageDigestContext { 127 | let context: UnsafeMutablePointer 128 | 129 | deinit { 130 | EVP_MD_CTX_destroy(context) 131 | } 132 | 133 | 134 | public init(_ messageDigest: MessageDigest) throws { 135 | let context: UnsafeMutablePointer! = EVP_MD_CTX_create() 136 | 137 | if EVP_DigestInit(context, messageDigest.messageDigest) == 0 { 138 | throw MessageDigestContextError.initializationFailed 139 | } 140 | 141 | guard let c = context else { 142 | throw MessageDigestContextError.initializationFailed 143 | } 144 | 145 | self.context = c 146 | } 147 | 148 | public func update(_ data: NSData) throws { 149 | 150 | if EVP_DigestUpdate(context, data.bytes, data.length) == 0 { 151 | throw MessageDigestContextError.updateFailed 152 | } 153 | } 154 | 155 | public func sign(privateKeyURL: String, passPhrase: String? = nil) throws -> NSData { 156 | 157 | 158 | // Load Private Key 159 | let privateKeyFilePointer = BIO_new_file(privateKeyURL, "r") 160 | guard let privateKeyFile = privateKeyFilePointer else { 161 | throw MessageDigestContextError.privateKeyNotFound 162 | } 163 | 164 | guard let privateKey = PEM_read_bio_PrivateKey(privateKeyFile, nil, nil, nil) else { 165 | throw MessageDigestContextError.privateKeyLoadFailed 166 | } 167 | 168 | if ERR_peek_error() != 0 { 169 | throw MessageDigestContextError.signFailed 170 | } 171 | 172 | var length: UInt32 = 8192 173 | var signature = [UInt8](repeating: 0, count: Int(length)) 174 | 175 | if EVP_SignFinal(context, &signature, &length, privateKey) == 0 { 176 | throw MessageDigestContextError.signFailed 177 | } 178 | 179 | EVP_PKEY_free(privateKey) 180 | BIO_free_all(privateKeyFilePointer) 181 | 182 | let signatureBytes = Array(signature.prefix(upTo: Int(length))) 183 | 184 | 185 | return NSData(bytes: signatureBytes, length: signatureBytes.count) 186 | } 187 | } 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /Sources/OpenCloudKit.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Foundation 4 | 5 | public enum CKEnvironment: String { 6 | case development 7 | case production 8 | } 9 | 10 | enum CKOperationType { 11 | case create 12 | case update 13 | case replace 14 | case forceReplace 15 | } 16 | 17 | public class CloudKit { 18 | 19 | public var environment: CKEnvironment = .development 20 | 21 | public var defaultAccount: CKAccount! 22 | 23 | public private(set) var containers: [CKContainerConfig] = [] 24 | 25 | public static let shared = CloudKit() 26 | 27 | // Temporary property to allow for debugging via console 28 | public var verbose: Bool = false 29 | 30 | public weak var delegate: OpenCloudKitDelegate? 31 | 32 | var pushConnections: [CKPushConnection] = [] 33 | 34 | private init() {} 35 | 36 | public func configure(with configuration: CKConfig) { 37 | self.containers = configuration.containers 38 | 39 | // Setup DefaultAccount 40 | let container = self.containers.first! 41 | if let serverAuth = container.serverToServerKeyAuth { 42 | 43 | // Setup Server Account 44 | defaultAccount = CKServerAccount(containerInfo: container.containerInfo, keyID: serverAuth.keyID, privateKeyFile: serverAuth.privateKeyFile) 45 | 46 | } else if let apiTokenAuth = container.apiTokenAuth { 47 | // Setup Anoymous Account 48 | defaultAccount = CKAccount(type: .anoymous, containerInfo: container.containerInfo, cloudKitAuthToken: apiTokenAuth) 49 | } 50 | } 51 | 52 | func containerConfig(forContainer container: CKContainer) -> CKContainerConfig? { 53 | return containers.filter({ (config) -> Bool in 54 | return config.containerIdentifier == container.containerIdentifier 55 | }).first 56 | } 57 | 58 | static func debugPrint(_ items: Any...) { 59 | if shared.verbose { 60 | print(items) 61 | } 62 | } 63 | 64 | func createPushConnection(for url: URL) { 65 | let connection = CKPushConnection(url: url) 66 | connection.callBack = { 67 | (notification) in 68 | 69 | self.delegate?.didRecieveRemoteNotification(notification) 70 | } 71 | 72 | pushConnections.append(connection) 73 | } 74 | 75 | public func registerForRemoteNotifications() { 76 | 77 | // Setup Create Token Operation 78 | let createTokenOperation = CKTokenCreateOperation(apnsEnvironment: environment) 79 | createTokenOperation.createTokenCompletionBlock = { 80 | (info, error) in 81 | 82 | if let info = info { 83 | // Register Token 84 | let registerOperation = CKRegisterTokenOperation(apnsEnvironment: info.apnsEnvironment, apnsToken: info.apnsToken) 85 | registerOperation.registerTokenCompletionBlock = { 86 | (tokenInfo, error) in 87 | 88 | if let error = error { 89 | // Notify delegate of error when registering for notifications 90 | self.delegate?.didFailToRegisterForRemoteNotifications(withError: error) 91 | } else if let info = tokenInfo { 92 | // Notify Delegate 93 | self.delegate?.didRegisterForRemoteNotifications(withToken: info.apnsToken) 94 | 95 | // Start connection with token 96 | self.createPushConnection(for: info.webcourierURL) 97 | 98 | 99 | } 100 | } 101 | registerOperation.start() 102 | 103 | } else if let error = error { 104 | // Notify delegate of error when registering for notifications 105 | self.delegate?.didFailToRegisterForRemoteNotifications(withError: error) 106 | 107 | } 108 | } 109 | 110 | createTokenOperation.start() 111 | } 112 | 113 | 114 | } 115 | 116 | public protocol OpenCloudKitDelegate: class { 117 | 118 | func didRecieveRemoteNotification(_ notification:CKNotification) 119 | 120 | func didFailToRegisterForRemoteNotifications(withError error: Error) 121 | 122 | func didRegisterForRemoteNotifications(withToken token: Data) 123 | 124 | } 125 | 126 | extension CKRecordID { 127 | var isDefaultName: Bool { 128 | return recordName == CKRecordZoneDefaultName 129 | } 130 | } 131 | 132 | 133 | public class CKRecordZoneID: NSObject { 134 | 135 | public init(zoneName: String, ownerName: String) { 136 | self.zoneName = zoneName 137 | self.ownerName = ownerName 138 | super.init() 139 | 140 | } 141 | 142 | public let zoneName: String 143 | 144 | public let ownerName: String 145 | 146 | convenience public required init?(dictionary: [String: Any]) { 147 | guard let zoneName = dictionary["zoneName"] as? String, let ownerName = dictionary["ownerRecordName"] as? String else { 148 | return nil 149 | } 150 | 151 | self.init(zoneName: zoneName, ownerName: ownerName) 152 | } 153 | 154 | } 155 | 156 | 157 | extension CKRecordZoneID: CKCodable { 158 | 159 | 160 | var dictionary: [String: Any] { 161 | 162 | var zoneIDDictionary: [String: Any] = [ 163 | "zoneName": zoneName.bridge() 164 | ] 165 | 166 | if ownerName != CKRecordZoneIDDefaultOwnerName { 167 | zoneIDDictionary["ownerRecordName"] = ownerName.bridge() 168 | } 169 | 170 | return zoneIDDictionary 171 | } 172 | } 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /Sources/SortDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SortDescriptor.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 1/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | #if os(Linux) 12 | public typealias NSSortDescriptor = SortDescriptor 13 | 14 | open class SortDescriptor: NSObject, NSSecureCoding, NSCopying { 15 | 16 | open var key: String? 17 | open var ascending: Bool 18 | 19 | public required init?(coder aDecoder: NSCoder) { 20 | fatalError() 21 | } 22 | 23 | open func encode(with aCoder: NSCoder) { 24 | fatalError() 25 | } 26 | 27 | public init(key: String?, ascending: Bool) { 28 | self.key = key 29 | self.ascending = ascending 30 | } 31 | 32 | 33 | static public var supportsSecureCoding: Bool { 34 | return true 35 | } 36 | 37 | open override func copy() -> Any { 38 | return copy(with: nil) 39 | } 40 | 41 | open func copy(with zone: NSZone? = nil) -> Any { 42 | fatalError() 43 | } 44 | 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import OpenCloudKitTests 3 | 4 | XCTMain([ 5 | testCase(OpenCloudKitTests.allTests), 6 | testCase(CKPredicateTests.allTests), 7 | testCase(CKConfigTests.allTests), 8 | testCase(CKRecordTests.allTests), 9 | testCase(CKShareMetadataTests.allTests), 10 | testCase(CKURLRequestTests.allTests), 11 | 12 | ]) 13 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/CKConfigTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKConfigTests.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 14/07/2016. 6 | // 7 | // 8 | 9 | import XCTest 10 | import Foundation 11 | 12 | @testable import OpenCloudKit 13 | 14 | class CKConfigTests: XCTestCase { 15 | 16 | func pathForTests() -> String { 17 | let parent = (#file).components(separatedBy: "/").dropLast().joined(separator: "/") 18 | return parent 19 | } 20 | 21 | static var allTests : [(String, (CKConfigTests) -> () throws -> Void)] { 22 | return [ 23 | ("testInitializingContainerConfigWithToken", testInitializingContainerConfigWithToken), 24 | ("testServerToServerAuthKeyEqualable", testServerToServerAuthKeyEqualable), 25 | ("testInitializingContainerConfigWithServerToServerAuthKey", testInitializingContainerConfigWithServerToServerAuthKey), 26 | ("testInitializingContainerConfigWithDictionary", testInitializingContainerConfigWithDictionary), 27 | ("testInitializingConfigWithFile", testInitializingConfigWithFile) 28 | ] 29 | } 30 | 31 | 32 | func testInitializingContainerConfigWithToken() { 33 | let containerID = "CONTAINER_ID" 34 | let apiToken = "API_TOKEN" 35 | let environment = CKEnvironment.development 36 | 37 | let containerConfig = CKContainerConfig(containerIdentifier: containerID, environment: environment, apiTokenAuth: apiToken) 38 | 39 | XCTAssertEqual(containerConfig.containerIdentifier, containerConfig.containerIdentifier) 40 | XCTAssertEqual(containerConfig.environment, environment) 41 | XCTAssertEqual(containerConfig.apnsEnvironment, environment) 42 | XCTAssertEqual(containerConfig.apiTokenAuth, apiToken) 43 | XCTAssertNil(containerConfig.serverToServerKeyAuth) 44 | 45 | } 46 | 47 | func testServerToServerAuthKeyEqualable() { 48 | let first = CKServerToServerKeyAuth(keyID: "KEY_ID", privateKeyFile: "eckey.pem", privateKeyPassPhrase: nil) 49 | let second = CKServerToServerKeyAuth(keyID: "KEY_ID", privateKeyFile: "eckey.pem", privateKeyPassPhrase: nil) 50 | let third = CKServerToServerKeyAuth(keyID: "abc123", privateKeyFile: "eckey.pem", privateKeyPassPhrase: "my pass phrase") 51 | 52 | XCTAssert(first == second) 53 | XCTAssert(second != third) 54 | } 55 | 56 | func testInitializingContainerConfigWithServerToServerAuthKey() { 57 | let containerID = "CONTAINER_ID" 58 | let environment = CKEnvironment.development 59 | let apnsEnvironment = CKEnvironment.production 60 | 61 | let serverToServerKeyAuth = CKServerToServerKeyAuth(keyID: "KEY_ID", privateKeyFile: "eckey.pem", privateKeyPassPhrase: nil) 62 | 63 | let containerConfig = CKContainerConfig(containerIdentifier: containerID, environment: environment, serverToServerKeyAuth: serverToServerKeyAuth, apnsEnvironment: apnsEnvironment) 64 | 65 | XCTAssertEqual(containerConfig.containerIdentifier, containerID) 66 | XCTAssertEqual(containerConfig.environment, environment) 67 | XCTAssertEqual(containerConfig.apnsEnvironment, apnsEnvironment) 68 | XCTAssertEqual(containerConfig.serverToServerKeyAuth!, serverToServerKeyAuth) 69 | XCTAssertNil(containerConfig.apiTokenAuth) 70 | 71 | } 72 | 73 | func testInitializingContainerConfigWithDictionary() { 74 | let containerID = "CONTAINER_ID" 75 | let environment = "development" 76 | 77 | let keyID = "KEY_ID" 78 | let privateKeyFile = "eckey.pem" 79 | let privateKeyPassPhrase = "PASSWORD" 80 | let serverToServerAuthKeyDict: [String: Any] = ["keyID": keyID, "privateKeyFile": privateKeyFile, "privateKeyPassPhrase": privateKeyPassPhrase] 81 | let serverToServerAuthKey = CKServerToServerKeyAuth(keyID: keyID, privateKeyFile: privateKeyFile, privateKeyPassPhrase: privateKeyPassPhrase) 82 | 83 | let dictionary: [String: Any] = [ 84 | "containerIdentifier": containerID, 85 | "environment": environment, 86 | "serverToServerKeyAuth": serverToServerAuthKeyDict 87 | ] 88 | 89 | let containerConfig = CKContainerConfig(dictionary: dictionary) 90 | if let containerConfig = containerConfig { 91 | XCTAssertEqual(containerConfig.containerIdentifier, containerID) 92 | XCTAssertEqual(containerConfig.environment, .development) 93 | XCTAssertEqual(containerConfig.apnsEnvironment, .development) 94 | XCTAssertEqual(containerConfig.serverToServerKeyAuth!, serverToServerAuthKey) 95 | XCTAssertNil(containerConfig.apiTokenAuth) 96 | } else { 97 | XCTAssertNil(containerConfig) 98 | } 99 | } 100 | 101 | func testInitializingConfigWithFile() { 102 | 103 | let filePath = "\(pathForTests())/Supporting/config.json" 104 | let config = try? CKConfig(contentsOfFile: filePath) 105 | if let config = config { 106 | 107 | XCTAssertNotNil(config.containers.first) 108 | let container = config.containers.first! 109 | XCTAssertEqual(container.containerIdentifier, "com.example.apple-samplecode.cloudkit-catalog") 110 | XCTAssertEqual(container.environment, .development) 111 | XCTAssertNotNil(container.serverToServerKeyAuth) 112 | 113 | } else { 114 | XCTAssertNotNil(config) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/CKRecordTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKRecordTests.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 15/11/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | import Foundation 11 | @testable import OpenCloudKit 12 | 13 | class CKRecordTests: XCTestCase { 14 | 15 | static var allTests : [(String, (CKRecordTests) -> () throws -> Void)] { 16 | return [ 17 | ("testSetIntValueForKey", testSetIntValueForKey), 18 | ("testSetDoubleValueForKey", testSetDoubleValueForKey), 19 | ("testSetFloatValueForKey", testSetFloatValueForKey), 20 | ("testSetUIntValueForKey", testSetUIntValueForKey), 21 | ("testSetStringValueForKey", testSetStringValueForKey), 22 | ("testJSONObjectForIntValue", testJSONObjectForIntValue), 23 | ("testJSONObjectForFloatValue", testJSONObjectForFloatValue), 24 | ("testJSONObjectForUIntValue", testJSONObjectForUIntValue), 25 | ("testJSONObjectForStringValue", testJSONObjectForStringValue), 26 | ("testJSONSerializationOfRecordValueStringArray", testJSONSerializationOfRecordValueStringArray) 27 | ] 28 | } 29 | 30 | let record = CKRecord(recordType: "Movie") 31 | 32 | override func setUp() { 33 | super.setUp() 34 | // Put setup code here. This method is called before the invocation of each test method in the class. 35 | } 36 | 37 | override func tearDown() { 38 | // Put teardown code here. This method is called after the invocation of each test method in the class. 39 | super.tearDown() 40 | } 41 | 42 | func testSetIntValueForKey() { 43 | record["int"] = 5 44 | let intValue = record["int"] as? Int 45 | XCTAssertEqual(intValue, 5) 46 | } 47 | 48 | func testSetDoubleValueForKey() { 49 | let val: Double = 1.2345 50 | record["double"] = val 51 | 52 | let doubleValue = record["double"] as? Double 53 | XCTAssertEqual(doubleValue, val) 54 | } 55 | 56 | func testSetFloatValueForKey() { 57 | let val: Float = 6.57 58 | record["float"] = val 59 | 60 | let floatValue = record["float"] as? Float 61 | XCTAssertEqual(floatValue, val) 62 | } 63 | 64 | func testSetUIntValueForKey() { 65 | let val:UInt = 3 66 | record["uint"] = val 67 | 68 | let uintValue = record["uint"] as? UInt 69 | XCTAssertEqual(uintValue, val) 70 | } 71 | 72 | func testSetStringValueForKey() { 73 | let string = "MySwiftString" 74 | record["string"] = string 75 | 76 | let stringValue = record["string"] as? String 77 | XCTAssertEqual(stringValue, string) 78 | } 79 | 80 | func testJSONObjectForIntValue() { 81 | record["int"] = 5 82 | let dictionary = record["int"]!.recordFieldDictionary.bridge() 83 | let expectedResult = ["value": NSNumber(value: 5)].bridge() 84 | 85 | XCTAssertEqual(dictionary, expectedResult) 86 | } 87 | 88 | func testJSONObjectForFloatValue() { 89 | let val: Float = 1.23 90 | record["float"] = val 91 | let dictionary = record["float"]!.recordFieldDictionary.bridge() 92 | let expectedResult = ["value": NSNumber(value: val)].bridge() 93 | 94 | XCTAssertEqual(dictionary, expectedResult) 95 | } 96 | 97 | func testJSONObjectForUIntValue() { 98 | let val: UInt = 4 99 | record["uint"] = val 100 | 101 | let dictionary = record["uint"]!.recordFieldDictionary.bridge() 102 | let expectedResult = ["value": NSNumber(value: val)].bridge() 103 | XCTAssertEqual(dictionary, expectedResult) 104 | 105 | } 106 | 107 | func testJSONObjectForStringValue() { 108 | 109 | let string = "MySwiftString" 110 | record["string"] = string 111 | 112 | let dictionary = record["string"]!.recordFieldDictionary.bridge() 113 | let expectedResult = ["value": string.bridge(), "type":"STRING".bridge()].bridge() 114 | XCTAssertEqual(dictionary, expectedResult) 115 | } 116 | 117 | func testJSONSerializationOfRecordValueStringArray() { 118 | let movieRecord = CKRecord(recordType: "Movie") 119 | movieRecord["title"] = "Finding Dory" 120 | movieRecord["directors"] = NSArray(array: ["Andrew Stanton", "Angus MacLane"]) 121 | 122 | let directorsJSONObject = movieRecord["directors"]!.recordFieldDictionary.bridge() 123 | let jsonObject = try? JSONSerialization.data(withJSONObject: directorsJSONObject, options: [.prettyPrinted]) 124 | XCTAssertNotNil(jsonObject) 125 | } 126 | 127 | 128 | } 129 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/CKShareMetadataTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKShareMetadataTests.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 17/10/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | import Foundation 11 | @testable import OpenCloudKit 12 | 13 | class CKShareMetadataTests: XCTestCase { 14 | 15 | let containerID = "iCloud.au.com.benjaminjohnson.Playroom" 16 | let environment: CKEnvironment = .development 17 | let apiToken = "69d9716386808b294a5378c1eca316a84c440f3d9f0a684738a0c353a510c14a" 18 | let databaseScope = CKDatabaseScope.public 19 | 20 | static var allTests : [(String, (CKShareMetadataTests) -> () throws -> Void)] { 21 | return [ 22 | ("testShareMetadataFromJSON", testShareMetadataFromJSON), 23 | // ("testAcceptShareOperation", testAcceptShareOperation) 24 | ] 25 | } 26 | 27 | func pathForTests() -> String { 28 | let parent = (#file).components(separatedBy: "/").dropLast().joined(separator: "/") 29 | return parent 30 | } 31 | 32 | func jsonURL() -> URL { 33 | return URL(fileURLWithPath: "\(pathForTests())/Supporting/sharemetadata.json") 34 | } 35 | 36 | func jsonData() -> Data { 37 | return try! Data(contentsOf: jsonURL()) 38 | } 39 | 40 | func json() -> [String: Any] { 41 | return try! JSONSerialization.jsonObject(with: jsonData(), options: []) as! [String: Any] 42 | } 43 | 44 | override func setUp() { 45 | super.setUp() 46 | 47 | let containerConfig = CKContainerConfig(containerIdentifier: containerID, environment: environment, apiTokenAuth: apiToken) 48 | CloudKit.shared.configure(with: CKConfig(container: containerConfig)) 49 | CloudKit.shared.defaultAccount.iCloudAuthToken = "55__39__AT2sc5/W9j51Y5fjoDNpREublhsscmckmN9unZCseJrltfiJ1Ey8Hjg/oTj+O7e5YFfRm2TL5GdxbSyefdqtXSrDq0krIvjsIRFFXatFlT/t+crygvpSHCC7RpPQEAfziO/NknwkbWaEhv2N/8n7KMZXnibqEtzLk501QgRodxm8sR2pQrfQxiQtfoFwF2E/hyvQVhiBd0w=__eyJYLUFQUExFLVdFQkFVVEgtUENTLUNsb3Vka2l0IjoiUVhCd2JEb3hPZ0V6L0ZRNWUyeitvZnNXaUVWdk05OGZwNmFQTUlCbWRremVwTFR2WWkvWGNycjFNbFhGZUg5UXBOUU52MWJqVFRmVFJKN240eHY4cGdCbE16eGtGUENZIiwiWC1BUFBMRS1XRUJBVVRILVBDUy1TaGFyaW5nIjoiUVhCd2JEb3hPZ0ZGdmU1TlBlamp4aU13VFYxTGZnbjRtNE1ZZEVNbUxTUlg0YjE4UXEvRGxGanJxVkRNOWVZSjhUK1pNaWkreXh2RHBZT0lsREJnWDRmSDRTSDFmWUxZIn0=" 50 | 51 | } 52 | 53 | 54 | func testShareMetadataFromJSON() { 55 | let shareMetaData = CKShareMetadata(dictionary: json()) 56 | guard let metadata = shareMetaData else { 57 | XCTAssertNotNil(shareMetaData) 58 | return 59 | } 60 | 61 | XCTAssertEqual(metadata.containerIdentifier, "iCloud.au.com.benjaminjohnson.test") 62 | XCTAssert(true) 63 | 64 | } 65 | 66 | func testAcceptShareOperation() { 67 | 68 | let exp = self.expectation(description: "Something") 69 | let shortGUID = CKShortGUID(value: "0NEa3AEPXwK71VM4gHFnvh2kw", shouldFetchRootRecord: true, rootRecordDesiredKeys: nil) 70 | let acceptShareOperation = CKAcceptSharesOperation(shortGUIDs: [shortGUID]) 71 | 72 | acceptShareOperation.acceptSharesCompletionBlock = { 73 | (error) in 74 | print(error as Any) 75 | exp.fulfill() 76 | } 77 | 78 | acceptShareOperation.perShareCompletionBlock = { 79 | (metaData, share, error) in 80 | print(metaData) 81 | 82 | } 83 | 84 | acceptShareOperation.start() 85 | self.waitForExpectations(timeout: 5.0) { (error) in 86 | print("Timeout \(error)") 87 | } 88 | } 89 | 90 | } 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/CKTokenRegistrationURLRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKCreateTokenTests.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 19/1/17. 6 | // 7 | // 8 | 9 | import XCTest 10 | import Foundation 11 | @testable import OpenCloudKit 12 | 13 | class CKCreateTokensTests: XCTestCase { 14 | 15 | 16 | let containerID = "iCloud.benjamin.CloudTest" 17 | let environment: CKEnvironment = .development 18 | let apiToken = "f91d18c0fdef4846b3f4d5fff48c3e1a915beaf5098733c8eaa2b1132d6e5445" 19 | let databaseScope = CKDatabaseScope.public 20 | 21 | override func setUp() { 22 | super.setUp() 23 | 24 | let containerConfig = CKContainerConfig(containerIdentifier: containerID, environment: environment, apiTokenAuth: apiToken) 25 | CloudKit.shared.configure(with: CKConfig(container: containerConfig)) 26 | } 27 | 28 | 29 | 30 | func testCreateTokenOperation() { 31 | CloudKit.shared.verbose = true 32 | let exp = expectation(description: "Create Token") 33 | let operation = CKTokenCreateOperation(apnsEnvironment: .development) 34 | operation.createTokenCompletionBlock = { 35 | (info, error) in 36 | 37 | if let info = info { 38 | print(info) 39 | } else if let error = error { 40 | print(error) 41 | } 42 | exp.fulfill() 43 | 44 | } 45 | 46 | operation.start() 47 | 48 | waitForExpectations(timeout: 10.0, handler: nil) 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/CKURLRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKURLRequestTests.swift 3 | // OpenCloudKit 4 | // 5 | // Created by Benjamin Johnson on 28/07/2016. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import OpenCloudKit 11 | import Foundation 12 | 13 | class CKURLRequestTests: XCTestCase { 14 | 15 | 16 | let containerID = "iCloud.benjamin.CloudTest" 17 | let environment: CKEnvironment = .development 18 | let apiToken = "AUTH_KEY" 19 | let databaseScope = CKDatabaseScope.public 20 | 21 | static var allTests : [(String, (CKURLRequestTests) -> () throws -> Void)] { 22 | return [ 23 | ("testCKQueryURLRequestURL", testCKQueryURLRequestURL), 24 | ("testCKModifySubscriptionsURLRequestURL", testCKModifySubscriptionsURLRequestURL), 25 | ("testCKModifyRecordsURL", testCKModifyRecordsURL), 26 | ("testCreateTokenURL", testCreateTokenURL), 27 | ("testRegisterTokenURL", testRegisterTokenURL) 28 | ] 29 | } 30 | 31 | override func setUp() { 32 | super.setUp() 33 | 34 | let containerConfig = CKContainerConfig(containerIdentifier: containerID, environment: environment, apiTokenAuth: apiToken) 35 | CloudKit.shared.configure(with: CKConfig(container: containerConfig)) 36 | } 37 | 38 | func testCKQueryURLRequestURL() { 39 | 40 | let queryURLRequest = CKQueryURLRequest(query: CKQuery(recordType: "Items", filters: []), cursor: nil, limit: 0, requestedFields: nil, zoneID: nil) 41 | 42 | 43 | let queryURLComponents = URLComponents(url: queryURLRequest.url, resolvingAgainstBaseURL: false)! 44 | XCTAssertEqual(queryURLComponents.host!, "api.apple-cloudkit.com") 45 | XCTAssertEqual(queryURLComponents.path, "/database/1/\(containerID)/\(environment)/\(databaseScope)/records/query") 46 | } 47 | 48 | func testCKModifySubscriptionsURLRequestURL() { 49 | 50 | let subscriptionURLRequest = CKModifySubscriptionsURLRequest(subscriptionsToSave: nil, subscriptionIDsToDelete: nil) 51 | 52 | let urlComponents = URLComponents(url: subscriptionURLRequest.url, resolvingAgainstBaseURL: false)! 53 | XCTAssertEqual(urlComponents.path, "/database/1/\(containerID)/\(environment)/\(databaseScope)/subscriptions/modify") 54 | } 55 | 56 | func testCKModifyRecordsURL() { 57 | let modifySubscriptionsURLRequest = CKModifyRecordsURLRequest(recordsToSave: nil, recordIDsToDelete: nil, isAtomic: true, database: CKContainer.default().publicCloudDatabase, savePolicy: .ChangedKeys, zoneID: nil) 58 | 59 | let urlComponents = URLComponents(url: modifySubscriptionsURLRequest.url, resolvingAgainstBaseURL: false)! 60 | XCTAssertEqual(urlComponents.path, "/database/1/\(containerID)/\(environment)/\(databaseScope)/records/modify") 61 | } 62 | 63 | func testCreateTokenURL() { 64 | let request = CKTokenCreateURLRequest(apnsEnvironment: .development) 65 | let url = URLComponents(url: request.url, resolvingAgainstBaseURL: false)! 66 | print(request.url) 67 | XCTAssertEqual(url.path, "/device/\(CKServerInfo.version)/\(containerID)/\(environment)/tokens/create") 68 | } 69 | 70 | func testRegisterTokenURL() { 71 | let request = CKTokenRegistrationURLRequest(token: Data(), apnsEnvironment: "\(environment)") 72 | let url = URLComponents(url: request.url, resolvingAgainstBaseURL: false)! 73 | XCTAssertEqual(url.path, "/device/\(CKServerInfo.version)/\(containerID)/\(environment)/tokens/register") 74 | } 75 | 76 | func assertDatabasePath(components: URLComponents, query: String) { 77 | XCTAssertEqual(components.host, "api.apple-cloudkit.com") 78 | XCTAssertEqual(components.path, "/database/1/\(containerID)/\(environment)/\(databaseScope)/\(query)") 79 | 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/OpenCloudKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | @testable import OpenCloudKit 4 | 5 | class OpenCloudKitTests: XCTestCase { 6 | 7 | let requestBodyString = "{\"zoneWide\":false,\"query\":{\"recordType\":\"Items\",\"filterBy\":[],\"sortBy\":[]}}" 8 | 9 | func pathForTests() -> String { 10 | let parent = (#file).components(separatedBy: "/").dropLast().joined(separator: "/") 11 | return parent 12 | } 13 | 14 | func ECKeyPath() -> String { 15 | return "\(pathForTests())/Supporting/testeckey.pem" 16 | } 17 | 18 | func publicECKeyPath() -> String { 19 | return "\(pathForTests())/Supporting/eckey.pub" 20 | } 21 | 22 | 23 | func testSHA256() { 24 | let message = "test" 25 | let data = message.data(using: String.Encoding.utf8)! 26 | let resultHash = (data as Data).sha256().base64EncodedString(options: []) 27 | let testSHA256Hash = "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=" 28 | XCTAssertEqual(resultHash, testSHA256Hash) 29 | } 30 | 31 | func testVerifySignedData() { 32 | 33 | let evpKey = try! EVPKey(contentsOfFile: publicECKeyPath(), type: EVPKeyType.Public) 34 | #if os(Linux) 35 | let data = try! NSData(contentsOf: URL(fileURLWithPath: "\(pathForTests())/Supporting/test.txt")) 36 | #else 37 | let data = NSData(contentsOf: URL(fileURLWithPath: "\(pathForTests())/Supporting/test.txt"))! 38 | #endif 39 | 40 | let signedBase64 = "MEUCIQCa5vSe3xRHpN4FuUeNeNNB7gHpexMN1RYal4wJCpHExAIgdi/IV/K88aeIzoM0YaWp4PkX9T1+1oZNKZQY679uqRk=" 41 | let signedData = NSData(base64Encoded: signedBase64, options: [])! 42 | 43 | let context = try! MessageVerifyContext(try! MessageDigest("sha256WithRSAEncryption"), withKey: evpKey) 44 | try! context.update(data: data) 45 | 46 | 47 | XCTAssert(context.verify(signature: signedData as NSData), "Signature should verify successfully") 48 | 49 | } 50 | 51 | func testRawPayload() { 52 | 53 | let requestDate = "2016-07-13T03:16:51Z" 54 | let urlPath = "/database/1/iCloud.benjamin.CloudTest/development/public/records/query" 55 | let requestBody = requestBodyString.data(using: String.Encoding.utf8)! 56 | let requestBodyData = NSData(bytes: requestBody.bytes, length: requestBody.bytes.count) 57 | 58 | // Should Equal 0sdWcosXLRqAQp9TQ4LzZOTgiETnGpqlODfsnN9Cqr0= 59 | 60 | let requestBodyHash = Data(requestBody).sha256().base64EncodedString(options: []) 61 | 62 | let rawPayload = CKServerRequestAuth.rawPayload(withRequestDate: requestDate, requestBody: requestBodyData, urlSubpath: urlPath) 63 | 64 | let expectedPayload = "\(requestDate):\(requestBodyHash):\(urlPath)" 65 | XCTAssertEqual(rawPayload, expectedPayload) 66 | 67 | } 68 | 69 | func testSignWithPrivateKey() { 70 | 71 | let requestBody = requestBodyString.data(using: String.Encoding.utf8)! 72 | let signedData = CKServerRequestAuth.sign(data: NSData(bytes: requestBody.bytes, length: requestBody.bytes.count) 73 | , privateKeyPath: ECKeyPath()) 74 | XCTAssertNotNil(signedData) 75 | 76 | //TODO: Verify the signature is correct 77 | 78 | } 79 | 80 | /* 81 | func testValidSignature() { 82 | 83 | let requestDate = "2016-07-13T03:16:51Z" 84 | let urlPath = "/database/1/iCloud.benjamin.CloudTest/development/public/records/query" 85 | let requestBody = NSData(data: requestBodyString.data(using: String.Encoding.utf8)!) 86 | 87 | let requestAuth = CKServerRequestAuth(requestBody: requestBody, urlPath: urlPath, privateKeyPath: ECKeyPath())! 88 | let signature = NSData(base64Encoded: requestAuth.signature, options: [])! 89 | 90 | 91 | // Verify signature 92 | let rawPayload = CKServerRequestAuth.rawPayload(withRequestDate: requestDate, requestBody: requestBody, urlSubpath: urlPath) 93 | let payload = rawPayload.data(using: String.Encoding.utf8)! as NSData 94 | 95 | let publicKey = try! EVPKey(contentsOfFile: publicECKeyPath(), type: EVPKeyType.Public) 96 | let context = try! MessageVerifyContext(try! MessageDigest("sha256WithRSAEncryption"), withKey: publicKey) 97 | try! context.update(data: payload) 98 | 99 | XCTAssert(context.verify(signature: signature), "Signature should verify successfully") 100 | } 101 | */ 102 | func testAuthenicateServerWithURLRequest() { 103 | 104 | let url = URL(string: "https://api.apple-cloudkit.com/database/1/iCloud.benjamin.CloudTest/development/public/records/query")! 105 | var urlRequest = URLRequest(url: url) 106 | urlRequest.httpBody = requestBodyString.data(using: String.Encoding.utf8)! 107 | 108 | let serverKeyID = "TEST_KEY" 109 | let ecKeyPath = ECKeyPath() 110 | 111 | let finalRequest = CKServerRequestAuth.authenticateServer(forRequest: urlRequest, serverKeyID: serverKeyID, privateKeyPath: ecKeyPath) 112 | 113 | if let finalRequest = finalRequest { 114 | XCTAssertEqual(finalRequest.allHTTPHeaderFields!["X-Apple-CloudKit-Request-KeyID"], serverKeyID) 115 | XCTAssertNotNil(finalRequest.allHTTPHeaderFields?["X-Apple-CloudKit-Request-ISO8601Date"]) 116 | XCTAssertNotNil(finalRequest.allHTTPHeaderFields?["X-Apple-CloudKit-Request-SignatureV1"]) 117 | 118 | } else { 119 | XCTAssertNotNil(finalRequest, "The returned URLRequest should not be nil, if the signing succeeded") 120 | } 121 | } 122 | 123 | 124 | 125 | 126 | static var allTests : [(String, (OpenCloudKitTests) -> () throws -> Void)] { 127 | return [ 128 | ("testSHA256", testSHA256), 129 | ("testVerifySignedData", testVerifySignedData), 130 | ("testRawPayload", testRawPayload), 131 | ("testSignWithPrivateKey", testSignWithPrivateKey), 132 | ("testAuthenicateServerWithURLRequest", testAuthenicateServerWithURLRequest) 133 | ] 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/Supporting/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "containers": [{ 3 | "containerIdentifier":"com.example.apple-samplecode.cloudkit-catalog", 4 | "environment": "development", 5 | 6 | "serverToServerKeyAuth": { 7 | "keyID": "KEY_ID", 8 | 9 | "privateKeyFile": "eckey.pem" 10 | } 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/Supporting/eckey.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAED/+H2wGpNk2zcPorCDOREC9fDD2u 3 | 1wNVGk5Zjd83g4+7YMrYjTHT3JOdjb+0Q8v+LhgU1ft1aXYHqIaAtzjosQ== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/Supporting/sharemetadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "shortGUID": { 3 | "value": "0NEa3AEPXwK71VM4gHFnvh2kw", 4 | "shouldFetchRootRecord": true 5 | }, 6 | "containerIdentifier": "iCloud.au.com.benjaminjohnson.test", 7 | "environment": "development", 8 | "databaseScope": "PRIVATE", 9 | "zoneID": { 10 | "zoneName": "Central", 11 | "ownerRecordName": "_8e1c7debce61157051ce0aef01e83c7d" 12 | }, 13 | "share": { 14 | "recordName": "Share-C1FBF52C-DAFE-45C4-BF3F-581532F70FCA", 15 | "recordType": "cloudkit.share", 16 | "fields": { 17 | "cloudkit.title": { 18 | "value": "Centre", 19 | "type": "STRING" 20 | } 21 | }, 22 | "recordChangeTag": "e", 23 | "created": { 24 | "timestamp": 1476619372869, 25 | "userRecordName": "_8e1c7debce61157051ce0aef01e83c7d", 26 | "deviceID": "1" 27 | }, 28 | "modified": { 29 | "timestamp": 1476619374676, 30 | "userRecordName": "_8e1c7debce61157051ce0aef01e83c7d", 31 | "deviceID": "iPad" 32 | }, 33 | "publicPermission": "READ_WRITE", 34 | "participants": [ 35 | { 36 | "userIdentity": { 37 | "userRecordName": "_8e1c7debce61157051ce0aef01e83c7d", 38 | "nameComponents": { 39 | "givenName": "Benjamin", 40 | "familyName": "Johnson" 41 | }, 42 | "lookupInfo": { 43 | "emailAddress": "benjamin@gmail.com" 44 | } 45 | }, 46 | "type": "OWNER", 47 | "acceptanceStatus": "ACCEPTED", 48 | "permission": "READ_WRITE" 49 | } 50 | ], 51 | "owner": { 52 | "userIdentity": { 53 | "userRecordName": "_8e1c7debce61157051ce0aef01e83c7d", 54 | "nameComponents": { 55 | "givenName": "Benjamin", 56 | "familyName": "Johnson" 57 | }, 58 | "lookupInfo": { 59 | "emailAddress": "benjamin@gmail.com" 60 | } 61 | }, 62 | "type": "OWNER", 63 | "acceptanceStatus": "ACCEPTED", 64 | "permission": "READ_WRITE" 65 | }, 66 | "currentUserParticipant": { 67 | "userIdentity": { 68 | "userRecordName": "_8e1c7debce61157051ce0aef01e83c7d", 69 | "nameComponents": { 70 | "givenName": "Benjamin", 71 | "familyName": "Johnson" 72 | }, 73 | "lookupInfo": { 74 | "emailAddress": "benjamin@gmail.com" 75 | } 76 | }, 77 | "type": "OWNER", 78 | "acceptanceStatus": "ACCEPTED", 79 | "permission": "READ_WRITE" 80 | }, 81 | "shortGUID": "0NEa3AEPXwK71VM4gHFnvh2kw" 82 | }, 83 | "rootRecordName": "D06A308D-DEE2-4654-9621-76072CEB150E", 84 | "rootRecord": { 85 | "recordName": "D06A308D-DEE2-4654-9621-76072CEB150E", 86 | "recordType": "Centre", 87 | "fields": { 88 | "name": { 89 | "value": "My centre", 90 | "type": "STRING" 91 | }, 92 | "pageToken": { 93 | "value": "My token", 94 | "type": "STRING" 95 | } 96 | }, 97 | "recordChangeTag": "d", 98 | "created": { 99 | "timestamp": 1476619372877, 100 | "userRecordName": "_8e1c7debce61157051ce0aef01e83c7d", 101 | "deviceID": "1" 102 | }, 103 | "modified": { 104 | "timestamp": 1476619372877, 105 | "userRecordName": "_8e1c7debce61157051ce0aef01e83c7d", 106 | "deviceID": "iPad" 107 | }, 108 | "share": { 109 | "recordName": "Share-C1FBF52C-DAFE-45C4-BF3F-581532F70FCA" 110 | }, 111 | "shortGUID": "0NEa3AEPXwK71VM4gHFnvh2kw" 112 | }, 113 | "participantType": "OWNER", 114 | "participantStatus": "ACCEPTED", 115 | "participantPermission": "READ_WRITE", 116 | "ownerIdentity": { 117 | "userRecordName": "_8e1c7debce61157051ce0aef01e83c7d", 118 | "nameComponents": { 119 | "givenName": "Benjamin", 120 | "familyName": "Johnson" 121 | }, 122 | "lookupInfo": { 123 | "emailAddress": "benjaminkylejohnson@gmail.com" 124 | } 125 | }, 126 | "potentialMatchList": [] 127 | } 128 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/Supporting/signed.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BennyKJohnson/OpenCloudKit/e6cd4511f3f3e795520cab5496d10faed6e143ec/Tests/OpenCloudKitTests/Supporting/signed.bin -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/Supporting/test.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /Tests/OpenCloudKitTests/Supporting/testeckey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIHo8HVekn3DTRowbTSnSnconrkfChxnAy1DZJPbBrm1BoAoGCCqGSM49 3 | AwEHoUQDQgAED/+H2wGpNk2zcPorCDOREC9fDD2u1wNVGk5Zjd83g4+7YMrYjTHT 4 | 3JOdjb+0Q8v+LhgU1ft1aXYHqIaAtzjosQ== 5 | -----END EC PRIVATE KEY----- 6 | --------------------------------------------------------------------------------