Swift: padding error with base64 encoded string - swift

I'm trying to upload a file as an attachment to my Frappe instance and running into a couple of problems. The first of which is the server reporting a padding error.
I'm attempting to read a file (on iPad simulator) as data, then convert to a base64 encoded string, and then using this as part of my httpbody. I've tried this with a couple of different file types, but for the purpose of this example i'm just using a local settings file.
The Frappe instance is (sometimes depending on the data) returning the following error:
File \"/usr/lib/python3.7/base64.py\", line 87, in b64decode\n return binascii.a2b_base64(s)\nbinascii.Error: Incorrect padding
Get data from local URL:
let settingsLocation = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("SettingsList.plist")
let fileData = try Data.init(contentsOf: settingsLocation)
This is then passed to the following function:
public func attachFileToCloudResource(resourceType: String, resourceName: String, attachment: Data) {
let fileAsString = attachment.base64EncodedString().replacingOccurrences(of: "+", with: "%2B")
var request = URLRequest(url: URL(string: FRAPPE_INSTANCE + FRAPPE_METHODS + FRAPPE_UPLOAD_ATTACHMENT)!)
var components = URLComponents(url: request.url!, resolvingAgainstBaseURL: false)!
components.queryItems = [
URLQueryItem(name: FRAPPE_DOCTYPE, value: resourceType),
URLQueryItem(name: FRAPPE_DOCNAME, value: resourceName),
URLQueryItem(name: FRAPPE_FILENAME, value: "testFile.xml"),
URLQueryItem(name: FRAPPE_DATA, value: fileAsString),
URLQueryItem(name: FRAPPE_PRIVATE, value: "1"),
URLQueryItem(name: FRAPPE_DECODE_BASE64, value: "1")
]
let query = components.url!.query
request.httpMethod = "POST"
request.addValue("token \(API_KEY):\(API_SECRET)", forHTTPHeaderField: "Authorization")
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = Data(query!.utf8)
// Session and dataTask send request below but not relevant to example
}
After a bit of googling, I discovered I can resolve the error by appending "==" to the file string, but this feels nasty/wrong.
let fileAsString = (attachment.base64EncodedString().replacingOccurrences(of: "+", with: "%2B") + "==")
Can someone please point out where I might be going wrong and how to do this properly?

I'm not particularly happy with this answer, but its the one I used, and from the small number of xml and pdf files I have tried is working for me.
From what can work out, there is no Swift method that automatically generates the padding and it needs to be done manually.
I based my solution on the answers to this question and wrote an extension to Data to provide the padding:
extension Data {
func base64EncodedStringWithPadding() -> String {
let base64String = self.base64EncodedString()
let remainder = base64String.count % 4
if remainder == 0 {
return base64String
} else {
let paddedLength = base64String.count + 4 - remainder
return base64String.padding(toLength: paddedLength, withPad: "=", startingAt: 0)
}
}
}
Which I then used in my function like this:
let fileAsString = attachment.base64EncodedStringWithPadding()

Related

How to upload data using the new swift async/await methods?

