Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7895ade
Inject siteID from `DefaultStoresManager` to `AlamofireNetwork`
itsmeichigo Dec 13, 2022
0099895
Add new request type for REST API requests
itsmeichigo Dec 13, 2022
3efefba
Configure application password use case in AlamofireNetwork
itsmeichigo Dec 13, 2022
f4e1519
Add new method to update headers for REST requests
itsmeichigo Dec 13, 2022
823607a
Update AlamofireNetwork to handle application password
itsmeichigo Dec 13, 2022
583d756
Use Task only for fetching application password
itsmeichigo Dec 14, 2022
763ba7f
Add a fallback Jetpack request to REST request to trigger when applic…
itsmeichigo Dec 14, 2022
168a6ac
Fix line limit violation
itsmeichigo Dec 14, 2022
9b0d019
Update comment for RESTRequest initializer
itsmeichigo Dec 14, 2022
17cae6a
Inject the complete use case to AlamofireNetwork from AuthenticatedState
itsmeichigo Dec 14, 2022
5e07911
Update comments for AlamofireNetwork
itsmeichigo Dec 14, 2022
85c31f8
Revert "Inject the complete use case to AlamofireNetwork from Authent…
itsmeichigo Dec 14, 2022
9740cbb
Separate authentication logic to a new class
itsmeichigo Dec 14, 2022
cc87a50
Make ApplicationPasswordUseCase injectable
itsmeichigo Dec 14, 2022
ad69556
Add unit tests for RequestAuthenticator
itsmeichigo Dec 14, 2022
4b5423e
Update comments for RESTRequest
itsmeichigo Dec 14, 2022
3ede699
Fix incorrect username and password when authenticating rest request
itsmeichigo Dec 15, 2022
d1ea84c
Reformat code for RequestAuthenticator
itsmeichigo Dec 15, 2022
139348b
Remove fallback request and header from
itsmeichigo Dec 16, 2022
b7942a5
Update JetpackRequest to be able to converted to a REST request
itsmeichigo Dec 16, 2022
63a76f6
Update RequestAuthenticator to return error if application password c…
itsmeichigo Dec 16, 2022
012d6c8
Revert changes to AuthenticatedState and DefaultStoresManager
itsmeichigo Dec 16, 2022
f779ff2
Fix line length violation
itsmeichigo Dec 16, 2022
c87f274
Keep `network` local in AuthenticatedState intializer
itsmeichigo Dec 16, 2022
1ef0e5b
Merge branch 'trunk' into feat/8389-network-update-for-rest-api
itsmeichigo Dec 19, 2022
fdf9566
Update unit tests for RequestAuthenticator
itsmeichigo Dec 19, 2022
498cfe8
Merge branch 'trunk' into feat/8389-network-update-for-rest-api
itsmeichigo Dec 20, 2022
ab3a53b
Update RequestAuthenticator to set up application password usecase in…
itsmeichigo Dec 20, 2022
55d4fc5
Merge branch 'trunk' into feat/8389-network-update-for-rest-api
itsmeichigo Dec 21, 2022
f3fa7aa
Create application password use case in RequestAuthenticator
itsmeichigo Dec 21, 2022
aa6d8e9
Return completion when self is not found
itsmeichigo Dec 21, 2022
67b9316
Simplify condition checks for enum case
itsmeichigo Dec 21, 2022
e66619a
Rename createRESTRequest to asRESTRequest
itsmeichigo Dec 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,9 @@
DEC51AF92769A212009F3DF4 /* SystemStatus+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC51AF82769A212009F3DF4 /* SystemStatus+Settings.swift */; };
DEC51AFB2769C66B009F3DF4 /* SystemStatusMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC51AFA2769C66B009F3DF4 /* SystemStatusMapperTests.swift */; };
DEC51B02276AFB35009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC51B01276AFB34009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift */; };
DEFBA74E29485A7600C35BA9 /* RESTRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFBA74D29485A7600C35BA9 /* RESTRequest.swift */; };
DEFBA7542949CE6600C35BA9 /* RequestAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFBA7532949CE6600C35BA9 /* RequestAuthenticator.swift */; };
DEFBA7562949D17400C35BA9 /* RequestAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFBA7552949D17300C35BA9 /* RequestAuthenticatorTests.swift */; };
E12552C526385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12552C426385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift */; };
E137619929151C7400FD098F /* error-wp-rest-forbidden.json in Resources */ = {isa = PBXBuildFile; fileRef = E137619829151C7400FD098F /* error-wp-rest-forbidden.json */; };
E137619B2915222100FD098F /* WordPressApiValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E137619A2915222100FD098F /* WordPressApiValidatorTests.swift */; };
Expand Down Expand Up @@ -1482,6 +1485,9 @@
DEC51AF82769A212009F3DF4 /* SystemStatus+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemStatus+Settings.swift"; sourceTree = "<group>"; };
DEC51AFA2769C66B009F3DF4 /* SystemStatusMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemStatusMapperTests.swift; sourceTree = "<group>"; };
DEC51B01276AFB34009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemStatus+DropinMustUsePlugin.swift"; sourceTree = "<group>"; };
DEFBA74D29485A7600C35BA9 /* RESTRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRequest.swift; sourceTree = "<group>"; };
DEFBA7532949CE6600C35BA9 /* RequestAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestAuthenticator.swift; sourceTree = "<group>"; };
DEFBA7552949D17300C35BA9 /* RequestAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestAuthenticatorTests.swift; sourceTree = "<group>"; };
E12552C426385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelAddressValidationSuccess.swift; sourceTree = "<group>"; };
E137619829151C7400FD098F /* error-wp-rest-forbidden.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "error-wp-rest-forbidden.json"; sourceTree = "<group>"; };
E137619A2915222100FD098F /* WordPressApiValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressApiValidatorTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1716,6 +1722,7 @@
B518662320A099BF00037A38 /* AlamofireNetwork.swift */,
B518662620A09BCC00037A38 /* MockNetwork.swift */,
D87F6150226591E10031A13B /* NullNetwork.swift */,
DEFBA7532949CE6600C35BA9 /* RequestAuthenticator.swift */,
);
path = Network;
sourceTree = "<group>";
Expand Down Expand Up @@ -1927,6 +1934,7 @@
B557D9FF209754FF005962F4 /* JetpackRequest.swift */,
DE34051228BDCA5100CF0D97 /* WordPressOrgRequest.swift */,
029C9E5B291507A40013E5EE /* UnauthenticatedRequest.swift */,
DEFBA74D29485A7600C35BA9 /* RESTRequest.swift */,
);
path = Requests;
sourceTree = "<group>";
Expand Down Expand Up @@ -2400,6 +2408,7 @@
isa = PBXGroup;
children = (
B57B1E6621C916850046E764 /* NetworkErrorTests.swift */,
DEFBA7552949D17300C35BA9 /* RequestAuthenticatorTests.swift */,
);
path = Network;
sourceTree = "<group>";
Expand Down Expand Up @@ -3095,6 +3104,7 @@
CE132BBC223859710029DB6C /* ProductTag.swift in Sources */,
26650332261FFA1A0079A159 /* ProductAddOnEnvelope.swift in Sources */,
D88D5A47230BC838007B6E01 /* ProductReview.swift in Sources */,
DEFBA74E29485A7600C35BA9 /* RESTRequest.swift in Sources */,
456930A9264EB576009ED69D /* ShippingLabelCarriersAndRates.swift in Sources */,
741B950120EBC8A700DD6E2D /* OrderCouponLine.swift in Sources */,
020D07BA23D8542000FD9580 /* UploadableMedia.swift in Sources */,
Expand All @@ -3108,6 +3118,7 @@
020D07B823D852BB00FD9580 /* Media.swift in Sources */,
B5BB1D0C20A2050300112D92 /* DateFormatter+Woo.swift in Sources */,
743E84EE2217244C00FAC9D7 /* ShipmentTrackingListMapper.swift in Sources */,
DEFBA7542949CE6600C35BA9 /* RequestAuthenticator.swift in Sources */,
451A97E5260B631E0059D135 /* ShippingLabelPredefinedPackage.swift in Sources */,
BAB373722795A1FB00837B4A /* OrderTaxLine.swift in Sources */,
EE54C89F2947782E00A9BF61 /* ApplicationPasswordUseCase.swift in Sources */,
Expand Down Expand Up @@ -3514,6 +3525,7 @@
0212683524C046CB00F8A892 /* MockNetwork+Path.swift in Sources */,
68BD37B328D9B8BD00C2A517 /* CustomerRemoteTests.swift in Sources */,
B554FA932180C17200C54DFF /* NoteHashListMapperTests.swift in Sources */,
DEFBA7562949D17400C35BA9 /* RequestAuthenticatorTests.swift in Sources */,
CC07866526790B1100BA9AC1 /* ShippingLabelPurchaseMapperTests.swift in Sources */,
74002D6A2118B26100A63C19 /* SiteVisitStatsMapperTests.swift in Sources */,
743E84FA221742E300FAC9D7 /* ShipmentsRemoteTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,10 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase {
}
}

@MainActor
init(username: String,
password: String,
siteAddress: String,
network: Network? = nil) async throws {
network: Network? = nil) throws {
self.siteAddress = siteAddress
self.username = username

Expand Down
82 changes: 50 additions & 32 deletions Networking/Networking/Network/AlamofireNetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@ import Combine
import Foundation
import Alamofire


extension Alamofire.MultipartFormData: MultipartFormData {}

/// AlamofireWrapper: Encapsulates all of the Alamofire OP's
///
public class AlamofireNetwork: Network {
/// WordPress.com Credentials.
///
private let credentials: Credentials?

private let backgroundSessionManager: Alamofire.SessionManager

/// WordPress.com Credentials.
/// Authenticator to update requests authorization header if possible.
///
private let credentials: Credentials?
private let requestAuthenticator: RequestAuthenticator

public var session: URLSession { SessionManager.default.session }

/// Public Initializer
///
public required init(credentials: Credentials?) {
self.credentials = credentials
self.requestAuthenticator = RequestAuthenticator(credentials: credentials)

// A unique ID is included in the background session identifier so that the session does not get invalidated when the initializer is called multiple
// times (e.g. when logging in).
Expand All @@ -45,12 +48,17 @@ public class AlamofireNetwork: Network {
/// - Yes. We do the above because the Jetpack Tunnel endpoint doesn't properly relay the correct statusCode.
///
public func responseData(for request: URLRequestConvertible, completion: @escaping (Data?, Error?) -> Void) {
let request = createRequest(wrapping: request)

Alamofire.request(request)
.responseData { response in
completion(response.value, response.networkingError)
requestAuthenticator.authenticateRequest(request) { result in
switch result {
case .success(let request):
Alamofire.request(request)
.responseData { response in
completion(response.value, response.networkingError)
}
case .failure(let error):
completion(nil, error)
}
}
}

/// Executes the specified Network Request. Upon completion, the payload will be sent back to the caller as a Data instance.
Expand All @@ -63,10 +71,15 @@ public class AlamofireNetwork: Network {
/// - completion: Closure to be executed upon completion.
///
public func responseData(for request: URLRequestConvertible, completion: @escaping (Swift.Result<Data, Error>) -> Void) {
let request = createRequest(wrapping: request)

Alamofire.request(request).responseData { response in
completion(response.result.toSwiftResult())
requestAuthenticator.authenticateRequest(request) { result in
switch result {
case .success(let request):
Alamofire.request(request).responseData { response in
completion(response.result.toSwiftResult())
}
case .failure(let error):
completion(.failure(error))
}
}
}

Expand All @@ -79,26 +92,39 @@ public class AlamofireNetwork: Network {
/// - Parameter request: Request that should be performed.
/// - Returns: A publisher that emits the result of the given request.
public func responseDataPublisher(for request: URLRequestConvertible) -> AnyPublisher<Swift.Result<Data, Error>, Never> {
let request = createRequest(wrapping: request)

return Future() { promise in
Alamofire.request(request).responseData { response in
let result = response.result.toSwiftResult()
promise(Swift.Result.success(result))
self.requestAuthenticator.authenticateRequest(request) { result in
switch result {
case .success(let request):
Alamofire.request(request).responseData { response in
let result = response.result.toSwiftResult()
promise(.success(result))
}
case .failure(let error):
promise(.success(.failure(error)))
}
}
}.eraseToAnyPublisher()
}

public func uploadMultipartFormData(multipartFormData: @escaping (MultipartFormData) -> Void,
to request: URLRequestConvertible,
completion: @escaping (Data?, Error?) -> Void) {
let request = createRequest(wrapping: request)

backgroundSessionManager.upload(multipartFormData: multipartFormData, with: request) { (encodingResult) in
switch encodingResult {
case .success(let upload, _, _):
upload.responseData { response in
completion(response.value, response.error)
requestAuthenticator.authenticateRequest(request) { [weak self] result in
guard let self else {
return completion(nil, nil)
}
switch result {
case .success(let request):
self.backgroundSessionManager.upload(multipartFormData: multipartFormData, with: request) { (encodingResult) in
switch encodingResult {
case .success(let upload, _, _):
upload.responseData { response in
completion(response.value, response.error)
}
case .failure(let error):
completion(nil, error)
}
}
case .failure(let error):
completion(nil, error)
Expand All @@ -107,14 +133,6 @@ public class AlamofireNetwork: Network {
}
}

private extension AlamofireNetwork {
func createRequest(wrapping request: URLRequestConvertible) -> URLRequestConvertible {
credentials.map { AuthenticatedRequest(credentials: $0, request: request) } ??
UnauthenticatedRequest(request: request)
}
}


// MARK: - Alamofire.DataResponse: Helper Methods
//
extension Alamofire.DataResponse {
Expand Down
79 changes: 79 additions & 0 deletions Networking/Networking/Network/RequestAuthenticator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Alamofire
import Foundation

/// Helper class to update requests with authorization header if possible.
///
final class RequestAuthenticator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever work extracting this into a separate class. 👏

/// WordPress.com Credentials.
///
private let credentials: Credentials?

/// The use case to handle authentication with application passwords.
///
private let applicationPasswordUseCase: ApplicationPasswordUseCase?

/// Sets up the authenticator with optional credentials and application password use case.
/// `applicationPasswordUseCase` can be injected for unit tests.
///
init(credentials: Credentials?, applicationPasswordUseCase: ApplicationPasswordUseCase? = nil) {
self.credentials = credentials
let useCase: ApplicationPasswordUseCase? = {
if let applicationPasswordUseCase {
return applicationPasswordUseCase
} else if case let .wporg(username, password, siteAddress) = credentials {
return try? DefaultApplicationPasswordUseCase(username: username,
password: password,
siteAddress: siteAddress)
} else {
return nil
}
}()
self.applicationPasswordUseCase = useCase
}

/// Updates a request with application password or WPCOM token if possible.
///
func authenticateRequest(_ request: URLRequestConvertible, completion: @escaping (Swift.Result<URLRequestConvertible, Error>) -> Void) {
guard let jetpackRequest = request as? JetpackRequest,
let useCase = applicationPasswordUseCase,
case let .wporg(_, _, siteAddress) = credentials,
let restRequest = jetpackRequest.asRESTRequest(with: siteAddress) else {
// Handle non-REST requests as before
return completion(.success(authenticateUsingWPCOMTokenIfPossible(request)))
}

Task(priority: .medium) {
let result: Swift.Result<URLRequestConvertible, Error>
do {
let authenticatedRequest = try await authenticateUsingApplicationPassword(restRequest, useCase: useCase)
result = .success(authenticatedRequest)
} catch {
result = .failure(error)
}
await MainActor.run {
completion(result)
}
}
}

/// Attempts authenticating a request with application password.
///
private func authenticateUsingApplicationPassword(_ restRequest: RESTRequest, useCase: ApplicationPasswordUseCase) async throws -> URLRequestConvertible {
let applicationPassword: ApplicationPassword = try await {
if let password = useCase.applicationPassword {
return password
}
return try await useCase.generateNewPassword()
}()
return try await MainActor.run {
return try restRequest.authenticateRequest(with: applicationPassword)
}
}

/// Attempts creating a request with WPCOM token if possible.
///
private func authenticateUsingWPCOMTokenIfPossible(_ request: URLRequestConvertible) -> URLRequestConvertible {
credentials.map { AuthenticatedRequest(credentials: $0, request: request) } ??
UnauthenticatedRequest(request: request)
}
}
23 changes: 21 additions & 2 deletions Networking/Networking/Requests/JetpackRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ struct JetpackRequest: Request {
///
let parameters: [String: Any]

/// Whether this request should be transformed to a REST request if application password is available.
///
private let availableAsRESTRequest: Bool


/// Designated Initializer.
///
Expand All @@ -43,8 +47,15 @@ struct JetpackRequest: Request {
/// - siteID: Identifier of the Jetpack-Connected site we'll query.
/// - path: RPC that should be called.
/// - parameters: Collection of Key/Value parameters, to be forwarded to the Jetpack Connected site.
///
init(wooApiVersion: WooAPIVersion, method: HTTPMethod, siteID: Int64, locale: String? = nil, path: String, parameters: [String: Any]? = nil) {
/// - availableAsRESTRequest: Whether the request should be transformed to a REST request if application password is available.
///
init(wooApiVersion: WooAPIVersion,
method: HTTPMethod,
siteID: Int64,
locale: String? = nil,
path: String,
parameters: [String: Any]? = nil,
availableAsRESTRequest: Bool = false) {
if [.mark1, .mark2].contains(wooApiVersion) {
DDLogWarn("⚠️ You are using an older version of the Woo REST API: \(wooApiVersion.rawValue), for path: \(path)")
}
Expand All @@ -54,6 +65,7 @@ struct JetpackRequest: Request {
self.locale = locale
self.path = path
self.parameters = parameters ?? [:]
self.availableAsRESTRequest = availableAsRESTRequest
}


Expand All @@ -69,6 +81,13 @@ struct JetpackRequest: Request {
func responseDataValidator() -> ResponseDataValidator {
return DotcomValidator()
}

func asRESTRequest(with siteURL: String) -> RESTRequest? {
guard availableAsRESTRequest else {
return nil
}
return RESTRequest(siteURL: siteURL, method: method, path: path, parameters: parameters)
}
}


Expand Down
Loading