From df2dffb02b0463960a89387fc9e04c36f120eb19 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 28 Jan 2021 12:04:45 +0100 Subject: [PATCH 1/3] Include parameters in multipart POST request --- WordPressKit/WordPressComRestApi.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/WordPressKit/WordPressComRestApi.swift b/WordPressKit/WordPressComRestApi.swift index 402a9627..10c060ad 100644 --- a/WordPressKit/WordPressComRestApi.swift +++ b/WordPressKit/WordPressComRestApi.swift @@ -290,11 +290,11 @@ open class WordPressComRestApi: NSObject { } /** - Executes a multipart POST using the current serializer, the parameters defined and the fileParts defined in the request + Executes a multipart POST to the specified endpoint defined on URLString, including the parameters and the fileParts defined in the request. This request will be streamed from disk, so it's ideally to be used for large media post uploads. - parameter URLString: the endpoint to connect - - parameter parameters: the parameters to use on the request + - parameter parameters: the parameters to use on the request (only String and Int parameters will be included) - parameter fileParts: the file parameters that are added to the multipart request - parameter requestEnqueued: callback to be called when the fileparts are serialized and request is added to the background session. Defaults to nil - parameter success: callback to be called on successful request @@ -327,6 +327,15 @@ open class WordPressComRestApi: NSObject { for filePart in fileParts { multipartFormData.append(filePart.url, withName: filePart.parameterName, fileName: filePart.filename, mimeType: filePart.mimeType) } + + if let parameters = parameters { + for (key, value) in parameters { + if value is String || value is Int { + multipartFormData.append("\(value)".data(using: .utf8)!, withName: key) + } + } + } + }, to: URLString, encodingCompletion: { (encodingResult) in switch encodingResult { case .success(let upload, _, _): From 84775760becd4ff34febb9b6c55be4342574e037 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 29 Jan 2021 16:21:57 +0100 Subject: [PATCH 2/3] Add body parts to multipartPOST request FilePart has been merged into BodyPart, a more generic class to handle part objects for this type of requests. Additionally all functions that are using multipartPOST requests haven been updated. --- WordPressKit/MediaServiceRemoteREST.m | 50 ++++++----- WordPressKit/PostServiceRemoteREST.m | 15 ++-- WordPressKit/WordPressComRestApi.swift | 85 +++++++++++++------ .../MockWordPressComRestApi.swift | 2 +- .../WordPressComRestApiTests.swift | 12 +-- 5 files changed, 104 insertions(+), 60 deletions(-) diff --git a/WordPressKit/MediaServiceRemoteREST.m b/WordPressKit/MediaServiceRemoteREST.m index 030065e0..95bd0b99 100644 --- a/WordPressKit/MediaServiceRemoteREST.m +++ b/WordPressKit/MediaServiceRemoteREST.m @@ -131,22 +131,24 @@ - (void)uploadMedia:(NSArray *)mediaItems NSString *apiPath = [NSString stringWithFormat:@"sites/%@/media/new", self.siteID]; NSString *requestUrl = [self pathForEndpoint:apiPath withVersion:ServiceRemoteWordPressComRESTApiVersion_1_1]; - NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:@{}]; - NSMutableArray *fileParts = [NSMutableArray array]; + + NSMutableArray *bodyParts = [NSMutableArray array]; for (RemoteMedia *remoteMedia in mediaItems) { NSString *type = remoteMedia.mimeType; NSString *filename = remoteMedia.file; - if (remoteMedia.postID != nil && [remoteMedia.postID compare:@(0)] == NSOrderedDescending) { - parameters[@"attrs[0][parent_id]"] = remoteMedia.postID; + NSNumber* postID = remoteMedia.postID; + if (postID != nil && [postID compare:@(0)] == NSOrderedDescending) { + BodyPart *parentIDPart = [[BodyPart alloc] initWithName:@"attrs[0][parent_id]" data:[NSData dataWithBytes:&postID length:sizeof(postID)]]; + [bodyParts addObject: parentIDPart]; } - FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:remoteMedia.localURL filename:filename mimeType:type]; - [fileParts addObject:filePart]; + BodyPart *mediaPart = [[BodyPart alloc] initWithName:@"media[]" url:remoteMedia.localURL fileName:filename mimeType:type]; + [bodyParts addObject:mediaPart]; } [self.wordPressComRestApi multipartPOST:requestUrl - parameters:parameters - fileParts:fileParts + parameters: nil + bodyParts:bodyParts requestEnqueued:^(NSNumber *taskID) { if (requestEnqueued) { requestEnqueued(taskID); @@ -193,8 +195,6 @@ - (void)uploadMedia:(RemoteMedia *)media NSString *requestUrl = [self pathForEndpoint:apiPath withVersion:ServiceRemoteWordPressComRESTApiVersion_1_1]; - NSDictionary *parameters = [self parametersForUploadMedia:media]; - if (media.localURL == nil || filename == nil || type == nil) { if (failure) { NSError *error = [NSError errorWithDomain:NSURLErrorDomain @@ -204,10 +204,11 @@ - (void)uploadMedia:(RemoteMedia *)media } return; } - FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:media.localURL filename:filename mimeType:type]; + + NSArray *bodyParts = [self bodyPartsForUploadMedia: media]; __block NSProgress *localProgress = [self.wordPressComRestApi multipartPOST:requestUrl - parameters:parameters - fileParts:@[filePart] + parameters:nil + bodyParts:bodyParts requestEnqueued:nil success:^(id _Nonnull responseObject, NSHTTPURLResponse * _Nullable httpResponse) { NSDictionary *response = (NSDictionary *)responseObject; @@ -402,18 +403,25 @@ - (NSDictionary *)parametersFromRemoteMedia:(RemoteMedia *)remoteMedia return [NSDictionary dictionaryWithDictionary:parameters]; } -- (NSDictionary *)parametersForUploadMedia:(RemoteMedia *)media +- (NSArray *)bodyPartsForUploadMedia:(RemoteMedia *)media { - NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; - - if (media.caption != nil) { - parameters[@"attrs[0][caption]"] = media.caption; + NSMutableArray *bodyParts = [NSMutableArray array]; + NSString *caption = media.caption; + NSNumber *postID = media.postID; + + if (caption != nil) { + BodyPart *captionPart = [[BodyPart alloc] initWithName:@"attrs[0][caption]" data:[caption dataUsingEncoding:NSUTF8StringEncoding]]; + [bodyParts addObject: captionPart]; } - if (media.postID != nil && [media.postID compare:@(0)] == NSOrderedDescending) { - parameters[@"attrs[0][parent_id]"] = media.postID; + if (postID != nil && [postID compare:@(0)] == NSOrderedDescending) { + BodyPart *parentIDPart = [[BodyPart alloc] initWithName:@"attrs[0][parent_id]" data:[NSData dataWithBytes:&postID length:sizeof(postID)]]; + [bodyParts addObject: parentIDPart]; } - return [NSDictionary dictionaryWithDictionary:parameters]; + BodyPart *mediaPart = [[BodyPart alloc] initWithName:@"media[]" url:media.localURL fileName:media.file mimeType:media.mimeType]; + [bodyParts addObject: mediaPart]; + + return bodyParts; } @end diff --git a/WordPressKit/PostServiceRemoteREST.m b/WordPressKit/PostServiceRemoteREST.m index 2e8bf1e6..15dc4ab7 100644 --- a/WordPressKit/PostServiceRemoteREST.m +++ b/WordPressKit/PostServiceRemoteREST.m @@ -158,14 +158,15 @@ - (void)createPost:(RemotePost *)post NSString *requestUrl = [self pathForEndpoint:path withVersion:ServiceRemoteWordPressComRESTApiVersion_1_2]; - NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:@{}]; - parameters[@"content"] = post.content; - parameters[@"title"] = post.title; - parameters[@"status"] = post.status; - FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:media.localURL filename:filename mimeType:type]; + + BodyPart *contentPart =[[BodyPart alloc] initWithName:@"content" data:[post.content dataUsingEncoding:NSUTF8StringEncoding]]; + BodyPart *titlePart = [[BodyPart alloc] initWithName:@"title" data:[post.content dataUsingEncoding:NSUTF8StringEncoding]]; + BodyPart *statusPart = [[BodyPart alloc] initWithName:@"status" data:[post.content dataUsingEncoding:NSUTF8StringEncoding]]; + BodyPart *mediaPart = [[BodyPart alloc] initWithName:@"media[]" url:media.localURL fileName:filename mimeType:type]; + [self.wordPressComRestApi multipartPOST:requestUrl - parameters:parameters - fileParts:@[filePart] + parameters:nil + bodyParts:@[contentPart, titlePart, statusPart, mediaPart] requestEnqueued:^(NSNumber *taskID) { if (requestEnqueued) { requestEnqueued(taskID); diff --git a/WordPressKit/WordPressComRestApi.swift b/WordPressKit/WordPressComRestApi.swift index 10c060ad..09bf9023 100644 --- a/WordPressKit/WordPressComRestApi.swift +++ b/WordPressKit/WordPressComRestApi.swift @@ -290,12 +290,12 @@ open class WordPressComRestApi: NSObject { } /** - Executes a multipart POST to the specified endpoint defined on URLString, including the parameters and the fileParts defined in the request. + Executes a multipart POST to the specified endpoint defined on URLString, including the parameters and bodyParts defined in the request. This request will be streamed from disk, so it's ideally to be used for large media post uploads. - parameter URLString: the endpoint to connect - - parameter parameters: the parameters to use on the request (only String and Int parameters will be included) - - parameter fileParts: the file parameters that are added to the multipart request + - parameter parameters: the parameters to use on the request + - parameter bodyParts: the body parameters that are added to the multipart request - parameter requestEnqueued: callback to be called when the fileparts are serialized and request is added to the background session. Defaults to nil - parameter success: callback to be called on successful request - parameter failure: callback to be called on failed request @@ -306,7 +306,7 @@ open class WordPressComRestApi: NSObject { */ @objc @discardableResult open func multipartPOST(_ URLString: String, parameters: [String: AnyObject]?, - fileParts: [FilePart], + bodyParts: [BodyPart], requestEnqueued: RequestEnqueuedBlock? = nil, success: @escaping SuccessResponseBlock, failure: @escaping FailureReponseBlock) -> Progress? { @@ -324,16 +324,8 @@ open class WordPressComRestApi: NSObject { } uploadSessionManager.upload(multipartFormData: { (multipartFormData) in - for filePart in fileParts { - multipartFormData.append(filePart.url, withName: filePart.parameterName, fileName: filePart.filename, mimeType: filePart.mimeType) - } - - if let parameters = parameters { - for (key, value) in parameters { - if value is String || value is Int { - multipartFormData.append("\(value)".data(using: .utf8)!, withName: key) - } - } + for part in bodyParts { + part.appendToFormData(multipartFormData) } }, to: URLString, encodingCompletion: { (encodingResult) in @@ -432,21 +424,64 @@ open class WordPressComRestApi: NSObject { } } -// MARK: - FilePart - -/// FilePart represents the infomartion needed to encode a file on a multipart form request -public final class FilePart: NSObject { - @objc let parameterName: String - @objc let url: URL - @objc let filename: String - @objc let mimeType: String +// MARK: - BodyPart - @objc public init(parameterName: String, url: URL, filename: String, mimeType: String) { - self.parameterName = parameterName +/// BodyPart represents the information needed to encode a part on a multipart form request +public final class BodyPart: NSObject { + @objc let name: String + @objc let data: Data? + @objc let url: URL? + @objc let fileName: String? + @objc let mimeType: String? + + @objc public init(name: String, data: Data) { + self.name = name + self.data = data + self.url = nil + self.fileName = nil + self.mimeType = nil + } + + @objc public init(name: String, url: URL) { + self.name = name + self.url = url + self.data = nil + self.fileName = nil + self.mimeType = nil + } + + @objc public init(name: String, url: URL, fileName: String?, mimeType: String?) { + self.name = name self.url = url - self.filename = filename + self.data = nil + self.fileName = fileName self.mimeType = mimeType } + + @objc public init(name: String, data: Data, fileName: String?, mimeType: String?) { + self.name = name + self.data = data + self.url = nil + self.fileName = fileName + self.mimeType = mimeType + } + + public func appendToFormData(_ multipartFormData: MultipartFormData) { + if let url = self.url { + if let fileName = self.fileName, let mimeType = self.mimeType { + multipartFormData.append(url, withName: self.name, fileName: fileName, mimeType: mimeType) + } else { + multipartFormData.append(url, withName: self.name) + } + } + else if let data = self.data { + if let fileName = self.fileName, let mimeType = self.mimeType { + multipartFormData.append(data, withName: self.name, fileName: fileName, mimeType: mimeType) + } else { + multipartFormData.append(data, withName: self.name) + } + } + } } // MARK: - Error processing diff --git a/WordPressKitTests/MockWordPressComRestApi.swift b/WordPressKitTests/MockWordPressComRestApi.swift index 3c783d39..8f6514fd 100644 --- a/WordPressKitTests/MockWordPressComRestApi.swift +++ b/WordPressKitTests/MockWordPressComRestApi.swift @@ -31,7 +31,7 @@ class MockWordPressComRestApi: WordPressComRestApi { override func multipartPOST(_ URLString: String, parameters: [String : AnyObject]?, - fileParts: [FilePart], + bodyParts: [BodyPart], requestEnqueued: RequestEnqueuedBlock? = nil, success: @escaping SuccessResponseBlock, failure: @escaping FailureReponseBlock) -> Progress? { diff --git a/WordPressKitTests/WordPressComRestApiTests.swift b/WordPressKitTests/WordPressComRestApiTests.swift index 486e7402..1f1fd061 100644 --- a/WordPressKitTests/WordPressComRestApiTests.swift +++ b/WordPressKitTests/WordPressComRestApiTests.swift @@ -187,7 +187,7 @@ class WordPressComRestApiTests: XCTestCase { } let expect = self.expectation(description: "One callback should be invoked") let api = WordPressComRestApi(oAuthToken: "fakeToken") - api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, fileParts: [], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in + api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, bodyParts: [], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in expect.fulfill() XCTFail("This call should fail") }, failure: { (error, httpResponse) in @@ -206,8 +206,8 @@ class WordPressComRestApiTests: XCTestCase { let expect = self.expectation(description: "One callback should be invoked") let api = WordPressComRestApi(oAuthToken: "fakeToken") - let filePart = FilePart(parameterName: "file", url: URL(fileURLWithPath: "/a.txt") as URL, filename: "a.txt", mimeType: "image/jpeg") - api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, fileParts: [filePart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in + let filePart = BodyPart(name: "file", url: URL(fileURLWithPath: "/a.txt") as URL, fileName: "a.txt", mimeType: "image/jpeg") + api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, bodyParts: [filePart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in expect.fulfill() XCTFail("This call should fail") }, failure: { (error, httpResponse) in @@ -230,8 +230,8 @@ class WordPressComRestApiTests: XCTestCase { let mediaURL = URL(fileURLWithPath: mediaPath) let expect = self.expectation(description: "One callback should be invoked") let api = WordPressComRestApi(oAuthToken: "fakeToken") - let filePart = FilePart(parameterName: "media[]", url: mediaURL as URL, filename: "test-image.jpg", mimeType: "image/jpeg") - let progress1 = api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, fileParts: [filePart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in + let mediaPart = BodyPart(name: "media[]", url: mediaURL as URL, fileName: "test-image.jpg", mimeType: "image/jpeg") + let progress1 = api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, bodyParts: [mediaPart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in XCTFail("This call should fail") }, failure: { (error, httpResponse) in print(error) @@ -240,7 +240,7 @@ class WordPressComRestApiTests: XCTestCase { } ) progress1?.cancel() - api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, fileParts: [filePart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in + api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, bodyParts: [mediaPart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in expect.fulfill() }, failure: { (error, httpResponse) in From d5c505c0dfe77c563a877e9598d138445f9163b4 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 9 Feb 2021 09:54:58 +0100 Subject: [PATCH 3/3] Update WordPressKit/PostServiceRemoteREST.m Co-authored-by: Ceyhun Ozugur --- WordPressKit/PostServiceRemoteREST.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPressKit/PostServiceRemoteREST.m b/WordPressKit/PostServiceRemoteREST.m index 15dc4ab7..c7910426 100644 --- a/WordPressKit/PostServiceRemoteREST.m +++ b/WordPressKit/PostServiceRemoteREST.m @@ -160,8 +160,8 @@ - (void)createPost:(RemotePost *)post BodyPart *contentPart =[[BodyPart alloc] initWithName:@"content" data:[post.content dataUsingEncoding:NSUTF8StringEncoding]]; - BodyPart *titlePart = [[BodyPart alloc] initWithName:@"title" data:[post.content dataUsingEncoding:NSUTF8StringEncoding]]; - BodyPart *statusPart = [[BodyPart alloc] initWithName:@"status" data:[post.content dataUsingEncoding:NSUTF8StringEncoding]]; + BodyPart *titlePart = [[BodyPart alloc] initWithName:@"title" data:[post.title dataUsingEncoding:NSUTF8StringEncoding]]; + BodyPart *statusPart = [[BodyPart alloc] initWithName:@"status" data:[post.status dataUsingEncoding:NSUTF8StringEncoding]]; BodyPart *mediaPart = [[BodyPart alloc] initWithName:@"media[]" url:media.localURL fileName:filename mimeType:type]; [self.wordPressComRestApi multipartPOST:requestUrl