WKWebView iOS 16 Web Share API on file: protocol - ios

I am attempting to use navigator.share in a file running in WKWebView on iOS 16. The file is loaded into the web view using a file: protocol path like so:
uiView.loadFileURL(Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "web")!, allowingReadAccessTo: Bundle.main.bundleURL)
I have this setting on:
configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
So I am able to load file: protocol files into the web view, but I don't seem to be able to use the Web Share API. I get this error:
NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.
However, when I point the web view at this HTTPS URL, it works:
https://mdn.github.io/dom-examples/web-share
This leads me to believe the Web Share API is not working because the HTML file loaded over the file: protocol is not viewed as secure and thus the JavaScript runtime don't treat the code on it as if it was running in a secure context.
As a result, I believe the access to the navigator.share API is forbidden.
Is there a way to configure WKWebView to allow access to secure context JS/DOM/web APIs without using a custom scheme (and adding an extra layer atop just loading my file from the bundle)?

The problem was not with the file: protocol or secure context.
I looked through WebKit source code for navigator.share and found that NotAllowedError gets returned whenever there don't seem to be conditions met for transient activation.
https://github.com/WebKit/WebKit/blob/d9bcb08521ddb86af9a713213d15488712919143/Source/WebCore/page/Navigator.cpp#L172
void Navigator::share(Document& document, const ShareData& data, Ref<DeferredPromise>&& promise)
{
if (!document.isFullyActive()) {
promise->reject(InvalidStateError);
return;
}
if (!validateWebSharePolicy(document)) {
promise->reject(NotAllowedError, "Third-party iframes are not allowed to call share() unless explicitly allowed via Feature-Policy (web-share)"_s);
return;
}
if (m_hasPendingShare) {
promise->reject(InvalidStateError, "share() is already in progress"_s);
return;
}
auto* window = this->window();
if (!window || !window->consumeTransientActivation()) {
promise->reject(NotAllowedError);
return;
}
if (!canShare(document, data)) {
promise->reject(TypeError);
return;
}
Transient activation is the thing where some web APIs will only run in response to a user gesture; the user gesture is either a part of the call stack leading up to the API call invocation or a user gesture has happened recently.
More about transient activation here:
https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation
In my case I did have a user gesture there but also a WebRTC peer connection ICE gathering flow which while seemingly quick I guess lasted too long and it reset the transient activation flag preventing me from using the Web Share API at the end of the ICE gathering phase.
The fix is to prepare whatever you need for the Web Share API data payload in advance and in response to the user gesture, such as a button click, call the web API straight away.

Related

Redirect API call fetches from Service Worker

This is a really annoying issue. I am using a third party login in my application. When a user logins in through the third party, it redirects an api call to the server.
ex: /api/signin/github?code=test&state=test
For some strange reason this API call is getting fetched from the service worker instead on the server which handles the login logic.
ex:
Without seeing your service worker's fetch event handler, it's hard to say exactly what code is responsible for that.
In general, though, if there are URLs for which you want to tell the service worker never to respond to, you can just avoid calling event.respondWith(...) when they trigger a fetch. There are lots of ways to avoid doing that, but an early return is straightforward:
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname === '/api/signin/github') {
// By returning without calling event.respondWith(),
// the request will be handled by the normal browser
// network stack.
return;
}
// Your fetch event response generation logic goes here.
event.respondWith(...);
});

"Internal" Error When Submitting Form With Firebase onCall Function on IOS Safari

