/*---------------------------------------------------------------------------*/
/* WebViewController.m                                                       */
/* copyright (c) innovaphone 2015                                            */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#include <errno.h>
#import "platform/platform.h"
#import "platform/IosNamespace.h"
#import "../ios_forms/FormsConst.h"
#import "PhoneIosDelegate.h"
#import "WebViewNavigationController.h"
#import "WebViewController.h"

#define SHOW_CONSOLE_MESSAGE_SOURCE

#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
#else
#if (defined(CLIENT_IOS_USE_WKWEBVIEW) && defined(CLIENT_IOS_USE_WKWEBVIEW_WINDOWS))
#else

@implementation NSObject (DidCreateJavaScriptContext)

- (void) webView:(id)webView didCreateJavaScriptContext:(JSContext*)context forFrame:(id)frame
{
    [context evaluateScript:@""
        "(function () {\n"
        "    'use strict';\n"
        "    /* global document, window, frames, setInterval, clearInterval */\n"
        "    if (typeof(window.clientiosSavedWindowOpen) == 'undefined') {\n"
        "        window.clientiosSavedWindowOpen = window.open;\n"
        "    }\n"
        "    var windowOpenFunction = function (url, name, specs, replace) {\n"
        "        if (name === '_self') {\n"
        "            window.clientiosSavedWindowOpen(url, name, specs, replace);\n"
        "        }\n"
        "        else {\n"
        "            var iframe = document.createElement('IFRAME');\n"
        "            iframe.setAttribute('src', 'com.innovaphone.clientioswindowopen' + url);\n"
        "            iframe.setAttribute('frameborder', '0');\n"
        "            iframe.style.width = '1px';\n"
        "            iframe.style.height = '1px';\n"
        "            document.body.appendChild(iframe);\n"
        "            document.body.removeChild(iframe);\n"
        "            iframe = null;\n"
        "        }\n"
        "    };\n"
        "    if (typeof(window.clientiosSavedWindowClose) == 'undefined') {\n"
        "        window.clientiosSavedWindowClose = window.close;\n"
        "    }\n"
        "    window.close = function () {\n"
        "        if (window.name === '_self') {\n"
        "            window.clientiosSavedWindowClose();\n"
        "        }\n"
        "        else {\n"
        "            var iframe = document.createElement('IFRAME');\n"
        "            iframe.setAttribute('src', 'com.innovaphone.clientioswindowclose://localhost');\n"
        "            iframe.setAttribute('frameborder', '0');\n"
        "            iframe.style.width = '1px';\n"
        "            iframe.style.height = '1px';\n"
        "            document.body.appendChild(iframe);\n"
        "            document.body.removeChild(iframe);\n"
        "            iframe = null;\n"
        "        }\n"
        "    };\n"
        "    var windowMakeHandler = function (anchor) {\n"
        "        return function () {\n"
        "            window.open(anchor.getAttribute('href'), '_blank');\n"
        "        };\n"
        "    };\n"
        "    window.open = windowOpenFunction;\n"
        "    window.clientiosMakeHandler = windowMakeHandler;\n"
        "    window.clientiosWebViewReadyInterval = setInterval(function () {\n"
        "        if (document.readyState === 'complete') {\n"
        "            var i, ab = document.getElementsByTagName('a'), abLength = ab.length;\n"
        "            for (i = 0; i < abLength; i += 1) {\n"
        "                if (ab[i].getAttribute('target') === '_blank') {\n"
        "                    ab[i].removeAttribute('target');\n"
        "                    ab[i].onclick = window.clientiosMakeHandler(ab[i]);\n"
        "                }\n"
        "            }\n"
        "            clearInterval(window.clientiosWebViewReadyInterval);\n"
        "        }\n"
        "    }, 10);\n"
        "    var f;\n"
        "    for (f = 0; f < frames.length; f++) {\n"
        "        try {\n"
        "            frames[f].window.open = windowOpenFunction;\n"
        "            var i, fb = frames[f].document.getElementsByTagName('a'), fbLength = fb.length;\n"
        "            for (i = 0; i < fbLength; i += 1) {\n"
        "                if (fb[i].getAttribute('target') === '_blank') {\n"
        "                    fb[i].removeAttribute('target');\n"
        "                    fb[i].onclick = windowMakeHandler(fb[i]);\n"
        "                }\n"
        "            }\n"
        "        } catch (e) {\n"
        "            /* Security error. Can't access frame of different origin. Fail silently. */\n"
        "        }\n"
        "    }\n"
        "}());\n" /*withSourceURL:[NSURL URLWithString:@"window_open_.js"]*/];
}

@end

#endif
#endif

@class DownloadConnection;
@interface DownloadConnection : NSObject <NSURLConnectionDelegate, NSURLConnectionDataDelegate>

@property (nonatomic) NSURLRequest *request;
@property (nonatomic) UIWebViewNavigationType navigationType;
@property (nonatomic) bool download;
@property (nonatomic) NSURLConnection *connection;
@property (nonatomic) NSString *filePath;
@property (nonatomic) int download_fd;
@property (nonatomic,strong) void (^completionHandler)(void);

- (id) initWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType completionHandler:(void (^)(void))completionHandler;

@end
@implementation DownloadConnection

- (id) initWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType completionHandler:(void (^)(void))completionHandler
{
    self = [super init];
    if (self) {
        _request = request;
        _navigationType = navigationType;
        _completionHandler = completionHandler;
        self.filePath = nil;
        self.download_fd = -1;
        self.download = false;
/*
        self.download = ((navigationType == UIWebViewNavigationTypeLinkClicked) &&
            (([request.URL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame) ||
             ([request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)));
*/
        if (self.download) {
            self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
            [self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
            [self.connection start];
        }
        else {
            self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
            [self.connection start];
        }
    }
    return self;
}

- (BOOL) connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
    return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

- (void) connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        NSData * cert_data = [WebViewController decodeBase64WithString:(((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).cert_string)];
        const char * begin, * end;
        begin = strstr(cert_data.bytes, "-----BEGIN CERTIFICATE-----");
        end = strstr(cert_data.bytes, "-----END CERTIFICATE-----");
        if (begin && end && (begin < end)) {
            begin += strlen("-----BEGIN CERTIFICATE-----");
            cert_data = [WebViewController decodeBase64WithString:[[NSString alloc] initWithBytes:begin length:end-begin encoding:NSASCIIStringEncoding]];
        }
        SecCertificateRef cert = SecCertificateCreateWithData(kCFAllocatorDefault, (CFDataRef)cert_data);
        if (cert != NULL){
            SecCertificateRef certArray[1] = {cert};
            CFArrayRef certs = CFArrayCreate(kCFAllocatorDefault, (void *)certArray, 1, NULL);
            SecPolicyRef policy = SecPolicyCreateBasicX509();
            SecTrustRef trust = NULL;
            OSStatus status = SecTrustCreateWithCertificates(certs, policy, &trust);
            if (status == errSecSuccess){
                SecTrustResultType trustType = kSecTrustResultInvalid;
                status = SecTrustEvaluate(trust, &trustType);

                // Evaulate the trust.
                switch (trustType) {
                    case kSecTrustResultInvalid:
                    case kSecTrustResultDeny:
                    case kSecTrustResultFatalTrustFailure:
                    case kSecTrustResultOtherError:
                        break;
                    case kSecTrustResultUnspecified:
                    case kSecTrustResultConfirm:
                    case kSecTrustResultRecoverableTrustFailure:
                    case kSecTrustResultProceed:
                        [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
                        break;
                }
            }
            if (trust) {
                CFRelease(trust);
            }
            if (policy) {
                CFRelease(policy);
            }
            if (certs) {
                CFRelease(certs);
            }
            CFRelease(cert);

/*
            SecCertificateRef certArray[1] = {cert};
            CFArrayRef certs = CFArrayCreate(kCFAllocatorDefault, (void *)certArray, 1, NULL);
            // NO for client, YES for server.  In this example, we are a client
            // replace "localhost" with the name of the server to which you are connecting
            SecPolicyRef policy = SecPolicyCreateSSL(NO, CFSTR("localhost"));
            SecTrustRef trust = NULL;
            // #2
//            CFArrayRef streamCertificates = [aStream propertyForKey:(NSString *) kCFStreamPropertySSLPeerCertificates];
            // #3
            SecTrustCreateWithCertificates(certs, policy, &trust);
            // #4
            SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)[NSArray arrayWithObject:(__bridge id)cert]);
            // #5
            SecTrustResultType trustResultType = kSecTrustResultInvalid;
            OSStatus status = SecTrustEvaluate(trust, &trustResultType);
            if (status == errSecSuccess){
                SecTrustResultType trustType = kSecTrustResultInvalid;
                status = SecTrustEvaluate(trust, &trustType);

                // Evaulate the trust.
                switch (trustType) {
                    case kSecTrustResultInvalid:
                    case kSecTrustResultDeny:
                    case kSecTrustResultFatalTrustFailure:
                    case kSecTrustResultOtherError:
                        break;
                    case kSecTrustResultUnspecified:
                    case kSecTrustResultConfirm:
                    case kSecTrustResultRecoverableTrustFailure:
                    case kSecTrustResultProceed:
                        [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
                        break;
                }
            }
            if (trust) {
                CFRelease(trust);
            }
            if (policy) {
                CFRelease(policy);
            }
            if (certs) {
                CFRelease(certs);
            }
            CFRelease(cert);
*/
        }
    }

    [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}

- (BOOL) connectionShouldUseCredentialStorage:(NSURLConnection *)connection
{
    return false;
}

- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"connection didFailWithError %@", error);
    if (self.download) {
        if (self.download_fd != -1) {
            close(self.download_fd);
            self.download_fd = -1;
        }
        dispatch_async(dispatch_get_main_queue(), self.completionHandler);
    }
    else {
        self.completionHandler();
    }
}

- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    if (self.download) {
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
        NSString *contentDisposition = nil;
        NSEnumerator *enumerator = httpResponse.allHeaderFields.keyEnumerator;
        NSString *key;
        while (((key = enumerator.nextObject) != nil) && ([key caseInsensitiveCompare:@"Content-Disposition"] != NSOrderedSame)) {
        }
        if (key != nil) {
            contentDisposition = [httpResponse.allHeaderFields objectForKey:key];
        }
        if ((contentDisposition != nil) && ([[contentDisposition substringWithRange:NSMakeRange(0, 10)] caseInsensitiveCompare:@"attachment"] == NSOrderedSame)) {
            NSString *downloadDir = [NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, YES) objectAtIndex:0];
            if (![[NSFileManager defaultManager] fileExistsAtPath:downloadDir]) {
                NSError *error;
                if (![[NSFileManager defaultManager] createDirectoryAtPath:downloadDir withIntermediateDirectories:YES attributes:nil error:&error]) {
                    NSLog(@"Failed to create '%@': %@", downloadDir, error.localizedDescription);
                }
            }
            self.filePath = [NSString stringWithFormat:@"%@/%@", downloadDir, response.suggestedFilename];
            self.download_fd = open(self.filePath.UTF8String, O_WRONLY | O_CREAT | O_APPEND, S_IREAD | S_IWRITE);
            if (self.download_fd == -1) {
                [connection cancel];
                dispatch_async(dispatch_get_main_queue(), self.completionHandler);
#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
#else
                [phoneIosDelegate.webViewController().webView loadRequest:self.request];
#endif
            }
        }
        else {
            [connection cancel];
            dispatch_async(dispatch_get_main_queue(), self.completionHandler);
#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
#else
            [phoneIosDelegate.webViewController().webView loadRequest:self.request];
#endif
        }
    }
    else {
        [connection cancel];
        self.completionHandler();
#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
#else
        [phoneIosDelegate.webViewController().webView loadRequest:self.request];
#endif
    }
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
   write(self.download_fd, data.bytes, data.length);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    if (self.download) {
        if (self.download_fd != -1) {
            close(self.download_fd);
            self.download_fd = -1;
        }
        ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).showToast([NSString stringWithFormat:@"Download complete %@", self.filePath]);
        dispatch_async(dispatch_get_main_queue(), self.completionHandler);
    }
}

@end

@interface WebViewController()

@property (nonatomic) bool Authenticated;
@property (nonatomic) UIDocumentInteractionController *documentInteractionController;
@property (nonatomic) JSContext *jsContext;
@property (nonatomic) SFSafariViewController *sfSafariViewNavigationController;
@property (nonatomic) NSString *downloadFilename;
@property (nonatomic) NSURL *previewUrl;
@property (nonatomic) float textZoomFactor;

@end
@implementation WebViewController

+ (void) openURL:(NSURL *)url
{
    if ([[UIApplication sharedApplication] respondsToSelector:@selector(openURL:options:completionHandler:)]) {
        [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success){}];
    }
    else {
        [[UIApplication sharedApplication] openURL:url];
    }
}

- (void) updateTextZoomFactor:(float)factor
{
#ifdef CLIENT_IOS_USE_WKWEBVIEW_WINDOWS
    unsigned i;
    if (self.navigationController != nil) {
        for (i = 0; i < self.navigationController.viewControllers.count; i++) {
            WebViewController *c = (WebViewController *)[self.navigationController.viewControllers objectAtIndex:i];
            c.textZoomFactor = factor;
            if (c.webViewWindow) {
                [c.webViewWindow setValue:[NSNumber numberWithFloat:c.textZoomFactor] forKey:@"textZoomFactor"];
            }
        }
    }
#endif
#ifdef CLIENT_IOS_USE_WKWEBVIEW
    self.textZoomFactor = factor;
    if (self.webView) {
        [self.webView setValue:[NSNumber numberWithFloat:self.textZoomFactor] forKey:@"textZoomFactor"];
    }
#endif
}

static const char base64EncodingTable[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const short base64DecodingTable[256] = {
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -2, -1, -1, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 62, -2, -2, -2, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -2, -2, -2,
    -2,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, -2,
    -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2
};

+ (NSString *) encodeBase64WithData:(NSData *)objData
{
    const unsigned char * objRawData = [objData bytes];
    char * objPointer;
    char * strResult;

    // Get the Raw Data length and ensure we actually have data
    int intLength = [objData length];
    if (intLength == 0) return nil;

    // Setup the String-based Result placeholder and pointer within that placeholder
    strResult = (char *)calloc((((intLength + 2) / 3) * 4) + 1, sizeof(char));
    objPointer = strResult;

    // Iterate through everything
    while (intLength > 2) { // keep going until we have less than 24 bits
        *objPointer++ = base64EncodingTable[objRawData[0] >> 2];
        *objPointer++ = base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
        *objPointer++ = base64EncodingTable[((objRawData[1] & 0x0f) << 2) + (objRawData[2] >> 6)];
        *objPointer++ = base64EncodingTable[objRawData[2] & 0x3f];

        // we just handled 3 octets (24 bits) of data
        objRawData += 3;
        intLength -= 3; 
    }

    // now deal with the tail end of things
    if (intLength != 0) {
        *objPointer++ = base64EncodingTable[objRawData[0] >> 2];
        if (intLength > 1) {
            *objPointer++ = base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
            *objPointer++ = base64EncodingTable[(objRawData[1] & 0x0f) << 2];
            *objPointer++ = '=';
        } else {
            *objPointer++ = base64EncodingTable[(objRawData[0] & 0x03) << 4];
            *objPointer++ = '=';
            *objPointer++ = '=';
        }
    }

    // Terminate the string-based result
    *objPointer = '\0';

    // Create result NSString object
    NSString *base64String = [NSString stringWithCString:strResult encoding:NSASCIIStringEncoding];

    // Free memory
    free(strResult);

    return base64String;
}

+ (NSData *) decodeBase64WithString:(NSString *)strBase64
{
    const char *objPointer = [strBase64 cStringUsingEncoding:NSASCIIStringEncoding];
    size_t intLength = strlen(objPointer);
    int intCurrent;
    int i = 0, j = 0, k;

    unsigned char *objResult = calloc(intLength, sizeof(unsigned char));

    // Run through the whole string, converting as we go
    while ( ((intCurrent = *objPointer++) != '\0') && (intLength-- > 0) ) {
        if (intCurrent == '=') {
            if (*objPointer != '=' && ((i % 4) == 1)) {// || (intLength > 0)) {
                // the padding character is invalid at this point -- so this entire string is invalid
                free(objResult);
                return nil;
            }
            continue;
        }

        intCurrent = base64DecodingTable[intCurrent];
        if (intCurrent == -1) {
            // we're at a whitespace -- simply skip over
            continue;
        } else if (intCurrent == -2) {
            // we're at an invalid character
            free(objResult);
            return nil;
        }

        switch (i % 4) {
            case 0:
                objResult[j] = intCurrent << 2;
                break;

            case 1:
                objResult[j++] |= intCurrent >> 4;
                objResult[j] = (intCurrent & 0x0f) << 4;
                break;

            case 2:
                objResult[j++] |= intCurrent >>2;
                objResult[j] = (intCurrent & 0x03) << 6;
                break;


            case 3:
                objResult[j++] |= intCurrent;
                break;
        }
        i++;
    }

    // mop things up if we ended on a boundary
    k = j;
    if (intCurrent == '=') {
        switch (i % 4) {
            case 1:
                // Invalid state
                free(objResult);
                return nil;

            case 2:
                k++;
                // flow through
            case 3:
                objResult[k] = 0;
        }
    }

    // Cleanup and setup the return NSData
    NSData * objData = [[NSData alloc] initWithBytes:objResult length:j];
    free(objResult);
    return objData;
}

+ (NSDictionary *) parseUrlQuery:(NSString *)query
{
    NSMutableDictionary *queryParameters = [[NSMutableDictionary alloc] init];
    int i;

    if (query != nil) {
        NSArray *queries = [query componentsSeparatedByString:@"&"];
        for (i = 0; i < queries.count; i++) {
            NSArray *pair = [[queries objectAtIndex:i] componentsSeparatedByString:@"="];
            if (pair.count == 2) {
                NSString *key = [pair.firstObject stringByRemovingPercentEncoding];
                if (queryParameters[key] == nil) {
                    NSString *value = [pair.lastObject stringByRemovingPercentEncoding];
                    if ((value.length >= 2) && ([value characterAtIndex:0] == '"') && ([value characterAtIndex:value.length - 1] == '"')) {
                        value = [value substringWithRange:NSMakeRange(1, value.length - 2)];
                    }
                    [queryParameters setObject:value forKey:key];
                }
            }
        }
    }
    return queryParameters;
}

- (bool) isLocalhostCertError:(NSError *)error
{
    return ((([error.domain isEqualToString:NSURLErrorDomain]
       && ((error.code == NSURLErrorSecureConnectionFailed)
        || (error.code == NSURLErrorServerCertificateHasBadDate)
        || (error.code == NSURLErrorServerCertificateUntrusted)
        || (error.code == NSURLErrorServerCertificateHasUnknownRoot)
        || (error.code == NSURLErrorServerCertificateNotYetValid)))
      || ([error.domain isEqualToString:(__bridge NSString *)kCFErrorDomainCFNetwork]
       && ((error.code == kCFURLErrorSecureConnectionFailed)
        || (error.code == kCFURLErrorServerCertificateHasBadDate)
        || (error.code == kCFURLErrorServerCertificateUntrusted)
        || (error.code == kCFURLErrorServerCertificateHasUnknownRoot)
        || (error.code == kCFURLErrorServerCertificateNotYetValid)))
      || ([error.domain isEqualToString:NSOSStatusErrorDomain]
       && ((error.code == errSSLXCertChainInvalid)
        || (error.code == errSSLUnknownRootCert)
        || (error.code == errSSLNoRootCert)
        || (error.code == errSSLCertExpired)
        || (error.code == errSSLCertNotYetValid)
        || (error.code == errSSLHostNameMismatch))))
     && (((error.userInfo[NSErrorFailingURLStringKey] != nil)
       && ([[NSURL URLWithString:error.userInfo[NSErrorFailingURLStringKey]].host caseInsensitiveCompare:@"localhost"] == NSOrderedSame)
       && (([NSURL URLWithString:error.userInfo[NSErrorFailingURLStringKey]].pathComponents.count < 2)
        || ([[NSURL URLWithString:error.userInfo[NSErrorFailingURLStringKey]].pathComponents[1] caseInsensitiveCompare:@"appproxy"] != NSOrderedSame)))
      || ((error.userInfo[NSURLErrorFailingURLErrorKey] != nil)
       && ([((NSURL *)(error.userInfo[NSURLErrorFailingURLErrorKey])).host caseInsensitiveCompare:@"localhost"] == NSOrderedSame)
       && ((((NSURL *)(error.userInfo[NSURLErrorFailingURLErrorKey])).pathComponents.count < 2)
        || ([((NSURL *)(error.userInfo[NSURLErrorFailingURLErrorKey])).pathComponents[1] caseInsensitiveCompare:@"appproxy"] != NSOrderedSame)))));
}

- (bool) isInitialLoadURLError:(NSError *)error
{
    return ([error.domain isEqualToString:NSURLErrorDomain]
     && (self.webViewWindow == nil)
     && (((error.userInfo[NSErrorFailingURLStringKey] != nil)
       && [((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).url_to_load hasPrefix:error.userInfo[NSErrorFailingURLStringKey]])
      || ((error.userInfo[NSURLErrorFailingURLErrorKey] != nil)
       && [((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).url_to_load hasPrefix:((NSURL *)(error.userInfo[NSURLErrorFailingURLErrorKey])).absoluteString])));
}

- (void) reload_runnable:(id)sender
{
    NSLog(@"webView reload");
    [[NSURLCache sharedURLCache] removeAllCachedResponses];
    [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).url_to_load]]];
}

