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