I'm making a simple POST request with some body-params, constructed like so:
_webView = [[WKWebView alloc] initWithFrame:self.bounds configuration:[WKWebViewConfiguration new]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:<some_URI>]];
[request setHTTPMethod:#"POST"];
NSString *paramsStr = #"someKey=someValue"
[request setHTTPBody:[paramsStr dataUsingEncoding:NSUTF8StringEncoding]];
[_webView loadRequest:request];
My server endpoint never receives any POST params (the http-body seems to be empty)
I've seen discussions about WKWebView not providing POST data in the navigation delegate, but the use-case for those questions has been to fetch form data off of the webView. Further, I read that that WKWebView bug has been fixed.
My use case is very simple, I just want to make a POST request from a webview, but it's still not working. I'm on iOS 14.4 FWIW. Any tips on what could be causing the POST data to not be available on the server?
Use JavaScript to solve the problem that WKWebView cannot send POST parameters
Before I start, let me talk about the implementation ideas, so that everyone can understand it. If something goes wrong, you can know the wrong place:
Put the HTML code of a POST request containing JavaScript in the project directory
Load the code of this POST request containing JavaScript to WKWebView
After loading, use Native to call JavaScript's POST method and pass in parameters to complete the request
HTML code to create a POST request containing JavaScript
Related code:
<html>
<head>
<script>
//调用格式: post('URL', {"key": "value"});
function post(path, params) {
var method = "post";
var form = document.createElement("form");
form.setAttribute("method", method);
form.setAttribute("action", path);
for(var key in params) {
if(params.hasOwnProperty(key)) {
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", key);
hiddenField.setAttribute("value", params[key]);
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
}
</script>
</head>
<body>
</body>
</html>
Copy this code and paste it into a text editor. You can choose any name, for example, save it as: JSPOST.html, and then copy it to the project directory. Remember to select the corresponding Target and check Copy items if needed (default It should be checked). At this time, you can use this JavaScript code to send a POST request with parameters.
Load the corresponding JavaScript code into WKWebView by loading a local web page
OC Code:
// JS sends the POST Flag, when it is true, it will call the JS POST method (only when the local JS is loaded for the first time)
self.needLoadJSPOST = YES;
// Create WKWebView
self.webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
//Set up proxy
self.webView.navigationDelegate = self;
// Get the path where JS is located
NSString *path = [[NSBundle mainBundle] pathForResource:#"JSPOST" ofType:#"html"];
// Get html content
NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
// load js
[self.webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];
// Add WKWebView to the current View
[self.view addSubview:self.webView];
This code is equivalent to loading the JavaScript scripts in the project into WKWebView, and we will see how to use it later. (Please change to your file name)
Native calls JavaScript scripts and passes in parameters to complete the POST request
Remember the section on the interaction between WKWebView and JavaScript? Now Native calls JavaScript. If you forget, please go ahead and review the story:-webView:didFinishNavigation: The proxy indicates that the page has been loaded. Let's do it here. The following code:
OC Code:
// Proxy method after loading
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
// Determine whether to load (only for the first time)
if (self.needLoadJSPOST) {
// Call the method of sending POST request using JS
[self postRequestWithJS];
// Set Flag to NO (you don’t need to load it later)
self.needLoadJSPOST = NO;
}
}
// Call JS to send POST request
- (void)postRequestWithJS {
// Send POST parameters
NSString *postData = #"\"username\":\"aaa\",\"password\":\"123\"";
// URL of the requested page
NSString *urlStr = #"http://www.postexample.com";
// Assembled into a string that calls JavaScript
NSString *jscript = [NSString stringWithFormat:#"post('%#', {%#});", urlStr, postData];
// NSLog(#"Javascript: %#", jscript);
// Call JS code
[self.webView evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) {
}];
}
you can see more here: http://www.qw021.com/article-22.html
Related
My goal with this code is to simply show the html code from the url request on a Label as the app is initialized:
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:#"http://192.168.25.242:8090"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[_webView loadRequest:request];
sleep(5);
self.myLabel.text = [_webView stringByEvaluatingJavaScriptFromString:#"document.body.innerHTML"];
}
I think the url request take some time to end, so I used sleep function trying to wait for it to end and then convert the webview results into string and load on the label... But is shows nothing. When I create a button to "self.myLabel.text = [_webView stringByEvaluatingJavaScriptFromString:#"document.body.innerHTML"];" after any webview load, it does work, and the label gets the html from the webview correctly.
What should I do?
The sleep() function won't work because the webView won't be given time to load. Instead, set _webView.delegate = self; and implement -webViewDidFinishLoad:, where you should set the label text.
I'm developing an Epub Reader for Ios, notice that i don't use any epub library and i parse the epub myself.
I needed a method for loading resource from epub into the UIWebView, for example images or CSS file ... (resource that requested in the html files).
for this purpose i decided to use NSURLCache class and override it's method (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
so i can intercept the request and find the right data from epub then create a NSCachedURLResponse and return it.
so far so good. BUT the the problem is that the data (i.e images) wont show in the webview. look at below code:
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
/*
the request.URL is something like this applewebdata://UUID/OEBPS/Images/cover.jpg ,
so the getResourcePathFromRequest give me the path OEBPS/Images/cover.jpg,
so i can find the right data in epub
*/
NSString *resourcePath = [self getResourcePathFromRequest:request.URL];
/*
ResourceResponse is a class contaning data and mimeType,
i tested this and the data and mimeType are good so this is not the problem
*/
ResourceResponse *response = [[self getBook] fetch:resourcePath];
NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[request URL] MIMEType:response.mMimeType expectedContentLength:[response.mData length] textEncodingName:#"UTF-8"];
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:urlResponse data:response.mData];
return cachedResponse;
}
at this point everything looks normal, the method getting called, the right data being found and the response object return, but it wont load the image into the webview.
so what is the problem?
tnx in advance.
NOTE: i'm new in ios programming world (coming from android:)
I loaded the data with base url (http://localhost) and everything went fine.
I'm building a small REST service to authorize users into my app.
At one point, the UIWebView I'm using to authorize the user, will go to https://myautholink.com/login.php. This page sends a JSON response with an authorization token. The thing about this page is that it receives some data via GET via my authorization form. I cannot use PHP sessions because you arrive to this page via:
header("location:https://myautholink.com/login.php?user_id=1&machine_id=machine_id&machine_name=machine_name&app_id=app_id");
Since the header function sends in headers, I cannot do a session_start(); at the same time.
I can get the UIWebView's request URL without a problem using the delegate methods:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSURLRequest *request = [webView request];
NSLog(#"%#", [[request URL] relativeString]);
if([[[request URL] absoluteString] isEqualToString:SPAtajosLoginLink])
{
//Store auth token and dismiss auth web view.
}
}
The thing is none of the NSURL methods seem to return the "clean" link without the parameters. I have looked at all the NSURL url-string related methods:
- (NSString *)absoluteString;
- (NSString *)relativeString; // The relative portion of a URL. If baseURL is nil, or if the receiver is itself absolute, this is the same as absoluteString
But absoluteString is always the full URL with the GET parameters and relativeString is always nil.
I'm scratching my head with this and I can't seem to find the solution. Any help will be appreciated.
Rather than mess about with your own string manipulation, hand off to NSURLComponents:
NSURLComponents *components = [NSURLComponents componentsWithURL:url];
components.query = nil; // remove the query
components.fragments = nil; // probably want to strip this too for good measure
url = [components URL];
On iOS 6 and earlier, you can bring in KSURLComponents to achieve the same result.
Example: http://www.google.com:80/a/b/c;params?m=n&o=p#fragment
Use these methods of NSURL:
scheme: http
host: www.google.com
port: 80
path: /a/b/c
relativePath: /a/b/c
parameterString: params
query: m=n&o=p
fragment: fragment
Or, in iOS 7, build a NSURLComponents instance, then use the methods scheme, user, password, host, port, path, query, fragment, to extract part of the URL as strings. Then build the base URL back.
NSString* baseURLString = [NSString stringWithFormat:#"%#://%#/%#", URL.scheme, ...
NSURL *baseURL = [NSURL URLWithString:baseURLString];
To update this answer for iOS 7 onwards:
NSURLComponents *components = [NSURLComponents componentsWithURL: url resolvingAgainstBaseURL: NO];
components.query = nil; // remove the query
components.fragment = nil; // probably want to strip this too for good measure
url = [components URL];
Please note also that there is no 'fragments' property. It's just 'fragment'.
Otherwise, this method is great. Much better than worrying about putting the URL back together properly with string manips.
I'm an iOS newb (.NET professional), so this may be a simple issue but I couldn't find anything through the SO search or Google (and maybe not looking for the right terms).
I'm writing an app that displays information from a DD-WRT router through it's web interface. I have no problem displaying the initial page and navigating through any of the other pages, but if I make any change on a form (and it redirects to apply.cgi or applyuser.cgi), the UIWebView is blank - it's supposed to display the same page, with the form submission changes. The site works fine in Mobile Safari, which I find intriguing, but I guess UIWebView isn't totally the same.
I think the iOS code is pretty standard for display a webpage, but I'll list it below. I can't give you access to my router because, well, that's not a good idea :) Hopefully someone with a DD-WRT router can help (or know what my issue is anyway).
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *sURL = #"http://user:pass#XXX.XXX.X.X";
NSURL *url = [NSURL URLWithString:sURL];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
self.webView.delegate = self ;
}
And I'm doing a few things with Javascript in the webViewDidFinishLoad method, but I know that's not the culprit because it still happens when I comment it out.
Well I figured out the problem on my own. I think part of it was putting the username & password in the URL (which was just a temporary measure) because I found that method provided the same results in mobile Safari and desktop Chrome.
So I added MKNetworkKit to my project that provided a simple way to add authentication to my request, and found I had to make a specific request to POST the data, then reloaded the page the to see the changes.
In the (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType method, I check if ([request.HTTPMethod isEqualToString:#"POST"]) and do this:
NSString *sPostData = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
NSArray *aPostData = [sPostData componentsSeparatedByString:#"&"];
NSMutableDictionary *dPostData = [[NSMutableDictionary alloc] init];
//i don't know if this is the best way to set a dictionary, but it works
for (id apd in aPostData)
{
NSString *key = [apd componentsSeparatedByString:#"="][0];
NSString *val = [apd componentsSeparatedByString:#"="][1];
[dPostData setValue:val forKey:key];
}
MKNetworkEngine *engine = [[MKNetworkEngine alloc] init];
MKNetworkOperation *op = [engine operationWithURLString:[request.URL description] params:dPostData httpMethod:#"POST"];
[op setUsername:#"myUserName" password:#"myPassword" basicAuth:YES];
self.postedRequest = TRUE; //a bool I set so, when it comes to webViewDidFinishLoad, I reload the current page
[op start]; //send POST operation
I am trying to achieve the following on iOS:
Always load local files to a UIWebView for static assets (.html, .js, etc.)
Allow an update protocol such that after some period of time we can return a different set of static assets for the same URLs
Download link for minimal example.
I have this working but it seems the NSURLCache is sometimes completely missed (reproducible) and the only reliable way to fix this has been to use nasty cachebusting tricks on the page being loaded. Another equally nasty hack is to destroy the UIWebView and create another.
As an example of this we have three versions of our webapp (red, blue and green for v1, v2 and v3 respectively):
Each screen is made up of a single HTML and JS file.
index.html
----------
<!DOCTYPE html>
<html>
<head>
</head>
<body style="background-color:#FF0000">
<h1 id="label" style="color:#FFFFFF"></h1>
<script src="app.js"></script>
</body>
</html>
app.js
------
var label = document.getElementById('label');
label.innerHTML = 'red';
Every 2 seconds the following happens:
We change what files the NSURLCache will return to make it return the different versions (note we implement this by overriding cachedResponseForRequest: rather than storeCachedResponse:forRequest:)
The UIWebView loads a dummy page "http://www.cacheddemo.co.uk/index.html"
The NSURLCache logic is implemented simply as a rotating NSMutableArray:
#implementation ExampleCache
- (id)init
{
self = [super initWithMemoryCapacity:8 * 1024 * 1024 diskCapacity:8 * 1024 * 1024 diskPath:#"webcache.db"];
if(self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(swapCache:) name:#"swapCache" object:nil];
self.cache = [#[#{#"index.html":#"index1.html", #"app.js":#"app1.js"},
#{#"index.html":#"index2.html", #"app.js":#"app2.js"},
#{#"index.html":#"index3.html", #"app.js":#"app3.js"}] mutableCopy];
}
return self;
}
- (void)swapCache:(NSNotification *)notification
{
[self.cache addObject:self.cache[0]];
[self.cache removeObjectAtIndex:0];
}
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
NSString *file = [[[request URL] pathComponents] lastObject];
NSString *mimeType;
if([file hasSuffix:#".html"]) {
mimeType = #"text/html";
} else if([file hasSuffix:#".js"]) {
mimeType = #"application/javascript";
}
if(mimeType) {
NSString *cachedFile = self.cache[0][file];
NSUInteger indexOfDot = [cachedFile rangeOfString:#"."].location;
NSString *path = [[NSBundle mainBundle] pathForResource:[cachedFile substringToIndex:indexOfDot] ofType:[cachedFile substringFromIndex:indexOfDot + 1] inDirectory:#"www"];
NSData *data = [NSData dataWithContentsOfFile:path];
if(data.length) {
NSLog(#"Response returned for %#", file);
NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[request URL] MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
NSCachedURLResponse *response = [[NSCachedURLResponse alloc] initWithResponse:urlResponse data:data];
return response;
}
}
NSLog(#"No response for %# - %#", file, request);
return nil;
}
The view controller logic uses GCD to reload the UIWebView after the delay:
- (void)viewDidLoad
{
[super viewDidLoad];
self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[self.webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
[self.webView setDelegate:self];
[self.view addSubview:self.webView];
[self loadContent];
}
- (void)loadContent
{
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#?%d", #"http://www.cacheddemo.co.uk/index.html", arc4random()]]]];
// [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.cacheddemo.co.uk/index.html"]]];
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[[NSNotificationCenter defaultCenter] postNotificationName:#"swapCache" object:nil];
[self loadContent];
});
}
The part I can not understand here is that adding the query string will make the page reloading work flawlessly (loads R - G - B - R - G - ...) - this is the uncommented line:
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#?%d", #"http://www.cacheddemo.co.uk/index.html", arc4random()]]]];
Once we get rid of the query string the NSURLCache stops being hit other than the first request so it just stays on the R page:
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.cacheddemo.co.uk/index.html"]]];
The fact that the query string causes the NSURLCache to act as it is supposed to indicates to me that the browser cache is interfering in some way. In my head I figure the caching levels work as so:
Check browser cache
If nothing returned so far - check NSURLCache
If nothing returned so far - check proxy server cache
Finally attempt to load the remote resource
How can we disable the browser cache entirely so we can completely control caching behaviour for UIWebView. Unfortunately I do not see an option to set the Cache-Control header in NSURLCache - I already tried returning a NSHTTPURLResponse with the headers set but this seems to be ignored.
I am not sure I understand correctly, but the cache-control has to be set on server side, with something like no-cache, expires and so on, and not on iOS side.
Second, by modifying the query string i.e. www.mysite.com/page?id=whatever..., iOS and any browser think the request is not the same, if you have opened the cache itself with some db editor, you should have seen one request, which is one database entry, for each changed query.
This trick of adding a random query string is quite useful for avoiding the browser to cache javascript file.
I hope I understand correctly your question.
The browser does appear to have its own "cache" above the networking layer's cache (i.e. NSURLCache).
Not sure if setting the cache control headers will solve it but if you want to try that you can do so in your code for cachedResponseForRequest. Where you create an NSURLResponse object you can use initWithURL:statusCode:HTTPVersion:headerFields: to set the header fields (including the cache control headers). In your cache control header you can try using both no-store and no-cache (e.g. http://blog.55minutes.com/2011/10/how-to-defeat-the-browser-back-button-cache/ ). If you try this please let us know if it does or does not work.
However, in practice I think the most pragmatic and maintainable solution will be to the use a number on the URL (i.e. cache busting). BTW instead of a random number you can use a simple increment that gets reset whenever the webview is allocated.