iOS web browser with a focus on security and privacy
at remove_ckhttpconnection 820 lines 30 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 WebKit; 16 17@implementation WebViewTab { 18 AppDelegate *appDelegate; 19} 20 21+ (WebViewTab *)openedWebViewTabByRandID:(NSString *)randID 22{ 23 for (WebViewTab *wvt in [[(AppDelegate *)[[UIApplication sharedApplication] delegate] webViewController] webViewTabs]) { 24 if ([wvt randID] != nil && [[wvt randID] isEqualToString:randID]) { 25 return wvt; 26 } 27 } 28 29 return nil; 30} 31 32- (id)initWithFrame:(CGRect)frame 33{ 34 return [self initWithFrame:frame withRestorationIdentifier:nil]; 35} 36 37- (id)initWithFrame:(CGRect)frame withRestorationIdentifier:(NSString *)rid 38{ 39 self = [super init]; 40 41 appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; 42 43 _viewHolder = [[UIView alloc] initWithFrame:frame]; 44 45 /* re-register user agent with our hash, which should only affect this UIWebView */ 46 [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"UserAgent": [NSString stringWithFormat:@"%@/%lu", [appDelegate defaultUserAgent], (unsigned long)self.hash] }]; 47 48 _webView = [[UIWebView alloc] initWithFrame:CGRectZero]; 49 _needsRefresh = FALSE; 50 if (rid != nil) { 51 [_webView setRestorationIdentifier:rid]; 52 _needsRefresh = TRUE; 53 } 54 [_webView setDelegate:self]; 55 [_webView setScalesPageToFit:YES]; 56 [_webView setAutoresizesSubviews:YES]; 57 [_webView setAllowsInlineMediaPlayback:YES]; 58 59 [_webView.scrollView setContentInset:UIEdgeInsetsMake(0, 0, 0, 0)]; 60 [_webView.scrollView setScrollIndicatorInsets:UIEdgeInsetsMake(0, 0, 0, 0)]; 61 [_webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal]; 62 63 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(webKitprogressEstimateChanged:) name:@"WebProgressEstimateChangedNotification" object:[_webView valueForKeyPath:@"documentView.webView"]]; 64 65 /* swiping goes back and forward in current webview */ 66 UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRightAction:)]; 67 [swipeRight setDirection:UISwipeGestureRecognizerDirectionRight]; 68 [swipeRight setDelegate:self]; 69 [self.webView addGestureRecognizer:swipeRight]; 70 71 UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeftAction:)]; 72 [swipeLeft setDirection:UISwipeGestureRecognizerDirectionLeft]; 73 [swipeLeft setDelegate:self]; 74 [self.webView addGestureRecognizer:swipeLeft]; 75 76 _titleHolder = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 0)]; 77 [_titleHolder setBackgroundColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:0.75]]; 78 79 _title = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 0)]; 80 [_title setTextColor:[UIColor whiteColor]]; 81 [_title setFont:[UIFont boldSystemFontOfSize:16.0]]; 82 [_title setLineBreakMode:NSLineBreakByTruncatingTail]; 83 [_title setTextAlignment:NSTextAlignmentCenter]; 84 [_title setText:@"New Tab"]; 85 86 _closer = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 0)]; 87 [_closer setTextColor:[UIColor whiteColor]]; 88 [_closer setFont:[UIFont systemFontOfSize:24.0]]; 89 [_closer setText:[NSString stringWithFormat:@"%C", 0x2715]]; 90 91 [_viewHolder addSubview:_titleHolder]; 92 [_viewHolder addSubview:_title]; 93 [_viewHolder addSubview:_closer]; 94 [_viewHolder addSubview:_webView]; 95 96 /* setup shadow that will be shown when zooming out */ 97 [[_viewHolder layer] setMasksToBounds:NO]; 98 [[_viewHolder layer] setShadowOffset:CGSizeMake(0, 0)]; 99 [[_viewHolder layer] setShadowRadius:8]; 100 [[_viewHolder layer] setShadowOpacity:0]; 101 102 _progress = @0.0; 103 104 [self updateFrame:frame]; 105 106 [self zoomNormal]; 107 108 [self setSecureMode:WebViewTabSecureModeInsecure]; 109 [self setApplicableHTTPSEverywhereRules:[[NSMutableDictionary alloc] initWithCapacity:6]]; 110 111 UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressMenu:)]; 112 [lpgr setDelegate:self]; 113 [_webView addGestureRecognizer:lpgr]; 114 115 for (UIView *_view in _webView.subviews) { 116 for (UIGestureRecognizer *recognizer in _view.gestureRecognizers) { 117 [recognizer addTarget:self action:@selector(webViewTouched:)]; 118 } 119 for (UIView *_sview in _view.subviews) { 120 for (UIGestureRecognizer *recognizer in _sview.gestureRecognizers) { 121 [recognizer addTarget:self action:@selector(webViewTouched:)]; 122 } 123 } 124 } 125 126 /* 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 */ 127 NSString *ua = [_webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"]; 128 NSArray *uap = [ua componentsSeparatedByString:@"/"]; 129 NSString *wvthash = uap[uap.count - 1]; 130 if (![[NSString stringWithFormat:@"%lu", (unsigned long)[self hash]] isEqualToString:wvthash]) 131 abort(); 132 133 return self; 134} 135 136- (void)dealloc 137{ 138 [[NSNotificationCenter defaultCenter] removeObserver:self name:@"WebProgressEstimateChangedNotification" object:[_webView valueForKeyPath:@"documentView.webView"]]; 139 [_webView setDelegate:nil]; 140 [_webView stopLoading]; 141 142 for (id gr in [_webView gestureRecognizers]) 143 [_webView removeGestureRecognizer:gr]; 144 145 _webView = nil; 146 147 [[self viewHolder] removeFromSuperview]; 148} 149 150/* for long press gesture recognizer to work properly */ 151- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { 152 if (![gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) 153 return NO; 154 155 if ([gestureRecognizer state] != UIGestureRecognizerStateBegan) 156 return YES; 157 158 BOOL haveLinkOrImage = NO; 159 160 NSArray *elements = [self elementsAtLocationFromGestureRecognizer:gestureRecognizer]; 161 for (NSDictionary *element in elements) { 162 NSString *k = [element allKeys][0]; 163 164 if ([k isEqualToString:@"a"] || [k isEqualToString:@"img"]) { 165 haveLinkOrImage = YES; 166 break; 167 } 168 } 169 170 if (haveLinkOrImage) { 171 /* 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 */ 172 if ([otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) { 173 otherGestureRecognizer.enabled = NO; 174 otherGestureRecognizer.enabled = YES; 175 } 176 177 return YES; 178 } 179 180 return NO; 181} 182 183- (void)webKitprogressEstimateChanged:(NSNotification*)notification 184{ 185 [self setProgress:[NSNumber numberWithFloat:[[notification object] estimatedProgress]]]; 186} 187 188- (void)updateFrame:(CGRect)frame 189{ 190 [self.viewHolder setFrame:frame]; 191 [self.webView setFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; 192 193 if ([[appDelegate webViewController] toolbarOnBottom]) { 194 [self.titleHolder setFrame:CGRectMake(0, frame.size.height, frame.size.width, 32)]; 195 [self.closer setFrame:CGRectMake(3, frame.size.height + 8, 18, 18)]; 196 [self.title setFrame:CGRectMake(22, frame.size.height + 8, frame.size.width - 22 - 22, 18)]; 197 } 198 else { 199 [self.titleHolder setFrame:CGRectMake(0, -26, frame.size.width, 32)]; 200 [self.closer setFrame:CGRectMake(3, -22, 18, 18)]; 201 [self.title setFrame:CGRectMake(22, -22, frame.size.width - 22 - 22, 18)]; 202 } 203} 204 205- (void)prepareForNewURL:(NSURL *)URL 206{ 207 [[self applicableHTTPSEverywhereRules] removeAllObjects]; 208 [self setSSLCertificate:nil]; 209 [self setUrl:URL]; 210} 211 212- (void)loadURL:(NSURL *)u 213{ 214 [self loadURL:u withForce:NO]; 215} 216 217- (void)loadURL:(NSURL *)u withForce:(BOOL)force 218{ 219 [self.webView stopLoading]; 220 [self prepareForNewURL:u]; 221 222 NSMutableURLRequest *ur = [NSMutableURLRequest requestWithURL:u]; 223 if (force) 224 [ur setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; 225 226 [self.webView loadRequest:ur]; 227} 228 229- (void)searchFor:(NSString *)query 230{ 231 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 232 NSDictionary *se = [[appDelegate searchEngines] objectForKey:[userDefaults stringForKey:@"search_engine"]]; 233 234 if (se == nil) 235 /* just pick the first search engine */ 236 se = [[appDelegate searchEngines] objectForKey:[[[appDelegate searchEngines] allKeys] firstObject]]; 237 238 NSDictionary *pp = [se objectForKey:@"post_params"]; 239 NSString *urls; 240 if (pp == nil) 241 urls = [[NSString stringWithFormat:[se objectForKey:@"search_url"], query] stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; 242 else 243 urls = [se objectForKey:@"search_url"]; 244 245 NSURL *url = [NSURL URLWithString:urls]; 246 if (pp == nil) { 247#ifdef TRACE 248 NSLog(@"[Tab %@] searching via %@", self.tabIndex, url); 249#endif 250 [self loadURL:url]; 251 } 252 else { 253 /* need to send this as a POST, so build our key val pairs */ 254 NSMutableString *params = [NSMutableString stringWithFormat:@""]; 255 for (NSString *key in [pp allKeys]) { 256 if (![params isEqualToString:@""]) 257 [params appendString:@"&"]; 258 259 [params appendString:[key stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]]; 260 [params appendString:@"="]; 261 262 NSString *val = [pp objectForKey:key]; 263 if ([val isEqualToString:@"%@"]) 264 val = [query stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; 265 [params appendString:val]; 266 } 267 268 [self.webView stopLoading]; 269 [self prepareForNewURL:url]; 270 271#ifdef TRACE 272 NSLog(@"[Tab %@] searching via POST to %@ (with params %@)", self.tabIndex, url, params); 273#endif 274 275 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; 276 [request setHTTPMethod:@"POST"]; 277 [request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]]; 278 [self.webView loadRequest:request]; 279 } 280} 281 282/* this will only fire for top-level requests (and iframes), not page elements */ 283- (BOOL)webView:(UIWebView *)__webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 284{ 285 NSURL *url = [request URL]; 286 287 /* treat endlesshttps?:// links clicked inside of web pages as normal links */ 288 if ([[[url scheme] lowercaseString] isEqualToString:@"endlesshttp"]) { 289 url = [NSURL URLWithString:[[url absoluteString] stringByReplacingCharactersInRange:NSMakeRange(0, [@"endlesshttp" length]) withString:@"http"]]; 290 [self loadURL:url]; 291 return NO; 292 } 293 else if ([[[url scheme] lowercaseString] isEqualToString:@"endlesshttps"]) { 294 url = [NSURL URLWithString:[[url absoluteString] stringByReplacingCharactersInRange:NSMakeRange(0, [@"endlesshttps" length]) withString:@"https"]]; 295 [self loadURL:url]; 296 return NO; 297 } 298 299 if (![[url scheme] isEqualToString:@"endlessipc"]) { 300 if ([[[request mainDocumentURL] absoluteString] isEqualToString:[[request URL] absoluteString]]) 301 [self prepareForNewURL:[request mainDocumentURL]]; 302 303 return YES; 304 } 305 306 /* endlessipc://fakeWindow.open/somerandomid?http... */ 307 308 NSString *action = [url host]; 309 310 NSString *param, *param2; 311 if ([[[request URL] pathComponents] count] >= 2) 312 param = [url pathComponents][1]; 313 if ([[[request URL] pathComponents] count] >= 3) 314 param2 = [url pathComponents][2]; 315 316 NSString *value = [[[url query] stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByRemovingPercentEncoding]; 317 318 if ([action isEqualToString:@"console.log"]) { 319 NSString *json = [[[url query] stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByRemovingPercentEncoding]; 320 NSLog(@"[Tab %@] [console.%@] %@", [self tabIndex], param, json); 321 /* no callback needed */ 322 return NO; 323 } 324 325#ifdef TRACE 326 NSLog(@"[Javascript IPC]: [%@] [%@] [%@] [%@]", action, param, param2, value); 327#endif 328 329 if ([action isEqualToString:@"noop"]) { 330 [self webView:__webView callbackWith:@""]; 331 } 332 else if ([action isEqualToString:@"window.open"]) { 333 /* only allow windows to be opened from mouse/touch events, like a normal browser's popup blocker */ 334 if (navigationType == UIWebViewNavigationTypeLinkClicked) { 335 WebViewTab *newtab = [[appDelegate webViewController] addNewTabForURL:nil]; 336 newtab.randID = param; 337 newtab.openedByTabHash = [NSNumber numberWithLong:self.hash]; 338 339 [self webView:__webView callbackWith:[NSString stringWithFormat:@"__endless.openedTabs[\"%@\"].opened = true;", param]]; 340 } 341 else { 342 /* TODO: show a "popup blocked" warning? */ 343 NSLog(@"[Tab %@] blocked non-touch window.open() (nav type %ldl)", self.tabIndex, (long)navigationType); 344 345 [self webView:__webView callbackWith:[NSString stringWithFormat:@"__endless.openedTabs[\"%@\"].opened = false;", param]]; 346 } 347 } 348 else if ([action isEqualToString:@"window.close"]) { 349 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Confirm" message:@"Allow this page to close its tab?" preferredStyle:UIAlertControllerStyleAlert]; 350 351 UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK action") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 352 [[appDelegate webViewController] removeTab:[self tabIndex]]; 353 }]; 354 355 UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel action") style:UIAlertActionStyleCancel handler:nil]; 356 [alertController addAction:cancelAction]; 357 [alertController addAction:okAction]; 358 359 [[appDelegate webViewController] presentViewController:alertController animated:YES completion:nil]; 360 361 [self webView:__webView callbackWith:@""]; 362 } 363 else if ([action hasPrefix:@"fakeWindow."]) { 364 WebViewTab *wvt = [[self class] openedWebViewTabByRandID:param]; 365 366 if (wvt == nil) { 367 [self webView:__webView callbackWith:[NSString stringWithFormat:@"delete __endless.openedTabs[\"%@\"];", [param stringEscapedForJavasacript]]]; 368 } 369 /* setters, just write into target webview */ 370 else if ([action isEqualToString:@"fakeWindow.setName"]) { 371 [[wvt webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.name = \"%@\";", [value stringEscapedForJavasacript]]]; 372 [self webView:__webView callbackWith:@""]; 373 } 374 else if ([action isEqualToString:@"fakeWindow.setLocation"]) { 375 [[wvt webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.location = \"%@\";", [value stringEscapedForJavasacript]]]; 376 [self webView:__webView callbackWith:@""]; 377 } 378 else if ([action isEqualToString:@"fakeWindow.setLocationParam"]) { 379 /* TODO: whitelist param since we're sending it raw */ 380 [[wvt webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.location.%@ = \"%@\";", param2, [value stringEscapedForJavasacript]]]; 381 [self webView:__webView callbackWith:@""]; 382 } 383 384 /* getters, pull from target webview and write back to caller internal parameters (not setters) */ 385 else if ([action isEqualToString:@"fakeWindow.getName"]) { 386 NSString *name = [[wvt webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.name;"]]; 387 [self webView:__webView callbackWith:[NSString stringWithFormat:@"__endless.openedTabs[\"%@\"]._name = \"%@\";", [param stringEscapedForJavasacript], [name stringEscapedForJavasacript]]]; 388 } 389 else if ([action isEqualToString:@"fakeWindow.getLocation"]) { 390 NSString *loc = [[wvt webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"JSON.stringify(window.location);"]]; 391 /* don't encode loc, it's (hopefully a safe) hash */ 392 [self webView:__webView callbackWith:[NSString stringWithFormat:@"__endless.openedTabs[\"%@\"]._location = new __endless.FakeLocation(%@)", [param stringEscapedForJavasacript], loc]]; 393 } 394 395 /* actions */ 396 else if ([action isEqualToString:@"fakeWindow.close"]) { 397 [[appDelegate webViewController] removeTab:[wvt tabIndex]]; 398 [self webView:__webView callbackWith:@""]; 399 } 400 } 401 402 return NO; 403} 404 405- (void)webViewDidStartLoad:(UIWebView *)__webView 406{ 407 /* reset and then let WebViewController animate to our actual progress */ 408 [self setProgress:@0.0]; 409 [self setProgress:@0.1]; 410 411 if (self.url == nil) 412 self.url = [[__webView request] URL]; 413} 414 415- (void)webViewDidFinishLoad:(UIWebView *)__webView 416{ 417#ifdef TRACE 418 NSLog(@"[Tab %@] finished loading page/iframe %@, security level is %lu", self.tabIndex, [[[__webView request] URL] absoluteString], self.secureMode); 419#endif 420 [self setProgress:@1.0]; 421 422 NSString *docTitle = [__webView stringByEvaluatingJavaScriptFromString:@"document.title"]; 423 NSString *finalURL = [__webView stringByEvaluatingJavaScriptFromString:@"window.location.href"]; 424 425 /* if we have javascript blocked, these will be empty */ 426 if (finalURL == nil || [finalURL isEqualToString:@""]) 427 finalURL = [[[__webView request] mainDocumentURL] absoluteString]; 428 if (docTitle == nil || [docTitle isEqualToString:@""]) 429 docTitle = finalURL; 430 431 [self.title setText:docTitle]; 432 self.url = [NSURL URLWithString:finalURL]; 433} 434 435- (void)webView:(UIWebView *)__webView didFailLoadWithError:(NSError *)error 436{ 437 self.url = self.webView.request.URL; 438 [self setProgress:@0]; 439 440 if ([[error domain] isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) 441 return; 442 443 /* "The operation couldn't be completed. (Cocoa error 3072.)" - useless */ 444 if ([[error domain] isEqualToString:NSCocoaErrorDomain] && error.code == NSUserCancelledError) 445 return; 446 447 NSString *msg = [error localizedDescription]; 448 449 /* https://opensource.apple.com/source/libsecurity_ssl/libsecurity_ssl-36800/lib/SecureTransport.h */ 450 if ([[error domain] isEqualToString:NSOSStatusErrorDomain]) { 451 switch (error.code) { 452 case errSSLProtocol: /* -9800 */ 453 msg = @"SSL protocol error"; 454 break; 455 case errSSLNegotiation: /* -9801 */ 456 msg = @"SSL handshake failed"; 457 break; 458 case errSSLXCertChainInvalid: /* -9807 */ 459 msg = @"SSL certificate chain verification error (self-signed certificate?)"; 460 break; 461 } 462 } 463 464 NSString *u; 465 if ((u = [[error userInfo] objectForKey:@"NSErrorFailingURLStringKey"]) != nil) 466 msg = [NSString stringWithFormat:@"%@\n\n%@", msg, u]; 467 468 if ([error userInfo] != nil) { 469 NSNumber *ok = [[error userInfo] objectForKey:ORIGIN_KEY]; 470 if (ok != nil && [ok boolValue] == NO) { 471#ifdef TRACE 472 NSLog(@"[Tab %@] not showing dialog for non-origin error: %@ (%@)", self.tabIndex, msg, error); 473#endif 474 [self webViewDidFinishLoad:__webView]; 475 return; 476 } 477 } 478 479#ifdef TRACE 480 NSLog(@"[Tab %@] showing error dialog: %@ (%@)", self.tabIndex, msg, error); 481#endif 482 483 UIAlertController *uiac = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Error", nil) message:msg preferredStyle:UIAlertControllerStyleAlert]; 484 [uiac addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Ok", nil) style:UIAlertActionStyleDefault handler:nil]]; 485 [[appDelegate webViewController] presentViewController:uiac animated:YES completion:nil]; 486 487 [self webViewDidFinishLoad:__webView]; 488} 489 490- (void)webView:(UIWebView *)__webView callbackWith:(NSString *)callback 491{ 492 NSString *finalcb = [NSString stringWithFormat:@"(function() { %@; __endless.ipcDone = (new Date()).getTime(); })();", callback]; 493 494#ifdef TRACE_IPC 495 NSLog(@"[Javascript IPC]: calling back with: %@", finalcb); 496#endif 497 498 [__webView stringByEvaluatingJavaScriptFromString:finalcb]; 499} 500 501- (void)setSSLCertificate:(SSLCertificate *)SSLCertificate 502{ 503 _SSLCertificate = SSLCertificate; 504 505 if (_SSLCertificate == nil) { 506#ifdef TRACE 507 NSLog(@"[Tab %@] setting securemode to insecure", self.tabIndex); 508#endif 509 [self setSecureMode:WebViewTabSecureModeInsecure]; 510 } 511 else if ([[self SSLCertificate] isEV]) { 512#ifdef TRACE 513 NSLog(@"[Tab %@] setting securemode to ev", self.tabIndex); 514#endif 515 [self setSecureMode:WebViewTabSecureModeSecureEV]; 516 } 517 else { 518#ifdef TRACE 519 NSLog(@"[Tab %@] setting securemode to secure", self.tabIndex); 520#endif 521 [self setSecureMode:WebViewTabSecureModeSecure]; 522 } 523} 524 525- (void)setProgress:(NSNumber *)pr 526{ 527 _progress = pr; 528 [[appDelegate webViewController] updateProgress]; 529} 530 531- (void)swipeRightAction:(UISwipeGestureRecognizer *)gesture 532{ 533 [self goBack]; 534} 535 536- (void)swipeLeftAction:(UISwipeGestureRecognizer *)gesture 537{ 538 [self goForward]; 539} 540 541- (void)webViewTouched:(UIEvent *)event 542{ 543 [[appDelegate webViewController] webViewTouched]; 544} 545 546- (void)longPressMenu:(UILongPressGestureRecognizer *)sender { 547 UIAlertController *alertController; 548 NSString *href, *img, *alt; 549 550 if (sender.state != UIGestureRecognizerStateBegan) 551 return; 552 553#ifdef TRACE 554 NSLog(@"[Tab %@] long-press gesture recognized", self.tabIndex); 555#endif 556 557 NSArray *elements = [self elementsAtLocationFromGestureRecognizer:sender]; 558 for (NSDictionary *element in elements) { 559 NSString *k = [element allKeys][0]; 560 NSDictionary *attrs = [element objectForKey:k]; 561 562 if ([k isEqualToString:@"a"]) { 563 href = [attrs objectForKey:@"href"]; 564 565 /* only use if image alt is blank */ 566 if (!alt || [alt isEqualToString:@""]) 567 alt = [attrs objectForKey:@"title"]; 568 } 569 else if ([k isEqualToString:@"img"]) { 570 img = [attrs objectForKey:@"src"]; 571 572 NSString *t = [attrs objectForKey:@"title"]; 573 if (t && ![t isEqualToString:@""]) 574 alt = t; 575 else 576 alt = [attrs objectForKey:@"alt"]; 577 } 578 } 579 580#ifdef TRACE 581 NSLog(@"[Tab %@] context menu href:%@, img:%@, alt:%@", self.tabIndex, href, img, alt); 582#endif 583 584 if (!(href || img)) { 585 sender.enabled = false; 586 sender.enabled = true; 587 return; 588 } 589 590 alertController = [UIAlertController alertControllerWithTitle:href message:alt preferredStyle:UIAlertControllerStyleActionSheet]; 591 592 UIAlertAction *openAction = [UIAlertAction actionWithTitle:@"Open" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 593 [self loadURL:[NSURL URLWithString:href]]; 594 }]; 595 596 UIAlertAction *openNewTabAction = [UIAlertAction actionWithTitle:@"Open in a New Tab" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 597 WebViewTab *newtab = [[appDelegate webViewController] addNewTabForURL:[NSURL URLWithString:href]]; 598 newtab.openedByTabHash = [NSNumber numberWithLong:self.hash]; 599 }]; 600 601 UIAlertAction *openSafariAction = [UIAlertAction actionWithTitle:@"Open in Safari" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 602 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:href] options:@{} completionHandler:nil]; 603 }]; 604 605 UIAlertAction *saveImageAction = [UIAlertAction actionWithTitle:@"Save Image" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 606 NSURL *imgurl = [NSURL URLWithString:img]; 607 [URLInterceptor temporarilyAllow:imgurl]; 608 NSData *imgdata = [NSData dataWithContentsOfURL:imgurl]; 609 if (imgdata) { 610 UIImage *i = [UIImage imageWithData:imgdata]; 611 UIImageWriteToSavedPhotosAlbum(i, self, nil, nil); 612 } 613 else { 614 UIAlertController *uiac = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Error", nil) message:[NSString stringWithFormat:@"An error occurred downloading image %@", img] preferredStyle:UIAlertControllerStyleAlert]; 615 [uiac addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Ok", nil) style:UIAlertActionStyleDefault handler:nil]]; 616 [[appDelegate webViewController] presentViewController:uiac animated:YES completion:nil]; 617 } 618 }]; 619 620 UIAlertAction *copyURLAction = [UIAlertAction actionWithTitle:@"Copy URL" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 621 [[UIPasteboard generalPasteboard] setString:(href ? href : img)]; 622 }]; 623 624 if (href) { 625 [alertController addAction:openAction]; 626 [alertController addAction:openNewTabAction]; 627 [alertController addAction:openSafariAction]; 628 } 629 630 if (img) 631 [alertController addAction:saveImageAction]; 632 633 [alertController addAction:copyURLAction]; 634 635 UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel") style:UIAlertActionStyleCancel handler:nil]; 636 [alertController addAction:cancelAction]; 637 638 UIPopoverPresentationController *popover = [alertController popoverPresentationController]; 639 if (popover) { 640 popover.sourceView = [sender view]; 641 CGPoint loc = [sender locationInView:[sender view]]; 642 /* offset for width of the finger */ 643 popover.sourceRect = CGRectMake(loc.x + 35, loc.y, 1, 1); 644 popover.permittedArrowDirections = UIPopoverArrowDirectionAny; 645 } 646 647 [[appDelegate webViewController] presentViewController:alertController animated:YES completion:nil]; 648} 649 650- (BOOL)canGoBack 651{ 652 return (self.openedByTabHash != nil || (self.webView && [self.webView canGoBack])); 653} 654 655- (BOOL)canGoForward 656{ 657 return !!(self.webView && [self.webView canGoForward]); 658} 659 660- (void)goBack 661{ 662 if ([self.webView canGoBack]) { 663 [[self webView] goBack]; 664 } 665 else if (self.openedByTabHash) { 666 for (WebViewTab *wvt in [[appDelegate webViewController] webViewTabs]) { 667 if ([wvt hash] == [self.openedByTabHash longValue]) { 668 [[appDelegate webViewController] removeTab:self.tabIndex andFocusTab:[wvt tabIndex]]; 669 return; 670 } 671 } 672 673 [[appDelegate webViewController] removeTab:self.tabIndex]; 674 } 675} 676 677- (void)goForward 678{ 679 if ([[self webView] canGoForward]) 680 [[self webView] goForward]; 681} 682 683- (void)refresh 684{ 685 [self setNeedsRefresh:FALSE]; 686 [[self webView] reload]; 687} 688 689- (void)forceRefresh 690{ 691 [self loadURL:[self url] withForce:YES]; 692} 693 694- (void)zoomOut 695{ 696 [[self webView] setUserInteractionEnabled:NO]; 697 698 [_titleHolder setHidden:false]; 699 [_title setHidden:false]; 700 [_closer setHidden:false]; 701 [[[self viewHolder] layer] setShadowOpacity:0.3]; 702 703 BOOL rotated = (self.viewHolder.frame.size.width > self.viewHolder.frame.size.height); 704 [[self viewHolder] setTransform:CGAffineTransformMakeScale(rotated ? ZOOM_OUT_SCALE_ROTATED : ZOOM_OUT_SCALE, rotated ? ZOOM_OUT_SCALE_ROTATED : ZOOM_OUT_SCALE)]; 705} 706 707- (void)zoomNormal 708{ 709 [[self webView] setUserInteractionEnabled:YES]; 710 711 [_titleHolder setHidden:true]; 712 [_title setHidden:true]; 713 [_closer setHidden:true]; 714 [[[self viewHolder] layer] setShadowOpacity:0]; 715 [[self viewHolder] setTransform:CGAffineTransformIdentity]; 716} 717 718- (NSArray *)elementsAtLocationFromGestureRecognizer:(UIGestureRecognizer *)uigr 719{ 720 CGPoint tap = [uigr locationInView:[self webView]]; 721 tap.y -= [[[self webView] scrollView] contentInset].top; 722 723 /* translate tap coordinates from view to scale of page */ 724 CGSize windowSize = CGSizeMake( 725 [[[self webView] stringByEvaluatingJavaScriptFromString:@"window.innerWidth"] intValue], 726 [[[self webView] stringByEvaluatingJavaScriptFromString:@"window.innerHeight"] intValue] 727 ); 728 CGSize viewSize = [[self webView] frame].size; 729 float ratio = windowSize.width / viewSize.width; 730 CGPoint tapOnPage = CGPointMake(tap.x * ratio, tap.y * ratio); 731 732 /* now find if there are usable elements at those coordinates and extract their attributes */ 733 NSString *json = [[self webView] stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"JSON.stringify(__endless.elementsAtPoint(%li, %li));", (long)tapOnPage.x, (long)tapOnPage.y]]; 734 if (json == nil) { 735 NSLog(@"[Tab %@] didn't get any JSON back from __endless.elementsAtPoint", self.tabIndex); 736 return @[]; 737 } 738 739 return [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil]; 740} 741 742- (void)handleKeyCommand:(UIKeyCommand *)keyCommand 743{ 744 BOOL ctrlKey = NO; 745 BOOL shiftKey = NO; 746 BOOL altKey = NO; 747 BOOL metaKey = NO; 748 749 int keycode = 0; 750 int keypress_keycode = 0; 751 752 NSString *keyAction = nil; 753 754 if ([keyCommand modifierFlags] & UIKeyModifierShift) 755 shiftKey = YES; 756 if ([keyCommand modifierFlags] & UIKeyModifierControl) 757 ctrlKey = YES; 758 if ([keyCommand modifierFlags] & UIKeyModifierAlternate) 759 altKey = YES; 760 if ([keyCommand modifierFlags] & UIKeyModifierCommand) 761 metaKey = YES; 762 763 for (int i = 0; i < sizeof(keyboard_map); i++) { 764 struct keyboard_map_entry kme = keyboard_map[i]; 765 766 if ([[keyCommand input] isEqualToString:@(kme.input)]) { 767 keycode = kme.keycode; 768 769 if (shiftKey) 770 keypress_keycode = kme.shift_keycode; 771 else 772 keypress_keycode = kme.keypress_keycode; 773 774 break; 775 } 776 } 777 778 if (!keycode) { 779 NSLog(@"[Tab %@] unknown hardware keyboard input: \"%@\"", self.tabIndex, [keyCommand input]); 780 return; 781 } 782 783 if ([[keyCommand input] isEqualToString:@" "]) 784 keyAction = @"__endless.smoothScroll(0, window.innerHeight * 0.75, 0, 0);"; 785 else if ([[keyCommand input] isEqualToString:@"UIKeyInputLeftArrow"]) 786 keyAction = @"__endless.smoothScroll(-75, 0, 0, 0);"; 787 else if ([[keyCommand input] isEqualToString:@"UIKeyInputRightArrow"]) 788 keyAction = @"__endless.smoothScroll(75, 0, 0, 0);"; 789 else if ([[keyCommand input] isEqualToString:@"UIKeyInputUpArrow"]) { 790 if (metaKey) 791 keyAction = @"__endless.smoothScroll(0, 0, 1, 0);"; 792 else 793 keyAction = @"__endless.smoothScroll(0, -75, 0, 0);"; 794 } 795 else if ([[keyCommand input] isEqualToString:@"UIKeyInputDownArrow"]) { 796 if (metaKey) 797 keyAction = @"__endless.smoothScroll(0, 0, 0, 1);"; 798 else 799 keyAction = @"__endless.smoothScroll(0, 75, 0, 0);"; 800 } 801 802 NSString *js = [NSString stringWithFormat:@"__endless.injectKey(%d, %d, %@, %@, %@, %@, %@);", 803 keycode, 804 keypress_keycode, 805 (ctrlKey ? @"true" : @"false"), 806 (altKey ? @"true" : @"false"), 807 (shiftKey ? @"true" : @"false"), 808 (metaKey ? @"true" : @"false"), 809 (keyAction ? [NSString stringWithFormat:@"function() { %@ }", keyAction] : @"null") 810 ]; 811 812#ifdef TRACE_KEYBOARD_INPUT 813 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"); 814 NSLog(@"%@", js); 815#endif 816 817 [[self webView] stringByEvaluatingJavaScriptFromString:js]; 818} 819 820@end