Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qdarwinwebview_p.h"
#include <QtWebView/qwebview.h>
#include <QtWebView/qwebviewloadinginfo.h>
#include <QtWebView/private/qwebviewfactory_p.h>
#include "qtwebviewfunctions.h"
#include <QtCore/private/qglobal_p.h>
#include <QtCore/qdatetime.h>
#include <QtCore/qmap.h>
#include <QtCore/qvariant.h>
#include <QtQuick/qquickrendercontrol.h>
#include <QtQuick/qquickwindow.h>
#include <CoreFoundation/CoreFoundation.h>
#include <WebKit/WebKit.h>
#include <QtCore/qjsondocument.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qmimedatabase.h>
#include <QtCore/qmimetype.h>
#ifdef Q_OS_IOS
#import <UIKit/UIKit.h>
#endif
#ifdef Q_OS_MACOS
#include <AppKit/AppKit.h>
typedef NSView UIView;
#endif
@interface QtWKWebViewDelegate : NSObject<WKNavigationDelegate> {
QPointer<QDarwinWebViewPrivate> qDarwinWebViewPrivate;
}
- (QtWKWebViewDelegate *)initWithQWebViewPrivate:(QDarwinWebViewPrivate *)webViewPrivate;
- (void)pageDone;
- (void)handleError:(NSError *)error;
// protocol:
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation
withError:(NSError *)error;
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation
withError:(NSError *)error;
@end
@implementation QtWKWebViewDelegate
- (QtWKWebViewDelegate *)initWithQWebViewPrivate:(QDarwinWebViewPrivate *)webViewPrivate
{
if ((self = [super init])) {
Q_ASSERT(webViewPrivate);
qDarwinWebViewPrivate = webViewPrivate;
}
return self;
}
- (void)pageDone
{
if (!qDarwinWebViewPrivate)
return;
emit qDarwinWebViewPrivate->q_ptr->loadProgressChanged(qDarwinWebViewPrivate->loadProgress());
}
- (void)handleError:(NSError *)error
{
if (!qDarwinWebViewPrivate)
return;
[self pageDone];
NSString *errorString = [error localizedDescription];
NSURL *failingURL = error.userInfo[@"NSErrorFailingURLKey"];
const QUrl url = [failingURL isKindOfClass:[NSURL class]]
? QUrl::fromNSURL(failingURL) : qDarwinWebViewPrivate->url();
emit qDarwinWebViewPrivate->q_ptr->loadingChanged(
QWebViewFactory::LoadingInfo::create(url, QWebViewLoadingInfo::LoadStatus::Failed,
QString::fromNSString(errorString)));
}
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation
{
Q_UNUSED(webView);
if (!qDarwinWebViewPrivate)
return;
// WKNavigationDelegate gives us per-frame notifications while the QWebView API
// should provide per-page notifications. Therefore we keep track of the last frame
// to be started, if that finishes or fails then we indicate that it has loaded.
if (qDarwinWebViewPrivate->wkNavigation != navigation)
qDarwinWebViewPrivate->wkNavigation = navigation;
else
return;
QUrl url = qDarwinWebViewPrivate->url();
emit qDarwinWebViewPrivate->q_ptr->urlChanged(url);
emit qDarwinWebViewPrivate->q_ptr->loadingChanged(
QWebViewFactory::LoadingInfo::create(url,
QWebViewLoadingInfo::LoadStatus::Started, QString()));
emit qDarwinWebViewPrivate->q_ptr->loadProgressChanged(qDarwinWebViewPrivate->loadProgress());
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
Q_UNUSED(webView);
if (!qDarwinWebViewPrivate)
return;
if (qDarwinWebViewPrivate->wkNavigation != navigation)
return;
[self pageDone];
emit qDarwinWebViewPrivate->q_ptr->loadingChanged(
QWebViewFactory::LoadingInfo::create(qDarwinWebViewPrivate->url(),
QWebViewLoadingInfo::LoadStatus::Succeeded, QString()));
}
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation
withError:(NSError *)error
{
Q_UNUSED(webView);
if (!qDarwinWebViewPrivate)
return;
if (qDarwinWebViewPrivate->wkNavigation != navigation)
return;
[self handleError:error];
}
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation
withError:(NSError *)error
{
Q_UNUSED(webView);
if (!qDarwinWebViewPrivate)
return;
if (qDarwinWebViewPrivate->wkNavigation != navigation)
return;
[self handleError:error];
}
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
__attribute__((availability(ios_app_extension,unavailable)))
{
Q_UNUSED(webView);
NSURL *url = navigationAction.request.URL;
const BOOL handled = (^{
// For links with target="_blank", open externally
if (!navigationAction.targetFrame)
return NO;
if ([WKWebView handlesURLScheme:url.scheme]
|| [webView.configuration urlSchemeHandlerForURLScheme:url.scheme]) {
return YES;
}
return NO;
})();
if (!handled) {
#ifdef Q_OS_MACOS
[[NSWorkspace sharedWorkspace] openURL:url];
#elif defined(Q_OS_IOS)
// Check if it can be opened first, if it is a file scheme then it can't
// be opened, therefore if it is a _blank target in that case we need to open
// inside the current webview
if ([[UIApplication sharedApplication] canOpenURL:url])
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
else if (!navigationAction.targetFrame)
[webView loadRequest:navigationAction.request];
#endif
}
decisionHandler(handled ? WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel);
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change
context:(void *)context {
Q_UNUSED(object);
Q_UNUSED(change);
Q_UNUSED(context);
if ([keyPath isEqualToString:@"estimatedProgress"]) {
emit qDarwinWebViewPrivate->q_ptr->loadProgressChanged(
qDarwinWebViewPrivate->loadProgress());
} else if ([keyPath isEqualToString:@"title"]) {
emit qDarwinWebViewPrivate->q_ptr->titleChanged(qDarwinWebViewPrivate->title());
}
}
@end
@interface QrcSchemeHandler : NSObject<WKURLSchemeHandler> {
}
// protocol:
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask;
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask;
@end
@implementation QrcSchemeHandler
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
QFile file(u':' + QUrl::fromNSURL(urlSchemeTask.request.URL).path());
bool isOpen = file.exists() && file.size() != 0 && file.open(QIODeviceBase::ReadOnly);
if (!isOpen) {
[urlSchemeTask didFailWithError:[NSError
errorWithDomain:NSURLErrorDomain
code:404
userInfo:@{ @"NSErrorFailingURLKey" : urlSchemeTask.request.URL }]];
return;
}
QFileInfo fileInfo(file);
QMimeDatabase mimeDatabase;
QMimeType mimeType = mimeDatabase.mimeTypeForFile(fileInfo);
[urlSchemeTask didReceiveResponse:[[NSURLResponse alloc]
initWithURL:urlSchemeTask.request.URL
MIMEType:mimeType.name().toNSString()
expectedContentLength:fileInfo.size()
textEncodingName:nil]];
QByteArray data = file.readAll();
[urlSchemeTask didReceiveData:data.toRawNSData()];
Q_ASSERT(file.atEnd());
file.close();
[urlSchemeTask didFinish];
}
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {}
@end
QT_BEGIN_NAMESPACE
QDarwinWebViewSettingsPrivate::QDarwinWebViewSettingsPrivate(WKWebViewConfiguration *conf)
: m_conf(conf)
{
}
bool QDarwinWebViewSettingsPrivate::doTestAttribute(WebAttribute attribute) const
{
switch (attribute) {
case QWebViewSettings::WebAttribute::LocalStorageEnabled:
return localStorageEnabled();
case QWebViewSettings::WebAttribute::JavaScriptEnabled:
return javaScriptEnabled();
case QWebViewSettings::WebAttribute::AllowFileAccess:
return allowFileAccess();
case QWebViewSettings::WebAttribute::LocalContentCanAccessFileUrls:
return localContentCanAccessFileUrls();
}
return false;
}
void QDarwinWebViewSettingsPrivate::doSetAttribute(WebAttribute attribute, bool value)
{
switch (attribute) {
case QWebViewSettings::WebAttribute::LocalStorageEnabled:
setLocalStorageEnabled(value);
break;
case QWebViewSettings::WebAttribute::JavaScriptEnabled:
setJavaScriptEnabled(value);
break;
case QWebViewSettings::WebAttribute::AllowFileAccess:
setAllowFileAccess(value);
break;
case QWebViewSettings::WebAttribute::LocalContentCanAccessFileUrls:
setLocalContentCanAccessFileUrls(value);
break;
}
}
bool QDarwinWebViewSettingsPrivate::localStorageEnabled() const
{
return m_conf.websiteDataStore.persistent;
}
bool QDarwinWebViewSettingsPrivate::javaScriptEnabled() const
{
bool isJsEnabled = m_conf.defaultWebpagePreferences.allowsContentJavaScript;
return isJsEnabled;
}
bool QDarwinWebViewSettingsPrivate::localContentCanAccessFileUrls() const
{
return m_localContentCanAccessFileUrls;
}
bool QDarwinWebViewSettingsPrivate::allowFileAccess() const
{
return m_allowFileAccess;
}
void QDarwinWebViewSettingsPrivate::setLocalContentCanAccessFileUrls(bool enabled)
{
// This will be checked in QDarwinWebViewPrivate::setUrl()
m_localContentCanAccessFileUrls = enabled;
}
void QDarwinWebViewSettingsPrivate::setJavaScriptEnabled(bool enabled)
{
m_conf.defaultWebpagePreferences.allowsContentJavaScript = enabled;
}
void QDarwinWebViewSettingsPrivate::setLocalStorageEnabled(bool enabled)
{
if (enabled == localStorageEnabled())
return;
if (enabled)
m_conf.websiteDataStore = [WKWebsiteDataStore defaultDataStore];
else
m_conf.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
}
void QDarwinWebViewSettingsPrivate::setAllowFileAccess(bool enabled)
{
// This will be checked in QDarwinWebViewPrivate::setUrl()
m_allowFileAccess = enabled;
}
QDarwinWebViewPrivate::QDarwinWebViewPrivate(QWebView *view) : QWebViewPrivate(view), wkWebView(nil)
{
auto config = [[[WKWebViewConfiguration alloc] init] autorelease];
[config setURLSchemeHandler:[[[QrcSchemeHandler alloc] init] autorelease] forURLScheme:@"qrc"];
CGRect frame = CGRectMake(0.0, 0.0, 400, 400);
wkWebView = [[WKWebView alloc] initWithFrame:frame configuration:config];
wkWebView.navigationDelegate = [[QtWKWebViewDelegate alloc] initWithQWebViewPrivate:this];
[wkWebView addObserver:wkWebView.navigationDelegate forKeyPath:@"estimatedProgress"
options:NSKeyValueObservingOptions(NSKeyValueObservingOptionNew)
context:nil];
[wkWebView addObserver:wkWebView.navigationDelegate forKeyPath:@"title"
options:NSKeyValueObservingOptions(NSKeyValueObservingOptionNew)
context:nil];
m_window = QWindow::fromWinId(reinterpret_cast<WId>(wkWebView));
if (m_window) {
m_window->setParent(view);
connect(view, &QWindow::widthChanged, m_window, &QWindow::setWidth);
connect(view, &QWindow::heightChanged, m_window, &QWindow::setHeight);
connect(view, &QWindow::visibleChanged, m_window, &QWindow::setVisible);
}
m_settings = new QDarwinWebViewSettingsPrivate(wkWebView.configuration);
}
QDarwinWebViewPrivate::~QDarwinWebViewPrivate()
{
[wkWebView stopLoading];
[wkWebView removeObserver:wkWebView.navigationDelegate forKeyPath:@"estimatedProgress"
context:nil];
[wkWebView removeObserver:wkWebView.navigationDelegate forKeyPath:@"title"
context:nil];
[wkWebView.navigationDelegate release];
wkWebView.navigationDelegate = nil;
[wkWebView release];
if (m_window)
delete m_window;
}
QUrl QDarwinWebViewPrivate::url() const
{
return QUrl::fromNSURL(wkWebView.URL);
}
void QDarwinWebViewPrivate::setUrl(const QUrl &url)
{
if (url.isValid()) {
if (url.isLocalFile()) {
// NOTE: Check if the file exists before attempting to load it, we follow the same
// asynchronous pattern as expected to not break the tests (Started + Failed).
bool exists = QFile::exists(url.toLocalFile());
// We need to pass local files via loadFileURL and the read access should cover
// the directory that the file is in, to facilitate loading referenced images etc
if (exists && m_settings->allowFileAccess()) {
if (m_settings->localContentCanAccessFileUrls())
[wkWebView loadFileURL:url.toNSURL() allowingReadAccessToURL:QUrl(url.toString(QUrl::RemoveFilename)).toNSURL()];
else
[wkWebView loadRequest:[NSURLRequest requestWithURL:url.toNSURL()]];
} else {
QMetaObject::invokeMethod(
q_ptr, &QWebView::loadingChanged, Qt::QueuedConnection,
QWebViewFactory::LoadingInfo::create(url, QWebViewLoadingInfo::LoadStatus::Started,
{}));
QMetaObject::invokeMethod(
q_ptr, &QWebView::loadingChanged, Qt::QueuedConnection,
QWebViewFactory::LoadingInfo::create(url, QWebViewLoadingInfo::LoadStatus::Failed,
exists
? QStringLiteral("Permission denied")
: QStringLiteral("File does not exist")));
return;
}
} else {
[wkWebView loadRequest:[NSURLRequest requestWithURL:url.toNSURL()]];
}
} else {
QMetaObject::invokeMethod(
q_ptr, &QWebView::loadingChanged, Qt::QueuedConnection,
QWebViewFactory::LoadingInfo::create(url, QWebViewLoadingInfo::LoadStatus::Failed,
QStringLiteral("Invalid URL")));
}
}
void QDarwinWebViewPrivate::loadHtml(const QString &html, const QUrl &baseUrl)
{
[wkWebView loadHTMLString:html.toNSString() baseURL:baseUrl.toNSURL()];
}
bool QDarwinWebViewPrivate::canGoBack() const
{
return wkWebView.canGoBack;
}
bool QDarwinWebViewPrivate::canGoForward() const
{
return wkWebView.canGoForward;
}
QString QDarwinWebViewPrivate::title() const
{
return QString::fromNSString(wkWebView.title);
}
int QDarwinWebViewPrivate::loadProgress() const
{
return int(wkWebView.estimatedProgress * 100);
}
bool QDarwinWebViewPrivate::isLoading() const
{
return wkWebView.loading;
}
void QDarwinWebViewPrivate::goBack()
{
[wkWebView goBack];
}
void QDarwinWebViewPrivate::goForward()
{
[wkWebView goForward];
}
void QDarwinWebViewPrivate::stop()
{
[wkWebView stopLoading];
}
void QDarwinWebViewPrivate::reload()
{
[wkWebView reload];
}
QVariant fromNSNumber(const NSNumber *number)
{
if (!number)
return QVariant();
if (strcmp([number objCType], @encode(BOOL)) == 0) {
return QVariant::fromValue(!![number boolValue]);
} else if (strcmp([number objCType], @encode(signed char)) == 0) {
return QVariant::fromValue([number charValue]);
} else if (strcmp([number objCType], @encode(unsigned char)) == 0) {
return QVariant::fromValue([number unsignedCharValue]);
} else if (strcmp([number objCType], @encode(signed short)) == 0) {
return QVariant::fromValue([number shortValue]);
} else if (strcmp([number objCType], @encode(unsigned short)) == 0) {
return QVariant::fromValue([number unsignedShortValue]);
} else if (strcmp([number objCType], @encode(signed int)) == 0) {
return QVariant::fromValue([number intValue]);
} else if (strcmp([number objCType], @encode(unsigned int)) == 0) {
return QVariant::fromValue([number unsignedIntValue]);
} else if (strcmp([number objCType], @encode(signed long long)) == 0) {
return QVariant::fromValue([number longLongValue]);
} else if (strcmp([number objCType], @encode(unsigned long long)) == 0) {
return QVariant::fromValue([number unsignedLongLongValue]);
} else if (strcmp([number objCType], @encode(float)) == 0) {
return QVariant::fromValue([number floatValue]);
} else if (strcmp([number objCType], @encode(double)) == 0) {
return QVariant::fromValue([number doubleValue]);
}
return QVariant();
}
QVariant fromJSValue(id result)
{
if ([result isKindOfClass:[NSString class]])
return QString::fromNSString(static_cast<NSString *>(result));
if ([result isKindOfClass:[NSNumber class]])
return fromNSNumber(static_cast<NSNumber *>(result));
if ([result isKindOfClass:[NSDate class]])
return QDateTime::fromNSDate(static_cast<NSDate *>(result));
if ([result isKindOfClass:[NSArray class]]
|| [result isKindOfClass:[NSDictionary class]]) {
@try {
// Round-trip via JSON, so we don't have to implement conversion
// from NSArray and NSDictionary manually.
// FIXME: NSJSONSerialization requires that any nested object
// is NSString, NSNumber, NSArray, NSDictionary, or NSNull, so
// nested NSDates are not supported -- meaning we support plain
// NSDate (above), but not in an array or dict. To handle this
// use-case we'd need a manual conversion.
auto jsonData = QByteArray::fromNSData(
[NSJSONSerialization dataWithJSONObject:result options:0 error:nil]);
QJsonParseError parseError;
auto jsonDocument = QJsonDocument::fromJson(jsonData, &parseError);
if (parseError.error == QJsonParseError::NoError)
return jsonDocument.toVariant();
} @catch (NSException *) {
return QVariant();
}
}
return QVariant();
}
void QDarwinWebViewPrivate::runJavaScript(
const QString &script, const std::function<void(const QVariant &)> &resultCallback)
{
std::function<void(const QVariant &)> callbackCopy = resultCallback;
QPointer<QDarwinWebViewPrivate> observer(this);
[wkWebView evaluateJavaScript:script.toNSString()
completionHandler:^(id result, NSError *error) {
QVariant r = error ? QVariant() : fromJSValue(result);
if (callbackCopy && observer)
callbackCopy(r);
}];
}
void QDarwinWebViewPrivate::setCookie(const QString &domain, const QString &name, const QString &value)
{
NSString *cookieDomain = domain.toNSString();
NSString *cookieName = name.toNSString();
NSString *cookieValue = value.toNSString();
WKHTTPCookieStore *cookieStore = wkWebView.configuration.websiteDataStore.httpCookieStore;
if (cookieStore == nullptr) {
return;
}
NSMutableDictionary *cookieProperties = [NSMutableDictionary dictionary];
[cookieProperties setObject:cookieName forKey:NSHTTPCookieName];
[cookieProperties setObject:cookieValue forKey:NSHTTPCookieValue];
[cookieProperties setObject:cookieDomain forKey:NSHTTPCookieDomain];
[cookieProperties setObject:cookieDomain forKey:NSHTTPCookieOriginURL];
[cookieProperties setObject:@"/" forKey:NSHTTPCookiePath];
[cookieProperties setObject:@"0" forKey:NSHTTPCookieVersion];
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:cookieProperties];
if (cookie == nullptr) {
return;
}
QPointer<QDarwinWebViewPrivate> observer(this);
[cookieStore setCookie:cookie
completionHandler:^{
if (observer)
emit q_ptr->cookieAdded(QString::fromNSString(cookie.domain),
QString::fromNSString(cookie.name));
}];
}
void QDarwinWebViewPrivate::deleteCookie(const QString &domain, const QString &name)
{
NSString *cookieDomain = domain.toNSString();
NSString *cookieName = name.toNSString();
WKHTTPCookieStore *cookieStore = wkWebView.configuration.websiteDataStore.httpCookieStore;
if (cookieStore == nullptr) {
return;
}
QPointer<QDarwinWebViewPrivate> observer(this);
[cookieStore getAllCookies:^(NSArray *cookies) {
NSHTTPCookie *cookie;
for (cookie in cookies) {
if ([cookie.domain isEqualToString:cookieDomain] && [cookie.name isEqualToString:cookieName]) {
[cookieStore deleteCookie:cookie
completionHandler:^{
if (observer)
emit q_ptr->cookieRemoved(QString::fromNSString(cookie.domain),
QString::fromNSString(cookie.name));
}];
}
}
}];
}
void QDarwinWebViewPrivate::deleteAllCookies()
{
WKHTTPCookieStore *cookieStore = wkWebView.configuration.websiteDataStore.httpCookieStore;
QPointer<QDarwinWebViewPrivate> observer(this);
[cookieStore getAllCookies:^(NSArray *cookies) {
NSHTTPCookie *cookie;
for (cookie in cookies) {
[cookieStore deleteCookie:cookie
completionHandler:^{
if (observer)
emit q_ptr->cookieRemoved(QString::fromNSString(cookie.domain),
QString::fromNSString(cookie.name));
}];
}
}];
}
QString QDarwinWebViewPrivate::httpUserAgent() const
{
return QString::fromNSString([wkWebView valueForKey:@"userAgent"]);
}
void QDarwinWebViewPrivate::setHttpUserAgent(const QString &userAgent)
{
if (!userAgent.isEmpty()) {
wkWebView.customUserAgent = userAgent.toNSString();
}
emit q_ptr->httpUserAgentStringChanged(userAgent);
}
QWebViewSettingsPrivate *QDarwinWebViewPrivate::settings() const
{
return m_settings;
}
QT_END_NAMESPACE