- (UIView *) previewController:(QLPreviewController *) controller transitionViewForPreviewItem:(id<QLPreviewItem>) item
{
    return ((self.webViewWindow != nil) ? self.webViewWindow : self.webView);
}

- (void) previewControllerWillDismiss:(QLPreviewController *) controller
{
}

- (void) previewControllerDidDismiss:(QLPreviewController *) controller
{
}

- (BOOL) previewController:(QLPreviewController *) controller shouldOpenURL:(NSURL *) url forPreviewItem:(id<QLPreviewItem>) item
{
    return true;
}

- (QLPreviewItemEditingMode) previewController:(QLPreviewController *) controller editingModeForPreviewItem:(id<QLPreviewItem>) previewItem
{
    return QLPreviewItemEditingModeDisabled;
}

- (void) previewController:(QLPreviewController *) controller didUpdateContentsOfPreviewItem:(id<QLPreviewItem>) previewItem
{
}

- (void) previewController:(QLPreviewController *) controller didSaveEditedCopyOfPreviewItem:(id<QLPreviewItem>) previewItem atURL:(NSURL *) modifiedContentsURL
{
}

- (NSInteger) numberOfPreviewItemsInPreviewController:(QLPreviewController *) controller
{
    return 1;
}

- (id<QLPreviewItem>) previewController:(QLPreviewController *) controller previewItemAtIndex:(NSInteger) index
{
    return self.previewUrl;
}

- (void) preview_open_runnable:(NSURL *)url
{
    self.previewUrl = url;
    QLPreviewController *previewController = [[QLPreviewController alloc] init];
    previewController.delegate = self;
    previewController.dataSource = self;
    previewController.modalPresentationStyle = UIModalPresentationCurrentContext;
    [self presentViewController:previewController animated:NO completion:nil];
}

- (bool) preview_open:(NSURL *)url
{
    if (![QLPreviewController canPreviewItem:url]) {
        return false;
    }
    dispatch_async(dispatch_get_main_queue(), ^{
        [self preview_open_runnable:url];
    });
    return true;
}

#ifdef CLIENT_IOS_USE_SFSAFARIVIEW_WINDOWS
- (void) safariViewControllerDidFinish:(SFSafariViewController *)controller
{
}
#endif

- (void) backButtonClick
{
    [self.navigationController dismissViewControllerAnimated:NO completion:nil];
    self.navigationController = nil;
}

- (UIViewController *) documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)interactionController 
{ 
    return self; 
} 

#ifdef CLIENT_IOS_USE_SFSAFARIVIEW

- (id) initWithURL:(NSURL *)url
{
    self = [super initWithURL:url];
    return self;
}

#endif
#if (defined(CLIENT_IOS_USE_WKWEBVIEW) || defined(CLIENT_IOS_USE_WKWEBVIEW_WINDOWS))

#pragma WKNavigationDelegate

- (void) userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    if (message.name && [message.name isEqualToString:@"save_base64_blob_data_"]) {
        [self saveBase64BlobData:message.body];
    }
    else if (message.name && [message.name isEqualToString:@"console_message_"]) {
        if (phone_ios_log_browser || ([message.body characterAtIndex:0] != '3')) {
            int i, j, k, l, len;
            len = 0;
            while ((len + 2 < ((NSString *)(message.body)).length) && ([message.body characterAtIndex:len] != ')')) {
                len++;
            }
            i = len;
            while ((i != 0) && ([message.body characterAtIndex:i - 1] != ':')) {
                i--;
            }
            if (i != 0) {
                i--;
            }
            l = i;
            while ((i != 0) && ([message.body characterAtIndex:i - 1] != ':')) {
                i--;
            }
            k = i;
            if (i != 0) {
                i--;
            }
            j = i;
            while ((i != 0) && ([message.body characterAtIndex:i - 1] != '/')) {
                i--;
            }
            NSLog(@"(%@,%@) %@",
                [message.body substringWithRange:NSMakeRange(i, j-i)],
                [message.body substringWithRange:NSMakeRange(k, l-k)],
                [message.body substringFromIndex:len + 2]);
        }
    }
}

- (void) webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
    self.navigationItem.title = webView.title;
}

- (void) window_open_runnable:(WebViewController *)webViewController
{
    self.navigationController.doPushViewController(webViewController, true);
}

- (WKWebView *) webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
    if ((navigationAction.targetFrame == nil) || !navigationAction.targetFrame.mainFrame) {
#ifdef CLIENT_IOS_USE_SFSAFARIVIEW_WINDOWS
        if (NSClassFromString(@"SFSafariViewController")) {
            self.sfSafariViewNavigationController = [[SFSafariViewController alloc] initWithURL:url];
            self.sfSafariViewNavigationController.delegate = self;
            [self presentViewController:self.sfSafariViewNavigationController animated:YES completion:nil];
        }
        else
#endif
        {
            if (self.navigationController == nil) {
                self.navigationController = [[WebViewNavigationController alloc] init];
                self.navigationController.backButtonDelegate = self;
                self.navigationController.statusBarHidden = self.statusBarHidden;
                self.navigationController.statusBarStyle = self.statusBarStyle;
                self.navigationController.statusBarColor = self.statusBarColor;
                self.navigationController.statusBarTextColor = self.statusBarTextColor;
                self.definesPresentationContext = true;
                self.navigationController.modalPresentationStyle = UIModalPresentationCurrentContext;
                [self presentViewController:self.navigationController animated:NO completion:nil];
            }
            WebViewController *webViewController = [[WebViewController alloc] init];
            WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
            if ([wkWebView respondsToSelector:NSSelectorFromString(@"isInspectable")]) {
#ifdef DEBUG
                [wkWebView setValue:[NSNumber numberWithBool:true] forKey:@"inspectable"];
#else
                [wkWebView setValue:[NSNumber numberWithBool:phone_ios_log_browser] forKey:@"inspectable"];
#endif
            }
            wkWebView.navigationDelegate = webViewController;
            wkWebView.UIDelegate = webViewController;
            [webViewController.view addSubview:wkWebView];
            webViewController.webViewWindow = wkWebView;
            webViewController.navigationController = self.navigationController;
            webViewController.statusBarHidden = self.statusBarHidden;
            webViewController.statusBarStyle = self.statusBarStyle;
            webViewController.statusBarColor = self.statusBarColor;
            webViewController.statusBarTextColor = self.statusBarTextColor;
            webViewController.textZoomFactor = self.textZoomFactor;
            dispatch_async(dispatch_get_main_queue(), ^{
                [self window_open_runnable:webViewController];
            });
            return wkWebView;
        }
    }
    return nil;
}

- (void) webViewDidClose:(WKWebView *)webView
{
    NSLog(@"webViewDidClose");
    self.navigationController.doPopViewController(true);
}

/*
- (void) webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:frame.request.URL.host message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
        completionHandler();
    }];
    [alert addAction:defaultAction];
    [self presentViewController:alert animated:YES completion:nil];
}

- (void) webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler
{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:frame.request.URL.host message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
        completionHandler(false);
    }];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
        completionHandler(true);
    }];
    [alert addAction:cancelAction];
    [alert addAction:okAction];
    [self presentViewController:alert animated:YES completion:nil];
}

- (void) webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler
{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:frame.request.URL.host message:message preferredStyle:UIAlertControllerStyleAlert];
    [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
        textField.text = defaultText;
    }];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
        completionHandler(nil);
    }];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
        completionHandler([alert.textFields objectAtIndex:0].text);
    }];
    [alert addAction:cancelAction];
    [alert addAction:okAction];
    [self presentViewController:alert animated:YES completion:nil];
}

- (void) webView:(WKWebView *)webView contextMenuConfigurationForElement:(WKContextMenuElementInfo *)elementInfo completionHandler:(void (^)(UIContextMenuConfiguration *configuration))completionHandler
{
}
*/

#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 145000)

- (void) download:(WKDownload *)download decideDestinationUsingResponse:(NSURLResponse *)response suggestedFilename:(NSString *)suggestedFilename completionHandler:(void (^)(NSURL *destination))completionHandler
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    self.downloadFilename = suggestedFilename;
    NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *downloadsDir = [documentDir stringByAppendingPathComponent:@"Downloads"];
    NSString *path = [downloadsDir stringByAppendingPathComponent:self.downloadFilename];
    if ([fileManager fileExistsAtPath:path]) {
        [fileManager removeItemAtPath:path error:nil];
    }
    completionHandler([NSURL fileURLWithPath:path]);
}

- (void) downloadDidFinish:(WKDownload *)download
{
    NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *downloadsDir = [documentDir stringByAppendingPathComponent:@"Downloads"];
    NSString *path = [downloadsDir stringByAppendingPathComponent:self.downloadFilename];
    if (![self preview_open:[NSURL fileURLWithPath:path]]) {
        self.documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:path]];
        self.documentInteractionController.delegate = self;
        [self.documentInteractionController presentOptionsMenuFromRect:CGRectZero inView:self.view animated:YES];
    }
}

- (void) download:(WKDownload *)download didFailWithError:(NSError *)error resumeData:(NSData *)resumeData
{
    NSLog(@"Download document write %@ failed: %@", self.downloadFilename, error.description);
    ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).showToast(error.localizedDescription);
}

- (void) webView:(WKWebView *)webView navigationAction:(WKNavigationAction *)navigationAction didBecomeDownload:(WKDownload *)download
{
    download.delegate = self;
}

- (void) webView:(WKWebView *)webView navigationResponse:(WKNavigationResponse *)navigationResponse didBecomeDownload:(WKDownload *)download
{
    download.delegate = self;
}

#endif