I am trying to submit a form but I get an "internal" error after submit on IOS Safari. Happened on two separate devices. I'm using Firebase functions onCall function. Client code:
var contactForm =
window.firebase.functions().httpsCallable('contactForm');
let result = await contactForm({ accountUID, foldersFilter,
firstName, lastName, email, cellNumber, dobDay, dobMonth })
And server code:
exports.contactForm = functions.https.onCall(( data, context ) => {
return contactForm.contactForm( data, context )
});
This function is called via form. The form works great on chrome, safari desktop, but for some reason it gets an internal errror sometimes when testing on IOS device. At first I thought it only happened when I was using autofill, but I've tested more and I get the same error when not using autofill too.
The confusing thing is my function code is actually never being called (I don't see any firebase function logs). Here is my console in safari:
The network connection was lost.
Fetch API cannot load https://us-central1-projectId.cloudfunctions.net/contactForm due to access control checks
Failed to load resource: The network connection was lost.
internal
Why won't this form submit on ios safari?
I fixed the issue. Turns out it has something to do with Google Cloud Functions being IPv4, and Safari requiring IPv6. I suspect this will become a bigger issue moving forward. I'm having to move all onCall Firebase functions to https triggers. In order to make https triggers work, you have to use a custom domain in Firebase hosting and rewrite to your function endpoint.
{
"hosting": {
...
"rewrites": [
{
"source": "/api/contactForm",
"function": "contactForm"
}
}
and so now instead of calling https://us-central1-projectId.cloudfunctions.net/contactForm to trigger my api. I call https://customdomain.com/api/contactForm

Intercept responses from WebSocket connection in UIWebView

I have an app which has a UIWebView inside of it with a loaded website. This website has a chart in it which is periodicly updated with data from remote server via websockets (socket.io).
Im new to websockets technology but Im trying to somehow intercept the chart data that the website is receiving from server via it.
Till now I have managed to catch http requests sent by the website of such address format: “http://website-address/socket.io/?auth_token=...”
I have the socket.io library for iOS but don’t know how to use it to somehow spoof the website connection and acquire the data downloaded by the website. Can anyone help? Is it even possible?
Switch to WKWebView if you can. Using javascript bridge is much easier there. That said, with UIWebView, you'd need to inject a script that adds a handler for events received by the socket that you are trying to listen to. You can either create an io variable by yourself but apparently the server needs auth token. If you cannot create an auth token, you can only do this if you have access to the io variable created by the website.
Then for adding a handler, you'll need to know what the event name is, that delivers the chart data. You can snoop around the website and see if you can find that. If you cannot all bets are off. Once we register a handler and get the data, we need to pass this back to your native code. This is where WKWebView would keep it clean by letting you add message handlers that can deliver messages from js to native code. For UIWebView you'll have to create a custom url scheme and spoof a navigation request to pass the data. Lets assume your custom url scheme is 'myApp'. Then the script you'd need to inject would be:
<script>
/* if you can access/create the auth token
var socket = io('http://website-address/socket.io/?auth_token=');
*/
var socket = getioReferenceCreatedByWebsite();
socket.on('<eventName>',function(){
window.location = 'myApp://<data>';
};
</script>
In your native code:
...
webView.delegate = self;
[webView stringByEvaluatingJavaScriptFromString:#"<theAboveJSAsAString>"];
....
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
if(request.URL.scheme == #"myApp"){
NSString *data = request.URL.path;
//handle the data
return NO;
}
return YES;
}
In regards to Santhosh R answer. I had the problem he mentioned where I could not get a reference to the websocket object as it was caught up in a closure.
I solved this by adding in a preload script which wraps the native Websocket object to store any instantiated websocket objects in an array and then return the newly created websocket object.
Here is the code.
in your WebView element add in a preload attribute.
<webview id="myWebview" src="http://exmple.com" preload="./interceptor.js"></webview>
and then in inteceptor.js
window.NativeWebsocket = WebSocket;
window.WebSocket = function(url, protocols){
window.interceptedWebsockets = [];
var ws = new NativeWebsocket(url, protocols);
interceptedWebsockets.push(ws);
return ws;
}
Then, inside your WebView context you can an access array of instantiated websocket objects using window.interceptedWebsockets

Differences between ChallengeHandler.submitChallengeAnswer(credentials) and WLAuthorizationManager.login(SECURITY_CHECK_NAME, credentials)

What are the differences between using ChallengeHandler.submitChallengeAnswer(credentials) and WLAuthorizationManager.login(SECURITY_CHECK_NAME, credentials)?
You may also want to login a user without any challenge being received. For example, showing a login screen as the first screen of the application, or showing a login screen after a logout, or a login failure. We call those scenarios preemptive logins.
You cannot call the submitChallengeAnswer API if there is no challenge to answer. For those scenarios, the Mobile Foundation SDK includes the login API:
WLAuthorizationManager.login(securityCheckName,credentials).then(
function () {
WL.Logger.debug("login onSuccess");
},
function (response) {
WL.Logger.debug("login onFailure: " + JSON.stringify(response));
});
If the credentials are wrong, the security check sends back a challenge.
It is the developer’s responsibility to know when to use login, as opposed to submitChallengeAnswer, based on the application’s needs. One way to achieve this is to define a Boolean flag, for example isChallenged, and set it to true when handleChallenge is reached, or set it to false in any other cases (failure, success, initialization, etc).
When the user clicks the Login button, you can dynamically choose which API to use:
if (isChallenged){
userLoginChallengeHandler.submitChallengeAnswer(credentials);
} else {
WLAuthorizationManager.login(securityCheckName,credentials).then(
//...
);
}
MobileFirst implements OAuth 2 authorization framework, https://www.ibm.com/support/knowledgecenter/en/SSHS8R_8.0.0/com.ibm.worklight.dev.doc/dev/c_oauth_security_model.html. There are two stages in the implementation, Obtaining an access token, and
Accessing a protected resources by using an access token.
ChallengeHandler APIs are used to implement the first stage, Obtaining
an access token. WLAuthorizationManager APIs are used to implement the
second stage, Accessing a protected resources. More details can be found
at https://www.ibm.com/support/knowledgecenter/SSHS8R_8.0.0/com.ibm.worklight.dev.doc/dev/c_oauth_client_apis.html?view=embed#c_oauth_client_apis

Determine if user has enabled application's Safari content blocker extension

I'm working on a Safari Content Blocking extension. I intend to show setup instructions if the extension is disabled and to show settings if it is conversely enabled. How can I determine if the extension is enabled by the user?
I've seen this method to detect if a custom keyboard is activated but there's no key on NSUserDefaults that relates to Safari Content Blockers.
As of iOS 10, there is a new method in SFContentBlockerManager to support this:
getStateOfContentBlocker(withIdentifier:completionHandler:)
And you call it like this (Swift 3):
SFContentBlockerManager.getStateOfContentBlocker(withIdentifier: "your.identifier.here", completionHandler: { (state, error) in
if let error = error {
// TODO: handle the error
}
if let state = state {
let contentBlockerIsEnabled = state.isEnabled
// TODO: do something with this value
}
})
You could utilize a SFSafariViewController to load a custom website. This website checks whether it is able to show something that your content blocker should block. Then redirect to the respective custom url (success/failure) that your app previously registered for. You could even use a hidden Safari View Controller without animation to avoid any distraction from the user's perspective (as shown here). (I guess this technique is used by former content blocker Peace)
Steps
App
Register custom URLs for success/failure
Register for notification callback using the NotificationCenter (e.g.
contentBlockerEnabled)
Use SFSafariViewController to show a custom website and include the following rule in blockerList.json:
{
"action": {
"type": "css-display-none",
"selector": ".blocked_selector"
},
"trigger": {
"url-filter": ".*"
}
}
Website
Check for blocked content:
if($('.blocked_selector').css('display') == "none") {
// Content blocker enabled
}
Redirect to custom URL (success/failure)
App
Post notification from
application:openURL:options: (success/failure based on called url)
Update 18/01
Following on from Tilo's hypothesis, I built the proposed solution. I wrote about what I learnt on Medium and you can grab the source files from GitHub.
TL;DR It works but only temperamentally due to the latency incurred of the content blocking rules database to update. A potential workaround is redirecting the test page to create an artificial delay.

Resources