Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit a398d95

Browse files
committed
Merge branch 'trunk' into wordpress-com-rest-api-3
2 parents 1168f2f + d068914 commit a398d95

11 files changed

+584
-181
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ _None._
4242

4343
### Bug Fixes
4444

45-
_None._
45+
- XMLRPC API progress is now always updated on the main thread. [#714]
4646

4747
### Internal Changes
4848

WordPressKit/AtomicLogs.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,11 @@ public class AtomicWebServerLogEntry: Decodable {
3333
public let httpUserAgent: String?
3434
public let requestTime: Double?
3535
public let requestType: String?
36-
public let requestURL: String?
36+
public let requestUrl: String?
3737
public let scheme: String?
3838
public let status: Int?
3939
public let timestamp: Int?
4040
public let type: String?
41-
public let userIP: String?
4241
}
4342

4443
public final class AtomicWebServerLogsResponse: Decodable {

WordPressKit/HTTPClient.swift

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ extension URLSession {
7979
if let parentProgress, parentProgress.totalUnitCount > parentProgress.completedUnitCount {
8080
let pending = parentProgress.totalUnitCount - parentProgress.completedUnitCount
8181
// The Jetpack/WordPress app requires task progress updates to be delievered on the main queue.
82-
let progressUpdator = parentProgress.update(totoalUnit: pending, with: task.progress, queue: .main)
82+
let progressUpdator = parentProgress.update(totalUnit: pending, with: task.progress, queue: .main)
8383

8484
parentProgress.cancellationHandler = { [weak task] in
8585
task?.cancel()
@@ -93,22 +93,21 @@ extension URLSession {
9393
for builder: HTTPRequestBuilder,
9494
completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void
9595
) throws -> URLSessionTask {
96-
var request = try builder.build(encodeMultipartForm: false)
97-
98-
// Use special `URLSession.uploadTask` API for multipart POST requests.
99-
if let multipart = builder.multipartForm, !multipart.isEmpty {
100-
let isBackgroundSession = configuration.identifier != nil
101-
102-
return try builder
103-
.encodeMultipartForm(request: &request, forceWriteToFile: isBackgroundSession)
104-
.map(
105-
left: {
106-
uploadTask(with: request, from: $0, completionHandler: completion)
107-
},
108-
right: {
109-
uploadTask(with: request, fromFile: $0, completionHandler: completion)
110-
}
111-
)
96+
var request = try builder.build(encodeBody: false)
97+
98+
let isBackgroundSession = configuration.identifier != nil
99+
let body = try builder.encodeMultipartForm(request: &request, forceWriteToFile: isBackgroundSession)
100+
?? builder.encodeXMLRPC(request: &request, forceWriteToFile: isBackgroundSession)
101+
if let body {
102+
// Use special `URLSession.uploadTask` API for multipart POST requests.
103+
return body.map(
104+
left: {
105+
uploadTask(with: request, from: $0, completionHandler: completion)
106+
},
107+
right: {
108+
uploadTask(with: request, fromFile: $0, completionHandler: completion)
109+
}
110+
)
112111
} else {
113112
// Use `URLSession.dataTask` for all other request
114113
return dataTask(with: request, completionHandler: completion)
@@ -194,12 +193,12 @@ extension WordPressAPIResult {
194193
}
195194

196195
extension Progress {
197-
func update(totoalUnit: Int64, with progress: Progress, queue: DispatchQueue) -> AnyCancellable {
196+
func update(totalUnit: Int64, with progress: Progress, queue: DispatchQueue) -> AnyCancellable {
198197
let start = self.completedUnitCount
199198
return progress.publisher(for: \.fractionCompleted, options: .new)
200199
.receive(on: queue)
201200
.sink { [weak self] fraction in
202-
self?.completedUnitCount = start + Int64(fraction * Double(totoalUnit))
201+
self?.completedUnitCount = start + Int64(fraction * Double(totalUnit))
203202
}
204203
}
205204
}

WordPressKit/HTTPRequestBuilder.swift

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import wpxmlrpc
23

34
/// A builder type that appends HTTP request data to a URL.
45
///
@@ -25,6 +26,7 @@ final class HTTPRequestBuilder {
2526
private var appendedQuery: [URLQueryItem] = []
2627
private var bodyBuilder: ((inout URLRequest) throws -> Void)?
2728
private(set) var multipartForm: [MultipartFormField]?
29+
private(set) var xmlrpcRequest: XMLRPCRequest?
2830

2931
init(url: URL) {
3032
assert(url.scheme == "http" || url.scheme == "https")
@@ -106,7 +108,7 @@ final class HTTPRequestBuilder {
106108
}
107109

108110
func body(json: @escaping () throws -> Data) -> Self {
109-
// 'charset' parmaeter is not required for json body. See https://www.rfc-editor.org/rfc/rfc8259.html#section-11
111+
// 'charset' parameter is not required for json body. See https://www.rfc-editor.org/rfc/rfc8259.html#section-11
110112
headers["Content-Type"] = "application/json"
111113
bodyBuilder = { req in
112114
req.httpBody = try json()
@@ -115,14 +117,14 @@ final class HTTPRequestBuilder {
115117
}
116118

117119
func body(xml: @escaping () throws -> Data) -> Self {
118-
headers["Content-Type"] = "application/xml; charset=utf-8"
120+
headers["Content-Type"] = "text/xml; charset=utf-8"
119121
bodyBuilder = { req in
120122
req.httpBody = try xml()
121123
}
122124
return self
123125
}
124126

125-
func build(encodeMultipartForm: Bool = false) throws -> URLRequest {
127+
func build(encodeBody: Bool = false) throws -> URLRequest {
126128
var components = original
127129

128130
var newPath = Self.join(components.percentEncodedPath, appendedPath)
@@ -156,13 +158,15 @@ final class HTTPRequestBuilder {
156158
request.addValue(value, forHTTPHeaderField: header)
157159
}
158160

159-
if encodeMultipartForm {
160-
let encoded = try self.encodeMultipartForm(request: &request, forceWriteToFile: false)
161-
switch encoded {
162-
case let .left(data):
163-
request.httpBody = data
164-
case let .right(url):
165-
request.httpBodyStream = InputStream(url: url)
161+
if encodeBody {
162+
let body = try encodeMultipartForm(request: &request, forceWriteToFile: false) ?? encodeXMLRPC(request: &request, forceWriteToFile: false)
163+
if let body {
164+
switch body {
165+
case let .left(data):
166+
request.httpBody = data
167+
case let .right(url):
168+
request.httpBodyStream = InputStream(url: url)
169+
}
166170
}
167171
}
168172

@@ -174,30 +178,48 @@ final class HTTPRequestBuilder {
174178
return request
175179
}
176180

177-
func encodeMultipartForm(request: inout URLRequest, forceWriteToFile: Bool) throws -> Either<Data, URL> {
181+
func encodeMultipartForm(request: inout URLRequest, forceWriteToFile: Bool) throws -> Either<Data, URL>? {
178182
guard let multipartForm, !multipartForm.isEmpty else {
179-
return .left(Data())
183+
return nil
180184
}
181185

182186
let boundery = String(format: "wordpresskit.%08x", Int.random(in: Int.min..<Int.max))
183187
request.setValue("multipart/form-data; boundary=\(boundery)", forHTTPHeaderField: "Content-Type")
184188
return try multipartForm
185189
.multipartFormDataStream(boundary: boundery, forceWriteToFile: forceWriteToFile)
190+
}
191+
192+
func encodeXMLRPC(request: inout URLRequest, forceWriteToFile: Bool) throws -> Either<Data, URL>? {
193+
guard let xmlrpcRequest else {
194+
return nil
195+
}
186196

197+
request.setValue("text/xml", forHTTPHeaderField: "Content-Type")
198+
let encoder = WPXMLRPCEncoder(method: xmlrpcRequest.method, andParameters: xmlrpcRequest.parameters)
199+
if forceWriteToFile {
200+
let fileName = "\(ProcessInfo.processInfo.globallyUniqueString)_file.xmlrpc"
201+
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
202+
try encoder.encode(toFile: fileURL.path)
203+
204+
var fileSize: AnyObject?
205+
try (fileURL as NSURL).getResourceValue(&fileSize, forKey: .fileSizeKey)
206+
if let fileSize = fileSize as? NSNumber {
207+
request.setValue(fileSize.stringValue, forHTTPHeaderField: "Content-Length")
208+
}
209+
210+
return .right(fileURL)
211+
} else {
212+
let data = try encoder.dataEncoded()
213+
request.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
214+
return try .left(encoder.dataEncoded())
215+
}
187216
}
188217
}
189218

190219
extension HTTPRequestBuilder {
191-
// FIXME: Not implemented yet
192-
func body(xmlrpc: Any /* XMLRPCRequest */) -> Self {
193-
body(xml: {
194-
fatalError("To be implemented")
195-
})
196-
}
197-
198-
// FIXME: Not implemented yet
199-
func appendXMLRPCArgument(value: Any) -> Self {
200-
fatalError("To be implemented")
220+
func body(xmlrpc method: String, parameters: [Any]? = nil) -> Self {
221+
self.xmlrpcRequest = XMLRPCRequest(method: method, parameters: parameters)
222+
return self
201223
}
202224
}
203225

@@ -279,3 +301,8 @@ extension Array where Element == URLQueryItem {
279301
}
280302

281303
}
304+
305+
struct XMLRPCRequest {
306+
var method: String
307+
var parameters: [Any]?
308+
}

WordPressKit/WordPressOrgXMLRPCApi.swift

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ open class WordPressOrgXMLRPCApi: NSObject {
77
public typealias SuccessResponseBlock = (AnyObject, HTTPURLResponse?) -> Void
88
public typealias FailureReponseBlock = (_ error: NSError, _ httpResponse: HTTPURLResponse?) -> Void
99

10+
public static var useURLSession = false
11+
1012
private let endpoint: URL
1113
private let userAgent: String?
1214
private var backgroundUploads: Bool
@@ -26,6 +28,13 @@ open class WordPressOrgXMLRPCApi: NSObject {
2628
///
2729
@objc public static let minimumSupportedVersion = "4.0"
2830

31+
private lazy var urlSession: URLSession = makeSession(configuration: .default)
32+
private lazy var uploadURLSession: URLSession = {
33+
backgroundUploads
34+
? makeSession(configuration: .background(withIdentifier: self.backgroundSessionIdentifier))
35+
: urlSession
36+
}()
37+
2938
private var _sessionManager: Alamofire.SessionManager?
3039
private var sessionManager: Alamofire.SessionManager {
3140
guard let sessionManager = _sessionManager else {
@@ -59,18 +68,32 @@ open class WordPressOrgXMLRPCApi: NSObject {
5968
sessionConfiguration.httpAdditionalHeaders = additionalHeaders
6069
let sessionManager = Alamofire.SessionManager(configuration: sessionConfiguration)
6170

62-
let sessionDidReceiveChallengeWithCompletion: ((URLSession, URLAuthenticationChallenge, @escaping(URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void) = { [weak self] session, authenticationChallenge, completionHandler in
63-
self?.urlSession(session, didReceive: authenticationChallenge, completionHandler: completionHandler)
71+
let sessionDidReceiveChallengeWithCompletion: ((URLSession, URLAuthenticationChallenge, @escaping(URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void) = { [sessionDelegate] session, authenticationChallenge, completionHandler in
72+
sessionDelegate.urlSession(session, didReceive: authenticationChallenge, completionHandler: completionHandler)
6473
}
6574
sessionManager.delegate.sessionDidReceiveChallengeWithCompletion = sessionDidReceiveChallengeWithCompletion
6675

67-
let taskDidReceiveChallengeWithCompletion: ((URLSession, URLSessionTask, URLAuthenticationChallenge, @escaping(URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void) = { [weak self] session, task, authenticationChallenge, completionHandler in
68-
self?.urlSession(session, task: task, didReceive: authenticationChallenge, completionHandler: completionHandler)
76+
let taskDidReceiveChallengeWithCompletion: ((URLSession, URLSessionTask, URLAuthenticationChallenge, @escaping(URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void) = { [sessionDelegate] session, task, authenticationChallenge, completionHandler in
77+
sessionDelegate.urlSession(session, task: task, didReceive: authenticationChallenge, completionHandler: completionHandler)
6978
}
7079
sessionManager.delegate.taskDidReceiveChallengeWithCompletion = taskDidReceiveChallengeWithCompletion
7180
return sessionManager
7281
}
7382

83+
private func makeSession(configuration sessionConfiguration: URLSessionConfiguration) -> URLSession {
84+
var additionalHeaders: [String: AnyObject] = ["Accept-Encoding": "gzip, deflate" as AnyObject]
85+
if let userAgent = self.userAgent {
86+
additionalHeaders["User-Agent"] = userAgent as AnyObject?
87+
}
88+
sessionConfiguration.httpAdditionalHeaders = additionalHeaders
89+
return URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
90+
}
91+
92+
// swiftlint:disable weak_delegate
93+
/// `URLSessionDelegate` for the URLSession instances in this class.
94+
private let sessionDelegate = SessionDelegate()
95+
// swiftlint:enable weak_delegate
96+
7497
/// Creates a new API object to connect to the WordPress XMLRPC API for the specified endpoint.
7598
///
7699
/// - Parameters:
@@ -96,16 +119,18 @@ open class WordPressOrgXMLRPCApi: NSObject {
96119
}
97120

98121
deinit {
99-
_sessionManager?.session.finishTasksAndInvalidate()
100-
_uploadSessionManager?.session.finishTasksAndInvalidate()
122+
for session in [urlSession, uploadURLSession, sessionManager.session, uploadSessionManager.session] {
123+
session.finishTasksAndInvalidate()
124+
}
101125
}
102126

103127
/**
104128
Cancels all ongoing and makes the session so the object will not fullfil any more request
105129
*/
106130
@objc open func invalidateAndCancelTasks() {
107-
sessionManager.session.invalidateAndCancel()
108-
uploadSessionManager.session.invalidateAndCancel()
131+
for session in [urlSession, uploadURLSession, sessionManager.session, uploadSessionManager.session] {
132+
session.invalidateAndCancel()
133+
}
109134
}
110135

111136
// MARK: - Network requests
@@ -155,14 +180,15 @@ open class WordPressOrgXMLRPCApi: NSObject {
155180
progress.totalUnitCount = requestProgress.totalUnitCount + 1
156181
progress.completedUnitCount = requestProgress.completedUnitCount
157182
}.response(queue: DispatchQueue.global()) { (response) in
158-
progress.completedUnitCount = progress.totalUnitCount
159183
do {
160184
let responseObject = try self.handleResponseWithData(response.data, urlResponse: response.response, error: response.error as NSError?)
161185
DispatchQueue.main.async {
186+
progress.completedUnitCount = progress.totalUnitCount
162187
success(responseObject, response.response)
163188
}
164189
} catch let error as NSError {
165190
DispatchQueue.main.async {
191+
progress.completedUnitCount = progress.totalUnitCount
166192
failure(error, response.response)
167193
}
168194
return
@@ -208,12 +234,13 @@ open class WordPressOrgXMLRPCApi: NSObject {
208234
}.response(queue: DispatchQueue.global()) { (response) in
209235
do {
210236
let responseObject = try self.handleResponseWithData(response.data, urlResponse: response.response, error: response.error as NSError?)
211-
progress.completedUnitCount = progress.totalUnitCount
212237
DispatchQueue.main.async {
238+
progress.completedUnitCount = progress.totalUnitCount
213239
success(responseObject, response.response)
214240
}
215241
} catch let error as NSError {
216242
DispatchQueue.main.async {
243+
progress.completedUnitCount = progress.totalUnitCount
217244
failure(error, response.response)
218245
}
219246
return
@@ -327,11 +354,13 @@ open class WordPressOrgXMLRPCApi: NSObject {
327354
}
328355
}
329356

330-
extension WordPressOrgXMLRPCApi {
357+
private class SessionDelegate: NSObject, URLSessionDelegate {
331358

332-
@objc public func urlSession(_ session: URLSession,
333-
didReceive challenge: URLAuthenticationChallenge,
334-
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
359+
@objc func urlSession(
360+
_ session: URLSession,
361+
didReceive challenge: URLAuthenticationChallenge,
362+
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
363+
) {
335364

336365
switch challenge.protectionSpace.authenticationMethod {
337366
case NSURLAuthenticationMethodServerTrust:
@@ -363,10 +392,12 @@ extension WordPressOrgXMLRPCApi {
363392
}
364393
}
365394

366-
@objc public func urlSession(_ session: URLSession,
367-
task: URLSessionTask,
368-
didReceive challenge: URLAuthenticationChallenge,
369-
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
395+
@objc func urlSession(
396+
_ session: URLSession,
397+
task: URLSessionTask,
398+
didReceive challenge: URLAuthenticationChallenge,
399+
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
400+
) {
370401

371402
switch challenge.protectionSpace.authenticationMethod {
372403
case NSURLAuthenticationMethodHTTPBasic:

0 commit comments

Comments
 (0)