The new features in Swift with async/await allow a better control about the process and a simplification in coding. But I cannot find out how this method can be applied for requests which go above a simple data reading. E.g. I need to pass a parameter in order to get a specific answer from a SQL database in the backend (accessed via php).
At first my code about the "standard" way to start with. This function reads all customer records and stores them into an account-array:
#available(iOS 15.0, *)
static func getCustomerListFromBackend() async throws -> [Account] {
let url = URL(string: "https://xxx.de/xxx/getCustomerList.php")!
let (data, _) = try await URLSession.shared.data(from: url)
var accounts: [Account] = []
accounts = try JSONDecoder().decode([Account].self, from: data)
return accounts
}
In order to make my question clear, now a piece of code in which the central statement does not work and exist. In this function I want to check whether a customer exists in the database and I need to pass the emailAddress as a parameter.
#available(iOS 15.0, *)
static func checkCustomerExistsInBackend(emailAddress: String) async throws -> String {
let url = URL(string: "https://xxx.de/xxx/checkCustomerexists.php")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var dataString = "EmailAddress=\(emailAddress)"
let dataD = dataString.data(using: .utf8)
// Statement which does not work but for which I need an alternative
let (data, _) = try await URLSession.shared.upload(request: request, data: dataD)
let answer = try JSONDecoder().decode(BackendMessage.self, from: data)
return answer.Message
}
Unfortunately there is no statement for URLSession.shared.upload(request: request, data: dataD) Until now (before async/await), I used URLSession.shared.uploadTask(with: request, from: dataD) and then used .resume() to process it. This method however gave me too many problems in controlling the right sequence of tasks in the app. Async/await could simplify this very much as in my first example.
So, is there a way to realize this? Any advice would be appreciated.
you could try using URLComponents something like:
func checkCustomerExistsInBackend(emailAddress: String) async throws -> String {
if let url = URL(string: "https://xxx.de/xxx/checkCustomerexists.php"),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.queryItems = [URLQueryItem(name: "EmailAddress", value: emailAddress)]
var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
let (data, _) = try await URLSession.shared.data(for: request)
let answer = try JSONDecoder().decode(BackendMessage.self, from: data)
return answer.Message
}
throw URLError(.badURL)
}
My question was answered by Florian Friedrich's comment and workingdog's answer as well. To the later one I had to make a little adoption which I want to reflect here in this wrap up in case it can be helpful for someone with a similar problem. I show here 2 solutions to my problem with a few remarks.
Applying Florian's answer.
This was straightforward and worked right away:
static func checkCustomerExistsInBackend(emailAddress: String) async throws -> String {
let url = URL(string: "https://xxx.de/xxx/checkCustomerexists.php")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
let dataString = "EmailAddress=\(emailAddress)"
let dataD = dataString.data(using: .utf8)
let (data, _) = try await URLSession.shared.upload(for: request, from: dataD!, delegate: nil)
let answer = try JSONDecoder().decode(BackendMessage.self, from: data)
return answer.Message
}
The proposal from workingdog:
Here I noticed that although the url appeared to be correctly set (ending with checkCustomerexists.php?EmailAddress=test#gmx.de), the parameter did not arrive in my php object. After some tests I found out that it works when I use GET instead of POST. So in my php file I changed the line $EmailAddress = $_POST[EmailAddress]; to $EmailAddress = $_GET['EmailAddress'];. (I am sure there is a good reason for this and I am just not experienced enough to recognize this.) Accordingly the code I use for workingdog's proposal is slightly adjusted:
func checkCustomerExistsInBackend3(emailAddress: String) async throws -> String {
if let url = URL(string: "https://xxx.de/xxx/checkCustomerexists.php"),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.queryItems = [URLQueryItem(name: "EmailAddress", value: emailAddress)]
var request = URLRequest(url: components.url!)
request.httpMethod = "GET"
let (data, _) = try await URLSession.shared.data(for: request)
let answer = try JSONDecoder().decode(BackendMessage.self, from: data)
return answer.Message
}
throw URLError(.badURL)
}

Alamofire post request with params in the URL in swift

