iOS web browser with a focus on security and privacy
at master 1013 lines 36 kB view raw
1/* 2 * Endless 3 * Copyright (c) 2014-2017 joshua stein <jcs@jcs.org> 4 * 5 * See LICENSE file for redistribution terms. 6 */ 7 8#import "AppDelegate.h" 9#import "URLInterceptor.h" 10#import "WebViewTab.h" 11 12#import "NSString+JavascriptEscape.h" 13#import "UIResponder+FirstResponder.h" 14 15#import "NSString+DTURLEncoding.h" 16#import "VForceTouchGestureRecognizer.h" 17 18@import WebKit; 19 20@implementation WebViewTab { 21 AppDelegate *appDelegate; 22 BOOL inForceTouch; 23 BOOL skipHistory; 24} 25 26+ (WebViewTab *)openedWebViewTabByRandID:(NSString *)randID 27{ 28 for (WebViewTab *wvt in [[(AppDelegate *)[[UIApplication sharedApplication] delegate] webViewController] webViewTabs]) { 29 if ([wvt randID] != nil && [[wvt randID] isEqualToString:randID]) { 30 return wvt; 31 } 32 } 33 34 return nil; 35} 36 37- (id)initWithFrame:(CGRect)frame 38{ 39 return [self initWithFrame:frame withRestorationIdentifier:nil]; 40} 41 42- (id)initWithFrame:(CGRect)frame withRestorationIdentifier:(NSString *)rid 43{ 44 self = [super init]; 45 46 appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; 47 48 _viewHolder = [[UIView alloc] initWithFrame:frame]; 49 50 /* re-register user agent with our hash, which should only affect this UIWebView */ 51 [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"UserAgent": [NSString stringWithFormat:@"%@/%lu", [appDelegate defaultUserAgent], (unsigned long)self.hash] }]; 52 53 _webView = [[UIWebView alloc] initWithFrame:CGRectZero]; 54 _needsRefresh = FALSE; 55 if (rid != nil) { 56 [_webView setRestorationIdentifier:rid]; 57 _needsRefresh = TRUE; 58 } 59 [_webView setDelegate:self]; 60 [_webView setScalesPageToFit:YES]; 61 [_webView setAutoresizesSubviews:YES]; 62 [_webView setAllowsInlineMediaPlayback:YES]; 63 64 [_webView.scrollView setContentInset:UIEdgeInsetsMake(0, 0, 0, 0)]; 65 [_webView.scrollView setScrollIndicatorInsets:UIEdgeInsetsMake(0, 0, 0, 0)]; 66 [_webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal]; 67 [_webView.scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever]; 68 69 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(webKitprogressEstimateChanged:) name:@"WebProgressEstimateChangedNotification" object:[_webView valueForKeyPath:@"documentView.webView"]]; 70 71 /* swiping goes back and forward in current webview */ 72 UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRightAction:)]; 73 [swipeRight setDirection:UISwipeGestureRecognizerDirectionRight]; 74 [swipeRight setDelegate:self]; 75 [self.webView addGestureRecognizer:swipeRight]; 76 77 UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeftAction:)]; 78 [swipeLeft setDirection:UISwipeGestureRecognizerDirectionLeft]; 79 [swipeLeft setDelegate:self]; 80 [self.webView addGestureRecognizer:swipeLeft]; 81 82 self.refresher = [[UIRefreshControl alloc] init]; 83 [self.refresher setAttributedTitle:[[NSAttributedString alloc] initWithString:NSLocalizedString(@"Pull to Refresh Page", nil)]]; 84 [self.refresher addTarget:self action:@selector(forceRefreshFromRefresher) forControlEvents:UIControlEventValueChanged]; 85 [self.webView.scrollView addSubview:self.refresher]; 86 87 _titleHolder = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 0)]; 88 [_titleHolder setBackgroundColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:0.75]]; 89 90 _title = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 0)]; 91 [_title setTextColor:[UIColor whiteColor]]; 92 [_title setFont:[UIFont boldSystemFontOfSize:16.0]]; 93 [_title setLineBreakMode:NSLineBreakByTruncatingTail]; 94 [_title setTextAlignment:NSTextAlignmentCenter]; 95 [_title setText:@"New Tab"]; 96 97 _closer = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 0)]; 98 [_closer setTextColor:[UIColor whiteColor]]; 99 [_closer setFont:[UIFont systemFontOfSize:24.0]]; 100 [_closer setText:[NSString stringWithFormat:@"%C", 0x2715]]; 101 102 [_viewHolder addSubview:_titleHolder]; 103 [_viewHolder addSubview:_title]; 104 [_viewHolder addSubview:_closer]; 105 [_viewHolder addSubview:_webView]; 106 107 /* setup shadow that will be shown when zooming out */ 108 [[_viewHolder layer] setMasksToBounds:NO]; 109 [[_viewHolder layer] setShadowOffset:CGSizeMake(0, 0)]; 110 [[_viewHolder layer] setShadowRadius:8]; 111 [[_viewHolder layer] setShadowOpacity:0]; 112 113 _progress = @0.0; 114 115 [self updateFrame:frame]; 116 117 [self zoomNormal]; 118 119 [self setSecureMode:WebViewTabSecureModeInsecure]; 120 [self setApplicableHTTPSEverywhereRules:[[NSMutableDictionary alloc] init]]; 121 [self setApplicableURLBlockerTargets:[[NSMutableDictionary alloc] init]]; 122 123 VForceTouchGestureRecognizer *forceTouch = [[VForceTouchGestureRecognizer alloc] initWithTarget:self action:@selector(pressedMenu:)]; 124 [forceTouch setDelegate:self]; 125 [forceTouch setPercentMinimalRequest:0.4]; 126 inForceTouch = NO; 127 [self.webView addGestureRecognizer:forceTouch]; 128 129 UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(pressedMenu:)]; 130 [lpgr setDelegate:self]; 131 [self.webView addGestureRecognizer:lpgr]; 132 133 for (UIView *_view in _webView.subviews) { 134 for (UIGestureRecognizer *recognizer in _view.gestureRecognizers) { 135 [recognizer addTarget:self action:@selector(webViewTouched:)]; 136 } 137 for (UIView *_sview in _view.subviews) { 138 for (UIGestureRecognizer *recognizer in _sview.gestureRecognizers) { 139 [recognizer addTarget:self action:@selector(webViewTouched:)]; 140 } 141 } 142 } 143 144 self.history = [[NSMutableArray alloc] initWithCapacity:HISTORY_SIZE]; 145 146 /* this doubles as a way to force the webview to initialize itself, otherwise the UA doesn't seem to set right before refreshing a previous restoration state */ 147 NSString *ua = [_webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"]; 148 NSArray *uap = [ua componentsSeparatedByString:@"/"]; 149 NSString *wvthash = uap[uap.count - 1]; 150 if (![[NSString stringWithFormat:@"%lu", (unsigned long)[self hash]] isEqualToString:wvthash]) 151 abort(); 152 153 return self; 154} 155 156- (void)dealloc 157{ 158 [[NSNotificationCenter defaultCenter] removeObserver:self name:@"WebProgressEstimateChangedNotification" object:[_webView valueForKeyPath:@"documentView.webView"]]; 159 160 void (^block)(void) = ^{ 161 [self->_webView setDelegate:nil]; 162 [self->_webView stopLoading]; 163 164 for (id gr in [self->_webView gestureRecognizers]) 165 [self->_webView removeGestureRecognizer:gr]; 166 167 self->_webView = nil; 168 169 [[self viewHolder] removeFromSuperview]; 170 }; 171 172 if ([NSThread isMainThread]) 173 block(); 174 else 175 dispatch_sync(dispatch_get_main_queue(), ^{ 176 block(); 177 }); 178} 179 180/* for long press gesture recognizer to work properly */ 181- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { 182 if (![gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) 183 return NO; 184 185 if ([gestureRecognizer state] != UIGestureRecognizerStateBegan) 186 return YES; 187 188 BOOL haveLinkOrImage = NO; 189 190 NSArray *elements = [self elementsAtLocationFromGestureRecognizer:gestureRecognizer]; 191 for (NSDictionary *element in elements) { 192 NSString *k = [element allKeys][0]; 193 194 if ([k isEqualToString:@"a"] || [k isEqualToString:@"img"]) { 195 haveLinkOrImage = YES; 196 break; 197 } 198 } 199 200 if (haveLinkOrImage) { 201 /* this is enough to cancel the touch when the long press gesture fires, so that the link being held down doesn't activate as a click once the finger is let up */ 202 if ([otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) { 203 otherGestureRecognizer.enabled = NO; 204 otherGestureRecognizer.enabled = YES; 205 } 206 207 return YES; 208 } 209 210 return NO; 211} 212 213- (void)webKitprogressEstimateChanged:(NSNotification*)notification 214{ 215 [self setProgress:[NSNumber numberWithFloat:[[notification object] estimatedProgress]]]; 216} 217 218- (void)updateFrame:(CGRect)frame 219{ 220 [self.viewHolder setFrame:frame]; 221 [self.webView setFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; 222 223 if ([[appDelegate webViewController] toolbarOnBottom]) { 224 [self.titleHolder setFrame:CGRectMake(0, frame.size.height, frame.size.width, 32)]; 225 [self.closer setFrame:CGRectMake(3, frame.size.height + 8, 18, 18)]; 226 [self.title setFrame:CGRectMake(22, frame.size.height + 8, frame.size.width - 22 - 22, 18)]; 227 } 228 else { 229 [self.titleHolder setFrame:CGRectMake(0, -26, frame.size.width, 32)]; 230 [self.closer setFrame:CGRectMake(3, -22, 18, 18)]; 231 [self.title setFrame:CGRectMake(22, -22, frame.size.width - 22 - 22, 18)]; 232 } 233} 234 235- (void)prepareForNewURL:(NSURL *)URL 236{ 237 [[self applicableHTTPSEverywhereRules] removeAllObjects]; 238 [[self applicableURLBlockerTargets] removeAllObjects]; 239 [self setSSLCertificate:nil]; 240 [self setUrl:URL]; 241} 242 243- (void)loadURL:(NSURL *)u 244{ 245 [self loadURL:u withForce:NO]; 246} 247 248- (void)loadURL:(NSURL *)u withForce:(BOOL)force 249{ 250 [self loadRequest:[NSURLRequest requestWithURL:u] withForce:force]; 251} 252 253- (void)loadRequest:(NSURLRequest *)req withForce:(BOOL)force 254{ 255 void (^block)(void) = ^{ 256 [self.webView stopLoading]; 257 [self prepareForNewURL:[req URL]]; 258 259 if (force) 260 [self setForcingRefresh:YES]; 261 262 [self.webView loadRequest:req]; 263 }; 264 265 if ([NSThread isMainThread]) 266 block(); 267 else 268 dispatch_sync(dispatch_get_main_queue(), ^{ 269 block(); 270 }); 271} 272 273- (void)searchFor:(NSString *)query 274{ 275 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 276 NSDictionary *se = [[appDelegate searchEngines] objectForKey:[userDefaults stringForKey:@"search_engine"]]; 277 278 if (se == nil) 279 /* just pick the first search engine */ 280 se = [[appDelegate searchEngines] objectForKey:[[[appDelegate searchEngines] allKeys] firstObject]]; 281 282 NSDictionary *pp = [se objectForKey:@"post_params"]; 283 NSString *urls; 284 if (pp == nil) 285 urls = [NSString stringWithFormat:[se objectForKey:@"search_url"], [query stringByURLEncoding]]; 286 else 287 urls = [se objectForKey:@"search_url"]; 288 289 NSURL *url = [NSURL URLWithString:urls]; 290 if (pp == nil) { 291#ifdef TRACE 292 NSLog(@"[Tab %@] searching via %@", self.tabIndex, url); 293#endif 294 [self loadURL:url]; 295 } 296 else { 297 /* need to send this as a POST, so build our key val pairs */ 298 NSMutableString *params = [NSMutableString stringWithFormat:@""]; 299 for (NSString *key in [pp allKeys]) { 300 if (![params isEqualToString:@""]) 301 [params appendString:@"&"]; 302 303 [params appendString:[key stringByURLEncoding]]; 304 [params appendString:@"="]; 305 306 NSString *val = [pp objectForKey:key]; 307 if ([val isEqualToString:@"%@"]) 308 val = [query stringByURLEncoding]; 309 [params appendString:val]; 310 } 311 312 [self.webView stopLoading]; 313 [self prepareForNewURL:url]; 314 315#ifdef TRACE 316 NSLog(@"[Tab %@] searching via POST to %@ (with params %@)", self.tabIndex, url, params); 317#endif 318 319 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; 320 [request setHTTPMethod:@"POST"]; 321 [request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]]; 322 [self.webView loadRequest:request]; 323 } 324} 325 326/* this will only fire for top-level requests (and iframes), not page elements */ 327- (BOOL)webView:(UIWebView *)__webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 328{ 329 NSURL *url = [request URL]; 330 331 /* treat endlesshttps?:// links clicked inside of web pages as normal links */ 332 if ([[[url scheme] lowercaseString] isEqualToString:@"endlesshttp"]) { 333 NSMutableURLRequest *tr = [request mutableCopy]; 334 [tr setURL:[NSURL URLWithString:[[url absoluteString] stringByReplacingCharactersInRange:NSMakeRange(0, [@"endlesshttp" length]) withString:@"http"]]]; 335 [self loadRequest:tr withForce:NO]; 336 return NO; 337 } 338 else if ([[[url scheme] lowercaseString] isEqualToString:@"endlesshttps"]) { 339 NSMutableURLRequest *tr = [request mutableCopy]; 340 [tr setURL:[NSURL URLWithString:[[url absoluteString] stringByReplacingCharactersInRange:NSMakeRange(0, [@"endlesshttps" length]) withString:@"https"]]]; 341 [self loadRequest:tr withForce:NO]; 342 return NO; 343 } 344 345 /* regular http/https urls */ 346 else if (![[url scheme] isEqualToString:@"endlessipc"]) { 347 /* try to prevent universal links from triggering by refusing the initial request and starting a new one */ 348 BOOL iframe = ![[[request URL] absoluteString] isEqualToString:[[request mainDocumentURL] absoluteString]]; 349 350 HostSettings *hs = [HostSettings settingsOrDefaultsForHost:[url host]]; 351 if ([hs boolSettingOrDefault:HOST_SETTINGS_KEY_UNIVERSAL_LINK_PROTECTION]) { 352 if (iframe && navigationType != UIWebViewNavigationTypeLinkClicked) { 353#ifdef TRACE 354 NSLog(@"[Tab %@] not doing universal link workaround for iframe %@", [self tabIndex], url); 355#endif 356 } else if (navigationType == UIWebViewNavigationTypeBackForward) { 357#ifdef TRACE 358 NSLog(@"[Tab %@] not doing universal link workaround for back/forward navigation to %@", [self tabIndex], url); 359#endif 360 } else if (navigationType == UIWebViewNavigationTypeFormSubmitted) { 361#ifdef TRACE 362 NSLog(@"[Tab %@] not doing universal link workaround for form submission to %@", [self tabIndex], url); 363#endif 364 } else if ([[[url scheme] lowercaseString] hasPrefix:@"http"] && ![NSURLProtocol propertyForKey:UNIVERSAL_LINKS_WORKAROUND_KEY inRequest:request]) { 365 NSMutableURLRequest *tr = [request mutableCopy]; 366 [NSURLProtocol setProperty:@YES forKey:UNIVERSAL_LINKS_WORKAROUND_KEY inRequest:tr]; 367#ifdef TRACE 368 NSLog(@"[Tab %@] doing universal link workaround for %@", [self tabIndex], url); 369#endif 370 [self loadRequest:tr withForce:NO]; 371 return NO; 372 } 373 } else { 374#ifdef TRACE 375 NSLog(@"[Tab %@] not doing universal link workaround for %@ due to HostSettings", [self tabIndex], url); 376#endif 377 } 378 379 if (!iframe) 380 [self prepareForNewURL:[request mainDocumentURL]]; 381 382 return YES; 383 } 384 385 /* endlessipc://fakeWindow.open/somerandomid?http... */ 386 387 NSString *action = [url host]; 388 389 NSString *param, *param2; 390 if ([[[request URL] pathComponents] count] >= 2) 391 param = [url pathComponents][1]; 392 if ([[[request URL] pathComponents] count] >= 3) 393 param2 = [url pathComponents][2]; 394 395 NSString *value = [[[url query] stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByRemovingPercentEncoding]; 396 397 if ([action isEqualToString:@"console.log"]) { 398 NSLog(@"[Tab %@] [console.%@] %@", [self tabIndex], param, value); 399 /* no callback needed */ 400 return NO; 401 } 402 403#ifdef TRACE 404 NSLog(@"[Javascript IPC]: [%@] [%@] [%@] [%@]", action, param, param2, value); 405#endif 406 407 if ([action isEqualToString:@"noop"]) { 408 [self webView:__webView callbackWith:@""]; 409 } 410 else if ([action isEqualToString:@"window.open"]) { 411 /* only allow windows to be opened from mouse/touch events, like a normal browser's popup blocker */ 412 if (navigationType == UIWebViewNavigationTypeLinkClicked) { 413 WebViewTab *newtab = [[appDelegate webViewController] addNewTabForURL:nil]; 414 newtab.randID = param; 415 newtab.openedByTabHash = [NSNumber numberWithLong:self.hash]; 416 417 [self webView:__webView callbackWith:[NSString stringWithFormat:@"__endless.openedTabs[\"%@\"].opened = true;", [param stringEscapedForJavasacript]]]; 418 } 419 else { 420 /* TODO: show a "popup blocked" warning? */ 421 NSLog(@"[Tab %@] blocked non-touch window.open() (nav type %ldl)", self.tabIndex, (long)navigationType); 422 423 [self webView:__webView callbackWith:[NSString stringWithFormat:@"__endless.openedTabs[\"%@\"].opened = false;", [param stringEscapedForJavasacript]]]; 424 } 425 } 426 else if ([action isEqualToString:@"window.close"]) { 427 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Confirm", nil) message:NSLocalizedString(@"Allow this page to close its tab?", nil) preferredStyle:UIAlertControllerStyleAlert]; 428 429 UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK action") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 430 [[self->appDelegate webViewController] removeTab:[self tabIndex]]; 431 }]; 432 433 UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel action") style:UIAlertActionStyleCancel handler:nil]; 434 [alertController addAction:cancelAction]; 435 [alertController addAction:okAction]; 436 437 [[appDelegate webViewController] presentViewController:alertController animated:YES completion:nil]; 438 439 [self webView:__webView callbackWith:@""]; 440 } 441 else if ([action hasPrefix:@"fakeWindow."]) { 442 WebViewTab *wvt = [[self class] openedWebViewTabByRandID:param]; 443 444 if (wvt == nil) { 445 [self webView:__webView callbackWith:[NSString stringWithFormat:@"delete __endless.openedTabs[\"%@\"];", [param stringEscapedForJavasacript]]]; 446 } 447 /* setters, just write into target webview */ 448 else if ([action isEqualToString:@"fakeWindow.setName"]) { 449 [[wvt webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.name = \"%@\";", [value stringEscapedForJavasacript]]]; 450 [self webView:__webView callbackWith:@""]; 451 } 452 else if ([action isEqualToString:@"fakeWindow.setLocation"]) { 453 [[wvt webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.location = \"%@\";", [value stringEscapedForJavasacript]]]; 454 [self webView:__webView callbackWith:@""]; 455 } 456 else if ([action isEqualToString:@"fakeWindow.setLocationParam"]) { 457 /* must match injected.js */ 458 NSArray *validParams = @[ @"hash", @"hostname", @"href", @"pathname", @"port", @"protocol", @"search", @"username", @"password", @"origin" ]; 459 460 if (param2 != nil && [validParams containsObject:param2]) 461 [[wvt webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.location.%@ = \"%@\";", param2, [value stringEscapedForJavasacript]]]; 462 else 463 NSLog(@"[Tab %@] window.%@ not implemented", self.tabIndex, param2); 464 465 [self webView:__webView callbackWith:@""]; 466 } 467 468 /* actions */ 469 else if ([action isEqualToString:@"fakeWindow.close"]) { 470 [[appDelegate webViewController] removeTab:[wvt tabIndex]]; 471 [self webView:__webView callbackWith:@""]; 472 } 473 } 474 475 return NO; 476} 477 478- (void)webViewDidStartLoad:(UIWebView *)__webView 479{ 480 /* reset and then let WebViewController animate to our actual progress */ 481 [self setProgress:@0.0]; 482 [self setProgress:@0.1]; 483 484 if (self.url == nil) 485 self.url = [[__webView request] URL]; 486} 487 488- (void)webViewDidFinishLoad:(UIWebView *)__webView 489{ 490#ifdef TRACE 491 NSLog(@"[Tab %@] finished loading page/iframe %@, security level is %lu", self.tabIndex, [[[__webView request] URL] absoluteString], self.secureMode); 492#endif 493 [self setProgress:@1.0]; 494 [self setForcingRefresh:NO]; 495 496 NSString *docTitle = [__webView stringByEvaluatingJavaScriptFromString:@"document.title"]; 497 NSString *finalURL = [__webView stringByEvaluatingJavaScriptFromString:@"window.location.href"]; 498 499 /* if we have javascript blocked, these will be empty */ 500 if (finalURL == nil || [finalURL isEqualToString:@""]) 501 finalURL = [[[__webView request] mainDocumentURL] absoluteString]; 502 if (docTitle == nil || [docTitle isEqualToString:@""]) 503 docTitle = finalURL; 504 505 /* if we're viewing just an image, scale it down to fit the screen width and color its background */ 506 NSString *ctype = [__webView stringByEvaluatingJavaScriptFromString:@"document.contentType"]; 507 if (ctype != nil && [ctype hasPrefix:@"image/"]) { 508 [__webView stringByEvaluatingJavaScriptFromString:@"(function(){ document.body.style.backgroundColor = '#202020'; var i = document.getElementsByTagName('img')[0]; if (i && i.clientWidth > window.innerWidth) { var m = document.createElement('meta'); m.name='viewport'; m.content='width=device-width, initial-scale=1, maximum-scale=5'; document.getElementsByTagName('head')[0].appendChild(m); i.style.width = '100%'; } })();"]; 509 } 510 511 [self.title setText:docTitle]; 512 self.url = [NSURL URLWithString:finalURL]; 513 514 if (!skipHistory) { 515 while (self.history.count > HISTORY_SIZE) 516 [self.history removeObjectAtIndex:0]; 517 518 if (self.history.count == 0 || ![[[self.history lastObject] objectForKey:@"url"] isEqualToString:finalURL]) 519 [self.history addObject:@{ @"url" : finalURL, @"title" : docTitle }]; 520 } 521 522 skipHistory = NO; 523} 524 525- (void)webView:(UIWebView *)__webView didFailLoadWithError:(NSError *)error 526{ 527 BOOL isTLSError = false; 528 529 self.url = __webView.request.URL; 530 [self setProgress:@0]; 531 532 if ([[error domain] isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) 533 return; 534 535 /* "The operation couldn't be completed. (Cocoa error 3072.)" - useless */ 536 if ([[error domain] isEqualToString:NSCocoaErrorDomain] && error.code == NSUserCancelledError) 537 return; 538 539 /* "Frame load interrupted" - not very helpful */ 540 if ([[error domain] isEqualToString:@"WebKitErrorDomain"] && error.code == 102) 541 return; 542 543 NSString *msg = [error localizedDescription]; 544 545 /* https://opensource.apple.com/source/libsecurity_ssl/libsecurity_ssl-36800/lib/SecureTransport.h */ 546 if ([[error domain] isEqualToString:NSOSStatusErrorDomain]) { 547 switch (error.code) { 548 case errSSLProtocol: /* -9800 */ 549 msg = NSLocalizedString(@"TLS protocol error", nil); 550 isTLSError = true; 551 break; 552 case errSSLNegotiation: /* -9801 */ 553 msg = NSLocalizedString(@"TLS handshake failed", nil); 554 isTLSError = true; 555 break; 556 case errSSLXCertChainInvalid: /* -9807 */ 557 msg = NSLocalizedString(@"TLS certificate chain verification error (self-signed certificate?)", nil); 558 isTLSError = true; 559 break; 560 } 561 } 562 563 NSString *u; 564 if ((u = [[error userInfo] objectForKey:@"NSErrorFailingURLStringKey"]) != nil) 565 msg = [NSString stringWithFormat:@"%@\n\n%@", msg, u]; 566 567 if ([error userInfo] != nil) { 568 NSNumber *ok = [[error userInfo] objectForKey:ORIGIN_KEY]; 569 if (ok != nil && [ok boolValue] == NO) { 570#ifdef TRACE 571 NSLog(@"[Tab %@] not showing dialog for non-origin error: %@ (%@)", self.tabIndex, msg, error); 572#endif 573 [self webViewDidFinishLoad:__webView]; 574 return; 575 } 576 } 577 578#ifdef TRACE 579 NSLog(@"[Tab %@] showing error dialog: %@ (%@)", self.tabIndex, msg, error); 580#endif 581 582 UIAlertController *uiac = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Error", nil) message:msg preferredStyle:UIAlertControllerStyleAlert]; 583 [uiac addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:nil]]; 584 585 if (u != nil && isTLSError && [[NSUserDefaults standardUserDefaults] boolForKey:@"allow_tls_error_ignore"]) { 586 [uiac addAction:[UIAlertAction 587 actionWithTitle:NSLocalizedString(@"Ignore for this host", nil) 588 style:UIAlertActionStyleDestructive 589 handler:^(UIAlertAction * _Nonnull action) { 590 591 /* 592 * self.url will hold the URL of the UIWebView which is the last *successful* request. 593 * We need the URL of the *failed* request, which should be in `u`. 594 * (From `error`'s `userInfo` dictionary. 595 */ 596 NSURL *url = [[NSURL alloc] initWithString:u]; 597 if (url != nil) { 598 HostSettings *hs = [HostSettings forHost:url.host]; 599 600 if (hs == nil) { 601 hs = [[HostSettings alloc] initForHost:url.host withDict:nil]; 602 } 603 604 [hs setSetting:HOST_SETTINGS_KEY_IGNORE_TLS_ERRORS toValue:HOST_SETTINGS_VALUE_YES]; 605 606 [hs save]; 607 [HostSettings persist]; 608 609 // Retry the failed request. 610 [self loadURL:url]; 611 } 612 }]]; 613 } 614 615 [[appDelegate webViewController] presentViewController:uiac animated:YES completion:nil]; 616 617 [self webViewDidFinishLoad:__webView]; 618} 619 620- (void)webView:(UIWebView *)__webView callbackWith:(NSString *)callback 621{ 622 NSString *finalcb = [NSString stringWithFormat:@"(function() { %@; __endless.ipcDone = (new Date()).getTime(); })();", callback]; 623 624#ifdef TRACE_IPC 625 NSLog(@"[Javascript IPC]: calling back with: %@", finalcb); 626#endif 627 628 [__webView stringByEvaluatingJavaScriptFromString:finalcb]; 629} 630 631- (void)setSSLCertificate:(SSLCertificate *)SSLCertificate 632{ 633 _SSLCertificate = SSLCertificate; 634 635 if (_SSLCertificate == nil) { 636#ifdef TRACE 637 NSLog(@"[Tab %@] setting securemode to insecure", self.tabIndex); 638#endif 639 [self setSecureMode:WebViewTabSecureModeInsecure]; 640 } 641 else if ([[self SSLCertificate] isEV]) { 642#ifdef TRACE 643 NSLog(@"[Tab %@] setting securemode to ev", self.tabIndex); 644#endif 645 [self setSecureMode:WebViewTabSecureModeSecureEV]; 646 } 647 else { 648#ifdef TRACE 649 NSLog(@"[Tab %@] setting securemode to secure", self.tabIndex); 650#endif 651 [self setSecureMode:WebViewTabSecureModeSecure]; 652 } 653} 654 655- (void)setProgress:(NSNumber *)pr 656{ 657 _progress = pr; 658 [[appDelegate webViewController] updateProgress]; 659} 660 661- (void)swipeRightAction:(UISwipeGestureRecognizer *)gesture 662{ 663 [self goBack]; 664} 665 666- (void)swipeLeftAction:(UISwipeGestureRecognizer *)gesture 667{ 668 [self goForward]; 669} 670 671- (void)webViewTouched:(UIEvent *)event 672{ 673 [[appDelegate webViewController] webViewTouched]; 674} 675 676- (void)pressedMenu:(UIGestureRecognizer *)event 677{ 678 UIAlertController *alertController; 679 NSString *href, *img, *alt; 680 681 if ([event isKindOfClass:[VForceTouchGestureRecognizer class]]) { 682 if ([event state] == UIGestureRecognizerStateBegan) { 683 inForceTouch = YES; 684 } else if ([event state] == UIGestureRecognizerStateChanged) { 685 inForceTouch = YES; 686 return; 687 } else { 688 inForceTouch = NO; 689 return; 690 } 691 } else if (inForceTouch || [event state] != UIGestureRecognizerStateBegan) { 692 return; 693 } 694 695#ifdef TRACE 696 NSLog(@"[Tab %@] %@ gesture recognized (%@)", [event class], self.tabIndex, event); 697#endif 698 699 NSArray *elements = [self elementsAtLocationFromGestureRecognizer:event]; 700 for (NSDictionary *element in elements) { 701 NSString *k = [element allKeys][0]; 702 NSDictionary *attrs = [element objectForKey:k]; 703 704 if ([k isEqualToString:@"a"]) { 705 href = [attrs objectForKey:@"href"]; 706 707 /* only use if image alt is blank */ 708 if (!alt || [alt isEqualToString:@""]) 709 alt = [attrs objectForKey:@"title"]; 710 } 711 else if ([k isEqualToString:@"img"]) { 712 img = [attrs objectForKey:@"src"]; 713 714 NSString *t = [attrs objectForKey:@"title"]; 715 if (t && ![t isEqualToString:@""]) 716 alt = t; 717 else 718 alt = [attrs objectForKey:@"alt"]; 719 } 720 } 721 722#ifdef TRACE 723 NSLog(@"[Tab %@] context menu href:%@, img:%@, alt:%@", self.tabIndex, href, img, alt); 724#endif 725 726 if (!(href || img)) { 727 event.enabled = false; 728 event.enabled = true; 729 return; 730 } 731 732 if (inForceTouch) { 733 /* taptic feedback */ 734 UINotificationFeedbackGenerator *uinfg = [[UINotificationFeedbackGenerator alloc] init]; 735 [uinfg prepare]; 736 [uinfg notificationOccurred:UINotificationFeedbackTypeSuccess]; 737 738 NSURL *u; 739 if (href) 740 u = [NSURL URLWithString:href]; 741 else if (img) 742 u = [NSURL URLWithString:img]; 743 744 if (u) { 745 WebViewTab *newtab = [[appDelegate webViewController] addNewTabForURL:u forRestoration:NO withAnimation:WebViewTabAnimationQuick withCompletionBlock:nil]; 746 newtab.openedByTabHash = [NSNumber numberWithLong:self.hash]; 747 } 748 749 return; 750 } 751 752 alertController = [UIAlertController alertControllerWithTitle:href message:alt preferredStyle:UIAlertControllerStyleActionSheet]; 753 754 UIAlertAction *openAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Open", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 755 [self loadURL:[NSURL URLWithString:href]]; 756 }]; 757 758 UIAlertAction *openNewTabAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Open in a New Tab", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 759 WebViewTab *newtab = [[self->appDelegate webViewController] addNewTabForURL:[NSURL URLWithString:href]]; 760 newtab.openedByTabHash = [NSNumber numberWithLong:self.hash]; 761 }]; 762 763 UIAlertAction *openSafariAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Open in Safari", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 764 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:href] options:@{} completionHandler:nil]; 765 }]; 766 767 UIAlertAction *saveImageAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Save Image", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 768 NSURL *imgurl = [NSURL URLWithString:img]; 769 [URLInterceptor temporarilyAllow:imgurl]; 770 NSData *imgdata = [NSData dataWithContentsOfURL:imgurl]; 771 if (imgdata) { 772 UIImage *i = [UIImage imageWithData:imgdata]; 773 UIImageWriteToSavedPhotosAlbum(i, self, nil, nil); 774 } 775 else { 776 UIAlertController *uiac = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Error", nil) message:[NSString stringWithFormat:NSLocalizedString(@"An error occurred downloading image %@", nil), img] preferredStyle:UIAlertControllerStyleAlert]; 777 [uiac addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:nil]]; 778 [[self->appDelegate webViewController] presentViewController:uiac animated:YES completion:nil]; 779 } 780 }]; 781 782 UIAlertAction *copyURLAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Copy URL", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 783 [[UIPasteboard generalPasteboard] setString:(href ? href : img)]; 784 }]; 785 786 if (href) { 787 [alertController addAction:openAction]; 788 [alertController addAction:openNewTabAction]; 789 [alertController addAction:openSafariAction]; 790 } 791 792 if (img) 793 [alertController addAction:saveImageAction]; 794 795 [alertController addAction:copyURLAction]; 796 797 UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel") style:UIAlertActionStyleCancel handler:nil]; 798 [alertController addAction:cancelAction]; 799 800 UIPopoverPresentationController *popover = [alertController popoverPresentationController]; 801 if (popover) { 802 popover.sourceView = [event view]; 803 CGPoint loc = [event locationInView:[event view]]; 804 /* offset for width of the finger */ 805 popover.sourceRect = CGRectMake(loc.x + 35, loc.y, 1, 1); 806 popover.permittedArrowDirections = UIPopoverArrowDirectionAny; 807 } 808 809 [[appDelegate webViewController] presentViewController:alertController animated:YES completion:nil]; 810} 811 812- (BOOL)canGoBack 813{ 814 return (self.openedByTabHash != nil || (self.webView && [self.webView canGoBack])); 815} 816 817- (BOOL)canGoForward 818{ 819 return !!(self.webView && [self.webView canGoForward]); 820} 821 822- (void)goBack 823{ 824 if ([self.webView canGoBack]) { 825 skipHistory = YES; 826 [[self webView] goBack]; 827 } 828 else if (self.openedByTabHash) { 829 for (WebViewTab *wvt in [[appDelegate webViewController] webViewTabs]) { 830 if ([wvt hash] == [self.openedByTabHash longValue]) { 831 [[appDelegate webViewController] removeTab:self.tabIndex andFocusTab:[wvt tabIndex]]; 832 return; 833 } 834 } 835 836 [[appDelegate webViewController] removeTab:self.tabIndex]; 837 } 838} 839 840- (void)goForward 841{ 842 if ([[self webView] canGoForward]) { 843 skipHistory = YES; 844 [[self webView] goForward]; 845 } 846} 847 848- (void)refresh 849{ 850 [self setNeedsRefresh:FALSE]; 851 skipHistory = YES; 852 [[self webView] reload]; 853} 854 855- (void)forceRefresh 856{ 857 skipHistory = YES; 858 [self loadURL:[self url] withForce:YES]; 859} 860 861- (void)forceRefreshFromRefresher 862{ 863 [self forceRefresh]; 864 865 /* delay just so it confirms to the user that something happened */ 866 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { 867 [self.refresher endRefreshing]; 868 }); 869} 870 871- (void)zoomOut 872{ 873 [[self webView] setUserInteractionEnabled:NO]; 874 875 [_titleHolder setHidden:false]; 876 [_title setHidden:false]; 877 [_closer setHidden:false]; 878 [[[self viewHolder] layer] setShadowOpacity:0.3]; 879 880 BOOL rotated = (self.viewHolder.frame.size.width > self.viewHolder.frame.size.height); 881 [[self viewHolder] setTransform:CGAffineTransformMakeScale(rotated ? ZOOM_OUT_SCALE_ROTATED : ZOOM_OUT_SCALE, rotated ? ZOOM_OUT_SCALE_ROTATED : ZOOM_OUT_SCALE)]; 882} 883 884- (void)zoomNormal 885{ 886 [[self webView] setUserInteractionEnabled:YES]; 887 888 [_titleHolder setHidden:true]; 889 [_title setHidden:true]; 890 [_closer setHidden:true]; 891 [[[self viewHolder] layer] setShadowOpacity:0]; 892 [[self viewHolder] setTransform:CGAffineTransformIdentity]; 893} 894 895- (NSArray *)elementsAtLocationFromGestureRecognizer:(UIGestureRecognizer *)uigr 896{ 897 CGPoint tap = [uigr locationInView:[self webView]]; 898 tap.y -= [[[self webView] scrollView] contentInset].top; 899 900 /* translate tap coordinates from view to scale of page */ 901 CGSize windowSize = CGSizeMake( 902 [[[self webView] stringByEvaluatingJavaScriptFromString:@"window.innerWidth"] intValue], 903 [[[self webView] stringByEvaluatingJavaScriptFromString:@"window.innerHeight"] intValue] 904 ); 905 CGSize viewSize = [[self webView] frame].size; 906 float ratio = windowSize.width / viewSize.width; 907 CGPoint tapOnPage = CGPointMake(tap.x * ratio, tap.y * ratio); 908 909 /* now find if there are usable elements at those coordinates and extract their attributes */ 910 NSString *json = [[self webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"JSON.stringify(__endless.elementsAtPoint(%li, %li));", (long)tapOnPage.x, (long)tapOnPage.y]]; 911 if (json == nil) { 912 NSLog(@"[Tab %@] didn't get any JSON back from __endless.elementsAtPoint", self.tabIndex); 913 return @[]; 914 } 915 916 return [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil]; 917} 918 919- (void)handleKeyCommand:(UIKeyCommand *)keyCommand 920{ 921 BOOL ctrlKey = NO; 922 BOOL shiftKey = NO; 923 BOOL altKey = NO; 924 BOOL metaKey = NO; 925 926 int keycode = 0; 927 int keypress_keycode = 0; 928 929 NSString *keyAction = nil; 930 931 if ([keyCommand modifierFlags] & UIKeyModifierShift) 932 shiftKey = YES; 933 if ([keyCommand modifierFlags] & UIKeyModifierControl) 934 ctrlKey = YES; 935 if ([keyCommand modifierFlags] & UIKeyModifierAlternate) 936 altKey = YES; 937 if ([keyCommand modifierFlags] & UIKeyModifierCommand) 938 metaKey = YES; 939 940 for (int i = 0; i < sizeof(keyboard_map); i++) { 941 struct keyboard_map_entry kme = keyboard_map[i]; 942 943 if ([[keyCommand input] isEqualToString:@(kme.input)]) { 944 keycode = kme.keycode; 945 946 if (shiftKey) 947 keypress_keycode = kme.shift_keycode; 948 else 949 keypress_keycode = kme.keypress_keycode; 950 951 break; 952 } 953 } 954 955 if (!keycode) { 956 NSLog(@"[Tab %@] unknown hardware keyboard input: \"%@\"", self.tabIndex, [keyCommand input]); 957 return; 958 } 959 960 if ([[keyCommand input] isEqualToString:@" "]) 961 keyAction = @"__endless.smoothScroll(0, window.innerHeight * 0.75, 0, 0);"; 962 else if ([[keyCommand input] isEqualToString:@"UIKeyInputLeftArrow"]) 963 keyAction = @"__endless.smoothScroll(-75, 0, 0, 0);"; 964 else if ([[keyCommand input] isEqualToString:@"UIKeyInputRightArrow"]) 965 keyAction = @"__endless.smoothScroll(75, 0, 0, 0);"; 966 else if ([[keyCommand input] isEqualToString:@"UIKeyInputUpArrow"]) { 967 if (metaKey) 968 keyAction = @"__endless.smoothScroll(0, 0, 1, 0);"; 969 else 970 keyAction = @"__endless.smoothScroll(0, -75, 0, 0);"; 971 } 972 else if ([[keyCommand input] isEqualToString:@"UIKeyInputDownArrow"]) { 973 if (metaKey) 974 keyAction = @"__endless.smoothScroll(0, 0, 0, 1);"; 975 else 976 keyAction = @"__endless.smoothScroll(0, 75, 0, 0);"; 977 } 978 979 NSString *js = [NSString stringWithFormat:@"__endless.injectKey(%d, %d, %@, %@, %@, %@, %@);", 980 keycode, 981 keypress_keycode, 982 (ctrlKey ? @"true" : @"false"), 983 (altKey ? @"true" : @"false"), 984 (shiftKey ? @"true" : @"false"), 985 (metaKey ? @"true" : @"false"), 986 (keyAction ? [NSString stringWithFormat:@"function() { %@ }", keyAction] : @"null") 987 ]; 988 989#ifdef TRACE_KEYBOARD_INPUT 990 NSLog(@"[Tab %@] hardware keyboard input: \"%@\", keycode %d, keypress keycode %d, modifiers (%ld): ctrl:%@, shift:%@, alt:%@, meta:%@", self.tabIndex, [keyCommand input], keycode, keypress_keycode, (long)[keyCommand modifierFlags], ctrlKey ? @"YES" : @"NO", shiftKey ? @"YES" : @"NO", altKey ? @"YES" : @"NO", metaKey ? @"YES" : @"NO"); 991 NSLog(@"%@", js); 992#endif 993 994 [[self webView] stringByEvaluatingJavaScriptFromString:js]; 995} 996 997/* UIActivityItemSource for URL sharing */ 998- (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController 999{ 1000 return [self url]; 1001} 1002 1003- (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType 1004{ 1005 return [self url]; 1006} 1007 1008- (NSString *)activityViewController:(UIActivityViewController *)activityViewController subjectForActivityType:(UIActivityType)activityType 1009{ 1010 return [[self title] text]; 1011} 1012 1013@end