- (void) webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
    NSURLRequest *request = navigationAction.request;
    NSDictionary *urlQueryParameters = [WebViewController parseUrlQuery:request.URL.query];
    UIWebViewNavigationType navigationType;
    switch(navigationAction.navigationType) {
    case WKNavigationTypeLinkActivated:   navigationType = UIWebViewNavigationTypeLinkClicked;     break;
    case WKNavigationTypeFormSubmitted:   navigationType = UIWebViewNavigationTypeFormSubmitted;   break;
    case WKNavigationTypeBackForward:     navigationType = UIWebViewNavigationTypeBackForward;     break;
    case WKNavigationTypeReload:          navigationType = UIWebViewNavigationTypeReload;          break;
    case WKNavigationTypeFormResubmitted: navigationType = UIWebViewNavigationTypeFormResubmitted; break;
    default:
    case WKNavigationTypeOther:           navigationType = UIWebViewNavigationTypeOther;           break;
    }

    if (([request.URL.scheme caseInsensitiveCompare:@"mailto"] == NSOrderedSame)
     || ([request.URL.scheme caseInsensitiveCompare:@"tel"] == NSOrderedSame)
     || ([request.URL.scheme caseInsensitiveCompare:@"sip"] == NSOrderedSame)
     || ([request.URL.scheme caseInsensitiveCompare:@"im"] == NSOrderedSame)) {
        [WebViewController openURL:request.URL];
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    if ([request.URL.scheme caseInsensitiveCompare:@"com.innovaphone.browsehttps"] == NSOrderedSame) {
        [WebViewController openURL:[NSURL URLWithString:[request.URL.absoluteString substringFromIndex:28]]];
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    if ([request.URL.scheme caseInsensitiveCompare:@"com.innovaphone.myapps"] == NSOrderedSame) {
        ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).forms_event_open_permalink([[request.URL.resourceSpecifier stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]);
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    if ((([request.URL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame)
      || ([request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
     && (request.URL.path != nil)
     && [request.URL.path hasSuffix:@"/appclient.htm"]
     && (urlQueryParameters[@"app"] != nil)) {
        PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
        NSString *safePath = (request.URL.path != nil) ? request.URL.path : @"";
        NSURL *cur = (phoneIosDelegate.connInfoCur != nil) ? [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", phoneIosDelegate.connInfoCur]] : nil;
        NSURL *pri = (phoneIosDelegate.connInfoPri != nil) ? [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", phoneIosDelegate.connInfoPri]] : nil;
        NSURL *sec = (phoneIosDelegate.connInfoSec != nil) ? [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", phoneIosDelegate.connInfoSec]] : nil;
        NSURL *master = (phoneIosDelegate.connInfoMaster != nil) ? [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", phoneIosDelegate.connInfoMaster]] : nil;
        if (((cur != nil) && (cur.host != nil) && ([request.URL.host caseInsensitiveCompare:cur.host] == NSOrderedSame) && (request.URL.port == cur.port) && [safePath hasPrefix:(cur.path != nil) ? cur.path : @""])
         || ((pri != nil) && (pri.host != nil) && ([request.URL.host caseInsensitiveCompare:pri.host] == NSOrderedSame) && (request.URL.port == pri.port) && [safePath hasPrefix:(pri.path != nil) ? pri.path : @""])
         || ((sec != nil) && (sec.host != nil) && ([request.URL.host caseInsensitiveCompare:sec.host] == NSOrderedSame) && (request.URL.port == sec.port) && [safePath hasPrefix:(sec.path != nil) ? sec.path : @""])
         || ((master != nil) && (master.host != nil) && ([request.URL.host caseInsensitiveCompare:master.host] == NSOrderedSame) && (request.URL.port == master.port) && [safePath hasPrefix:(master.path != nil) ? master.path : @""])) {
            phoneIosDelegate.forms_event_open_permalink(urlQueryParameters[@"app"]);
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
    }
    if (([navigationAction respondsToSelector:@selector(shouldPerformDownload)]
      && [navigationAction shouldPerformDownload]
      && ([request.URL.scheme caseInsensitiveCompare:@"blob"] != NSOrderedSame))
     || ((navigationType == UIWebViewNavigationTypeLinkClicked)
      && (!navigationAction.targetFrame
       || ([navigationAction.targetFrame.request.URL.pathExtension caseInsensitiveCompare:@"htm"] == NSOrderedSame)
       || ([navigationAction.targetFrame.request.URL.pathExtension caseInsensitiveCompare:@"html"] == NSOrderedSame))
      && (([request.URL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame)
       || ([request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
      && (request.URL.pathExtension.length != 0)
      && ([request.URL.pathExtension caseInsensitiveCompare:@"htm"] != NSOrderedSame)
      && ([request.URL.pathExtension caseInsensitiveCompare:@"html"] != NSOrderedSame)
      && ([request.URL.pathExtension caseInsensitiveCompare:@"js"] != NSOrderedSame)
      && ([request.URL.pathExtension caseInsensitiveCompare:@"css"] != NSOrderedSame)
      && ([request.URL.pathExtension caseInsensitiveCompare:@"xml"] != NSOrderedSame)
      && ([request.URL.pathExtension caseInsensitiveCompare:@"xsl"] != NSOrderedSame)
      && ([request.URL.pathExtension caseInsensitiveCompare:@"xslt"] != NSOrderedSame)
      && ([request.URL.pathExtension caseInsensitiveCompare:@"xsd"] != NSOrderedSame)
      && ([request.URL.pathExtension caseInsensitiveCompare:@"php"] != NSOrderedSame))) {
        if ([self preview_open:request.URL]) {
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
        NSURLRequest *req = [[NSURLRequest alloc] initWithURL:request.URL];
        [NSURLConnection sendAsynchronousRequest:req queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *resp, NSData *respData, NSError *error) {
            if ((resp != nil) && (respData != nil)) {
                NSMutableString *fileName = [[NSMutableString alloc] init];
                if (resp.suggestedFilename) {
                    [fileName appendString:resp.suggestedFilename];
                    if (![resp.suggestedFilename containsString:@"."]) {
                        [fileName appendString:@".txt"];
                    }
                }
                else {
                    [fileName appendString:request.URL.lastPathComponent];
                    if (![request.URL.lastPathComponent containsString:@"."]) {
                        [fileName appendString:@".txt"];
                    }
                }
                NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
                NSString *downloadsDir = [documentDir stringByAppendingPathComponent:@"Downloads"];
                NSString *path = [downloadsDir stringByAppendingPathComponent:fileName];
                NSError *errorC = nil;
                BOOL success = [respData writeToFile:path options:NSDataWritingFileProtectionComplete error:&errorC];
                if (success) {
                    if (![self preview_open:[NSURL fileURLWithPath:path]]) {
                        self.documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:path]];
                        self.documentInteractionController.delegate = self;
                        [self.documentInteractionController presentOptionsMenuFromRect:CGRectZero inView:self.view animated:YES];
                    }
                }
                else {
                    NSLog(@"Download document write %@ failed: %@", fileName, errorC.description);
                    ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).showToast(errorC.localizedDescription);
                }
            }
            else {
                NSLog(@"Download document failed: %@", error.description);
                ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).showToast(error.localizedDescription);
            }
        }];
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 145000)
    if ([request.URL.scheme caseInsensitiveCompare:@"blob"] == NSOrderedSame) {
        NSLog(@"Download blob %d %@", (NSClassFromString(@"WKDownload") != nil), request.URL);
        if (NSClassFromString(@"WKDownload") != nil) {
            decisionHandler(WKNavigationActionPolicyDownload);
            return;
        }
    }
#endif

    bool result = (((([request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) &&
                     ((webView.URL == nil) || ([webView.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))) ||
                    ([request.URL.scheme caseInsensitiveCompare:@"wss"] == NSOrderedSame)) &&
                   (self.webViewWindow == nil) &&
                   ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).cert_string) ? self.Authenticated : true;
    if (!result) {
        [[DownloadConnection alloc] initWithRequest:request navigationType:navigationType completionHandler:^{
            PhoneIosDelegate * phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            phoneIosDelegate.webViewController().Authenticated = YES;
        }];
    }
    decisionHandler(result ? WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel);
}

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
{
    int i;
    bool attachment;

    NSURLResponse *response = navigationResponse.response;
    attachment = false;
    if ([response isKindOfClass:NSHTTPURLResponse.class]) {
        NSDictionary * headerFields = ((NSHTTPURLResponse *) response).allHeaderFields;
        for (i = 0; i < headerFields.count; i++) {
            if ([headerFields.allKeys[i] caseInsensitiveCompare:@"Content-Disposition"] == NSOrderedSame) {
                NSString *contentDisposition = [headerFields valueForKey:headerFields.allKeys[i]];
                NSRange range = [contentDisposition rangeOfString:@"attachment" options:NSCaseInsensitiveSearch];
                attachment = (range.location != NSNotFound);
            }
        }
    }
    if (attachment) {
        if ([self preview_open:response.URL]) {
            decisionHandler(WKNavigationResponsePolicyCancel);
            return;
        }
        NSURLRequest *req = [[NSURLRequest alloc] initWithURL:response.URL];
        [NSURLConnection sendAsynchronousRequest:req queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *resp, NSData *respData, NSError *error) {
            if ((resp != nil) && (respData != nil)) {
                NSMutableString *fileName = [[NSMutableString alloc] init];
                if (resp.suggestedFilename) {
                    [fileName appendString:resp.suggestedFilename];
                    if (![resp.suggestedFilename containsString:@"."]) {
                        [fileName appendString:@".txt"];
                    }
                }
                else {
                    [fileName appendString:response.URL.lastPathComponent];
                    if (![response.URL.lastPathComponent containsString:@"."]) {
                        [fileName appendString:@".txt"];
                    }
                }
                NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
                NSString *downloadsDir = [documentDir stringByAppendingPathComponent:@"Downloads"];
                NSString *path = [downloadsDir stringByAppendingPathComponent:fileName];
                NSError *errorC = nil;
                BOOL success = [respData writeToFile:path options:NSDataWritingFileProtectionComplete error:&errorC];
                if (success) {
                    if (![self preview_open:[NSURL fileURLWithPath:path]]) {
                        self.documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:path]];
                        self.documentInteractionController.delegate = self;
                        [self.documentInteractionController presentOptionsMenuFromRect:CGRectZero inView:self.view animated:YES];
                    }
                }
                else {
                    NSLog(@"Download document write %@ failed: %@", fileName, errorC.description);
                    ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).showToast(errorC.localizedDescription);
                }
            }
            else {
                NSLog(@"Download document failed: %@", error.description);
                ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).showToast(error.localizedDescription);
            }
        }];
        decisionHandler(WKNavigationResponsePolicyCancel);
        return;
    }
    if ([response.URL.scheme caseInsensitiveCompare:@"blob"] == NSOrderedSame) {
        NSLog(@"Download blob %d %@ %@ %@", (NSClassFromString(@"WKDownload") != nil), response.URL, response.suggestedFilename, response.MIMEType);
        NSString *filename = response.suggestedFilename;
        if ([filename caseInsensitiveCompare:@"Unknown"] == NSOrderedSame) {
            if ([response.MIMEType isEqualToString:@"text/calendar"]) {
                filename = [filename stringByAppendingString:@".ics"];
            }
        }
        WKWebView * webView = (self.webViewWindow != nil) ? self.webViewWindow : self.webView;
        [webView evaluateJavaScript:[NSString stringWithFormat:@"%@%@%@%@%@%@%@%@%@", @""
            "function findHref(searchedDocument, searchedHref) {\n"
            "    var result = searchedDocument.querySelector('[href=\"' + searchedHref + '\"]');\n"
            "    if (result) return result;\n"
            "    var items = searchedDocument.querySelectorAll('iframe');\n"
            "    for (var i = 0; i < items.length; i++) {\n"
            "        result = findHref (items[i].contentWindow.document, searchedHref);\n"
            "        if(result) return result;\n"
            "    }\n"
            "    return null;\n"
            "}\n"
            "(async function download() {\n"
            "    const url = '", response.URL.absoluteURL.absoluteString, @"';\n"
            "    var filename = '", filename, @"';\n"
            "    var mimetype = '", response.MIMEType, @"';\n"
            "    try {\n"
            "        // we use a second try block here to have more detailed error information\n"
            "        // because of the nature of JS the outer try-catch doesn't know anything where the error happended\n"
            "        var res;\n"
            "        try {\n"
            "            res = await fetch(url, { credentials: 'include' });\n"
            "        } catch (err) {\n"
            "            console.log('fetch threw, error: ' + err + ', url: ' + url);\n"
            "            return;\n"
            "        }\n"
            "        if (!res.ok) {\n"
            "            console.log('Response status was not ok, status: ' + res.status + ', url: ' + url);\n"
            "            return;\n"
            "        }\n"
            "        var result = findHref(document, '", response.URL.absoluteURL.absoluteString, @"');\n"
            "        if (result && result.download) {\n"
            "            filename = result.download;\n"
            "        }\n"
            "        const contentDisp = res.headers.get('content-disposition');\n"
            "        if (contentDisp) {\n"
            "            filename = contentDisp.split('filename=')[1].split(';')[0];\n"
            "        }\n"
            "        const contentType = res.headers.get('content-type');\n"
            "        if (contentType) {\n"
            "            mimetype = contentType;\n"
            "        }\n"
            "        var data;\n"
            "        try {\n"
            "            data = await res.blob();\n"
            "        } catch (err) {\n"
            "            console.log('res.blob() threw, error: ' + err + ', url: ' + url);\n"
            "            return;\n"
            "        }\n"
            "        const reader = new FileReader();\n"
            "        reader.onloadend = function() {\n"
            "            window.webkit.messageHandlers.save_base64_blob_data_.postMessage(mimetype + ':' + filename + ';' + reader.result);\n"
            "        }\n"
            "        reader.addEventListener('error', function(err) {\n"
            "            console.log('FileReader threw, error: ' + err);\n"
            "        });\n"
            "        reader.readAsDataURL(data);\n"
            "    } catch (err) {\n"
            "        // TODO: better log the error, currently only TypeError: Type error\n"
            "        console.log('Javascript error while downloading document, url: ' + url + ', err: ' + err);\n"
            "    }\n"
            "})();\n"
            "// null is needed here as this eval returns the last statement and we can't return a promise\n"
            "null;\n"] completionHandler:nil];

        decisionHandler(WKNavigationResponsePolicyCancel);
        return;
    }
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 145000)
    if (!navigationResponse.canShowMIMEType) {
        if (NSClassFromString(@"WKDownload") != nil) {
            decisionHandler(WKNavigationResponsePolicyDownload);
            return;
        }
    }
#endif
    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)navigationResponse.response;
    NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:httpResponse.allHeaderFields forURL:httpResponse.URL];
    for (i = 0; i < cookies.count; i++) {
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:(NSHTTPCookie *)[cookies objectAtIndex:i]];
    }
    decisionHandler(WKNavigationResponsePolicyAllow);
}

- (void) saveBase64BlobData:(NSString *)messageBody
{
    int i, j;

    NSLog(@"saveBase64BlobData %@", messageBody);
    i = 0;
    while ([messageBody characterAtIndex:i] != ':') {
        i++;
    }
    j = i;
    do {
        i++;
    } while ([messageBody characterAtIndex:i] != ';');

    NSString *mimetype = [messageBody substringToIndex:j];
    NSString *fileName = [messageBody substringWithRange:NSMakeRange(j+1, i-j-1)];
    NSString *base64String = [messageBody substringFromIndex:i+1];
    NSRange range = [base64String rangeOfString:@";base64,"];
    if (range.location != NSNotFound) {
        base64String = [messageBody substringFromIndex:i+1 + range.location + 8];
    }
    NSData *respData = [[NSData alloc] initWithBase64Encoding:base64String];

    NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *downloadsDir = [documentDir stringByAppendingPathComponent:@"Downloads"];
    NSString *path = [downloadsDir stringByAppendingPathComponent:fileName];
    NSError *errorC = nil;
    BOOL success = [respData writeToFile:path options:NSDataWritingFileProtectionComplete error:&errorC];
    if (success) {
        if (![self preview_open:[NSURL fileURLWithPath:path]]) {
            self.documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:path]];
            self.documentInteractionController.delegate = self;
            [self.documentInteractionController presentOptionsMenuFromRect:CGRectZero inView:self.view animated:YES];
        }
    }
    else {
        NSLog(@"Download document write %@ failed: %@", fileName, errorC.description);
        ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).showToast(errorC.localizedDescription);
    }
}