I have a post method with base url and various parameters appended in the base url itself.
The code is given below:
let todosEndpoint:String = "https://xxxxxxx/api/post_schedule_form_data?service_type=nanny&start_date=07/20/2020&start_time=06:00&end_date=07/20/2020&end_time=09:00&work_description=Work Description&special_instructions=Special Instructions&location_address=location_address&postal_code=abc123&current_profile_id=10"
let header: HTTPHeaders = ["Content-Type":"application/json","x-token":self.usertoken!]
print("the url is",todosEndpoint)
AF.request(todosEndpoint, method: .post, encoding: JSONEncoding.default, headers: header)
.responseJSON { response in
switch response.result {
case .success(let json):
print("Validation Successful",json)
case let .failure(error):
print(error)
}
}
I do not get any response in the code.What could the error be?
Your headers are not going through correctly...
Also, you're passing " " (spaces) in your URL, just replace them with "%20".
The correct way to do this would be:
let url:String = "https://xxxxxxx/api/post_schedule_form_data?service_type=nanny&start_date=07/20/2020&start_time=06:00&end_date=07/20/2020&end_time=09:00&work_description=Work Description&special_instructions=Special Instructions&location_address=location_address&postal_code=abc123&current_profile_id=10".replacingOccurrences(of: " ", with: "%20")
var request = URLRequest(url: NSURL(string: url)! as URL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(self.usertoken!, forHTTPHeaderField: "x-token")
AF.request(request).responseJSON { response in
switch response.result {
case .success(let json):
print("Validation Successful",json)
case let .failure(error):
print(error)
}
}
You are force unwrapping self.usertoken, so beware of that as well!
Also seems like the date in the URL has "/" which you must change to "%2F" in the URL or better the format in the backend!
Looks to me like you've got a number of illegal characters in your query params (e.g. ":", "/", " ").
I suggest you build your URL using URLComponents. Pass in the endpoint and the query params and then get back the path. That will show you what needs to be escaped.
Edit:
I guess I was wrong about ":" and "/". It looks like those are legal as part of a query param value.
This code:
var components = URLComponents()
components.scheme = "https"
components.host = "xxxxxxx"
components.path = "/api/post_schedule_form_data"
components.queryItems = [
URLQueryItem(name: "service_type", value: "nanny"),
URLQueryItem(name: "start_date", value: "07/20/2020"),
URLQueryItem(name: "start_time", value: "06:00"),
URLQueryItem(name: "end_date", value: "07/20/2020"),
URLQueryItem(name: "end_time", value: "09:00"),
URLQueryItem(name: "work_description", value: "Work Description"),
URLQueryItem(name: "special_instructions", value: "Special Instructions"),
URLQueryItem(name: "location_address", value: "location_address"),
URLQueryItem(name: "postal_code", value: "abc123"),
URLQueryItem(name: "current_profile_id", value: "10"),
]
print(components.string ?? "nil")
Outputs the string
https://xxxxxxx/api/post_schedule_form_data?service_type=nanny&start_date=07/20/2020&start_time=06:00&end_date=07/20/2020&end_time=09:00&work_description=Work%20Description&special_instructions=Special%20Instructions&location_address=location_address&postal_code=abc123&current_profile_id=10
Note that the spaces get escaped as %20.
That is probably the only illegal part of your URL string.
URLComponents is your friend because it applies escaping to the different parts of the URL as needed, and enforces the correct escaping rules for each of the different parts of the URL.
Edit #2:
This code:
let urlString: String = "https://xxxxxxx/api/post_schedule_form_data?service_type=nanny&start_date=07/20/2020&start_time=06:00&end_date=07/20/2020&end_time=09:00&work_description=Work Description&special_instructions=Special Instructions&location_address=location_address&postal_code=abc123&current_profile_id=10"
let componentsFromString = URLComponents(string: urlString)
print(componentsFromString?.string ?? "nil" )
displays "nil"
That tells you there is something illegal about your URL string.

How can I convert the string to URL which include a ? in the string by Swift 4.2

As the title, I am using Google Places API, the URL has a question mark, when I send a request, it will become %3F, how can I modify my code by Swift 4.2??
I found a lot of information but they were using Swift 2 or 3, so it is unavailable for me!
UPDATE my code
let urlString = "\(GOOGLE_MAP_API_BASEURL)key=\(GMS_HTTP_KEY)&input=\(keyword)&sessiontoken=\(ver4uuid!)"
print(urlString)
if let encodedString = urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics
.union(CharacterSet.urlPathAllowed)
.union(CharacterSet.urlHostAllowed)) {
print("encodedString: \(encodedString)")
You are supposed to encode only the query parameters (although probably only keyword actually needs encoding), not the whole URL.
func urlEncode(_ string: String) -> String {
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: "!*'();:#&=+$,/?%#[]")
return string.addingPercentEncoding(withAllowedCharacters: allowed) ?? ""
)
let urlString = "\(GOOGLE_MAP_API_BASEURL)key=\(urlEncode(GMS_HTTP_KEY))&input=\(urlEncode(keyword))&sessiontoken=\(urlEncode(ver4uuid!))"
print(urlString)
Also see Swift - encode URL about the correct way to encode URL parameters.
In this case I highly recommend to use URLComponents and URLQueryItem which handles the percent escaping very smoothly.
var urlComponents = URLComponents(string: GOOGLE_MAP_API_BASEURL)!
let queryItems = [URLQueryItem(name: "key", value: GMS_HTTP_KEY),
URLQueryItem(name: "input", value: keyword),
URLQueryItem(name: "sessiontoken", value: ver4uuid!)]
urlComponents.queryItems = queryItems
let url = urlComponents.url!

Image to String using Base64 in swift 4

