Loading PDF Binary retreived from injected AJAX GET in WKWebView to PDFKit - swift

I'm trying to intercept an ajax delivered pdf file from an authentication-secured website. Since ajax delivers the file, the URL does not contain a ".pdf" link for downloading but rather delivers the PDF file as content-disposition attachment.
As of now, when the decidePolicyFor navigationResponse delegate method detects an "application/pdf" mimetype, I call an evaluateJavaScript method with the following code:
func readUrlContent(url: NSURL, completionHandler: #escaping (_ result: String) -> Void) {
self.evaluateJavaScript("(function() { var result = ''; $.ajax({type: 'GET', url: '\(url)', contentType: 'application/pdf; charset=utf-8', success: function(r) {result = r}, failure: function() {result = null}, async: false }); return result })()", completionHandler: { (response, error) -> Void in
let result = response as! String
completionHandler(result)
})
}
With this injected code (which I use because I had issues with cookies and authentication on the website using WKWebView), I can successfully print the PDF Binary string returned as "result" in the completion handler to the log. The issue now is that I need to get this pdf binary data into a PDF document using PDFKit.
When I take this binary string and do:
self.data = result.data(using: .utf8)
self.performSegue(withIdentifier: "pdfSegue", sender: nil)
And pass the binary PDF string to a PDFKit ViewController to display it, all pages appear blank and I get the following errors:
"encountered unexpected object type: 7."
"missing or invalid object number."
"missing or invalid cross-reference stream."
"invalid stream length 3555; should be 6444."
"FlateDecode: decoding error: incorrect header check."
etc. etc. etc.
How/can I get this PDF binary data returned and view it using PDFKit?

Related

Vapor: how to not receive a particular upload?

In Vapor, how does one setup to check and decline an upload request prior to any part of such file being uploaded to the server?
My current attempt in Vapor 3 is with a route handler structured like:
func imagesUploadOneHandler(_ request: Request) throws -> EventLoopFuture<HTTPResponseStatus> {
let headers = request.http.headers
let headersUploadToken: [String] = headers["Upload-Token"]
if headersUploadToken.count != 1 || headersUploadToken[0] != aValidToken {
return HTTPResponseStatus.notAcceptable
}
// http body content type: 'application/octet-stream'
let dataFuture: EventLoopFuture<Data> = request.http.body.consumeData(max: 50_000_000, on: request)
let futureHTTPResponseStatus = dataFuture.map(to: HTTPResponseStatus.self, {
(data: Data) -> HTTPResponseStatus in
// ... other code
return HTTPResponseStatus.ok
})
return futureHTTPResponseStatus
}
Firstly, the above will not compile. The line return HTTPResponseStatus.notAcceptable has a compile time error "return HTTPResponseStatus.notAcceptable". How to convert HTTPResponseStatus to EventLoopFuture<HTTPResponseStatus> has been elusive.
Secondly, can some code prior to request.http.body.consumeData(...) in a route handler prevent an upload of the file content? Or, is some middleware needed instead to avoid uploading the data content (e.g. some large file) from the http.body?

File upload using Swift Vapor 3.0