- (void) webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error
{
    NSLog(@"webView didFailNavigation %@", error);
    if ([self isLocalhostCertError:error]) {
        ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).startFixCert();
    }
    else if ([self isInitialLoadURLError:error]) {
        [self performSelector:@selector(reload_runnable:) withObject:nil afterDelay:0.5];
    }
}

- (void) webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error
{
    NSLog(@"webView didFailProvisionalNavigation %@", error);
    if ([self isLocalhostCertError:error]) {
        ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).startFixCert();
    }
    else if ([self isInitialLoadURLError:error]) {
        [self performSelector:@selector(reload_runnable:) withObject:nil afterDelay:0.5];
    }
}

- (void) webViewWebContentProcessDidTerminate:(WKWebView *)webView
{
    NSLog(@"webView webViewWebContentProcessDidTerminate");
    [self performSelector:@selector(reload_runnable:) withObject:nil afterDelay:0.5];
}

- (void) webView:(WKWebView *)webView requestMediaCapturePermissionForOrigin:(WKSecurityOrigin *)origin initiatedByFrame:(WKFrameInfo *)frame type:(NSInteger/*WKMediaCaptureType*/)type decisionHandler:(void (^)(NSInteger/*WKPermissionDecision*/ decision))decisionHandler
{
    NSLog(@"webView requestMediaCapturePermissionForOrigin %u %@ %@", (unsigned int) type, origin, frame);

    decisionHandler(([((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).url_to_load hasPrefix:[NSString stringWithFormat:@"%@://%@:%u/", origin.protocol, origin.host, (unsigned int)(origin.port)]] &&
        ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusAuthorized)) ? 1/*WKPermissionDecisionGrant*/ : 0/*WKPermissionDecisionPrompt*/);
}

- (void) webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    NSLog(@"webView didReceiveAuthenticationChallenge %u %@ %@ %@", (unsigned int)(challenge.previousFailureCount), challenge.protectionSpace.authenticationMethod, challenge.protectionSpace.host, challenge.protectionSpace.realm);

    if ((challenge.previousFailureCount == 0) && (challenge.proposedCredential != nil) && [challenge.proposedCredential hasPassword]) {
//        completionHandler(NSURLSessionAuthChallengeUseCredential, challenge.proposedCredential);
        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
        return;
    }
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]
     || [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPDigest]) {
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Security"
            message:[NSString stringWithFormat:@"%@ %@ with identification %@",
                (challenge.previousFailureCount == 0) ? @"Authentication requested for" : @"Wrong credentials supplied for",
                challenge.protectionSpace.host,
                challenge.protectionSpace.realm]
            preferredStyle:UIAlertControllerStyleAlert];

        [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
            textField.placeholder = @"Username";
        }];

        [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
            textField.placeholder = @"Password";
            textField.secureTextEntry = true;
        }];

        UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
            completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialWithUser:[alert.textFields objectAtIndex:0].text password:[alert.textFields objectAtIndex:1].text persistence:NSURLCredentialPersistenceForSession]);
        }];
        [alert addAction:okAction];

        UIAlertAction *alwaysAction = [UIAlertAction actionWithTitle:@"Always" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
            completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialWithUser:[alert.textFields objectAtIndex:0].text password:[alert.textFields objectAtIndex:1].text persistence:NSURLCredentialPersistencePermanent]);
        }];
        [alert addAction:alwaysAction];

        UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
        }];
        [alert addAction:cancelAction];

        [self presentViewController:alert animated:YES completion:nil];
    }
/*
    else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ((challenge.protectionSpace.host.length >= sizeof("innovaphone.com") - 1)
         && ([[challenge.protectionSpace.host substringFromIndex:challenge.protectionSpace.host.length - (sizeof("innovaphone.com") - 1)] caseInsensitiveCompare:@"innovaphone.com"] == NSOrderedSame)) {
            completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
            return;
        }
    }
*/
    else {
        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
        return;
    }
}

/*
- (void) keepWkWebViewActive:(id)object
{
    [self.webView evaluateJavaScript:@"1+1" completionHandler:nil];
    [self performSelector:@selector(keepWkWebViewActive:) withObject:nil afterDelay:0.1];
}
*/

#endif
#if (!defined(CLIENT_IOS_USE_WKWEBVIEW) || !defined(CLIENT_IOS_USE_WKWEBVIEW_WINDOWS))

#pragma UIWebViewDelegate

#if !defined(SHOW_CONSOLE_MESSAGE_SOURCE)
+ (void) onConsoleMessage
{
    NSMutableString * s = [NSMutableString stringWithCapacity:1024];
    int i, j, k;
    i = 0;
    if (([JSContext currentArguments].count != 0) && [[[JSContext currentArguments] objectAtIndex:i] isString]) {
        NSString * format = [[[JSContext currentArguments] objectAtIndex:0] toString];
        i = 1;
        k = 0;
        j = k;
        while ((k < format.length) && ([format characterAtIndex:k] != '%')) {
            k++;
        }
        if (k != j) {
            [s appendString:[format substringWithRange:NSMakeRange(j, k-j)]];
        }
        while ((k + 1 < format.length) && (i < [JSContext currentArguments].count)) {
            switch ([format characterAtIndex:k+1]) {
            case '%':
                [s appendString:@"%"];
                break;
            default:
                [s appendFormat:@"%@", [[JSContext currentArguments] objectAtIndex:i]];
                i++;
                break;
            }
            k += 2;
            j = k;
            while ((k < format.length) && ([format characterAtIndex:k] != '%')) {
                k++;
            }
            if (k != j) {
                [s appendString:[format substringWithRange:NSMakeRange(j, k-j)]];
            }
        }
        if (k < format.length) {
            [s appendString:[format substringWithRange:NSMakeRange(k, format.length-k)]];
        }
    }
    while (i < [JSContext currentArguments].count) {
        [s appendFormat:@"%@", [[JSContext currentArguments] objectAtIndex:i]];
        i++;
    }
    NSLog(@"(*.js,) %@", s);
}
#endif

