Skip to content

Commit 9efcea3

Browse files
committed
Merge branch 'trunk' into feat/8648-ipp-migration
2 parents f86a79d + 9726ccb commit 9efcea3

File tree

56 files changed

+1464
-320
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1464
-320
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<!--
22
Contains editorialized release notes. Raw release notes should go into `RELEASE-NOTES.txt`.
33
-->
4+
## 11.9
5+
Exciting news! It's possible to generate every variation combinations from your attributes. Please try it out and share your feedback!
6+
47
## 11.8
58
You’ll notice some nice visual changes to the look of the app! Take a look and please share your feedback – more updates to come in the next few weeks!
69

Hardware/Hardware/CardReader/UnderlyingError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ extension UnderlyingError: LocalizedError {
450450
"the device does not meet minimum requirements.")
451451
case .commandNotAllowedDuringCall:
452452
return NSLocalizedString("The built-in reader cannot be used during a phone call. Please try again after " +
453-
"you finish your call",
453+
"you finish your call.",
454454
comment: "Error message shown when the built-in reader cannot be used because " +
455455
"there is a call in progress")
456456
case .invalidAmount:

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 32 additions & 12 deletions
Large diffs are not rendered by default.

Networking/Networking/ApplicationPassword/RequestAuthenticator.swift renamed to Networking/Networking/ApplicationPassword/ApplicationPasswordAuthenticator.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
enum RequestAuthenticatorError: Error {
1+
enum ApplicationPasswordAuthenticatorError: Error {
22
case applicationPasswordUseCaseNotAvailable
33
case applicationPasswordNotAvailable
44
}
55

6-
protocol RequestAuthenticator {
6+
protocol ApplicationPasswordAuthenticator {
77
/// Credentials to authenticate the URLRequest
88
///
99
var credentials: Credentials? { get }
@@ -26,7 +26,7 @@ protocol RequestAuthenticator {
2626

2727
/// Authenticates request
2828
///
29-
public struct DefaultRequestAuthenticator: RequestAuthenticator {
29+
public struct DefaultApplicationPasswordAuthenticator: ApplicationPasswordAuthenticator {
3030
/// Credentials to authenticate the URLRequest
3131
///
3232
let credentials: Credentials?
@@ -71,7 +71,7 @@ public struct DefaultRequestAuthenticator: RequestAuthenticator {
7171
///
7272
func generateApplicationPassword() async throws {
7373
guard let applicationPasswordUseCase else {
74-
throw RequestAuthenticatorError.applicationPasswordUseCaseNotAvailable
74+
throw ApplicationPasswordAuthenticatorError.applicationPasswordUseCaseNotAvailable
7575
}
7676
let _ = try await applicationPasswordUseCase.generateNewPassword()
7777
return
@@ -84,7 +84,7 @@ public struct DefaultRequestAuthenticator: RequestAuthenticator {
8484
}
8585
}
8686

87-
private extension DefaultRequestAuthenticator {
87+
private extension DefaultApplicationPasswordAuthenticator {
8888
/// To check whether the given URLRequest is a REST API request
8989
///
9090
func isRestAPIRequest(_ urlRequest: URLRequest) -> Bool {
@@ -110,7 +110,7 @@ private extension DefaultRequestAuthenticator {
110110
///
111111
func authenticateUsingApplicationPasswordIfPossible(_ urlRequest: URLRequest) throws -> URLRequest {
112112
guard let applicationPassword = applicationPasswordUseCase?.applicationPassword else {
113-
throw RequestAuthenticatorError.applicationPasswordNotAvailable
113+
throw ApplicationPasswordAuthenticatorError.applicationPasswordNotAvailable
114114
}
115115

116116
return AuthenticatedRESTRequest(applicationPassword: applicationPassword, request: urlRequest).asURLRequest()

Networking/Networking/ApplicationPassword/RequestProcessor.swift renamed to Networking/Networking/ApplicationPassword/ApplicationPasswordRequestProcessor.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,29 @@ import Foundation
33

44
/// Authenticates and retries requests
55
///
6-
final class RequestProcessor {
6+
final class ApplicationPasswordRequestProcessor {
77
private var requestsToRetry = [RequestRetryCompletion]()
88

99
private var isAuthenticating = false
1010

11-
private let requestAuthenticator: RequestAuthenticator
11+
private let requestAuthenticator: ApplicationPasswordAuthenticator
1212

13-
init(requestAuthenticator: RequestAuthenticator) {
13+
init(requestAuthenticator: ApplicationPasswordAuthenticator) {
1414
self.requestAuthenticator = requestAuthenticator
1515
}
1616
}
1717

1818
// MARK: Request Authentication
1919
//
20-
extension RequestProcessor: RequestAdapter {
20+
extension ApplicationPasswordRequestProcessor: RequestAdapter {
2121
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
2222
return try requestAuthenticator.authenticate(urlRequest)
2323
}
2424
}
2525

2626
// MARK: Retrying Request
2727
//
28-
extension RequestProcessor: RequestRetrier {
28+
extension ApplicationPasswordRequestProcessor: RequestRetrier {
2929
func should(_ manager: Alamofire.SessionManager,
3030
retry request: Alamofire.Request,
3131
with error: Error,
@@ -48,7 +48,7 @@ extension RequestProcessor: RequestRetrier {
4848

4949
// MARK: Helpers
5050
//
51-
private extension RequestProcessor {
51+
private extension ApplicationPasswordRequestProcessor {
5252
func generateApplicationPassword() {
5353
Task(priority: .medium) {
5454
isAuthenticating = true
@@ -66,7 +66,7 @@ private extension RequestProcessor {
6666

6767
func shouldRetry(_ error: Error) -> Bool {
6868
// Need to generate application password
69-
if .applicationPasswordNotAvailable == error as? RequestAuthenticatorError {
69+
if .applicationPasswordNotAvailable == error as? ApplicationPasswordAuthenticatorError {
7070
return true
7171
}
7272

Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,11 @@ final public class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase
8484
throw ApplicationPasswordUseCaseError.failedToConstructLoginOrAdminURLUsingSiteAddress
8585
}
8686
// Prepares the authenticator with username and password
87-
let authenticator = CookieNonceAuthenticator(username: username,
88-
password: password,
89-
loginURL: loginURL,
90-
adminURL: adminURL,
91-
version: Constants.defaultWPVersion,
92-
nonce: nil)
93-
self.network = WordPressOrgNetwork(authenticator: authenticator)
87+
let config = CookieNonceAuthenticatorConfiguration(username: username,
88+
password: password,
89+
loginURL: loginURL,
90+
adminURL: adminURL)
91+
self.network = WordPressOrgNetwork(configuration: config)
9492
}
9593
}
9694

@@ -218,6 +216,5 @@ private extension DefaultApplicationPasswordUseCase {
218216
enum Constants {
219217
static let loginPath = "/wp-login.php"
220218
static let adminPath = "/wp-admin/"
221-
static let defaultWPVersion = "5.6.0" // a default version that supports Ajax nonce retrieval
222219
}
223220
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import Alamofire
2+
import Foundation
3+
4+
/// An authenticator to handle cookie-nonce authentication.
5+
/// This differs from WordPressKit's version by handling the nonce retrieval as a separate request
6+
/// instead of a redirect from the login request - to fix issues with Pressable sites.
7+
///
8+
/// This authenticator uses Ajax nonce retrieval method by default
9+
/// since we are not supporting sites with WP versions earlier than 5.6.0.
10+
///
11+
final class CookieNonceAuthenticator: RequestRetrier & RequestAdapter {
12+
private let username: String
13+
private let password: String
14+
private let loginURL: URL
15+
private let adminURL: URL
16+
private var nonce: String?
17+
18+
private var canRetry = true
19+
private var isAuthenticating = false
20+
private var requestsToRetry = [RequestRetryCompletion]()
21+
22+
init(configuration: CookieNonceAuthenticatorConfiguration) {
23+
self.username = configuration.username
24+
self.password = configuration.password
25+
self.loginURL = configuration.loginURL
26+
self.adminURL = configuration.adminURL
27+
}
28+
29+
// MARK: Request Adapter
30+
31+
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
32+
guard let nonce else {
33+
return urlRequest
34+
}
35+
var adaptedRequest = urlRequest
36+
adaptedRequest.addValue(nonce, forHTTPHeaderField: "X-WP-Nonce")
37+
return adaptedRequest
38+
}
39+
40+
// MARK: Retrier
41+
func should(_ manager: SessionManager, retry request: Alamofire.Request, with error: Swift.Error, completion: @escaping RequestRetryCompletion) {
42+
guard
43+
canRetry,
44+
// Only retry once
45+
request.retryCount == 0,
46+
// And don't retry the login request
47+
request.request?.url != loginURL,
48+
// Only retry because of failed authorization
49+
case .responseValidationFailed(reason: .unacceptableStatusCode(code: 401)) = error as? AFError
50+
else {
51+
return completion(false, 0.0)
52+
}
53+
54+
requestsToRetry.append(completion)
55+
if !isAuthenticating {
56+
startLoginSequence(manager: manager)
57+
}
58+
}
59+
60+
enum Error: Swift.Error {
61+
case invalidNewPostURL
62+
case postLoginFailed(Swift.Error)
63+
case missingNonce
64+
case unknown(Swift.Error)
65+
}
66+
}
67+
68+
// MARK: Private helpers
69+
private extension CookieNonceAuthenticator {
70+
71+
func startLoginSequence(manager: SessionManager) {
72+
DDLogInfo("Starting Cookie+Nonce login sequence for \(loginURL)")
73+
guard let nonceRetrievalURL = buildNonceRequestURL(base: adminURL),
74+
let nonceRequest = try? URLRequest(url: nonceRetrievalURL, method: .get) else {
75+
return invalidateLoginSequence(error: .invalidNewPostURL)
76+
}
77+
Task(priority: .medium) {
78+
do {
79+
try await handleSiteCredentialLogin(manager: manager)
80+
let page = try await handleNonceRetrieval(request: nonceRequest, manager: manager)
81+
guard let nonce = readNonceFromAjaxAction(html: page) else {
82+
throw CookieNonceAuthenticator.Error.missingNonce
83+
}
84+
self.nonce = nonce
85+
successfulLoginSequence()
86+
} catch let error as CookieNonceAuthenticator.Error {
87+
invalidateLoginSequence(error: error)
88+
} catch {
89+
DDLogError("⛔️ Cookie nonce authenticator failed with uncaught error: \(error)")
90+
}
91+
}
92+
}
93+
94+
func handleSiteCredentialLogin(manager: SessionManager) async throws {
95+
let request = authenticatedRequest()
96+
return try await withCheckedThrowingContinuation { continuation in
97+
manager.request(request)
98+
.validate()
99+
.response { response in
100+
if let error = response.error {
101+
continuation.resume(throwing: error)
102+
} else {
103+
continuation.resume(returning: ())
104+
}
105+
}
106+
}
107+
}
108+
109+
func handleNonceRetrieval(request: URLRequest, manager: SessionManager) async throws -> String {
110+
try await withCheckedThrowingContinuation { continuation -> Void in
111+
manager.request(request)
112+
.validate()
113+
.responseString { response in
114+
switch response.result {
115+
case .failure(let error):
116+
continuation.resume(throwing: error)
117+
case .success(let page):
118+
continuation.resume(returning: page)
119+
}
120+
}
121+
}
122+
}
123+
124+
func successfulLoginSequence() {
125+
DDLogInfo("Completed Cookie+Nonce login sequence for \(loginURL)")
126+
completeRequests(true)
127+
}
128+
129+
func invalidateLoginSequence(error: Error) {
130+
canRetry = false
131+
if case .postLoginFailed(let originalError) = error {
132+
let nsError = originalError as NSError
133+
if nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorNotConnectedToInternet {
134+
canRetry = true
135+
}
136+
}
137+
DDLogInfo("Aborting Cookie+Nonce login sequence for \(loginURL)")
138+
completeRequests(false)
139+
isAuthenticating = false
140+
}
141+
142+
func completeRequests(_ shouldRetry: Bool) {
143+
requestsToRetry.forEach { (completion) in
144+
completion(shouldRetry, 0.0)
145+
}
146+
requestsToRetry.removeAll()
147+
}
148+
149+
func authenticatedRequest() -> URLRequest {
150+
var request = URLRequest(url: loginURL)
151+
152+
request.httpMethod = "POST"
153+
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
154+
155+
var parameters = [URLQueryItem]()
156+
parameters.append(URLQueryItem(name: "log", value: username))
157+
parameters.append(URLQueryItem(name: "pwd", value: password))
158+
parameters.append(URLQueryItem(name: "rememberme", value: "true"))
159+
var components = URLComponents()
160+
components.queryItems = parameters
161+
request.httpBody = components.percentEncodedQuery?.data(using: .utf8)
162+
return request
163+
}
164+
165+
func readNonceFromAjaxAction(html: String) -> String? {
166+
html.isEmpty ? nil : html
167+
}
168+
169+
func buildNonceRequestURL(base: URL) -> URL? {
170+
URL(string: "admin-ajax.php?action=rest-nonce", relativeTo: base)
171+
}
172+
}

Networking/Networking/Model/Product/Product.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
465465
public func encode(to encoder: Encoder) throws {
466466
var container = encoder.container(keyedBy: CodingKeys.self)
467467

468+
try container.encode(productID, forKey: .productID)
469+
468470
try container.encode(images, forKey: .images)
469471

470472
try container.encode(name, forKey: .name)

Networking/Networking/Network/AlamofireNetwork.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ public class AlamofireNetwork: Network {
2828

2929
/// Authenticator to update requests authorization header if possible.
3030
///
31-
private let requestAuthenticator: RequestProcessor
31+
private let requestAuthenticator: ApplicationPasswordRequestProcessor
3232

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

3535
/// Public Initializer
3636
///
3737
public required init(credentials: Credentials?) {
3838
self.requestConverter = RequestConverter(credentials: credentials)
39-
self.requestAuthenticator = RequestProcessor(requestAuthenticator: DefaultRequestAuthenticator(credentials: credentials))
39+
self.requestAuthenticator = ApplicationPasswordRequestProcessor(requestAuthenticator: DefaultApplicationPasswordAuthenticator(credentials: credentials))
4040
}
4141

4242
/// Executes the specified Network Request. Upon completion, the payload will be sent back to the caller as a Data instance.

Networking/Networking/Network/WordPressOrgNetwork.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
import Alamofire
22
import Combine
33
import Foundation
4-
import WordPressKit
4+
5+
/// Configuration for handling cookie nonce authentication.
6+
///
7+
public struct CookieNonceAuthenticatorConfiguration {
8+
let username: String
9+
let password: String
10+
let loginURL: URL
11+
let adminURL: URL
12+
13+
public init(username: String, password: String, loginURL: URL, adminURL: URL) {
14+
self.username = username
15+
self.password = password
16+
self.loginURL = loginURL
17+
self.adminURL = adminURL
18+
}
19+
}
520

621
/// Class to handle WP.org REST API requests.
722
///
823
public final class WordPressOrgNetwork: Network {
924

10-
private let authenticator: Authenticator?
25+
private let authenticator: CookieNonceAuthenticator
1126
private let userAgent: String?
1227

1328
private lazy var sessionManager: Alamofire.SessionManager = {
@@ -27,8 +42,8 @@ public final class WordPressOrgNetwork: Network {
2742

2843
public var session: URLSession { sessionManager.session }
2944

30-
public init(authenticator: Authenticator? = nil, userAgent: String = UserAgent.defaultUserAgent) {
31-
self.authenticator = authenticator
45+
public init(configuration: CookieNonceAuthenticatorConfiguration, userAgent: String = UserAgent.defaultUserAgent) {
46+
self.authenticator = CookieNonceAuthenticator(configuration: configuration)
3247
self.userAgent = userAgent
3348
}
3449

0 commit comments

Comments
 (0)