my php code creates an empty image on server
here is my code (swift4) :
var encoded_img = imageTobase64(image: image1.image!)
func convertImageToBase64(image: UIImage) -> String {
let imageData = UIImagePNGRepresentation(image)!
return imageData.base64EncodedString(options: Data.Base64EncodingOptions.lineLength64Characters)
}
php code :
$decodimg = base64_decode(_POST["encoded_img"]);
file_put_contents("images/".$imgname,$decodimg);
And the code to prepare the request:
#IBAction func BtnSend(_ sender: UIButton) {
var url = "http://xxxxxx/msg.php"
var encoded_img = imageTobase64(image: image1.image!)
let postData = NSMutableData(data: ("message=" + message).data(using: String.Encoding.utf8)!)
postData.append(("&encoded_img=" + encoded_img).data(using: String.Encoding.utf8)!)
request = NSMutableURLRequest(url: NSURL(string: url)! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 20.0)
request.httpMethod = "POST"
request.httpBody = postData as Data
let session = URLSession.shared
let dataTask = session.dataTask(with:
request as URLRequest, completionHandler:
{ (data, response, error)-> Void in
...
})
dataTask.resume()
The fundamental issue is that your x-www-form-urlencoded request is not well-formed. You have explicitly requested it to create base64 string with newline characters in it, but those are not allowed in x-www-form-urlencoded unless you percent encode them. Plus, we don't know what sort of characters are inside message.
I would suggest:
Not request newline characters to be added to the base64 string unless you really needed them; but
Percent escape the string values, anyway, as I don't know what sort of values you have for message.
Thus:
let parameters = [
"message": message,
"encoded_img": convertToBase64(image: image1.image!)
]
let session = URLSession.shared
let url = URL(string: "http://xxxxxx/msg.php")!
var request = URLRequest(url: url, timeoutInterval: 20.0)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") // not necessary, but best practice
request.setValue("application/json", forHTTPHeaderField: "Accept") // again, not necessary, but best practice; set this to whatever format you're expecting the response to be
request.httpBody = parameters.map { key, value in
let keyString = key.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
let valueString = value.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
return keyString + "=" + valueString
}.joined(separator: "&").data(using: .utf8)
let dataTask = session.dataTask(with:request) { data, response, error in
guard error == nil,
let data = data,
let httpResponse = response as? HTTPURLResponse,
(200 ..< 300) ~= httpResponse.statusCode else {
print(error ?? "Unknown error", response ?? "Unknown response")
return
}
// process `data` here
}
dataTask.resume()
where
func convertToBase64(image: UIImage) -> String {
return UIImagePNGRepresentation(image)!
.base64EncodedString()
}
and
extension CharacterSet {
/// Character set containing characters allowed in query value as outlined in RFC 3986.
///
/// RFC 3986 states that the following characters are "reserved" characters.
///
/// - General Delimiters: ":", "#", "[", "]", "#", "?", "/"
/// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
///
/// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
/// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
/// should be percent-escaped in the query string.
///
/// - parameter string: The string to be percent-escaped.
///
/// - returns: The percent-escaped string.
static var urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]#" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
return allowed
}()
}
Alternatively, you could consider using Alamofire which gets you out of the weeds of creating well-formed x-www-form-urlencoded requests.

xcode error 'consecutive statements on a line must be separated by' when creating endpoint URL for REST API datatask

I am a beginner, trying to code a POST datarequest to post a vote to the 'rating' field of a Drupalnode (so that users can rate movies). I have followed online guides, carefully copying the syntax, but in Xcode am receiving this error for for this line:
let movieEndpoint: String = https://www.examplesitename.com/film1
The red error message is "consecutive statements on a line must be separated by a ';'
The error highlights the ':' after https, and suggests "fix it" with an ';' but changing it to https;www.examplesitename.com/film1 then brings up another red error 'expected expression' (and doesn't seem correct as it is a URL)
For context, below is my code, (which I hope will work to post my data request but haven't been able to check yet)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let movieEndpoint: String = https://www.sitename.com/film1
guard let movieURL = NSURL(string: movieEndpoint) else {
print("Error: cannot create URL")
return
}
let movieUrlRequest = NSMutableURLRequest(URL: movieURL)
movieUrlRequest.HTTPMethod = "POST"
let task = session.dataTaskWithRequest(movieUrlRequest, completionHandler:{ _, _, _ in })
let newRating = ["rating": 50, "userId": 1,]
let jsonRating: NSData
do {
jsonRating = try NSJSONSerialization.dataWithJSONObject(newRating, options: [])
movieUrlRequest.HTTPBody = jsonRating
} catch {
print("Error: cannot create JSON from todo")
return
}
movieUrlRequest.HTTPBody = jsonRating
task.resume()
}
Thank you for any help you can give me.
The proper way to declare a String in Swift is to add " " around the string.
Fix your code like this:
let movieEndpoint: String = "https://www.sitename.com/film1"