- (void) webViewDidFinishLoad:(UIWebView *)webView
{
    // get JSContext from UIWebView instance
    JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    // enable error logging
    [context setExceptionHandler:^(JSContext *context, JSValue *value) {
        NSLog(@"(*.js,) %@", value);
    }];

    [context setObject:^() {
        if (phone_ios_log_browser || ([[[[JSContext currentArguments] objectAtIndex:0] toString] characterAtIndex:0] != '3')) {
            NSString * sourceId = [[[JSContext currentArguments] objectAtIndex:1] toString];
            int i, j, k, l;
            i = sourceId.length;
            while ((i != 0) && ([sourceId characterAtIndex:i - 1] != ':')) {
                i--;
            }
            if (i != 0) {
                i--;
            }
            l = i;
            while ((i != 0) && ([sourceId characterAtIndex:i - 1] != ':')) {
                i--;
            }
            k = i;
            if (i != 0) {
                i--;
            }
            j = i;
            while ((i != 0) && ([sourceId characterAtIndex:i - 1] != '/')) {
                i--;
            }
            NSLog(@"(%@,%@) %@",
                [sourceId substringWithRange:NSMakeRange(i, j-i)],
                [sourceId substringWithRange:NSMakeRange(k, l-k)],
                [[JSContext currentArguments] objectAtIndex:2]);
        }
        } forKeyedSubscript:@"console_message_"];

    [context evaluateScript:@""
/*
        "window.addEventListener('error', function(e) {\n"
        "    console_message_(0, '(' + e.filename + ':' + e.lineno + ':' + e.colno + ')', e.message);\n"
        "});\n"
*/
        "var custom_saved_console_ = custom_saved_console_ || console;\n"
        "var console = {\n"
        "log: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.log && (typeof custom_saved_console_.log.apply === 'function')) custom_saved_console_.log.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    console_message_(3, new Error().stack, s);\n"
        "},\n"
        "trace: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.trace && (typeof custom_saved_console_.trace.apply === 'function')) custom_saved_console_.trace.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    console_message_(3, new Error().stack, s);\n"
        "},\n"
        "debug: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.debug && (typeof custom_saved_console_.debug.apply === 'function')) custom_saved_console_.debug.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    console_message_(3, new Error().stack, s);\n"
        "},\n"
        "info: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.info && (typeof custom_saved_console_.info.apply === 'function')) custom_saved_console_.info.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    console_message_(2, new Error().stack, s);\n"
        "},\n"
        "warn: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.warn && (typeof custom_saved_console_.warn.apply === 'function')) custom_saved_console_.warn.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    console_message_(1, new Error().stack, s);\n"
        "},\n"
        "error: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.error && (typeof custom_saved_console_.error.apply === 'function')) custom_saved_console_.error.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    console_message_(0, new Error().stack, s);\n"
        "},\n"
        "assert: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.assert && (typeof custom_saved_console_.assert.apply === 'function')) custom_saved_console_.assert.apply(null, arguments);\n"
        "    if ((arguments.length != 0) && !arguments[0]) {\n"
        "        var i = 1;\n"
        "        var s = '';\n"
        "        if ((arguments.length != 1) && (typeof arguments[1] === 'string')) {\n"
        "            i = 1;\n"
        "            var k = 0;\n"
        "            var j = k;\n"
        "            while ((k < arguments[1].length) && (arguments[1][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[1].substring(j, k);\n"
        "            }\n"
        "            while ((k + 1 < arguments[1].length) && (i < arguments.length)) {\n"
        "                switch (arguments[1][k+1]) {\n"
        "                case '%':\n"
        "                    s += '%';\n"
        "                    break;\n"
        "                default:\n"
        "                    s += JSON.stringify(arguments[i]);\n"
        "                    i++;\n"
        "                    break;\n"
        "                }\n"
        "                k += 2;\n"
        "                j = k;\n"
        "                while ((k < arguments[1].length) && (arguments[1][k] != '%')) {\n"
        "                    k++;\n"
        "                }\n"
        "                if (k != j) {\n"
        "                    s += arguments[1].substring(j, k);\n"
        "                }\n"
        "            }\n"
        "            if (k < arguments[1].length) {\n"
        "                s += arguments[1].substring(k, arguments[1].length);\n"
        "            }\n"
        "        }\n"
        "        while (i < arguments.length) {\n"
        "            s += JSON.stringify(arguments[i]);\n"
        "            i++;\n"
        "        }\n"
        "        console_message_(0, new Error().stack, s);\n"
        "    }\n"
        "},\n"
        "custom: true\n"
        "}\n" /*withSourceURL:[NSURL URLWithString:@"console_message_.js"]*/];

/*
    NSLog(@"webViewDidFinishLoad main frame %@ %@", webView.request.URL, context);
    NSArray *frames = [webView valueForKeyPath:@"documentView.webView.mainFrame.childFrames"];
    for (i = 0; i < frames.count; i++) {
        JSContext *context = [[frames objectAtIndex:i] valueForKeyPath:@"javaScriptContext"];
        NSLog(@"webViewDidFinishLoad child %u %@", i, context);
    }
*/
    self.jsContext = context;
    if (self.parentJsContext != nil) {
        context[@"window"][@"opener"] = self.parentJsContext[@"window"];
    }
#if !defined(SHOW_CONSOLE_MESSAGE_SOURCE)
    context[@"console"][@"error"] = ^() { [WebViewController onConsoleMessage]; };
    context[@"console"][@"warn"]  = ^() { [WebViewController onConsoleMessage]; };
    context[@"console"][@"info"]  = ^() { [WebViewController onConsoleMessage]; };
    context[@"console"][@"trace"] = ^() { [WebViewController onConsoleMessage]; };
    context[@"console"][@"log"]   = ^() { if (phone_ios_log_browser) [WebViewController onConsoleMessage]; };
#endif
    self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
}

- (void) window_open_runnable:(NSURL *)url
{
#ifdef CLIENT_IOS_USE_SFSAFARIVIEW_WINDOWS
    if (NSClassFromString(@"SFSafariViewController")) {
        self.sfSafariViewNavigationController = [[SFSafariViewController alloc] initWithURL:url];
        self.sfSafariViewNavigationController.delegate = self;
        [self presentViewController:self.sfSafariViewNavigationController animated:YES completion:nil];
    }
    else
#endif
    {
        if (self.navigationController == nil) {
            self.navigationController = [[WebViewNavigationController alloc] init];
            self.navigationController.backButtonDelegate = self;
            self.navigationController.statusBarHidden = self.statusBarHidden;
            self.navigationController.statusBarStyle = self.statusBarStyle;
            self.navigationController.statusBarColor = self.statusBarColor;
            self.navigationController.statusBarTextColor = self.statusBarTextColor;
            self.definesPresentationContext = true;
            self.navigationController.modalPresentationStyle = UIModalPresentationCurrentContext;
            [self presentViewController:self.navigationController animated:NO completion:nil];
        }
        WebViewController *webViewController = [[WebViewController alloc] init];
        webViewController.startUrl = url;
        webViewController.navigationController = self.navigationController;
        webViewController.parentJsContext = self.jsContext;
        webViewController.statusBarHidden = self.statusBarHidden;
        webViewController.statusBarStyle = self.statusBarStyle;
        webViewController.statusBarColor = self.statusBarColor;
        webViewController.statusBarTextColor = self.statusBarTextColor;
        webViewController.textZoomFactor = self.textZoomFactor;
        self.navigationController.doPushViewController(webViewController, true);
    }
}

- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSDictionary *urlQueryParameters = [WebViewController parseUrlQuery:request.URL.query];
    int i;
    if (([request.URL.scheme caseInsensitiveCompare:@"mailto"] == NSOrderedSame)
     || ([request.URL.scheme caseInsensitiveCompare:@"tel"] == NSOrderedSame)
     || ([request.URL.scheme caseInsensitiveCompare:@"sip"] == NSOrderedSame)
     || ([request.URL.scheme caseInsensitiveCompare:@"im"] == NSOrderedSame)) {
        [WebViewController openURL:request.URL];
        return NO;
    }
    if ([request.URL.scheme caseInsensitiveCompare:@"com.innovaphone.browsehttps"] == NSOrderedSame) {
        [WebViewController openURL:[NSURL URLWithString:[request.URL.absoluteString substringFromIndex:28]]];
        return NO;
    }
    NSArray *urlFragments = [request.URL.absoluteString componentsSeparatedByString:@"com.innovaphone.clientioswindowopen"];
    if (((NSString *)[urlFragments objectAtIndex:0]).length != request.URL.absoluteString.length) {
        NSMutableString *urlString = [NSMutableString stringWithCapacity:request.URL.absoluteString.length];
        for (i = 0; i < urlFragments.count; i++) {
            [urlString appendString:((NSString *)[urlFragments objectAtIndex:i])];
        }
        NSURL *url = [NSURL URLWithString:urlString]; 
        dispatch_async(dispatch_get_main_queue(), ^{
            [self window_open_runnable:url];
        });
        return NO;
    }
    if ([request.URL.absoluteString containsString:@"com.innovaphone.clientioswindowclose"]) {
        self.navigationController.doPopViewController(true);
        return NO;
    }
    if ((([request.URL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame)
      || ([request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
     && (request.URL.path != nil)
     && [request.URL.path hasSuffix:@"/appclient.htm"]
     && (urlQueryParameters[@"app"] != nil)) {
        PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
        NSString *safePath = (request.URL.path != nil) ? request.URL.path : @"";
        NSURL *cur = (phoneIosDelegate.connInfoCur != nil) ? [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", phoneIosDelegate.connInfoCur]] : nil;
        NSURL *pri = (phoneIosDelegate.connInfoPri != nil) ? [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", phoneIosDelegate.connInfoPri]] : nil;
        NSURL *sec = (phoneIosDelegate.connInfoSec != nil) ? [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", phoneIosDelegate.connInfoSec]] : nil;
        NSURL *master = (phoneIosDelegate.connInfoMaster != nil) ? [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", phoneIosDelegate.connInfoMaster]] : nil;
        if (((cur != nil) && (cur.host != nil) && ([request.URL.host caseInsensitiveCompare:cur.host] == NSOrderedSame) && (request.URL.port == cur.port) && [safePath hasPrefix:(cur.path != nil) ? cur.path : @""])
         || ((pri != nil) && (pri.host != nil) && ([request.URL.host caseInsensitiveCompare:pri.host] == NSOrderedSame) && (request.URL.port == pri.port) && [safePath hasPrefix:(pri.path != nil) ? pri.path : @""])
         || ((sec != nil) && (sec.host != nil) && ([request.URL.host caseInsensitiveCompare:sec.host] == NSOrderedSame) && (request.URL.port == sec.port) && [safePath hasPrefix:(sec.path != nil) ? sec.path : @""])
         || ((master != nil) && (master.host != nil) && ([request.URL.host caseInsensitiveCompare:master.host] == NSOrderedSame) && (request.URL.port == master.port) && [safePath hasPrefix:(master.path != nil) ? master.path : @""])) {
            phoneIosDelegate.forms_event_open_permalink(urlQueryParameters[@"app"]);
            return NO;
        }
    }
    if ((navigationType == UIWebViewNavigationTypeLinkClicked)
     && (!navigationAction.targetFrame
      || ([navigationAction.targetFrame.request.URL.pathExtension caseInsensitiveCompare:@"htm"] == NSOrderedSame)
      || ([navigationAction.targetFrame.request.URL.pathExtension caseInsensitiveCompare:@"html"] == NSOrderedSame))
     && (([request.URL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame)
      || ([request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
     && (request.URL.pathExtension.length != 0)
     && ([request.URL.pathExtension caseInsensitiveCompare:@"htm"] != NSOrderedSame)
     && ([request.URL.pathExtension caseInsensitiveCompare:@"html"] != NSOrderedSame)
     && ([request.URL.pathExtension caseInsensitiveCompare:@"js"] != NSOrderedSame)
     && ([request.URL.pathExtension caseInsensitiveCompare:@"css"] != NSOrderedSame)
     && ([request.URL.pathExtension caseInsensitiveCompare:@"xml"] != NSOrderedSame)
     && ([request.URL.pathExtension caseInsensitiveCompare:@"xsl"] != NSOrderedSame)
     && ([request.URL.pathExtension caseInsensitiveCompare:@"xslt"] != NSOrderedSame)
     && ([request.URL.pathExtension caseInsensitiveCompare:@"xsd"] != NSOrderedSame)
     && ([request.URL.pathExtension caseInsensitiveCompare:@"php"] != NSOrderedSame)) {
        if ([self preview_open:request.URL]) {
            return NO;
        }
        NSURLRequest *req = [[NSURLRequest alloc] initWithURL:request.URL];
        [NSURLConnection sendAsynchronousRequest:req queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *resp, NSData *respData, NSError *error) {
            if ((resp != nil) && (respData != nil)) {
                NSMutableString *fileName = [[NSMutableString alloc] init];
                if (resp.suggestedFilename) {
                    [fileName appendString:resp.suggestedFilename];
                    if (![resp.suggestedFilename containsString:@"."]) {
                        [fileName appendString:@".txt"];
                    }
                }
                else {
                    [fileName appendString:request.URL.lastPathComponent];
                    if (![request.URL.lastPathComponent containsString:@"."]) {
                        [fileName appendString:@".txt"];
                    }
                }
                NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
                NSString *downloadsDir = [documentDir stringByAppendingPathComponent:@"Downloads"];
                NSString *path = [downloadsDir stringByAppendingPathComponent:fileName];
                NSError *errorC = nil;
                BOOL success = [respData writeToFile:path options:NSDataWritingFileProtectionComplete error:&errorC];
                if (success) {
                    if (![self preview_open:[NSURL fileURLWithPath:path]]) {
                        self.documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:path]];
                        self.documentInteractionController.delegate = self;
                        [self.documentInteractionController presentOptionsMenuFromRect:CGRectZero inView:self.view animated:YES];
                    }
                }
                else {
                    NSLog(@"Download document write %@ failed: %@", fileName, errorC.description);
                    ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).showToast(errorC.localizedDescription);
                }
            }
            else {
                NSLog(@"Download document failed: %@", error.description);
                ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).showToast(error.localizedDescription);
            }
        }];
        return NO;
    }
/*
    if ((navigationType == UIWebViewNavigationTypeLinkClicked)
     && (([request.URL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame)
      || ([request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))) {
        [[DownloadConnection alloc] initWithRequest:request navigationType:navigationType completionHandler:^{
        }];
        return NO;
    }
*/
    bool result = ((([request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) ||
                    ([request.URL.scheme caseInsensitiveCompare:@"wss"] == NSOrderedSame)) &&
                   ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).cert_string) ? self.Authenticated : true;
    if (!result) {
/* MacOSX
        NSData * cert_data = [WebViewController decodeBase64WithString:(((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).cert_string)];
        const char * begin, * end;
        begin = strstr(cert_data.bytes, "-----BEGIN CERTIFICATE-----");
        end = strstr(cert_data.bytes, "-----END CERTIFICATE-----");
        if (begin && end && (begin < end)) {
            begin += strlen("-----BEGIN CERTIFICATE-----");
            cert_data = [WebViewController decodeBase64WithString:[[NSString alloc] initWithBytes:begin length:end-begin encoding:NSASCIIStringEncoding]];
        }
        SecCertificateRef cert = SecCertificateCreateWithData(kCFAllocatorDefault, (CFDataRef)cert_data);
        if (cert != NULL){
            OSStatus err = SecItemAdd((CFDictionaryRef)[NSDictionary dictionaryWithObjectsAndKeys:(id) kSecClassCertificate, kSecClass, cert, kSecValueRef, nil], NULL);
            if ((err == noErr) || (err == errSecDuplicateItem)) {
//                SecPolicyRef x509Policy = SecPolicyCreateBasicX509();
//                err = SecTrustSetUserTrust(cert, x509Policy, kSecTrustResultProceed);
//                CFRelease(x509Policy);

//                NSDictionary *newTrustSettings = @{(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot]};
//                err = SecTrustSettingsSetTrustSettings(certificate, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(newTrustSettings));

                int allowErr = -2147409654;
                SecPolicyRef x509Policy = SecPolicyCreateBasicX509();
                
                NSDictionary *x509Settings = @{
                    (id)kSecTrustSettingsPolicy : (__bridge id)x509Policy,
                    (id)kSecTrustSettingsAllowedError : @(allowErr),
                    (id)kSecTrustSettingsResult : @(kSecTrustSettingsResultTrustRoot)
                };
                
                NSDictionary *otherSettings = @{
                    (id)kSecTrustSettingsAllowedError : @(allowErr),
                    (id)kSecTrustSettingsResult : @(kSecTrustSettingsResultTrustRoot)
                };
                
                CFArrayRef trustSettings = (__bridge CFArrayRef)@[x509Settings, otherSettings];
                err = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, trustSettings);
                
                CFRelease(x509Policy);
            }
            CFRelease(cert);
        }
*/
        [[DownloadConnection alloc] initWithRequest:request navigationType:navigationType completionHandler:^{
            PhoneIosDelegate * phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            phoneIosDelegate.webViewController().Authenticated = YES;
        }];
    }
    return result;
}

- (void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
    NSLog(@"webView didFailLoadWithError %@", error);
    if ([self isLocalhostCertError:error]) {
        ((PhoneIosDelegate *)[UIApplication sharedApplication].delegate).startFixCert();
    }
    else if ([self isInitialLoadURLError:error]) {
        [self performSelector:@selector(reload_runnable:) withObject:nil afterDelay:0.5];
    }
}

#endif

- (void) logBrowser_:(bool)log
{
#ifdef DEBUG
#else
#ifdef CLIENT_IOS_USE_WKWEBVIEW_WINDOWS
    unsigned i;
    if (self.navigationController != nil) {
        for (i = 0; i < self.navigationController.viewControllers.count; i++) {
            WebViewController *c = (WebViewController *)[self.navigationController.viewControllers objectAtIndex:i];
            if (c.webViewWindow) {
                if ([c.webViewWindow respondsToSelector:NSSelectorFromString(@"isInspectable")]) {
                    [c.webViewWindow setValue:[NSNumber numberWithBool:log] forKey:@"inspectable"];
                }
            }
        }
    }
#endif
#ifdef CLIENT_IOS_USE_WKWEBVIEW
    if (self.webView) {
        if ([self.webView respondsToSelector:NSSelectorFromString(@"isInspectable")]) {
            [self.webView setValue:[NSNumber numberWithBool:log] forKey:@"inspectable"];
        }
    }
#endif
#endif
}

- (void) setStatusBarAppearance_:(BOOL)hidden style:(UIStatusBarStyle)style color:(UIColor *)color textColor:(UIColor *)textColor
{
    self.statusBarHidden = hidden;
    self.statusBarStyle = style;
    self.statusBarColor = color;
    self.statusBarTextColor = textColor;
    [self.view setBackgroundColor:self.statusBarColor];
    [self setNeedsStatusBarAppearanceUpdate];
/*
    if (self.navigationController != nil) {
        self.navigationController.statusBarHidden = hidden;
        self.navigationController.statusBarStyle = style;
        self.navigationController.statusBarColor = color;
        self.navigationController.statusBarTextColor = textColor;
        [self.navigationController setNeedsStatusBarAppearanceUpdate];
    }
*/
}

- (void) viewDidLoad
{
    WebViewController *__weak weakSelf = self;
    _logBrowser = ^ void (bool log) { [weakSelf logBrowser_:log]; };
    _setStatusBarAppearance = ^ void (BOOL hidden, UIStatusBarStyle style, UIColor *color, UIColor *textColor) { [weakSelf setStatusBarAppearance_:hidden style:style color:color  textColor:textColor]; };

    self.Authenticated = false;

    [super viewDidLoad];

#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
#else
#if (defined(CLIENT_IOS_USE_WKWEBVIEW) || defined(CLIENT_IOS_USE_WKWEBVIEW_WINDOWS))
#if !defined(CLIENT_IOS_USE_WKWEBVIEW)
    if (self.navigationController != nil)
#endif
    {
        WKUserScript *userScript = [[WKUserScript alloc] initWithSource: @""
        "window.addEventListener('error', function(e) {\n"
        "    window.webkit.messageHandlers.console_message_.postMessage('0 (' + e.filename + ':' + e.lineno + ':' + e.colno + ') ' + e.message);\n"
        "});\n"
        "var custom_saved_console_ = custom_saved_console_ || console;\n"
        "var console = {\n"
        "log: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.log && (typeof custom_saved_console_.log.apply === 'function')) custom_saved_console_.log.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    window.webkit.messageHandlers.console_message_.postMessage('3 (' + new Error().stack + ') ' + s);\n"
        "},\n"
        "trace: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.trace && (typeof custom_saved_console_.trace.apply === 'function')) custom_saved_console_.trace.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    window.webkit.messageHandlers.console_message_.postMessage('3 (' + new Error().stack + ') ' + s);\n"
        "},\n"
        "debug: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.debug && (typeof custom_saved_console_.debug.apply === 'function')) custom_saved_console_.debug.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    window.webkit.messageHandlers.console_message_.postMessage('3 (' + new Error().stack + ') ' + s);\n"
        "},\n"
        "info: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.info && (typeof custom_saved_console_.info.apply === 'function')) custom_saved_console_.info.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    window.webkit.messageHandlers.console_message_.postMessage('2 (' + new Error().stack + ') ' + s);\n"
        "},\n"
        "warn: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.warn && (typeof custom_saved_console_.warn.apply === 'function')) custom_saved_console_.warn.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    window.webkit.messageHandlers.console_message_.postMessage('1 (' + new Error().stack + ') ' + s);\n"
        "},\n"
        "error: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.error && (typeof custom_saved_console_.error.apply === 'function')) custom_saved_console_.error.apply(null, arguments);\n"
        "    var i = 0;\n"
        "    var s = '';\n"
        "    if ((arguments.length != 0) && (typeof arguments[0] === 'string')) {\n"
        "        i = 1;\n"
        "        var k = 0;\n"
        "        var j = k;\n"
        "        while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "            k++;\n"
        "        }\n"
        "        if (k != j) {\n"
        "            s += arguments[0].substring(j, k);\n"
        "        }\n"
        "        while ((k + 1 < arguments[0].length) && (i < arguments.length)) {\n"
        "            switch (arguments[0][k+1]) {\n"
        "            case '%':\n"
        "                s += '%';\n"
        "                break;\n"
        "            default:\n"
        "                s += JSON.stringify(arguments[i]);\n"
        "                i++;\n"
        "                break;\n"
        "            }\n"
        "            k += 2;\n"
        "            j = k;\n"
        "            while ((k < arguments[0].length) && (arguments[0][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[0].substring(j, k);\n"
        "            }\n"
        "        }\n"
        "        if (k < arguments[0].length) {\n"
        "            s += arguments[0].substring(k, arguments[0].length);\n"
        "        }\n"
        "    }\n"
        "    while (i < arguments.length) {\n"
        "        s += JSON.stringify(arguments[i]);\n"
        "        i++;\n"
        "    }\n"
        "    window.webkit.messageHandlers.console_message_.postMessage('0 (' + new Error().stack + ') ' + s);\n"
        "},\n"
        "assert: function() {\n"
        "    if(custom_saved_console_ && custom_saved_console_.assert && (typeof custom_saved_console_.assert.apply === 'function')) custom_saved_console_.assert.apply(null, arguments);\n"
        "    if ((arguments.length != 0) && !arguments[0]) {\n"
        "        var i = 1;\n"
        "        var s = '';\n"
        "        if ((arguments.length != 1) && (typeof arguments[1] === 'string')) {\n"
        "            i = 1;\n"
        "            var k = 0;\n"
        "            var j = k;\n"
        "            while ((k < arguments[1].length) && (arguments[1][k] != '%')) {\n"
        "                k++;\n"
        "            }\n"
        "            if (k != j) {\n"
        "                s += arguments[1].substring(j, k);\n"
        "            }\n"
        "            while ((k + 1 < arguments[1].length) && (i < arguments.length)) {\n"
        "                switch (arguments[1][k+1]) {\n"
        "                case '%':\n"
        "                    s += '%';\n"
        "                    break;\n"
        "                default:\n"
        "                    s += JSON.stringify(arguments[i]);\n"
        "                    i++;\n"
        "                    break;\n"
        "                }\n"
        "                k += 2;\n"
        "                j = k;\n"
        "                while ((k < arguments[1].length) && (arguments[1][k] != '%')) {\n"
        "                    k++;\n"
        "                }\n"
        "                if (k != j) {\n"
        "                    s += arguments[1].substring(j, k);\n"
        "                }\n"
        "            }\n"
        "            if (k < arguments[1].length) {\n"
        "                s += arguments[1].substring(k, arguments[1].length);\n"
        "            }\n"
        "        }\n"
        "        while (i < arguments.length) {\n"
        "            s += JSON.stringify(arguments[i]);\n"
        "            i++;\n"
        "        }\n"
        "        window.webkit.messageHandlers.console_message_.postMessage('0 (' + new Error().stack + ') ' + s);\n"
        "    }\n"
        "},\n"
        "custom: true\n"
        "}\n" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:false];

        WKWebView *wkWebView;
        if (self.webViewWindow != nil) {
            wkWebView = self.webViewWindow;
        }
        else {
//            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
//            configuration.processPool = phoneIosDelegate.wkWebViewProcessPool;
            configuration.allowsInlineMediaPlayback = true;
            configuration.mediaPlaybackRequiresUserAction = false;
            configuration.requiresUserActionForMediaPlayback = false;
            configuration.allowsPictureInPictureMediaPlayback = true;
            configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
//            configuration.dataDetectorTypes = WKDataDetectorTypeAll;
            configuration.preferences = [[WKPreferences alloc] init];
            configuration.preferences.javaScriptEnabled = true;
            configuration.preferences.javaScriptCanOpenWindowsAutomatically = true;
/*
            if ([configuration.preferences respondsToSelector:@selector(inactiveSchedulingPolicy)]) {
                configuration.preferences.inactiveSchedulingPolicy = WKInactiveSchedulingPolicyNone;
            }
*/

            configuration.userContentController = [[WKUserContentController alloc] init];
            [configuration.userContentController addScriptMessageHandler:self name:@"console_message_"];
            [configuration.userContentController addScriptMessageHandler:self name:@"save_base64_blob_data_"];
            [configuration.userContentController addUserScript:userScript];

            configuration.applicationNameForUserAgent = [NSString stringWithFormat:@"%s/%s %s", PHONE_IOS_LOG_APP_NAME, _BUILD_STRING_,
#if TARGET_IPHONE_SIMULATOR
                "(iOS Simulator)"
#else
                "(iOS)"
#endif
                ];
            wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
            if ([wkWebView respondsToSelector:NSSelectorFromString(@"isInspectable")]) {
#ifdef DEBUG
                [wkWebView setValue:[NSNumber numberWithBool:true] forKey:@"inspectable"];
#else
                [wkWebView setValue:[NSNumber numberWithBool:phone_ios_log_browser] forKey:@"inspectable"];
#endif
            }
            [wkWebView setValue:[NSNumber numberWithFloat:(self.textZoomFactor == 0.0) ? 1.0 : self.textZoomFactor] forKey:@"textZoomFactor"];
            if (self.navigationController == nil) {
                self.webView = wkWebView;
                wkWebView.scrollView.scrollEnabled = false;
                wkWebView.opaque = false;
            }
            else {
                self.webViewWindow = wkWebView;
            }
            [self.view addSubview:wkWebView];
        }

        wkWebView.translatesAutoresizingMaskIntoConstraints = false;
        [wkWebView.superview addConstraint:[NSLayoutConstraint constraintWithItem:wkWebView.superview attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:wkWebView attribute:NSLayoutAttributeBottom multiplier:1 constant:0]];
        [wkWebView.superview addConstraint:[NSLayoutConstraint constraintWithItem:wkWebView.superview attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:wkWebView attribute:NSLayoutAttributeTop multiplier:1 constant:0]];
        [wkWebView.superview addConstraint:[NSLayoutConstraint constraintWithItem:wkWebView.superview attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:wkWebView attribute:NSLayoutAttributeLeft multiplier:1 constant:0]];
        [wkWebView.superview addConstraint:[NSLayoutConstraint constraintWithItem:wkWebView.superview attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:wkWebView attribute:NSLayoutAttributeRight multiplier:1 constant:0]];
        wkWebView.navigationDelegate = self;
        wkWebView.UIDelegate = self;
        if (self.startUrl != nil) {
            [wkWebView loadRequest:[NSURLRequest requestWithURL:self.startUrl]];
        }
/*
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(keepWkWebViewActive:) object:nil];
        [self performSelector:@selector(keepWkWebViewActive:) withObject:nil afterDelay:0.1];
*/
    }
#endif
#if (!defined(CLIENT_IOS_USE_WKWEBVIEW) || !defined(CLIENT_IOS_USE_WKWEBVIEW_WINDOWS))
#ifdef CLIENT_IOS_USE_WKWEBVIEW_WINDOWS
    if (self.navigationController == nil)
#endif
    {
        UIWebView *uiWebView = self.webView;
        if (self.navigationController == nil) {
            uiWebView.scrollView.scrollEnabled = false;
            int i;

            i = 0;
            while ((i < uiWebView.scrollView.subviews.count)
                && ![((UIView *)[uiWebView.scrollView.subviews objectAtIndex:i]).class.description isEqualToString:@"UIWebBrowserView"]) {
                i++;
            }
            if (i < uiWebView.scrollView.subviews.count) {
                UIView *view = (UIView *)[uiWebView.scrollView.subviews objectAtIndex:i];
                i = 0;
                while ((i < view.gestureRecognizers.count)
                    && (![(UIGestureRecognizer *)[view.gestureRecognizers objectAtIndex:i] isKindOfClass:UITapGestureRecognizer.class]
                     || (((UITapGestureRecognizer *)[view.gestureRecognizers objectAtIndex:i]).numberOfTapsRequired != 2)
                     || (((UITapGestureRecognizer *)[view.gestureRecognizers objectAtIndex:i]).numberOfTouchesRequired != 1))) {
                    i++;
                }
                if (i < view.gestureRecognizers.count) {
                    ((UITapGestureRecognizer *)[view.gestureRecognizers objectAtIndex:i]).enabled = false;
                }
            }
        }
        else {
            uiWebView = [[UIWebView alloc] initWithFrame:self.view.frame];
            uiWebView.scalesPageToFit = true;
            [self.view addSubview:uiWebView];
            uiWebView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
            self.webViewWindow = uiWebView;
        }
        uiWebView.delegate = self;
        uiWebView.mediaPlaybackRequiresUserAction = false;
        uiWebView.allowsInlineMediaPlayback = true;
        if (self.startUrl != nil) {
            [uiWebView loadRequest:[NSURLRequest requestWithURL:self.startUrl]];
        }
    }
#endif
#endif
}

- (void) viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

- (void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
}

- (void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
}

- (void) viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
}

- (void) dealloc
{
}

- (UIStatusBarStyle) preferredStatusBarStyle
{ 
    return self.statusBarStyle;
}

- (BOOL) prefersStatusBarHidden
{
    return self.statusBarHidden;
}

- (UIInterfaceOrientationMask) supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskAll;
}

- (BOOL) shouldAutorotate
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    return (!phoneIosDelegate.orientation_adaptation_off && !UIDevice.currentDevice.proximityState);
}

@end
