iOS web browser with a focus on security and privacy
at master 403 lines 15 kB view raw
1/* 2 * Endless 3 * Copyright (c) 2014-2018 joshua stein <jcs@jcs.org> 4 * 5 * See LICENSE file for redistribution terms. 6 */ 7 8#import <AVFoundation/AVFoundation.h> 9 10#import "AppDelegate.h" 11#import "Bookmark.h" 12#import "HTTPSEverywhere.h" 13#import "URLInterceptor.h" 14 15#import "UIResponder+FirstResponder.h" 16 17@implementation AppDelegate 18{ 19 NSMutableArray *_keyCommands; 20 NSMutableArray *_allKeyBindings; 21 NSArray *_allCommandsAndKeyBindings; 22} 23 24- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions 25{ 26 [self initializeDefaults]; 27 28#ifdef USE_DUMMY_URLINTERCEPTOR 29 [NSURLProtocol registerClass:[DummyURLInterceptor class]]; 30#else 31 [URLInterceptor setup]; 32 [NSURLProtocol registerClass:[URLInterceptor class]]; 33#endif 34 35 self.hstsCache = [HSTSCache retrieve]; 36 self.cookieJar = [[CookieJar alloc] init]; 37 [Bookmark retrieveList]; 38 39 /* handle per-version upgrades or migrations */ 40 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 41 long lastBuild = [userDefaults integerForKey:@"last_build"]; 42 43 NSNumberFormatter *f = [[NSNumberFormatter alloc] init]; 44 f.numberStyle = NSNumberFormatterDecimalStyle; 45 long thisBuild = [[f numberFromString:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]] longValue]; 46 47 if (lastBuild != thisBuild) { 48 NSLog(@"migrating from build %ld -> %ld", lastBuild, thisBuild); 49 [HostSettings migrateFromBuild:lastBuild toBuild:thisBuild]; 50 51 [userDefaults setInteger:thisBuild forKey:@"last_build"]; 52 [userDefaults synchronize]; 53 } 54 55 self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 56 self.window.backgroundColor = [UIColor groupTableViewBackgroundColor]; 57 self.window.rootViewController = [[WebViewController alloc] init]; 58 self.window.rootViewController.restorationIdentifier = @"WebViewController"; 59 60 [self adjustMuteSwitchBehavior]; 61 62 return YES; 63} 64 65- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 66{ 67 [self.window makeKeyAndVisible]; 68 69 if (launchOptions != nil && [launchOptions objectForKey:UIApplicationLaunchOptionsShortcutItemKey]) { 70 [self handleShortcut:[launchOptions objectForKey:UIApplicationLaunchOptionsShortcutItemKey]]; 71 } 72 73 return YES; 74} 75 76- (void)applicationWillResignActive:(UIApplication *)application 77{ 78 [application ignoreSnapshotOnNextApplicationLaunch]; 79 [[self webViewController] viewIsNoLongerVisible]; 80} 81 82- (void)applicationDidEnterBackground:(UIApplication *)application 83{ 84 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 85 86 if (![self areTesting]) { 87 [HostSettings persist]; 88 [[self hstsCache] persist]; 89 } 90 91 if ([userDefaults boolForKey:@"clear_on_background"]) { 92 [[self webViewController] removeAllTabs]; 93 [[self cookieJar] clearAllNonWhitelistedData]; 94 } 95 else 96 [[self cookieJar] clearAllOldNonWhitelistedData]; 97 98 [application ignoreSnapshotOnNextApplicationLaunch]; 99} 100 101- (void)applicationDidBecomeActive:(UIApplication *)application 102{ 103 [[self webViewController] viewIsVisible]; 104} 105 106- (void)applicationWillTerminate:(UIApplication *)application 107{ 108 /* this definitely ends our sessions */ 109 [[self cookieJar] clearAllNonWhitelistedData]; 110 111 [application ignoreSnapshotOnNextApplicationLaunch]; 112} 113 114- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation 115{ 116#ifdef TRACE 117 NSLog(@"[AppDelegate] request to open url at launch: %@", url); 118#endif 119 if ([[[url scheme] lowercaseString] isEqualToString:@"endlesshttp"]) 120 url = [NSURL URLWithString:[[url absoluteString] stringByReplacingCharactersInRange:NSMakeRange(0, [@"endlesshttp" length]) withString:@"http"]]; 121 else if ([[[url scheme] lowercaseString] isEqualToString:@"endlesshttps"]) 122 url = [NSURL URLWithString:[[url absoluteString] stringByReplacingCharactersInRange:NSMakeRange(0, [@"endlesshttps" length]) withString:@"https"]]; 123 124 /* delay until we're done drawing the UI */ 125 self.urlToOpenAtLaunch = url; 126 127 return YES; 128} 129 130- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options 131{ 132#ifdef TRACE 133 NSLog(@"[AppDelegate] request to open url: %@", url); 134#endif 135 136 if ([[[url scheme] lowercaseString] isEqualToString:@"endlesshttp"]) 137 url = [NSURL URLWithString:[[url absoluteString] stringByReplacingCharactersInRange:NSMakeRange(0, [@"endlesshttp" length]) withString:@"http"]]; 138 else if ([[[url scheme] lowercaseString] isEqualToString:@"endlesshttps"]) 139 url = [NSURL URLWithString:[[url absoluteString] stringByReplacingCharactersInRange:NSMakeRange(0, [@"endlesshttps" length]) withString:@"https"]]; 140 141 [[self webViewController] dismissViewControllerAnimated:YES completion:nil]; 142 [[self webViewController] addNewTabForURL:url]; 143 144 return YES; 145} 146 147- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler 148{ 149 [self handleShortcut:shortcutItem]; 150 completionHandler(YES); 151} 152 153- (BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier { 154 if ([extensionPointIdentifier isEqualToString:UIApplicationKeyboardExtensionPointIdentifier]) { 155 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 156 return [userDefaults boolForKey:@"third_party_keyboards"]; 157 } 158 return YES; 159} 160 161- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder 162{ 163 if ([self areTesting]) 164 return NO; 165 166 /* if we tried last time and failed, the state might be corrupt */ 167 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 168 if ([userDefaults objectForKey:STATE_RESTORE_TRY_KEY] != nil) { 169 NSLog(@"[AppDelegate] previous startup failed, not restoring application state"); 170 [userDefaults removeObjectForKey:STATE_RESTORE_TRY_KEY]; 171 return NO; 172 } 173 else 174 [userDefaults setBool:YES forKey:STATE_RESTORE_TRY_KEY]; 175 176 [userDefaults synchronize]; 177 178 return YES; 179} 180 181- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder 182{ 183 if ([self areTesting]) 184 return NO; 185 186 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 187 if ([userDefaults boolForKey:@"clear_on_background"]) 188 return NO; 189 190 return YES; 191} 192 193- (NSArray<UIKeyCommand *> *)keyCommands 194{ 195 if (!_keyCommands) { 196 _keyCommands = [[NSMutableArray alloc] init]; 197 198 [_keyCommands addObject:[UIKeyCommand keyCommandWithInput:@"[" modifierFlags:UIKeyModifierCommand action:@selector(handleKeyboardShortcut:) discoverabilityTitle:NSLocalizedString(@"Go Back", nil)]]; 199 [_keyCommands addObject:[UIKeyCommand keyCommandWithInput:@"]" modifierFlags:UIKeyModifierCommand action:@selector(handleKeyboardShortcut:) discoverabilityTitle:NSLocalizedString(@"Go Forward", nil)]]; 200 201 [_keyCommands addObject:[UIKeyCommand keyCommandWithInput:@"b" modifierFlags:UIKeyModifierCommand action:@selector(handleKeyboardShortcut:) discoverabilityTitle:NSLocalizedString(@"Show Bookmarks", nil)]]; 202 203 [_keyCommands addObject:[UIKeyCommand keyCommandWithInput:@"l" modifierFlags:UIKeyModifierCommand action:@selector(handleKeyboardShortcut:) discoverabilityTitle:NSLocalizedString(@"Focus URL Field", nil)]]; 204 205 [_keyCommands addObject:[UIKeyCommand keyCommandWithInput:@"t" modifierFlags:UIKeyModifierCommand action:@selector(handleKeyboardShortcut:) discoverabilityTitle:NSLocalizedString(@"Create New Tab", nil)]]; 206 [_keyCommands addObject:[UIKeyCommand keyCommandWithInput:@"w" modifierFlags:UIKeyModifierCommand action:@selector(handleKeyboardShortcut:) discoverabilityTitle:NSLocalizedString(@"Close Tab", nil)]]; 207 208 for (int i = 1; i <= 10; i++) 209 [_keyCommands addObject:[UIKeyCommand keyCommandWithInput:[NSString stringWithFormat:@"%d", (i == 10 ? 0 : i)] modifierFlags:UIKeyModifierCommand action:@selector(handleKeyboardShortcut:) discoverabilityTitle:[NSString stringWithFormat:NSLocalizedString(@"Switch to Tab %d", nil), i]]]; 210 } 211 212 if (!_allKeyBindings) { 213 _allKeyBindings = [[NSMutableArray alloc] init]; 214 const long modPermutations[] = { 215 UIKeyModifierAlphaShift, 216 UIKeyModifierShift, 217 UIKeyModifierControl, 218 UIKeyModifierAlternate, 219 UIKeyModifierCommand, 220 UIKeyModifierCommand | UIKeyModifierAlternate, 221 UIKeyModifierCommand | UIKeyModifierControl, 222 UIKeyModifierControl | UIKeyModifierAlternate, 223 UIKeyModifierControl | UIKeyModifierCommand, 224 UIKeyModifierControl | UIKeyModifierAlternate | UIKeyModifierCommand, 225 kNilOptions, 226 }; 227 228 NSString *chars = @"`1234567890-=\b\tqwertyuiop[]\\asdfghjkl;'\rzxcvbnm,./ "; 229 for (int j = 0; j < sizeof(modPermutations); j++) { 230 for (int i = 0; i < [chars length]; i++) { 231 NSString *c = [chars substringWithRange:NSMakeRange(i, 1)]; 232 233 [_allKeyBindings addObject:[UIKeyCommand keyCommandWithInput:c modifierFlags:modPermutations[j] action:@selector(handleKeyboardShortcut:)]]; 234 } 235 236 [_allKeyBindings addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:modPermutations[j] action:@selector(handleKeyboardShortcut:)]]; 237 [_allKeyBindings addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:modPermutations[j] action:@selector(handleKeyboardShortcut:)]]; 238 [_allKeyBindings addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:modPermutations[j] action:@selector(handleKeyboardShortcut:)]]; 239 [_allKeyBindings addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:modPermutations[j] action:@selector(handleKeyboardShortcut:)]]; 240 [_allKeyBindings addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:modPermutations[j] action:@selector(handleKeyboardShortcut:)]]; 241 } 242 243 _allCommandsAndKeyBindings = [_keyCommands arrayByAddingObjectsFromArray:_allKeyBindings]; 244 } 245 246 /* if settings are up or something else, ignore shortcuts */ 247 if (![[self topViewController] isKindOfClass:[WebViewController class]]) 248 return nil; 249 250 id cur = [UIResponder currentFirstResponder]; 251 if (cur == nil || [NSStringFromClass([cur class]) isEqualToString:@"UIWebView"]) 252 return _allCommandsAndKeyBindings; 253 else { 254#ifdef TRACE_KEYBOARD_INPUT 255 NSLog(@"[AppDelegate] current first responder is a %@, only passing shortcuts", NSStringFromClass([cur class])); 256#endif 257 return _keyCommands; 258 } 259} 260 261- (void)handleKeyboardShortcut:(UIKeyCommand *)keyCommand 262{ 263 if ([keyCommand modifierFlags] == UIKeyModifierCommand) { 264 if ([[keyCommand input] isEqualToString:@"b"]) { 265 [[self webViewController] showBookmarksForEditing:NO]; 266 return; 267 } 268 269 if ([[keyCommand input] isEqualToString:@"l"]) { 270 [[self webViewController] focusUrlField]; 271 return; 272 } 273 274 if ([[keyCommand input] isEqualToString:@"t"]) { 275 [[self webViewController] addNewTabForURL:nil forRestoration:NO withAnimation:WebViewTabAnimationDefault withCompletionBlock:^(BOOL finished) { 276 [[self webViewController] focusUrlField]; 277 }]; 278 return; 279 } 280 281 if ([[keyCommand input] isEqualToString:@"w"]) { 282 [[self webViewController] removeTab:[[[self webViewController] curWebViewTab] tabIndex]]; 283 return; 284 } 285 286 if ([[keyCommand input] isEqualToString:@"["]) { 287 [[[self webViewController] curWebViewTab] goBack]; 288 return; 289 } 290 291 if ([[keyCommand input] isEqualToString:@"]"]) { 292 [[[self webViewController] curWebViewTab] goForward]; 293 return; 294 } 295 296 for (int i = 0; i <= 9; i++) { 297 if ([[keyCommand input] isEqualToString:[NSString stringWithFormat:@"%d", i]]) { 298 [[self webViewController] switchToTab:[NSNumber numberWithInt:(i == 0 ? 9 : i - 1)]]; 299 return; 300 } 301 } 302 } 303 304 if ([self webViewController] && [[self webViewController] curWebViewTab]) 305 [[[self webViewController] curWebViewTab] handleKeyCommand:keyCommand]; 306} 307 308- (UIViewController *)topViewController 309{ 310 return [self topViewController:[UIApplication sharedApplication].keyWindow.rootViewController]; 311} 312 313- (UIViewController *)topViewController:(UIViewController *)rootViewController 314{ 315 if (rootViewController.presentedViewController == nil) 316 return rootViewController; 317 318 if ([rootViewController.presentedViewController isMemberOfClass:[UINavigationController class]]) { 319 UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController; 320 UIViewController *lastViewController = [[navigationController viewControllers] lastObject]; 321 return [self topViewController:lastViewController]; 322 } 323 324 UIViewController *presentedViewController = (UIViewController *)rootViewController.presentedViewController; 325 return [self topViewController:presentedViewController]; 326} 327 328- (void)initializeDefaults 329{ 330 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 331 332 NSString *plistPath = [[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"InAppSettings.bundle"] stringByAppendingPathComponent:@"Root.inApp.plist"]; 333 NSDictionary *settingsDictionary = [NSDictionary dictionaryWithContentsOfFile:plistPath]; 334 335 for (NSDictionary *pref in [settingsDictionary objectForKey:@"PreferenceSpecifiers"]) { 336 NSString *key = [pref objectForKey:@"Key"]; 337 if (key == nil) 338 continue; 339 340 if ([userDefaults objectForKey:key] == NULL) { 341 NSObject *val = [pref objectForKey:@"DefaultValue"]; 342 if (val == nil) 343 continue; 344 345 [userDefaults setObject:val forKey:key]; 346#ifdef TRACE 347 NSLog(@"[AppDelegate] initialized default preference for %@ to %@", key, val); 348#endif 349 } 350 } 351 352 if (![userDefaults synchronize]) { 353 NSLog(@"[AppDelegate] failed saving preferences"); 354 abort(); 355 } 356 357 _searchEngines = [NSMutableDictionary dictionaryWithContentsOfFile:[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"SearchEngines.plist"]]; 358} 359 360- (BOOL)areTesting 361{ 362 if (NSClassFromString(@"XCTestProbe") != nil) { 363 NSLog(@"we are testing"); 364 return YES; 365 } 366 else { 367 NSDictionary *environment = [[NSProcessInfo processInfo] environment]; 368 if (environment[@"ARE_UI_TESTING"]) { 369 NSLog(@"we are UI testing"); 370 return YES; 371 } 372 } 373 374 return NO; 375} 376 377- (void)handleShortcut:(UIApplicationShortcutItem *)shortcutItem 378{ 379 if ([[shortcutItem type] containsString:@"OpenNewTab"]) { 380 [[self webViewController] dismissViewControllerAnimated:YES completion:nil]; 381 [[self webViewController] addNewTabFromToolbar:nil]; 382 } else if ([[shortcutItem type] containsString:@"ClearData"]) { 383 [[self webViewController] removeAllTabs]; 384 [[self cookieJar] clearAllNonWhitelistedData]; 385 } else { 386 NSLog(@"[AppDelegate] need to handle action %@", [shortcutItem type]); 387 } 388} 389 390- (void)adjustMuteSwitchBehavior 391{ 392 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 393 394 if ([userDefaults boolForKey:@"mute_with_switch"]) { 395 /* setting AVAudioSessionCategoryAmbient will prevent audio from UIWebView from pausing already-playing audio from other apps */ 396 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; 397 [[AVAudioSession sharedInstance] setActive:NO error:nil]; 398 } else { 399 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; 400 } 401} 402 403@end