iOS web browser with a focus on security and privacy
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