I'm trying to create a simple vapor service so I can upload video files (one at a time) through an api.
From my app I'm uploading the video file using Alamofire:
func uploadVideo(video: URL) {
Alamofire.upload(videoFileURL, to: "http://localhost:8080/upload").responseString { response in
debugPrint(response)
}
}
The vapor controller method is like this (this is where I don't know how to do it):
func upload(_ req: Request) throws -> String {
let data = try req.content.decode(Data.self).map(to: Data.self) { video in
try Data(video).write(to: URL(fileURLWithPath: "/Users/eivindml/Desktop/video.mp4"))
return Data(video)
}
return "Video uploaded";
}
How do I get the video file from the request and into the right format so I can write it do disk?
The method upload() is called correctly etc, as it works if I just have the last return statement.
Looking at your function it appears you're not handling your future response correctly, or extracting the data.
func upload(_ req: Request) throws -> Future<String> {
return try req.content.decode(File.self).map(to: String.self) { (file) in
try file.data.write(to: URL(fileURLWithPath: "/Users/eivindml/Desktop/\(file.filename)"))
return "File uploaded"
}
}
See if this helps.

Posting a file in Swift

I am trying to post a file to a web API from Swift. The request I send is received fine, but it doesn't have any file in it. I was able to peek at request from the C# controller and HttpContext.Current.Request.Files.Count is 0. I suspect that instead of attaching a file to the body of the request, it just puts in the file's contents as raw data.
Code creating request:
func upload(_ fileName:URL)
{
let requestUrl:URL = URL(string: uploadURL)!
let request = NSMutableURLRequest(url: requestUrl)
request.httpMethod = "POST"
request.httpBodyStream = InputStream(url: fileName)
let (data, response, error) = URLSession.shared.synchronousDataTaskWithRequest(request as URLRequest)
}
What am I doing wrong?
If it is helpful to understand what kind of request I am trying to build, this is the working code in JS which creates a FormData object from an HTML <input type='file'> element:
$.ajax({
url: $('#apiURL').val() + 'Upload'
type: 'POST',
contentType: false,
processData: false,
data: new FormData($('form')[0]),
success: function (data)
{
// doing successful things
},
error: function (jqXHR, textStatus, errorThrown)
{
alert("Failed");
}
});
I basically just want to send a file to the same controller it references but in swift...
My google-fu wasn't up to par. Copy-paste of the answer given with minor code changes to work with the data types I already had made it work:
https://stackoverflow.com/a/26163136/6825722

How to handle multipart request with Vapor 3

I'm a vapor beginner and I chose to start with Vapor 3-rc because it seems to break change from Vaport 2. Unfortunately, there isn't a complete documentation for now.
I'm currently trying to upload a simple txt file from Postman to my Vapor 3 local server.
Here's my route
let uploadController = FileUploadController()
router.post("uploadtxt", use: uploadController.uploadTXT)
and my controller
final class FileUploadController {
func uploadTXT(_ req: Request) throws -> Future<String> {
return try req.content.decode(MultipartForm.self).map(to: String.self, { form in
let file = try form.getFile(named: "txtfile")
return file.filename ?? "no-file"
})
}
}
First, by executing the Postman request, the server returns:
{"error":true,"reason":"There is no configured decoder for multipart\/form-data; boundary=...}
By investigating the source code and the limited documentation on this, it seems that I should declare a decoder to support multipart incoming requests.
So I did:
var contentConfig = ContentConfig.default()
let decoder = FormURLDecoder()
contentConfig.use(decoder: decoder, for: .multipart)
services.register(contentConfig)
I used FormURLDecoder because it seemed to be the closest class for my needs IMO, implementing BodyDecoder
Now it infite-loops into func decode<T>(_ type: T.Type) throws -> T where T: Decodable of FormURLSingleValueDecoder, and I'm stuck here with very few web resource.
I ended on the Vapor slack, which is a good place to find some info & a bit of help.
The solution is quite simple. Instead of using req.content.decode(MultipartForm.self), prefer use MultipartForm.decode(from: req) (...deleted code sample)
EDIT:
AS #axello said, MultipartForm does not exist anymore. I'm now using req.content.decode(...) method to parse the multipart data. The idea is to create an object that reflects your HTML form inputs. And Codable will magically map the data into the object for you.
For example, with this form:
<form method="POST" action="upload" enctype="multipart/form-data" class="inputForm">
<input type="name" name="filename">
<input type="file" name="filedata">
<input type="submit" name="GO" value="Send" class="send">
</form>
I created this small struct
fileprivate struct MyFile: Content {
var filename: String
var filedata: Data
}
And, in my controller:
func uploadTXT(_ req: Request) throws -> Future<String> {
return try req.content.decode(MyFile.self).map(to: String.self, { myFile in
let filename = myFile.filename // this is the first input
// ... and the second one:
guard let fileContent = String(data: myFile.filedata, encoding: .utf8) else {
throw Abort(.badRequest, reason: "Unreadable CSV file")
}
print(fileContent)
return filename
})
}

Pass an uploaded image directly from json file

I am trying to upload an image from my app to my heroku backend and then pass that to Stripe for verification. Here is my swift code to upload and pass the image:
#IBAction func registerAccountButtonWasPressed(sender: UIButton) {
let dob = self.dobTextField.text!.components(separatedBy: "/")
let URL = "https://splitterstripeservertest.herokuapp.com/account/create"
var imageData = UIImageJPEGRepresentation(photoID,1)
let params = ["year": UInt(dob[2])! as UInt] as [String : Any]
let manager = AFHTTPSessionManager()
let operation = manager.post(URL, parameters: params, constructingBodyWith: { (formData: AFMultipartFormData!) -> Void in
formData.appendPart(withFileData: imageData!, name: "file", fileName: "photoID.jpg", mimeType: "image/jpeg")
}, success: { (operation, responseObject) -> Void in
print(responseObject!)
}) { (operation, error) -> Void in
self.handleError(error as NSError)
}
}
I've deleted the list of params above out and left one for readability.
Is there a way to then receive this file and upload it to stripe without having to save it by passing the file parameter? like so:
Stripe::FileUpload.create(
{
:purpose => 'identity_document',
:file => params[file]
},
{:stripe_account => params[stripe_account]}
)
Also in the stripe docs it says to upload the file to 'https://uploads.stripe.com/v1/files' but then shows code to put in your backend, does Stripe::FileUpload.create do the uploading to stripe for me?
Any insight on either would be great thanks.
You need to first upload the file to Stripe's API using the "create file upload" call. You can then use the file upload's ID (file_...) to update the account's legal_entity.verification.document attribute.
(This process is explained here:
https://stripe.com/docs/connect/identity-verification#uploading-a-file
https://stripe.com/docs/connect/identity-verification#attaching-the-file)
Since the file is provided by the user, you have two choices for creating the file upload:
have your app upload the file to your backend server, then on your backend, use the file to create the file upload
create the file upload directly from the app (using your publishable API key), and send the resulting file_upload's ID (file_...) to your backend
Here's an example for creating file uploads client-side, using jQuery: https://jsfiddle.net/captainapollo/d8yd3761/.
You could do the same thing from your iOS app's code. Basically all you need to do is send a POST request to https://uploads.stripe.com/v1/files with an Authorization header with value Bearer pk_... (where pk_... is your publishable API key) and type multipart/form-data, with the file's contents as the request's body. This blog post should be helpful for sending multipart/form-data requests using Swift: http://www.kaleidosblog.com/how-to-upload-images-using-swift-2-send-multipart-post-request
Thanks to #Ywain I was able to come up with this solution for IOS Swift 4. I created the file upload directly from the app and retrieved the file_upload's ID to send to my backend to attach to the Connect Account. I did this by importing Alamofire and SwiftyJSON.
let heads = ["Authorization": "Bearer \(stripePublishableKey)"]
let imageData = image!.jpegData(compressionQuality: 1.0)
let fileName = "\(NSUUID().uuidString).jpeg"
Alamofire.upload(multipartFormData: { multipart in
multipart.append(imageData!, withName: "file", fileName: fileName, mimeType: "image/jpeg")
multipart.append(("identity_document").data(using: .utf8)!, withName :"purpose")
}, to: fileUrl!, method: .post, headers: heads) { encodingResult in
switch encodingResult {
case .success(let upload, _, _):
upload.responseJSON { answer in
let value = JSON(answer.value!)
let FileID = value["id"].stringValue
// Use fileID and Connect Account ID to create params to send to backend.
let params: [String: Any] = [
"acct_id": ConnectID!,
"fileID": FileID,
]
print("statusCode: \(String(describing: answer.response?.statusCode))")
}
case .failure(let encodingError):
print("multipart upload encodingError: \(encodingError)")
}
}