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 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