diff --git a/WordPressKit.podspec b/WordPressKit.podspec index 565715d2..ba685a99 100644 --- a/WordPressKit.podspec +++ b/WordPressKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "WordPressKit" - s.version = "4.24.0" + s.version = "4.25.0" s.summary = "WordPressKit offers a clean and simple WordPress.com and WordPress.org API." s.description = <<-DESC diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index e61e678b..502962c8 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -344,8 +344,13 @@ 8B2F4BEF24ACCC120056C08A /* RemoteReaderCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B2F4BEE24ACCC120056C08A /* RemoteReaderCard.swift */; }; 8B2F4BF124ACE3C30056C08A /* RemoteReaderInterest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B2F4BF024ACE3C30056C08A /* RemoteReaderInterest.swift */; }; 8B52B901257AC5A200221663 /* Date+endOfDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B52B900257AC5A200221663 /* Date+endOfDay.swift */; }; + 8B749DED25AF3E4600023F03 /* JetpackCapabilitiesServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B749DEC25AF3E4600023F03 /* JetpackCapabilitiesServiceRemote.swift */; }; + 8B749E8225AF7DDA00023F03 /* JetpackCapabilitiesServiceRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B749E8125AF7DDA00023F03 /* JetpackCapabilitiesServiceRemoteTests.swift */; }; + 8B749E8625AF808600023F03 /* jetpack-capabilities-107159616-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 8B749E8525AF808600023F03 /* jetpack-capabilities-107159616-success.json */; }; + 8B749E8A25AF819700023F03 /* jetpack-capabilities-malformed.json in Resources */ = {isa = PBXBuildFile; fileRef = 8B749E8925AF819700023F03 /* jetpack-capabilities-malformed.json */; }; 8BB66DB02523C181000B29DA /* ReaderPostServiceRemote+V2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BB66DAF2523C181000B29DA /* ReaderPostServiceRemote+V2.swift */; }; 8BE67ED324AD05D3004DB4C9 /* Decodable+DictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE67ED224AD05D3004DB4C9 /* Decodable+DictionaryTests.swift */; }; + 8BFB4E6625B07905004D026E /* jetpack-capabilities-34197361-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 8BFB4E6525B07905004D026E /* jetpack-capabilities-34197361-success.json */; }; 8C5734F925681A6A005E61EE /* Enum+UnknownCaseRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C5734F825681A6A005E61EE /* Enum+UnknownCaseRepresentable.swift */; }; 9309994D1F1657C600F006A1 /* ThemeServiceRemote.h in Headers */ = {isa = PBXBuildFile; fileRef = 9309994B1F1657C600F006A1 /* ThemeServiceRemote.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9309994E1F1657C600F006A1 /* ThemeServiceRemote.m in Sources */ = {isa = PBXBuildFile; fileRef = 9309994C1F1657C600F006A1 /* ThemeServiceRemote.m */; }; @@ -488,6 +493,7 @@ BA9A7F7F24C6895600925E81 /* plugin-directory-jetpack-beta.json in Resources */ = {isa = PBXBuildFile; fileRef = BA9A7F7E24C6895600925E81 /* plugin-directory-jetpack-beta.json */; }; BAB0E36424AD599700B3D22C /* MockPluginStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB0E36324AD599700B3D22C /* MockPluginStateProvider.swift */; }; BAFA775624ADAB3C000F0D3A /* MockPluginDirectoryEntryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFA775524ADAB3C000F0D3A /* MockPluginDirectoryEntryProvider.swift */; }; + C785325625B5F46C006CEAFB /* JetpackThreatFixStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C785325525B5F46C006CEAFB /* JetpackThreatFixStatus.swift */; }; D813437621F6D70D0060D99A /* SiteSegmentsResponseDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D813437521F6D70D0060D99A /* SiteSegmentsResponseDecodingTests.swift */; }; D813437821F6D7DC0060D99A /* site-segments-single.json in Resources */ = {isa = PBXBuildFile; fileRef = D813437721F6D7DC0060D99A /* site-segments-single.json */; }; D816857121EDACD10049883E /* WordPressComServiceRemote+SiteSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = D816857021EDACD10049883E /* WordPressComServiceRemote+SiteSegments.swift */; }; @@ -909,8 +915,13 @@ 8B2F4BEE24ACCC120056C08A /* RemoteReaderCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReaderCard.swift; sourceTree = ""; }; 8B2F4BF024ACE3C30056C08A /* RemoteReaderInterest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReaderInterest.swift; sourceTree = ""; }; 8B52B900257AC5A200221663 /* Date+endOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+endOfDay.swift"; sourceTree = ""; }; + 8B749DEC25AF3E4600023F03 /* JetpackCapabilitiesServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackCapabilitiesServiceRemote.swift; sourceTree = ""; }; + 8B749E8125AF7DDA00023F03 /* JetpackCapabilitiesServiceRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackCapabilitiesServiceRemoteTests.swift; sourceTree = ""; }; + 8B749E8525AF808600023F03 /* jetpack-capabilities-107159616-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "jetpack-capabilities-107159616-success.json"; sourceTree = ""; }; + 8B749E8925AF819700023F03 /* jetpack-capabilities-malformed.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "jetpack-capabilities-malformed.json"; sourceTree = ""; }; 8BB66DAF2523C181000B29DA /* ReaderPostServiceRemote+V2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderPostServiceRemote+V2.swift"; sourceTree = ""; }; 8BE67ED224AD05D3004DB4C9 /* Decodable+DictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decodable+DictionaryTests.swift"; sourceTree = ""; }; + 8BFB4E6525B07905004D026E /* jetpack-capabilities-34197361-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "jetpack-capabilities-34197361-success.json"; sourceTree = ""; }; 8C5734F825681A6A005E61EE /* Enum+UnknownCaseRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Enum+UnknownCaseRepresentable.swift"; sourceTree = ""; }; 9309994B1F1657C600F006A1 /* ThemeServiceRemote.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThemeServiceRemote.h; sourceTree = ""; }; 9309994C1F1657C600F006A1 /* ThemeServiceRemote.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThemeServiceRemote.m; sourceTree = ""; }; @@ -1061,6 +1072,7 @@ BAFA775524ADAB3C000F0D3A /* MockPluginDirectoryEntryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPluginDirectoryEntryProvider.swift; sourceTree = ""; }; BEEC8B5D92DA614468900BD7 /* Pods-WordPressKit.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKit.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressKit/Pods-WordPressKit.release-alpha.xcconfig"; sourceTree = ""; }; C5953994B3865AF409BA4210 /* Pods-WordPressKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressKitTests/Pods-WordPressKitTests.release.xcconfig"; sourceTree = ""; }; + C785325525B5F46C006CEAFB /* JetpackThreatFixStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackThreatFixStatus.swift; sourceTree = ""; }; CA5ABD95F40077D001644BCC /* Pods-WordPressKit.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKit.release-internal.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressKit/Pods-WordPressKit.release-internal.xcconfig"; sourceTree = ""; }; D813437521F6D70D0060D99A /* SiteSegmentsResponseDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSegmentsResponseDecodingTests.swift; sourceTree = ""; }; D813437721F6D7DC0060D99A /* site-segments-single.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-segments-single.json"; sourceTree = ""; }; @@ -1172,6 +1184,7 @@ 32FC1D26255C91ED00CD0A7B /* JetpackScan.swift */, 32FC20CD255DCC6100CD0A7B /* JetpackScanThreat.swift */, 3297E15525645C7D00287D21 /* JetpackCredentials.swift */, + C785325525B5F46C006CEAFB /* JetpackThreatFixStatus.swift */, ); path = "Jetpack Scan"; sourceTree = ""; @@ -1567,6 +1580,7 @@ 1769DEA924729AFF00F42EFC /* HomepageSettingsServiceRemote.swift */, 9A2D0B27225E0119009E585F /* JetpackServiceRemote.swift */, 32FC1D27255C91ED00CD0A7B /* JetpackScanServiceRemote.swift */, + 8B749DEC25AF3E4600023F03 /* JetpackCapabilitiesServiceRemote.swift */, 74DA56361F06EB0500FE9BF4 /* MediaServiceRemote.h */, 74DA562E1F06EAF000FE9BF4 /* MediaServiceRemoteREST.h */, 74DA562F1F06EAF000FE9BF4 /* MediaServiceRemoteREST.m */, @@ -1972,6 +1986,9 @@ 4625BAF6253E130800C04AAD /* page-layout-blog-layouts-malformed.json */, 462422302548BFEE002B8A12 /* nux-starter-designs-success.json */, 462422342548C031002B8A12 /* nux-starter-designs-malformed.json */, + 8B749E8525AF808600023F03 /* jetpack-capabilities-107159616-success.json */, + 8BFB4E6525B07905004D026E /* jetpack-capabilities-34197361-success.json */, + 8B749E8925AF819700023F03 /* jetpack-capabilities-malformed.json */, 3297E1E32564683600287D21 /* jetpack-scan-in-progress.json */, 3297E2842564746800287D21 /* jetpack-scan-unavailable.json */, 3297E1EB2564694C00287D21 /* jetpack-scan-idle-success-no-threats.json */, @@ -2024,6 +2041,7 @@ children = ( 3297E1DC2564649D00287D21 /* Scan */, 9A2D0B2A225E0E22009E585F /* JetpackServiceRemoteTests.swift */, + 8B749E8125AF7DDA00023F03 /* JetpackCapabilitiesServiceRemoteTests.swift */, ); name = Jetpack; sourceTree = ""; @@ -2359,6 +2377,7 @@ 40819771221DFDB700A298E4 /* stats-posts-data.json in Resources */, 740B23EE1F17FB7E00067A2A /* xmlrpc-malformed-request-xml-error.xml in Resources */, 826016F91F9FAF6300533B6C /* activity-log-success-3.json in Resources */, + 8B749E8A25AF819700023F03 /* jetpack-capabilities-malformed.json in Resources */, 93BD27551EE73442002BB00B /* auth-send-login-email-invalid-client-failure.json in Resources */, 826016FD1F9FAF6300533B6C /* activity-log-bad-json-failure.json in Resources */, 74C473CD1EF336BD009918F2 /* site-active-purchases-bad-json-failure.json in Resources */, @@ -2423,6 +2442,7 @@ 40819775221E497D00A298E4 /* stats-published-posts.json in Resources */, FFA4D4B12423B33800BF5180 /* wp-pages.json in Resources */, 74D67F151F15C2D70010C5ED /* site-roles-success.json in Resources */, + 8BFB4E6625B07905004D026E /* jetpack-capabilities-34197361-success.json in Resources */, 9A881753223C01E400A3AB20 /* jetpack-service-error-site-is-jetpack.json in Resources */, D8DB404221EF22B500B8238E /* site-segments-multiple.json in Resources */, 740B23E11F17FB4200067A2A /* xmlrpc-metaweblog-editpost-bad-xml-failure.xml in Resources */, @@ -2433,6 +2453,7 @@ BA3F139024A0AAF8006367A3 /* plugin-install-succeeds.json in Resources */, 74D67F131F15C2D70010C5ED /* site-roles-auth-failure.json in Resources */, 74B040721EF8B366002C6258 /* rest-site-settings.json in Resources */, + 8B749E8625AF808600023F03 /* jetpack-capabilities-107159616-success.json in Resources */, 93BD27611EE73442002BB00B /* me-sites-empty-success.json in Resources */, 40E4698D2017D2E30030DB5F /* plugin-directory-new.json in Resources */, 4081977C221F153B00A298E4 /* stats-visits-day.json in Resources */, @@ -2601,6 +2622,7 @@ 8B2F4BE724ABC8A90056C08A /* ReaderPostServiceRemote+Cards.swift in Sources */, 8B16CE8E25250039007BE5A9 /* RemoteReaderPost.swift in Sources */, 74E2294E1F1E73FE0085F7F2 /* RemotePublicizeService.swift in Sources */, + 8B749DED25AF3E4600023F03 /* JetpackCapabilitiesServiceRemote.swift in Sources */, 9F3E0BA22087345F009CB5BA /* ServiceRequest.swift in Sources */, E1A6605F1FD694ED00BAC339 /* PluginDirectoryEntry.swift in Sources */, 9A2D0B28225E0119009E585F /* JetpackServiceRemote.swift in Sources */, @@ -2659,6 +2681,7 @@ E6D0EE621F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift in Sources */, 7430C9B61F1927C50051B8E6 /* RemoteReaderSiteInfo.m in Sources */, 74DA56351F06EAF000FE9BF4 /* MediaServiceRemoteXMLRPC.m in Sources */, + C785325625B5F46C006CEAFB /* JetpackThreatFixStatus.swift in Sources */, 93F50A381F226B9300B5BEBA /* WordPressComServiceRemote.m in Sources */, 9F4E52002088E38200424676 /* ObjectValidation.swift in Sources */, 7430C9B81F1927C50051B8E6 /* RemoteReaderTopic.m in Sources */, @@ -2805,6 +2828,7 @@ 32AF21E3236DEB3C001C6502 /* PostServiceRemoteRESTAutosaveTests.swift in Sources */, 3236F79A24AE406D0088E8F3 /* ReaderTopicServiceRemote+InterestsTests.swift in Sources */, 74B5F0DE1EF82A9600B411E7 /* BlogServiceRemoteRESTTests.m in Sources */, + 8B749E8225AF7DDA00023F03 /* JetpackCapabilitiesServiceRemoteTests.swift in Sources */, 74E2294B1F1E73340085F7F2 /* SharingServiceRemoteTests.m in Sources */, 73B3DAD621FBB20D00B2CF18 /* WordPressComRestApiTests+Locale.swift in Sources */, 3297E1DE2564653A00287D21 /* JetpackScanServiceRemoteTests.swift in Sources */, diff --git a/WordPressKit/Activity.swift b/WordPressKit/Activity.swift index a8e42ae4..ec4ac1fe 100644 --- a/WordPressKit/Activity.swift +++ b/WordPressKit/Activity.swift @@ -248,6 +248,7 @@ public class RestoreStatus { public let status: Status public let progress: Int public let message: String? + public let currentEntry: String? public let errorCode: String? public let failureReason: String? @@ -266,6 +267,7 @@ public class RestoreStatus { status = restoreStatusEnum progress = dictionary["progress"] as? Int ?? 0 message = dictionary["message"] as? String + currentEntry = dictionary["current_entry"] as? String errorCode = dictionary["error_code"] as? String failureReason = dictionary["reason"] as? String } diff --git a/WordPressKit/Jetpack Scan/JetpackScanThreat.swift b/WordPressKit/Jetpack Scan/JetpackScanThreat.swift index 9762af7e..679f83e4 100644 --- a/WordPressKit/Jetpack Scan/JetpackScanThreat.swift +++ b/WordPressKit/Jetpack Scan/JetpackScanThreat.swift @@ -68,7 +68,7 @@ public struct JetpackScanThreat: Decodable { public let `extension`: JetpackThreatExtension? /// The type of threat this is - private(set) var type: ThreatType = .unknown + public private(set) var type: ThreatType = .unknown /// If this is a file based threat this will provide code context to be displayed /// Context example: diff --git a/WordPressKit/Jetpack Scan/JetpackThreatFixStatus.swift b/WordPressKit/Jetpack Scan/JetpackThreatFixStatus.swift new file mode 100644 index 00000000..0a43e890 --- /dev/null +++ b/WordPressKit/Jetpack Scan/JetpackThreatFixStatus.swift @@ -0,0 +1,68 @@ +import Foundation + +public struct JetpackThreatFixResponse: Decodable { + public let success: Bool + public let threats: [JetpackThreatFixStatus] + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + success = try container.decode(Bool.self, forKey: .success) + + let statusDict = try container.decode([String: [String: String]].self, forKey: .threats) + var statusArray: [JetpackThreatFixStatus] = [] + + for (threatId, status) in statusDict { + guard let id = Int(threatId), let statusValue = status["status"] else { + throw ResponseError.decodingFailure + } + + let fixStatus = JetpackThreatFixStatus(with: id, status: statusValue) + statusArray.append(fixStatus) + } + + threats = statusArray + } + + + /// Returns true the fixing status is complete, and all threats are no longer in progress + public var finished: Bool { + return inProgress.count <= 0 + } + + /// Returns all the fixed threats + public var fixed: [JetpackThreatFixStatus] { + return threats.filter { $0.status == .fixed } + } + + /// Returns all the in progress threats + public var inProgress: [JetpackThreatFixStatus] { + return threats.filter { $0.status == .inProgress } + } + + private enum CodingKeys: String, CodingKey { + case success = "ok", threats + } + + enum ResponseError: Error { + case decodingFailure + } +} + +public struct JetpackThreatFixStatus { + public enum ThreatFixStatus: String, Decodable, UnknownCaseRepresentable { + case inProgress = "in_progress" + case fixed + + case unknown + static let unknownCase: Self = .unknown + } + + public let threatId: Int + public let status: ThreatFixStatus + + public init(with threatId: Int, status: String){ + self.threatId = threatId + self.status = ThreatFixStatus(rawValue: status) + } +} diff --git a/WordPressKit/JetpackCapabilitiesServiceRemote.swift b/WordPressKit/JetpackCapabilitiesServiceRemote.swift new file mode 100644 index 00000000..e084d8d1 --- /dev/null +++ b/WordPressKit/JetpackCapabilitiesServiceRemote.swift @@ -0,0 +1,41 @@ +import Foundation + +/// A service that returns the Jetpack Capabilities for a set of blogs +open class JetpackCapabilitiesServiceRemote: ServiceRemoteWordPressComREST { + + /// Returns a Dictionary of capabilities for each given siteID + /// - Parameters: + /// - siteIds: an array of Int representing siteIDs + /// - success: a success block that accepts a dictionary as a parameter + open func `for`(siteIds: [Int], success: @escaping ([String: AnyObject]) -> Void) { + var jetpackCapabilities: [String: AnyObject] = [:] + let dispatchGroup = DispatchGroup() + let dispatchQueue = DispatchQueue(label: "com.rewind.capabilities") + + siteIds.forEach { siteID in + dispatchGroup.enter() + + dispatchQueue.async { + let endpoint = "sites/\(siteID)/rewind/capabilities" + let path = self.path(forEndpoint: endpoint, withVersion: ._2_0) + + self.wordPressComRestApi.GET(path, + parameters: nil, + success: { response, _ in + if let capabilities = (response as? [String: AnyObject])?["capabilities"] as? [String] { + jetpackCapabilities["\(siteID)"] = capabilities as AnyObject + } + + dispatchGroup.leave() + }, failure: { error, _ in + dispatchGroup.leave() + }) + } + } + + dispatchGroup.notify(queue: .global(qos: .background)) { + success(jetpackCapabilities) + } + } + +} diff --git a/WordPressKit/JetpackRestoreTypes.swift b/WordPressKit/JetpackRestoreTypes.swift index 31f3d2d7..bb81b967 100644 --- a/WordPressKit/JetpackRestoreTypes.swift +++ b/WordPressKit/JetpackRestoreTypes.swift @@ -1,12 +1,26 @@ import Foundation -public struct JetpackRestoreTypes: Decodable { - public let themes: Bool - public let plugins: Bool - public let uploads: Bool - public let sqls: Bool - public let roots: Bool - public let contents: Bool +public struct JetpackRestoreTypes { + public var themes: Bool + public var plugins: Bool + public var uploads: Bool + public var sqls: Bool + public var roots: Bool + public var contents: Bool + + public init(themes: Bool = true, + plugins: Bool = true , + uploads: Bool = true, + sqls: Bool = true, + roots: Bool = true, + contents: Bool = true) { + self.themes = themes + self.plugins = plugins + self.uploads = uploads + self.sqls = sqls + self.roots = roots + self.contents = contents + } func toDictionary() -> [String: AnyObject] { return [ diff --git a/WordPressKit/JetpackScanServiceRemote.swift b/WordPressKit/JetpackScanServiceRemote.swift index 1f82f1cd..d8690ac1 100644 --- a/WordPressKit/JetpackScanServiceRemote.swift +++ b/WordPressKit/JetpackScanServiceRemote.swift @@ -4,6 +4,7 @@ import CocoaLumberjack public class JetpackScanServiceRemote: ServiceRemoteWordPressComREST { + // MARK: - Scanning public func getScanAvailableForSite(_ siteID: Int, success: @escaping(Bool) -> Void, failure: @escaping(Error) -> Void) { getScanForSite(siteID, success: { (scan) in success(scan.isEnabled) @@ -16,12 +17,6 @@ public class JetpackScanServiceRemote: ServiceRemoteWordPressComREST { }, failure: failure) } - public func getThreatsForSite(_ siteID: Int, success: @escaping([JetpackScanThreat]?) -> Void, failure: @escaping(Error) -> Void) { - getScanForSite(siteID, success: { scan in - success(scan.threats) - }, failure: failure) - } - /// Starts a scan for a site public func startScanForSite(_ siteID: Int, success: @escaping(Bool) -> Void, failure: @escaping(Error) -> Void) { let path = self.scanPath(for: siteID, with: "enqueue") @@ -58,6 +53,73 @@ public class JetpackScanServiceRemote: ServiceRemoteWordPressComREST { }) } + + // MARK: - Threats + public enum ThreatError: Swift.Error { + case invalidResponse + } + + public func getThreatsForSite(_ siteID: Int, success: @escaping([JetpackScanThreat]?) -> Void, failure: @escaping(Error) -> Void) { + getScanForSite(siteID, success: { scan in + success(scan.threats) + }, failure: failure) + } + + /// Begins the fix process for multiple threats + public func fixThreats(_ threats: [JetpackScanThreat], siteID: Int, success: @escaping(JetpackThreatFixResponse) -> Void, failure: @escaping(Error) -> Void) { + let path = self.path(forEndpoint: "sites/\(siteID)/alerts/fix", withVersion: ._2_0) + let parameters = ["threat_ids": threats.map { $0.id as AnyObject }] as [String: AnyObject] + + wordPressComRestApi.POST(path, parameters: parameters, success: { (response, _) in + do { + let decoder = JSONDecoder.apiDecoder + let data = try JSONSerialization.data(withJSONObject: response, options: []) + let envelope = try decoder.decode(JetpackThreatFixResponse.self, from: data) + + success(envelope) + } catch { + failure(error) + } + }, failure: { (error, _) in + failure(error) + }) + } + + + /// Begins the fix process for a single threat + public func fixThreat(_ threat: JetpackScanThreat, siteID: Int, success: @escaping(JetpackThreatFixStatus) -> Void, failure: @escaping(Error) -> Void) { + fixThreats([threat], siteID: siteID, success: { response in + guard let status = response.threats.first else { + failure(ThreatError.invalidResponse) + return + } + + success(status) + }, failure: { error in + failure(error) + }) + } + + /// Returns the fix status for multiple threats + public func getFixStatusForThreats(_ threats: [JetpackScanThreat], siteID: Int, success: @escaping(JetpackThreatFixResponse) -> Void, failure: @escaping(Error) -> Void) { + let path = self.path(forEndpoint: "sites/\(siteID)/alerts/fix", withVersion: ._2_0) + let parameters = ["threat_ids": threats.map { $0.id as AnyObject }] as [String: AnyObject] + + wordPressComRestApi.GET(path, parameters: parameters, success: { (response, _) in + do { + let decoder = JSONDecoder.apiDecoder + let data = try JSONSerialization.data(withJSONObject: response, options: []) + let envelope = try decoder.decode(JetpackThreatFixResponse.self, from: data) + + success(envelope) + } catch { + failure(error) + } + }, failure: { (error, _) in + failure(error) + }) + } + // MARK: - Private private func scanPath(for siteID: Int, with path: String? = nil) -> String { var endpoint = "sites/\(siteID)/scan/" @@ -68,6 +130,4 @@ public class JetpackScanServiceRemote: ServiceRemoteWordPressComREST { return self.path(forEndpoint: endpoint, withVersion: ._2_0) } - - } diff --git a/WordPressKit/ReaderPostServiceRemote+V2.swift b/WordPressKit/ReaderPostServiceRemote+V2.swift index 34ba0438..a8004179 100644 --- a/WordPressKit/ReaderPostServiceRemote+V2.swift +++ b/WordPressKit/ReaderPostServiceRemote+V2.swift @@ -44,4 +44,53 @@ extension ReaderPostServiceRemote { return self.path(forEndpoint: endpoint, withVersion: ._2_0) } + + + /// Sets the `is_seen` status for a given post. + /// + /// - Parameter seen: the post is to be marked seen or not (unseen) + /// - Parameter feedID: feedID of the ReaderPost + /// - Parameter feedItemID: feedItemID of the ReaderPost + /// - Parameter success: Called when the request succeeds + /// - Parameter failure: Called when the request fails + @objc + public func markPostSeen(seen: Bool, + feedID: NSNumber, + feedItemID: NSNumber, + success: @escaping (() -> Void), + failure: @escaping ((Error) -> Void)) { + + let endpoint = seen ? SeenEndpoints.seen : SeenEndpoints.unseen + let path = self.path(forEndpoint: endpoint, withVersion: ._2_0) + + let params = [ + "feed_id": feedID, + "feed_item_ids": [feedItemID], + "source": "reader-ios" + ] as [String: AnyObject] + + wordPressComRestApi.POST(path, parameters: params, success: { (responseObject, httpResponse) in + guard let response = responseObject as? [String: AnyObject], + let status = response["status"] as? Bool, + status == true else { + failure(MarkSeenError.failed) + return + } + success() + }, failure: { (error, httpResponse) in + failure(error) + }) + } + + private enum MarkSeenError: Error { + case failed + } + + private struct SeenEndpoints { + // Creates a new `seen` entry (i.e. mark as seen) + static let seen = "seen-posts/seen/new" + // Removes the `seen` entry (i.e. mark as unseen) + static let unseen = "seen-posts/seen/delete" + } + } diff --git a/WordPressKit/RemoteReaderPost.h b/WordPressKit/RemoteReaderPost.h index aa625eec..9856430f 100644 --- a/WordPressKit/RemoteReaderPost.h +++ b/WordPressKit/RemoteReaderPost.h @@ -27,6 +27,7 @@ @property (nonatomic) BOOL isLiked; @property (nonatomic) BOOL isReblogged; @property (nonatomic) BOOL isWPCom; +@property (nonatomic) BOOL isSeen; @property (nonatomic, strong) NSNumber *likeCount; @property (nonatomic, strong) NSNumber *score; @property (nonatomic, strong) NSNumber *siteID; diff --git a/WordPressKit/RemoteReaderPost.m b/WordPressKit/RemoteReaderPost.m index 935deed6..e664a168 100644 --- a/WordPressKit/RemoteReaderPost.m +++ b/WordPressKit/RemoteReaderPost.m @@ -32,6 +32,7 @@ NSString * const PostRESTKeyIsFollowing = @"is_following"; NSString * const PostRESTKeyIsJetpack = @"is_jetpack"; NSString * const PostRESTKeyIsReblogged = @"is_reblogged"; +NSString * const PostRESTKeyIsSeen = @"is_seen"; NSString * const PostRESTKeyLikeCount = @"like_count"; NSString * const PostRESTKeyLikesEnabled = @"likes_enabled"; NSString * const PostRESTKeyName = @"name"; @@ -110,6 +111,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict; self.isFollowing = [[dict numberForKey:PostRESTKeyIsFollowing] boolValue]; self.isLiked = [[dict numberForKey:PostRESTKeyILike] boolValue]; self.isReblogged = [[dict numberForKey:PostRESTKeyIsReblogged] boolValue]; + self.isSeen = [[dict numberForKey:PostRESTKeyIsSeen] boolValue]; self.isWPCom = [self isWPComFromPostDictionary:dict]; self.likeCount = [dict numberForKey:PostRESTKeyLikeCount]; self.permalink = [self stringOrEmptyString:[dict stringForKey:PostRESTKeyURL]]; diff --git a/WordPressKitTests/JetpackCapabilitiesServiceRemoteTests.swift b/WordPressKitTests/JetpackCapabilitiesServiceRemoteTests.swift new file mode 100644 index 00000000..2085b0ec --- /dev/null +++ b/WordPressKitTests/JetpackCapabilitiesServiceRemoteTests.swift @@ -0,0 +1,48 @@ +import XCTest + +@testable import WordPressKit + +class JetpackCapabilitiesServiceRemoteTests: RemoteTestCase, RESTTestable { + let mockRemoteApi = MockWordPressComRestApi() + var service: JetpackCapabilitiesServiceRemote! + + override func setUp() { + super.setUp() + + service = JetpackCapabilitiesServiceRemote(wordPressComRestApi: getRestApi()) + } + + /// Return the capabilities for the given siteIDs + func testSuccessCapabilities() { + let expect = expectation(description: "Get the available capabilities") + stubRemoteResponse("wpcom/v2/sites/34197361/rewind/capabilities", filename: "jetpack-capabilities-34197361-success.json", contentType: .ApplicationJSON) + stubRemoteResponse("wpcom/v2/sites/107159616/rewind/capabilities", filename: "jetpack-capabilities-107159616-success.json", contentType: .ApplicationJSON) + + service.for(siteIds: [34197361, 107159616], success: { capabilities in + XCTAssertTrue(capabilities.count == 2) + XCTAssertTrue((capabilities["34197361"] as? [String])!.isEmpty) + XCTAssertTrue((capabilities["107159616"] as? [String])!.contains("backup")) + XCTAssertTrue((capabilities["107159616"] as? [String])!.contains("backup-realtime")) + XCTAssertTrue((capabilities["107159616"] as? [String])!.contains("scan")) + XCTAssertTrue((capabilities["107159616"] as? [String])!.contains("antispam")) + expect.fulfill() + }) + + waitForExpectations(timeout: timeout, handler: nil) + } + + /// When a single request fails, the associated capabilities are not returned + func testSingleRequestFails() { + let expect = expectation(description: "Get the available capabilities") + stubRemoteResponse("wpcom/v2/sites/34197361/rewind/capabilities", filename: "jetpack-capabilities-34197361-success.json", contentType: .ApplicationJSON) + stubRemoteResponse("wpcom/v2/sites/107159616/rewind/capabilities", filename: "jetpack-capabilities-malformed.json", contentType: .ApplicationJSON) + + service.for(siteIds: [34197361, 107159616], success: { capabilities in + XCTAssertTrue(capabilities.count == 1) + expect.fulfill() + }) + + waitForExpectations(timeout: timeout, handler: nil) + } + +} diff --git a/WordPressKitTests/Mock Data/jetpack-capabilities-107159616-success.json b/WordPressKitTests/Mock Data/jetpack-capabilities-107159616-success.json new file mode 100644 index 00000000..1333c868 --- /dev/null +++ b/WordPressKitTests/Mock Data/jetpack-capabilities-107159616-success.json @@ -0,0 +1,8 @@ +{ + "capabilities": [ + "backup", + "backup-realtime", + "scan", + "antispam" + ] +} diff --git a/WordPressKitTests/Mock Data/jetpack-capabilities-34197361-success.json b/WordPressKitTests/Mock Data/jetpack-capabilities-34197361-success.json new file mode 100644 index 00000000..b2cff73f --- /dev/null +++ b/WordPressKitTests/Mock Data/jetpack-capabilities-34197361-success.json @@ -0,0 +1,3 @@ +{ + "capabilities": [] +} diff --git a/WordPressKitTests/Mock Data/jetpack-capabilities-malformed.json b/WordPressKitTests/Mock Data/jetpack-capabilities-malformed.json new file mode 100644 index 00000000..a106c2f9 --- /dev/null +++ b/WordPressKitTests/Mock Data/jetpack-capabilities-malformed.json @@ -0,0 +1,3 @@ +{ + malformed +}