/* Copyright 2024 New Vector Ltd. Copyright 2019 The Matrix.org Foundation C.I.C Copyright 2018 New Vector Ltd Copyright 2016 OpenMarket Ltd SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ #import "MXKWebViewViewController.h" #import "NSBundle+MatrixKit.h" #import #import "MXKSwiftHeader.h" NSString *const kMXKWebViewViewControllerPostMessageJSLog = @"jsLog"; // Override console.* logs methods to send WebKit postMessage events to native code. // Note: this code has a minimal support of multiple parameters in console.log() NSString *const kMXKWebViewViewControllerJavaScriptEnableLog = @"console.debug = console.log; console.info = console.log; console.warn = console.log; console.error = console.log;" \ @"console.log = function() {" \ @" var msg = arguments[0];" \ @" for (var i = 1; i < arguments.length; i++) {" \ @" msg += ' ' + arguments[i];" \ @" }" \ @" window.webkit.messageHandlers.%@.postMessage(msg);" \ @"};"; @interface MXKWebViewViewController () { BOOL enableDebug; // Right buttons bar state before loading the webview NSArray *originalRightBarButtonItems; } @end @implementation MXKWebViewViewController - (instancetype)init { self = [super init]; if (self) { enableDebug = NO; } return self; } - (id)initWithURL:(NSString*)URL { self = [self init]; if (self) { _URL = URL; } return self; } - (id)initWithLocalHTMLFile:(NSString*)localHTMLFile { self = [self init]; if (self) { _localHTMLFile = localHTMLFile; } return self; } - (void)enableDebug { // We can only call addScriptMessageHandler on a given message only once if (enableDebug) { return; } enableDebug = YES; // Redirect all console.* logging methods into a WebKit postMessage event with name "jsLog" [webView.configuration.userContentController addScriptMessageHandler:self name:kMXKWebViewViewControllerPostMessageJSLog]; NSString *javaScriptString = [NSString stringWithFormat:kMXKWebViewViewControllerJavaScriptEnableLog, kMXKWebViewViewControllerPostMessageJSLog]; [webView evaluateJavaScript:javaScriptString completionHandler:nil]; } - (void)finalizeInit { [super finalizeInit]; } - (void)viewDidLoad { [super viewDidLoad]; originalRightBarButtonItems = self.navigationItem.rightBarButtonItems; // Init the webview webView = [[WKWebView alloc] initWithFrame:self.view.frame]; webView.backgroundColor= [UIColor whiteColor]; webView.navigationDelegate = self; webView.UIDelegate = self; [webView setTranslatesAutoresizingMaskIntoConstraints:NO]; [self.view addSubview:webView]; // Force webview in full width (to handle auto-layout in case of screen rotation) NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:webView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0]; NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:webView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0]; // Force webview in full height #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:webView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]; NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:webView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.bottomLayoutGuide attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]; #pragma clang diagnostic pop [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]]; backButton = [[UIBarButtonItem alloc] initWithTitle:[VectorL10n back] style:UIBarButtonItemStylePlain target:self action:@selector(goBack)]; if (_URL.length) { self.URL = _URL; } else if (_localHTMLFile.length) { self.localHTMLFile = _localHTMLFile; } } - (void)destroy { if (webView) { webView.navigationDelegate = nil; [webView stopLoading]; [webView removeFromSuperview]; webView = nil; } backButton = nil; _URL = nil; _localHTMLFile = nil; [super destroy]; } - (void)dealloc { [self destroy]; } - (void)setURL:(NSString *)URL { [webView stopLoading]; _URL = URL; _localHTMLFile = nil; if (URL.length) { NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; [webView loadRequest:request]; } } - (void)setLocalHTMLFile:(NSString *)localHTMLFile { [webView stopLoading]; _localHTMLFile = localHTMLFile; _URL = nil; if (localHTMLFile.length) { NSString* htmlString = [NSString stringWithContentsOfFile:localHTMLFile encoding:NSUTF8StringEncoding error:nil]; [webView loadHTMLString:htmlString baseURL:nil]; } } - (void)goBack { if (webView.canGoBack) { [webView goBack]; } else if (_localHTMLFile.length) { // Reload local html file self.localHTMLFile = _localHTMLFile; } } #pragma mark - WKNavigationDelegate - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { // Handle back button visibility here BOOL canGoBack = webView.canGoBack; if (_localHTMLFile.length && !canGoBack) { // Check whether the current content is not the local html file canGoBack = (![webView.URL.absoluteString isEqualToString:@"about:blank"]); } // bwi #7861 don't overwrite other barbutton items here if (self.navigationItem.rightBarButtonItem == nil) { if (canGoBack) { self.navigationItem.rightBarButtonItem = backButton; } else { // Reset the original state self.navigationItem.rightBarButtonItems = originalRightBarButtonItems; } } } - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler { NSURLProtectionSpace *protectionSpace = [challenge protectionSpace]; // We handle here only the server trust authentication. // We fallback to the default logic for other cases. if (protectionSpace.authenticationMethod != NSURLAuthenticationMethodServerTrust || !protectionSpace.serverTrust) { completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); return; } SecTrustRef serverTrust = [protectionSpace serverTrust]; // Check first whether there are some pinned certificates (certificate included in the bundle). NSArray *paths = [[NSBundle mainBundle] pathsForResourcesOfType:@"cer" inDirectory:@"."]; if (paths.count) { NSMutableArray *pinnedCertificates = [NSMutableArray array]; for (NSString *path in paths) { NSData *certificateData = [NSData dataWithContentsOfFile:path]; [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)]; } // Only use these certificates to pin against, and do not trust the built-in anchor certificates. SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates); } else { // Check whether some certificates have been trusted by the user (self-signed certificates support). NSSet *certificates = [MXAllowedCertificates sharedInstance].certificates; if (certificates.count) { NSMutableArray *allowedCertificates = [NSMutableArray array]; for (NSData *certificateData in certificates) { [allowedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)]; } // Add all the allowed certificates to the chain of trust SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)allowedCertificates); // Reenable trusting the built-in anchor certificates in addition to those passed in via the SecTrustSetAnchorCertificates API. SecTrustSetAnchorCertificatesOnly(serverTrust, false); } } // Re-evaluate the trust policy SecTrustResultType secresult = kSecTrustResultInvalid; if (SecTrustEvaluate(serverTrust, &secresult) != errSecSuccess) { // Reject the server auth if an error occurs completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); } else { switch (secresult) { case kSecTrustResultUnspecified: // The OS trusts this certificate implicitly. case kSecTrustResultProceed: // The user explicitly told the OS to trust it. { NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; completionHandler(NSURLSessionAuthChallengeUseCredential, credential); break; } default: { completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); break; } } } } #pragma mark - WKUIDelegate - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(nonnull WKWebViewConfiguration *)configuration forNavigationAction:(nonnull WKNavigationAction *)navigationAction windowFeatures:(nonnull WKWindowFeatures *)windowFeatures { // Make sure we open links with `target="_blank"` within this webview if (!navigationAction.targetFrame.isMainFrame) { [webView loadRequest:navigationAction.request]; } return nil; } #pragma mark - WKScriptMessageHandler - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:kMXKWebViewViewControllerPostMessageJSLog]) { MXLogDebug(@"-- JavaScript: %@", message.body); } } #pragma mark - BWI: WebViewLinkPolicy -(void)webView:(WKWebView *)webview decidePolicyForNavigationAction:(nonnull WKNavigationAction *)navigationAction decisionHandler:(nonnull void (^)(WKNavigationActionPolicy))decisionHandler { if (navigationAction.navigationType == WKNavigationTypeLinkActivated) { // bwi: clicked links should be opened in system browser if (navigationAction.request.URL) { [[UIApplication sharedApplication] vc_open:navigationAction.request.URL completionHandler:nil]; } decisionHandler(WKNavigationActionPolicyCancel); } else { // bwi: Open url in webview decisionHandler(WKNavigationActionPolicyAllow); } } @end