| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/web_view/shell/shell_view_controller.h" |
| |
| #import <MobileCoreServices/MobileCoreServices.h> |
| #import <UniformTypeIdentifiers/UniformTypeIdentifiers.h> |
| |
| #import "ios/web_view/shell/shell_auth_service.h" |
| #import "ios/web_view/shell/shell_autofill_delegate.h" |
| #import "ios/web_view/shell/shell_translation_delegate.h" |
| #import "ios/web_view/shell/shell_trusted_vault_provider.h" |
| |
| // Externed accessibility identifier. |
| NSString* const kWebViewShellBackButtonAccessibilityLabel = @"Back"; |
| NSString* const kWebViewShellForwardButtonAccessibilityLabel = @"Forward"; |
| NSString* const kWebViewShellAddressFieldAccessibilityLabel = @"Address field"; |
| NSString* const kWebViewShellJavaScriptDialogTextFieldAccessibilityIdentifier = |
| @"WebViewShellJavaScriptDialogTextFieldAccessibilityIdentifier"; |
| |
| @interface ShellViewController () <CWVAutofillDataManagerObserver, |
| CWVDownloadTaskDelegate, |
| CWVLeakCheckServiceObserver, |
| CWVNavigationDelegate, |
| CWVUIDelegate, |
| CWVSyncControllerDelegate, |
| UIScrollViewDelegate, |
| UITextFieldDelegate> |
| // Header containing navigation buttons and |field|. |
| @property(nonatomic, strong) UIView* headerBackgroundView; |
| // Header containing navigation buttons and |field|. |
| @property(nonatomic, strong) UIView* headerContentView; |
| // Button to navigate backwards. |
| @property(nonatomic, strong) UIButton* backButton; |
| // Button to navigate forwards. |
| @property(nonatomic, strong) UIButton* forwardButton; |
| // Button that either refresh the page or stops the page load. |
| @property(nonatomic, strong) UIButton* reloadOrStopButton; |
| // Button that shows the menu |
| @property(nonatomic, strong) UIButton* menuButton; |
| // Text field used for navigating to URLs. |
| @property(nonatomic, strong) UITextField* field; |
| // Container for |webView|. |
| @property(nonatomic, strong) UIView* contentView; |
| // Handles the autofill of the content displayed in |webView|. |
| @property(nonatomic, strong) ShellAutofillDelegate* autofillDelegate; |
| // Handles the translation of the content displayed in |webView|. |
| @property(nonatomic, strong) ShellTranslationDelegate* translationDelegate; |
| // The on-going download task if any. |
| @property(nonatomic, strong, nullable) CWVDownloadTask* downloadTask; |
| // The path to a local file which the download task is writing to. |
| @property(nonatomic, strong, nullable) NSString* downloadFilePath; |
| // A controller to show a "Share" menu for the downloaded file. |
| @property(nonatomic, strong, nullable) |
| UIDocumentInteractionController* documentInteractionController; |
| // Service that provides authentication to ChromeWebView. |
| @property(nonatomic, strong) ShellAuthService* authService; |
| // Provides trusted vault functions to ChromeWebView. |
| @property(nonatomic, strong) ShellTrustedVaultProvider* trustedVaultProvider; |
| // The newly opened popup windows e.g., by JavaScript function "window.open()", |
| // HTML "<a target='_blank'>". |
| @property(nonatomic, strong) NSMutableArray<CWVWebView*>* popupWebViews; |
| // A list of active leak checks. These map to a list of passwords since |
| // you can have multiple passwords that map to the same canonical leak check. |
| @property(nonatomic, strong) |
| NSMutableDictionary<CWVLeakCheckCredential*, NSMutableArray<CWVPassword*>*>* |
| pendingLeakChecks; |
| |
| - (void)back; |
| - (void)forward; |
| - (void)reloadOrStop; |
| // Disconnects and release the |webView|. |
| - (void)removeWebView; |
| // Resets translate settings back to default. |
| - (void)resetTranslateSettings; |
| @end |
| |
| @implementation ShellViewController |
| |
| @synthesize autofillDelegate = _autofillDelegate; |
| @synthesize backButton = _backButton; |
| @synthesize contentView = _contentView; |
| @synthesize field = _field; |
| @synthesize forwardButton = _forwardButton; |
| @synthesize reloadOrStopButton = _reloadOrStopButton; |
| @synthesize menuButton = _menuButton; |
| @synthesize headerBackgroundView = _headerBackgroundView; |
| @synthesize headerContentView = _headerContentView; |
| @synthesize webView = _webView; |
| @synthesize translationDelegate = _translationDelegate; |
| @synthesize downloadTask = _downloadTask; |
| @synthesize downloadFilePath = _downloadFilePath; |
| @synthesize documentInteractionController = _documentInteractionController; |
| @synthesize authService = _authService; |
| @synthesize trustedVaultProvider = _trustedVaultProvider; |
| @synthesize popupWebViews = _popupWebViews; |
| @synthesize pendingLeakChecks = _pendingLeakChecks; |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| |
| self.popupWebViews = [[NSMutableArray alloc] init]; |
| |
| // View creation. |
| self.headerBackgroundView = [[UIView alloc] init]; |
| self.headerContentView = [[UIView alloc] init]; |
| self.contentView = [[UIView alloc] init]; |
| self.backButton = [[UIButton alloc] init]; |
| self.forwardButton = [[UIButton alloc] init]; |
| self.reloadOrStopButton = [[UIButton alloc] init]; |
| self.menuButton = [[UIButton alloc] init]; |
| self.field = [[UITextField alloc] init]; |
| |
| // View hierarchy. |
| [self.view addSubview:_headerBackgroundView]; |
| [self.view addSubview:_contentView]; |
| [_headerBackgroundView addSubview:_headerContentView]; |
| [_headerContentView addSubview:_backButton]; |
| [_headerContentView addSubview:_forwardButton]; |
| [_headerContentView addSubview:_reloadOrStopButton]; |
| [_headerContentView addSubview:_menuButton]; |
| [_headerContentView addSubview:_field]; |
| |
| // Additional view setup. |
| _headerBackgroundView.backgroundColor = [UIColor colorWithRed:66.0 / 255.0 |
| green:133.0 / 255.0 |
| blue:244.0 / 255.0 |
| alpha:1.0]; |
| |
| [_backButton setImage:[UIImage imageNamed:@"ic_back"] |
| forState:UIControlStateNormal]; |
| _backButton.tintColor = [UIColor whiteColor]; |
| [_backButton addTarget:self |
| action:@selector(back) |
| forControlEvents:UIControlEventTouchUpInside]; |
| [_backButton addTarget:self |
| action:@selector(logBackStack) |
| forControlEvents:UIControlEventTouchDragOutside]; |
| [_backButton setAccessibilityLabel:kWebViewShellBackButtonAccessibilityLabel]; |
| |
| [_forwardButton setImage:[UIImage imageNamed:@"ic_forward"] |
| forState:UIControlStateNormal]; |
| _forwardButton.tintColor = [UIColor whiteColor]; |
| [_forwardButton addTarget:self |
| action:@selector(forward) |
| forControlEvents:UIControlEventTouchUpInside]; |
| [_forwardButton addTarget:self |
| action:@selector(logForwardStack) |
| forControlEvents:UIControlEventTouchDragOutside]; |
| [_forwardButton |
| setAccessibilityLabel:kWebViewShellForwardButtonAccessibilityLabel]; |
| |
| _reloadOrStopButton.tintColor = [UIColor whiteColor]; |
| [_reloadOrStopButton addTarget:self |
| action:@selector(reloadOrStop) |
| forControlEvents:UIControlEventTouchUpInside]; |
| |
| _menuButton.tintColor = [UIColor whiteColor]; |
| [_menuButton setImage:[UIImage imageNamed:@"ic_menu"] |
| forState:UIControlStateNormal]; |
| [_menuButton addTarget:self |
| action:@selector(showMainMenu) |
| forControlEvents:UIControlEventTouchUpInside]; |
| |
| _field.placeholder = @"Search or type URL"; |
| _field.backgroundColor = [UIColor whiteColor]; |
| _field.tintColor = _headerBackgroundView.backgroundColor; |
| [_field setContentHuggingPriority:UILayoutPriorityDefaultLow - 1 |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| _field.delegate = self; |
| _field.layer.cornerRadius = 2.0; |
| _field.keyboardType = UIKeyboardTypeWebSearch; |
| _field.autocapitalizationType = UITextAutocapitalizationTypeNone; |
| _field.clearButtonMode = UITextFieldViewModeWhileEditing; |
| _field.autocorrectionType = UITextAutocorrectionTypeNo; |
| UIView* spacerView = [[UIView alloc] init]; |
| spacerView.frame = CGRectMake(0, 0, 8, 8); |
| _field.leftViewMode = UITextFieldViewModeAlways; |
| _field.leftView = spacerView; |
| |
| // Constraints. |
| _headerBackgroundView.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [_headerBackgroundView.topAnchor |
| constraintEqualToAnchor:self.view.topAnchor], |
| [_headerBackgroundView.leadingAnchor |
| constraintEqualToAnchor:self.view.leadingAnchor], |
| [_headerBackgroundView.trailingAnchor |
| constraintEqualToAnchor:self.view.trailingAnchor], |
| [_headerBackgroundView.bottomAnchor |
| constraintEqualToAnchor:_headerContentView.bottomAnchor], |
| ]]; |
| |
| _headerContentView.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [_headerContentView.topAnchor |
| constraintEqualToAnchor:_headerBackgroundView.safeAreaLayoutGuide |
| .topAnchor], |
| [_headerContentView.leadingAnchor |
| constraintEqualToAnchor:_headerBackgroundView.safeAreaLayoutGuide |
| .leadingAnchor], |
| [_headerContentView.trailingAnchor |
| constraintEqualToAnchor:_headerBackgroundView.safeAreaLayoutGuide |
| .trailingAnchor], |
| [_headerContentView.heightAnchor constraintEqualToConstant:56.0], |
| ]]; |
| |
| _contentView.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [_contentView.topAnchor |
| constraintEqualToAnchor:_headerBackgroundView.bottomAnchor], |
| [_contentView.leadingAnchor |
| constraintEqualToAnchor:self.view.leadingAnchor], |
| [_contentView.trailingAnchor |
| constraintEqualToAnchor:self.view.trailingAnchor], |
| [_contentView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], |
| ]]; |
| |
| _backButton.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [_backButton.leadingAnchor |
| constraintEqualToAnchor:_headerContentView.safeAreaLayoutGuide |
| .leadingAnchor |
| constant:16.0], |
| [_backButton.centerYAnchor |
| constraintEqualToAnchor:_headerContentView.centerYAnchor], |
| ]]; |
| |
| _forwardButton.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [_forwardButton.leadingAnchor |
| constraintEqualToAnchor:_backButton.trailingAnchor |
| constant:16.0], |
| [_forwardButton.centerYAnchor |
| constraintEqualToAnchor:_headerContentView.centerYAnchor], |
| ]]; |
| |
| _reloadOrStopButton.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [_reloadOrStopButton.leadingAnchor |
| constraintEqualToAnchor:_forwardButton.trailingAnchor |
| constant:16.0], |
| [_reloadOrStopButton.centerYAnchor |
| constraintEqualToAnchor:_headerContentView.centerYAnchor], |
| ]]; |
| _menuButton.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [_menuButton.leadingAnchor |
| constraintEqualToAnchor:_reloadOrStopButton.trailingAnchor |
| constant:16.0], |
| [_menuButton.centerYAnchor |
| constraintEqualToAnchor:_headerContentView.centerYAnchor], |
| ]]; |
| |
| _field.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [_field.leadingAnchor constraintEqualToAnchor:_menuButton.trailingAnchor |
| constant:16.0], |
| [_field.centerYAnchor |
| constraintEqualToAnchor:_headerContentView.centerYAnchor], |
| [_field.trailingAnchor |
| constraintEqualToAnchor:_headerContentView.safeAreaLayoutGuide |
| .trailingAnchor |
| constant:-16.0], |
| [_field.heightAnchor constraintEqualToConstant:32.0], |
| ]]; |
| |
| [CWVWebView setUserAgentProduct:@"Dummy/1.0"]; |
| CWVWebView.chromeContextMenuEnabled = YES; |
| |
| CWVWebView.webInspectorEnabled = YES; |
| |
| _authService = [[ShellAuthService alloc] init]; |
| CWVSyncController.dataSource = _authService; |
| |
| _trustedVaultProvider = |
| [[ShellTrustedVaultProvider alloc] initWithAuthService:_authService]; |
| CWVSyncController.trustedVaultProvider = _trustedVaultProvider; |
| |
| _pendingLeakChecks = [NSMutableDictionary dictionary]; |
| |
| CWVWebViewConfiguration* configuration = |
| [CWVWebViewConfiguration defaultConfiguration]; |
| [configuration.autofillDataManager addObserver:self]; |
| configuration.syncController.delegate = self; |
| [configuration.leakCheckService addObserver:self]; |
| [configuration.userContentController |
| addMessageHandler:^(NSDictionary* payload) { |
| NSLog(@"message handler payload received =\n%@", payload); |
| } |
| forCommand:@"messageHandlerCommand"]; |
| self.webView = [self createWebViewWithConfiguration:configuration]; |
| } |
| |
| - (void)applicationFinishedRestoringState { |
| [super applicationFinishedRestoringState]; |
| |
| // The scroll view is reset on state restoration. So the delegate must be |
| // reassigned. |
| self.webView.scrollView.delegate = self; |
| } |
| |
| - (UIStatusBarStyle)preferredStatusBarStyle { |
| return UIStatusBarStyleLightContent; |
| } |
| |
| - (void)observeValueForKeyPath:(NSString*)keyPath |
| ofObject:(id)object |
| change:(NSDictionary<NSKeyValueChangeKey, id>*)change |
| context:(void*)context { |
| if ([keyPath isEqualToString:@"canGoBack"]) { |
| _backButton.enabled = [_webView canGoBack]; |
| } else if ([keyPath isEqualToString:@"canGoForward"]) { |
| _forwardButton.enabled = [_webView canGoForward]; |
| } else if ([keyPath isEqualToString:@"loading"]) { |
| NSString* imageName = _webView.loading ? @"ic_stop" : @"ic_reload"; |
| [_reloadOrStopButton setImage:[UIImage imageNamed:imageName] |
| forState:UIControlStateNormal]; |
| } |
| } |
| |
| - (void)back { |
| if ([_webView canGoBack]) { |
| [_webView goBack]; |
| } |
| } |
| |
| - (void)logBackStack { |
| if (!_webView.canGoBack) { |
| return; |
| } |
| CWVBackForwardList* list = _webView.backForwardList; |
| CWVBackForwardListItemArray* backList = list.backList; |
| for (size_t i = 0; i < backList.count; i++) { |
| CWVBackForwardListItem* item = backList[i]; |
| NSLog(@"BackStack Item #%ld: <URL='%@', title='%@'>", i, item.URL, |
| item.title); |
| } |
| } |
| |
| - (void)forward { |
| if ([_webView canGoForward]) { |
| [_webView goForward]; |
| } |
| } |
| |
| - (void)logForwardStack { |
| if (!_webView.canGoForward) { |
| return; |
| } |
| CWVBackForwardList* list = _webView.backForwardList; |
| CWVBackForwardListItemArray* forwardList = list.forwardList; |
| for (size_t i = 0; i < forwardList.count; i++) { |
| CWVBackForwardListItem* item = forwardList[i]; |
| NSLog(@"ForwardStack Item #%ld: <URL='%@', title='%@'>", i, item.URL, |
| item.title); |
| } |
| } |
| |
| - (void)reloadOrStop { |
| if (_webView.loading) { |
| [_webView stopLoading]; |
| } else { |
| [_webView reload]; |
| } |
| } |
| |
| - (void)showAddressData { |
| CWVAutofillDataManager* dataManager = |
| _webView.configuration.autofillDataManager; |
| [dataManager fetchProfilesWithCompletionHandler:^( |
| NSArray<CWVAutofillProfile*>* _Nonnull profiles) { |
| NSMutableArray<NSString*>* descriptions = [profiles |
| valueForKey:NSStringFromSelector(@selector(debugDescription))]; |
| NSString* message = [descriptions componentsJoinedByString:@"\n\n"]; |
| UIAlertController* alertController = [self actionSheetWithTitle:@"Addresses" |
| message:message]; |
| for (CWVAutofillProfile* profile in profiles) { |
| NSString* title = [NSString |
| stringWithFormat:@"Delete %@", @([profiles indexOfObject:profile])]; |
| UIAlertAction* action = |
| [UIAlertAction actionWithTitle:title |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* theAction) { |
| [dataManager deleteProfile:profile]; |
| }]; |
| [alertController addAction:action]; |
| } |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Done" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| |
| [self presentViewController:alertController animated:YES completion:nil]; |
| }]; |
| } |
| |
| - (void)showCreditCardData { |
| CWVAutofillDataManager* dataManager = |
| _webView.configuration.autofillDataManager; |
| [dataManager fetchCreditCardsWithCompletionHandler:^( |
| NSArray<CWVCreditCard*>* _Nonnull creditCards) { |
| NSMutableArray<NSString*>* descriptions = [creditCards |
| valueForKey:NSStringFromSelector(@selector(debugDescription))]; |
| NSString* message = [descriptions componentsJoinedByString:@"\n\n"]; |
| UIAlertController* alertController = |
| [self actionSheetWithTitle:@"Credit cards" message:message]; |
| __weak ShellViewController* weakSelf = self; |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"Manage Google pay cards" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| __weak ShellViewController* strongSelf = |
| weakSelf; |
| NSString* URL; |
| if ([CWVFlags sharedInstance] |
| .usesSyncAndWalletSandbox) { |
| URL = @"https://pay.sandbox.google.com/" |
| @"payments/home#paymentMethods"; |
| } else { |
| URL = @"https://pay.google.com/payments/" |
| @"home#paymentMethods"; |
| } |
| NSURLRequest* request = [NSURLRequest |
| requestWithURL:[NSURL URLWithString:URL]]; |
| [strongSelf.webView loadRequest:request]; |
| }]]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Done" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| }]; |
| } |
| |
| - (void)showPasswordData { |
| __weak ShellViewController* weakSelf = self; |
| CWVAutofillDataManager* dataManager = |
| _webView.configuration.autofillDataManager; |
| [dataManager fetchPasswordsWithCompletionHandler:^( |
| NSArray<CWVPassword*>* _Nonnull passwords) { |
| NSMutableArray<NSString*>* descriptions = [NSMutableArray array]; |
| for (CWVPassword* password in passwords) { |
| NSString* description = [NSString |
| stringWithFormat:@"%@:\n%@", @([passwords indexOfObject:password]), |
| password.debugDescription]; |
| [descriptions addObject:description]; |
| } |
| NSString* message = [descriptions componentsJoinedByString:@"\n\n"]; |
| |
| UIAlertController* alertController = [self actionSheetWithTitle:@"Passwords" |
| message:message]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Add new" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf showAddNewPasswordDialog]; |
| }]]; |
| |
| for (CWVPassword* password in passwords) { |
| NSString* title = [NSString |
| stringWithFormat:@"Select %@", @([passwords indexOfObject:password])]; |
| UIAlertAction* action = |
| [UIAlertAction actionWithTitle:title |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* theAction) { |
| [weakSelf showMenuForPassword:password]; |
| }]; |
| [alertController addAction:action]; |
| } |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Done" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| }]; |
| } |
| |
| - (void)showAddNewPasswordDialog { |
| CWVAutofillDataManager* dataManager = |
| _webView.configuration.autofillDataManager; |
| UIAlertController* alertController = |
| [UIAlertController alertControllerWithTitle:@"Add password" |
| message:nil |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| [alertController |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = @"Username"; |
| }]; |
| [alertController |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = @"Password"; |
| }]; |
| [alertController |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = @"Site"; |
| }]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| __weak UIAlertController* weakAlertController = alertController; |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"Done" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| NSString* username = |
| weakAlertController.textFields[0].text; |
| NSString* password = |
| weakAlertController.textFields[1].text; |
| NSString* site = |
| weakAlertController.textFields[2].text; |
| [dataManager addNewPasswordForUsername:username |
| password:password |
| site:site]; |
| }]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)showMenuForPassword:(CWVPassword*)password { |
| UIAlertController* alertController = |
| [self actionSheetWithTitle:password.title |
| message:password.debugDescription]; |
| CWVAutofillDataManager* dataManager = |
| _webView.configuration.autofillDataManager; |
| |
| __weak ShellViewController* weakSelf = self; |
| UIAlertAction* update = |
| [UIAlertAction actionWithTitle:@"Update" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* theAction) { |
| [weakSelf showUpdateDialogForPassword:password]; |
| }]; |
| [alertController addAction:update]; |
| |
| UIAlertAction* delete = |
| [UIAlertAction actionWithTitle:@"Delete" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* theAction) { |
| [dataManager deletePassword:password]; |
| }]; |
| [alertController addAction:delete]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Done" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)showUpdateDialogForPassword:(CWVPassword*)password { |
| CWVAutofillDataManager* dataManager = |
| _webView.configuration.autofillDataManager; |
| UIAlertController* alertController = |
| [UIAlertController alertControllerWithTitle:password.title |
| message:password.debugDescription |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| [alertController |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = @"New username"; |
| }]; |
| [alertController |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = @"New password"; |
| }]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| __weak UIAlertController* weakAlertController = alertController; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Done" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| NSString* newUsername = |
| weakAlertController.textFields |
| .firstObject.text; |
| NSString* newPassword = |
| weakAlertController.textFields |
| .lastObject.text; |
| [dataManager |
| updatePassword:password |
| newUsername:newUsername |
| newPassword:newPassword]; |
| }]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)showSyncMenu { |
| CWVSyncController* syncController = _webView.configuration.syncController; |
| |
| NSString* message = [NSString |
| stringWithFormat:@"Passphrase required: %@\nTrusted vault keys required: " |
| @"%@\nTrusted vault recoverability degraded: %@", |
| @(syncController.passphraseNeeded), |
| @(syncController.trustedVaultKeysRequired), |
| @(syncController.trustedVaultRecoverabilityDegraded)]; |
| UIAlertController* alertController = [self actionSheetWithTitle:@"Sync menu" |
| message:message]; |
| |
| CWVIdentity* currentIdentity = syncController.currentIdentity; |
| __weak ShellViewController* weakSelf = self; |
| if (currentIdentity) { |
| NSString* title = [NSString |
| stringWithFormat:@"Stop syncing for %@", currentIdentity.email]; |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:title |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [syncController stopSyncAndClearIdentity]; |
| }]]; |
| |
| if (syncController.passphraseNeeded) { |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"Unlock using passphrase" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf showPassphraseUnlockAlert]; |
| }]]; |
| } else if (syncController.trustedVaultKeysRequired) { |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"Fetch trusted vault keys" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf.trustedVaultProvider |
| showFetchKeysFlowForIdentity: |
| currentIdentity |
| fromViewController:weakSelf]; |
| }]]; |
| } else if (syncController.trustedVaultRecoverabilityDegraded) { |
| [alertController |
| addAction: |
| [UIAlertAction |
| actionWithTitle:@"Fix degraded recoverability" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf.trustedVaultProvider |
| showFixDegradedRecoverabilityFlowForIdentity: |
| currentIdentity |
| fromViewController: |
| weakSelf]; |
| }]]; |
| } |
| } else { |
| for (CWVIdentity* identity in [_authService identities]) { |
| NSString* title = |
| [NSString stringWithFormat:@"Start sync with %@", identity.email]; |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:title |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [syncController |
| startSyncWithIdentity:identity]; |
| }]]; |
| } |
| |
| NSString* sandboxTitle = [CWVFlags sharedInstance].usesSyncAndWalletSandbox |
| ? @"Use production sync/wallet" |
| : @"Use sandbox sync/wallet"; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:sandboxTitle |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [CWVFlags sharedInstance] |
| .usesSyncAndWalletSandbox ^= YES; |
| }]]; |
| } |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Show autofill data" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf showAddressData]; |
| }]]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Show credit card data" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf showCreditCardData]; |
| }]]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Show password data" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf showPasswordData]; |
| }]]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Check leaked passwords" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf checkLeakedPasswords]; |
| }]]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Check weak passwords" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf checkWeakPasswords]; |
| }]]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Check reused passwords" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf checkReusedPasswords]; |
| }]]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)showPassphraseUnlockAlert { |
| UIAlertController* alertController = |
| [UIAlertController alertControllerWithTitle:@"Unlock sync" |
| message:@"Enter passphrase" |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| __weak UIAlertController* weakAlertController = alertController; |
| CWVSyncController* syncController = _webView.configuration.syncController; |
| UIAlertAction* submit = [UIAlertAction |
| actionWithTitle:@"Unlock" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| UITextField* textField = |
| weakAlertController.textFields.firstObject; |
| NSString* passphrase = textField.text; |
| BOOL result = [syncController unlockWithPassphrase:passphrase]; |
| NSLog(@"Sync passphrase unlock result: %d", result); |
| }]; |
| |
| [alertController addAction:submit]; |
| |
| UIAlertAction* cancel = |
| [UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]; |
| [alertController addAction:cancel]; |
| |
| [alertController |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = @"passphrase"; |
| textField.keyboardType = UIKeyboardTypeDefault; |
| }]; |
| |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)showMainMenu { |
| UIAlertController* alertController = [self actionSheetWithTitle:@"Main menu" |
| message:nil]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| |
| __weak ShellViewController* weakSelf = self; |
| |
| // Toggles the incognito mode. |
| NSString* incognitoActionTitle = _webView.configuration.persistent |
| ? @"Enter incognito" |
| : @"Exit incognito"; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:incognitoActionTitle |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf toggleIncognito]; |
| }]]; |
| |
| // Removes the web view from the view hierarchy, releases it, and recreates |
| // the web view with the same configuration. This is for testing deallocation |
| // and sharing configuration. |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"Recreate web view" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| CWVWebViewConfiguration* configuration = |
| weakSelf.webView.configuration; |
| [weakSelf removeWebView]; |
| weakSelf.webView = [weakSelf |
| createWebViewWithConfiguration:configuration]; |
| }]]; |
| |
| // Developers can choose to use system or Chrome context menu in the shell |
| // app. This will also recreate the web view. |
| BOOL chromeContextMenuEnabled = CWVWebView.chromeContextMenuEnabled; |
| NSString* contextMenuSwitchActionTitle = [NSString |
| stringWithFormat:@"%@ Chrome context menu", |
| chromeContextMenuEnabled ? @"Disable" : @"Enable"]; |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:contextMenuSwitchActionTitle |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| CWVWebView.chromeContextMenuEnabled = |
| !chromeContextMenuEnabled; |
| NSLog(@"Chrome context menu is %@ now.", |
| !chromeContextMenuEnabled ? @"OFF" : @"ON"); |
| CWVWebViewConfiguration* configuration = |
| weakSelf.webView.configuration; |
| [weakSelf removeWebView]; |
| weakSelf.field.text = @""; |
| weakSelf.webView = [weakSelf |
| createWebViewWithConfiguration:configuration]; |
| }]]; |
| |
| // Resets all translation settings to default values. |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Reset translate settings" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf resetTranslateSettings]; |
| }]]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Request translation offer" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf requestTranslationOffer]; |
| }]]; |
| |
| // Shows sync menu. |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Sync menu" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf showSyncMenu]; |
| }]]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Show Certificate Details" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf showCertificateDetails]; |
| }]]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Evaluate JavaScript" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf showEvaluateJavaScriptUI]; |
| }]]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"User Scripts" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf showUserScriptsUI]; |
| }]]; |
| |
| if (self.downloadTask) { |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel download" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf.downloadTask cancel]; |
| }]]; |
| } |
| |
| // Using native FiP through is only available for 16.0, otherwise fallback |
| // to iGA JS FiP. |
| if (@available(iOS 16.0, *)) { |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Start Find In Page" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf startFindInPageSession]; |
| }]]; |
| } |
| |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)showCertificateDetails { |
| CWVX509Certificate* certificate = [[_webView visibleSSLStatus] certificate]; |
| NSString* message; |
| |
| if (certificate) { |
| message = [NSString stringWithFormat:@"Issuer: %@\nExpires: %@", |
| certificate.issuerDisplayName, |
| certificate.validExpiry]; |
| } else { |
| message = @"No Certificate"; |
| } |
| |
| UIAlertController* alertController = |
| [self actionSheetWithTitle:@"Certificate Details" message:message]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Done" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)checkLeakedPasswords { |
| CWVAutofillDataManager* dataManager = |
| _webView.configuration.autofillDataManager; |
| |
| // Request a check for any password in autofill data manager that is not |
| // currently being requested. |
| [dataManager fetchPasswordsWithCompletionHandler:^( |
| NSArray<CWVPassword*>* _Nonnull passwords) { |
| NSMutableArray<CWVLeakCheckCredential*>* credentialsToCheck = |
| [NSMutableArray array]; |
| for (CWVPassword* password in passwords) { |
| CWVLeakCheckCredential* credential = [CWVLeakCheckCredential |
| canonicalLeakCheckCredentialWithPassword:password]; |
| NSMutableArray<CWVPassword*>* passwordsForCredential = |
| self.pendingLeakChecks[credential]; |
| if (!passwordsForCredential) { |
| passwordsForCredential = [NSMutableArray array]; |
| self.pendingLeakChecks[credential] = passwordsForCredential; |
| [credentialsToCheck addObject:credential]; |
| } |
| [passwordsForCredential addObject:password]; |
| } |
| |
| NSLog(@"Checking leaks for %@ credentials.", @(credentialsToCheck.count)); |
| [self.webView.configuration.leakCheckService |
| checkCredentials:credentialsToCheck]; |
| }]; |
| } |
| |
| - (void)checkWeakPasswords { |
| CWVAutofillDataManager* dataManager = |
| _webView.configuration.autofillDataManager; |
| [dataManager fetchPasswordsWithCompletionHandler:^( |
| NSArray<CWVPassword*>* _Nonnull passwords) { |
| NSLog(@"Checking weak status of %@ passwords.", @(passwords.count)); |
| for (CWVPassword* password in passwords) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| BOOL isWeak = [CWVWeakCheckUtils isPasswordWeak:password.password]; |
| NSLog(@"Weak password status for %@ at %@ is %s", password.username, |
| password.site, isWeak ? "true" : "false"); |
| }); |
| } |
| }]; |
| } |
| |
| - (void)checkReusedPasswords { |
| CWVAutofillDataManager* dataManager = |
| _webView.configuration.autofillDataManager; |
| [dataManager fetchPasswordsWithCompletionHandler:^( |
| NSArray<CWVPassword*>* _Nonnull passwords) { |
| NSLog(@"Checking reuse status of %@ passwords.", @(passwords.count)); |
| [self.webView.configuration.reuseCheckService |
| checkReusedPasswords:passwords |
| completionHandler:^(NSSet<NSString*>* reusedPasswords) { |
| int useCount = 0; |
| for (CWVPassword* password in passwords) { |
| if ([reusedPasswords containsObject:password.password]) { |
| useCount++; |
| } |
| } |
| NSLog(@"%@ reused password(s).", @(reusedPasswords.count)); |
| NSLog(@"Used %d times.", useCount); |
| |
| UIAlertController* alertController = [UIAlertController |
| alertControllerWithTitle:@"Reuse Check Complete" |
| message:[NSString |
| stringWithFormat: |
| @"%@ reused password(s). " |
| @"Used %d times.", |
| @(reusedPasswords.count), |
| useCount] |
| preferredStyle:UIAlertControllerStyleAlert]; |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"OK" |
| style:UIAlertActionStyleDefault |
| handler:nil]]; |
| [self presentViewController:alertController |
| animated:YES |
| completion:nil]; |
| }]; |
| }]; |
| } |
| |
| - (void)showEvaluateJavaScriptUI { |
| UIAlertController* alertController = |
| [self alertControllerWithTitle:@"Evaluate JavaScript" |
| message:nil |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| [alertController |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = @"alert('Hello')"; |
| }]; |
| |
| __weak UIAlertController* weakAlertController = alertController; |
| __weak ShellViewController* weakSelf = self; |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"Evaluate" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| NSString* javascript = |
| weakAlertController.textFields[0].text; |
| [weakSelf evaluateJavaScript:javascript]; |
| }]]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)evaluateJavaScript:(NSString*)javascript { |
| [self.webView |
| evaluateJavaScript:javascript |
| completion:^(id result, NSError* error) { |
| if (error) { |
| NSLog( |
| @"JavaScript evaluation FAILED with error: %@ result: %@", |
| error, result); |
| } else { |
| NSLog(@"JavaScript evaluation finished with result: %@", |
| result); |
| } |
| }]; |
| } |
| |
| - (void)showUserScriptsUI { |
| UIAlertController* alertController = |
| [self alertControllerWithTitle:@"Add or Remove User Scripts" |
| message:@"This will also recreate the web view." |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| [alertController |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = @"All frames script"; |
| }]; |
| |
| [alertController |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = @"Main frame script"; |
| }]; |
| |
| __weak UIAlertController* weakAlertController = alertController; |
| __weak ShellViewController* weakSelf = self; |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"Add" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| NSString* allFramesSource = |
| weakAlertController.textFields[0].text; |
| NSString* mainFrameSource = |
| weakAlertController.textFields[1].text; |
| [weakSelf |
| addUserScriptForAllFrames:allFramesSource |
| forMainFrameOnly:mainFrameSource]; |
| }]]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Remove All" |
| style:UIAlertActionStyleDestructive |
| handler:^(UIAlertAction* action) { |
| [weakSelf removeAllUserScripts]; |
| }]]; |
| |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)addUserScriptForAllFrames:(nullable NSString*)allFramesSource |
| forMainFrameOnly:(nullable NSString*)mainFrameSource { |
| CWVWebViewConfiguration* configuration = self.webView.configuration; |
| [self removeWebView]; |
| if (allFramesSource.length) { |
| CWVUserScript* allFramesScript = |
| [[CWVUserScript alloc] initWithSource:allFramesSource |
| forMainFrameOnly:NO]; |
| [configuration.userContentController addUserScript:allFramesScript]; |
| } |
| if (mainFrameSource.length) { |
| CWVUserScript* mainFrameScript = |
| [[CWVUserScript alloc] initWithSource:mainFrameSource |
| forMainFrameOnly:YES]; |
| [configuration.userContentController addUserScript:mainFrameScript]; |
| } |
| self.webView = [self createWebViewWithConfiguration:configuration]; |
| } |
| |
| - (void)removeAllUserScripts { |
| CWVWebViewConfiguration* configuration = self.webView.configuration; |
| [self removeWebView]; |
| [configuration.userContentController removeAllUserScripts]; |
| self.webView = [self createWebViewWithConfiguration:configuration]; |
| } |
| |
| - (void)resetTranslateSettings { |
| CWVWebViewConfiguration* configuration = |
| [CWVWebViewConfiguration defaultConfiguration]; |
| [configuration.preferences resetTranslationSettings]; |
| } |
| |
| - (void)requestTranslationOffer { |
| BOOL offered = [_webView.translationController requestTranslationOffer]; |
| NSLog(@"Manual translation was offered: %d", offered); |
| } |
| |
| - (void)toggleIncognito { |
| BOOL wasPersistent = _webView.configuration.persistent; |
| [self removeWebView]; |
| CWVWebViewConfiguration* newConfiguration = |
| wasPersistent ? [CWVWebViewConfiguration nonPersistentConfiguration] |
| : [CWVWebViewConfiguration defaultConfiguration]; |
| self.webView = [self createWebViewWithConfiguration:newConfiguration]; |
| } |
| |
| - (void)startFindInPageSession { |
| // Using native FiP through is only available for 16.0, otherwise fallback |
| // to iGA JS FiP. |
| if (@available(iOS 16.0, *)) { |
| if ([_webView.findInPageController canFindInPage]) { |
| [_webView.findInPageController startFindInPage]; |
| } |
| } |
| } |
| |
| - (CWVWebView*)createWebViewWithConfiguration: |
| (CWVWebViewConfiguration*)configuration { |
| // Set a non empty CGRect to avoid DCHECKs that occur when a load happens |
| // after state restoration, and before the view hierarchy is laid out for the |
| // first time. |
| // https://source.chromium.org/chromium/chromium/src/+/main:ios/web/web_state/ui/crw_web_request_controller.mm;l=518;drc=df887034106ef438611326745a7cd276eedd4953 |
| CGRect frame = CGRectMake(0, 0, 1, 1); |
| CWVWebView* webView = [[CWVWebView alloc] initWithFrame:frame |
| configuration:configuration]; |
| [_contentView addSubview:webView]; |
| |
| // Gives a restoration identifier so that state restoration works. |
| webView.restorationIdentifier = @"webView"; |
| |
| // Configure delegates. |
| webView.navigationDelegate = self; |
| webView.UIDelegate = self; |
| _translationDelegate = [[ShellTranslationDelegate alloc] init]; |
| webView.translationController.delegate = _translationDelegate; |
| _autofillDelegate = [[ShellAutofillDelegate alloc] init]; |
| webView.autofillController.delegate = _autofillDelegate; |
| webView.scrollView.delegate = self; |
| |
| // Constraints. |
| webView.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [webView.topAnchor |
| constraintEqualToAnchor:_contentView.safeAreaLayoutGuide.topAnchor], |
| [webView.leadingAnchor |
| constraintEqualToAnchor:_contentView.safeAreaLayoutGuide.leadingAnchor], |
| [webView.trailingAnchor |
| constraintEqualToAnchor:_contentView.safeAreaLayoutGuide |
| .trailingAnchor], |
| [webView.bottomAnchor |
| constraintEqualToAnchor:_contentView.safeAreaLayoutGuide.bottomAnchor], |
| ]]; |
| |
| [webView addObserver:self |
| forKeyPath:@"canGoBack" |
| options:NSKeyValueObservingOptionNew | |
| NSKeyValueObservingOptionInitial |
| context:nil]; |
| [webView addObserver:self |
| forKeyPath:@"canGoForward" |
| options:NSKeyValueObservingOptionNew | |
| NSKeyValueObservingOptionInitial |
| context:nil]; |
| [webView addObserver:self |
| forKeyPath:@"loading" |
| options:NSKeyValueObservingOptionNew | |
| NSKeyValueObservingOptionInitial |
| context:nil]; |
| |
| [webView |
| addMessageHandler:^(NSDictionary* payload) { |
| NSLog(@"webview message handler payload received =\n%@", payload); |
| } |
| forCommand:@"webViewMessageHandlerCommand"]; |
| |
| return webView; |
| } |
| |
| - (void)removeWebView { |
| [_webView removeFromSuperview]; |
| [_webView removeObserver:self forKeyPath:@"canGoBack"]; |
| [_webView removeObserver:self forKeyPath:@"canGoForward"]; |
| [_webView removeObserver:self forKeyPath:@"loading"]; |
| [_webView removeMessageHandlerForCommand:@"webViewMessageHandlerCommand"]; |
| |
| _webView = nil; |
| } |
| |
| - (void)dealloc { |
| [_webView removeObserver:self forKeyPath:@"canGoBack"]; |
| [_webView removeObserver:self forKeyPath:@"canGoForward"]; |
| [_webView removeObserver:self forKeyPath:@"loading"]; |
| [_webView removeMessageHandlerForCommand:@"webViewMessageHandlerCommand"]; |
| } |
| |
| - (BOOL)textFieldShouldReturn:(UITextField*)field { |
| NSURL* URL = [NSURL URLWithString:field.text]; |
| if (URL.scheme.length == 0) { |
| NSString* enteredText = field.text; |
| enteredText = |
| [enteredText stringByAddingPercentEncodingWithAllowedCharacters: |
| [NSCharacterSet URLQueryAllowedCharacterSet]]; |
| enteredText = [NSString |
| stringWithFormat:@"https://www.google.com/search?q=%@", enteredText]; |
| URL = [NSURL URLWithString:enteredText]; |
| } |
| NSURLRequest* request = [NSURLRequest requestWithURL:URL]; |
| [_webView loadRequest:request]; |
| [field resignFirstResponder]; |
| [self updateToolbar]; |
| return YES; |
| } |
| |
| - (void)updateToolbar { |
| // Do not update the URL if the text field is currently being edited. |
| if ([_field isFirstResponder]) { |
| return; |
| } |
| |
| [_field setText:[[_webView visibleURL] absoluteString]]; |
| } |
| |
| - (UIAlertController*)actionSheetWithTitle:(nullable NSString*)title |
| message:(nullable NSString*)message { |
| return [self alertControllerWithTitle:title |
| message:message |
| preferredStyle:UIAlertControllerStyleActionSheet]; |
| } |
| |
| - (UIAlertController*)alertControllerWithTitle:(nullable NSString*)title |
| message:(nullable NSString*)message |
| preferredStyle: |
| (UIAlertControllerStyle)preferredStyle { |
| UIAlertController* alertController = |
| [UIAlertController alertControllerWithTitle:title |
| message:message |
| preferredStyle:preferredStyle]; |
| alertController.popoverPresentationController.sourceView = _menuButton; |
| alertController.popoverPresentationController.sourceRect = |
| CGRectMake(CGRectGetWidth(_menuButton.bounds) / 2, |
| CGRectGetHeight(_menuButton.bounds), 1, 1); |
| return alertController; |
| } |
| |
| - (void)closePopupWebView { |
| if (self.popupWebViews.count) { |
| [self.popupWebViews.lastObject removeFromSuperview]; |
| [self.popupWebViews removeLastObject]; |
| } |
| } |
| |
| #pragma mark CWVUIDelegate methods |
| |
| - (CWVWebView*)webView:(CWVWebView*)webView |
| createWebViewWithConfiguration:(CWVWebViewConfiguration*)configuration |
| forNavigationAction:(CWVNavigationAction*)action { |
| NSLog(@"Create new CWVWebView for %@. User initiated? %@", action.request.URL, |
| action.userInitiated ? @"Yes" : @"No"); |
| |
| CWVWebView* newWebView = [self createWebViewWithConfiguration:configuration]; |
| [self.popupWebViews addObject:newWebView]; |
| |
| UIButton* closeWindowButton = [[UIButton alloc] init]; |
| [closeWindowButton setImage:[UIImage imageNamed:@"ic_stop"] |
| forState:UIControlStateNormal]; |
| closeWindowButton.tintColor = [UIColor blackColor]; |
| closeWindowButton.backgroundColor = [UIColor whiteColor]; |
| [closeWindowButton addTarget:self |
| action:@selector(closePopupWebView) |
| forControlEvents:UIControlEventTouchUpInside]; |
| |
| [newWebView addSubview:closeWindowButton]; |
| closeWindowButton.translatesAutoresizingMaskIntoConstraints = NO; |
| [NSLayoutConstraint activateConstraints:@[ |
| [closeWindowButton.topAnchor constraintEqualToAnchor:newWebView.topAnchor |
| constant:16.0], |
| [closeWindowButton.centerXAnchor |
| constraintEqualToAnchor:newWebView.centerXAnchor], |
| ]]; |
| |
| return newWebView; |
| } |
| |
| - (void)webViewDidClose:(CWVWebView*)webView { |
| NSLog(@"webViewDidClose"); |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| requestMediaCapturePermissionForType:(CWVMediaCaptureType)type |
| decisionHandler: |
| (void (^)(CWVPermissionDecision decision)) |
| decisionHandler { |
| NSString* mediaCaptureType; |
| switch (type) { |
| case CWVMediaCaptureTypeCamera: |
| mediaCaptureType = @"Camera"; |
| break; |
| case CWVMediaCaptureTypeMicrophone: |
| mediaCaptureType = @"Microphone"; |
| break; |
| case CWVMediaCaptureTypeCameraAndMicrophone: |
| mediaCaptureType = @"Camera and Microphone"; |
| break; |
| } |
| |
| NSString* title = |
| [NSString stringWithFormat:@"Request %@ Permission", mediaCaptureType]; |
| UIAlertController* alertController = |
| [UIAlertController alertControllerWithTitle:title |
| message:nil |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"Grant" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| decisionHandler(CWVPermissionDecisionGrant); |
| }]]; |
| [alertController |
| addAction:[UIAlertAction |
| actionWithTitle:@"Deny" |
| style:UIAlertActionStyleDestructive |
| handler:^(UIAlertAction* action) { |
| decisionHandler(CWVPermissionDecisionDeny); |
| }]]; |
| |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| contextMenuConfigurationForElement:(CWVHTMLElement*)element |
| completionHandler:(void (^)(UIContextMenuConfiguration*)) |
| completionHandler { |
| void (^copyHandler)(UIAction*) = ^(UIAction* action) { |
| NSDictionary* item = @{ |
| (NSString*)(UTTypeURL) : element.hyperlink.absoluteString, |
| (NSString*)(UTTypeUTF8PlainText) : [element.hyperlink.absoluteString |
| dataUsingEncoding:NSUTF8StringEncoding], |
| }; |
| [[UIPasteboard generalPasteboard] setItems:@[ item ]]; |
| }; |
| |
| UIContextMenuConfiguration* configuration = [UIContextMenuConfiguration |
| configurationWithIdentifier:nil |
| previewProvider:^{ |
| UIViewController* controller = [[UIViewController alloc] init]; |
| CGRect frame = CGRectMake(10, 200, 200, 21); |
| UILabel* label = [[UILabel alloc] initWithFrame:frame]; |
| label.text = @"iOS13 Preview Page"; |
| [controller.view addSubview:label]; |
| return controller; |
| } |
| actionProvider:^(id _) { |
| NSArray* actions = @[ |
| [UIAction actionWithTitle:@"Copy Link" |
| image:nil |
| identifier:nil |
| handler:copyHandler], |
| [UIAction actionWithTitle:@"Cancel" |
| image:nil |
| identifier:nil |
| handler:^(id ignore){ |
| }] |
| ]; |
| NSString* menuTitle = |
| [NSString stringWithFormat:@"iOS13 Context Menu: %@", |
| element.hyperlink.absoluteString]; |
| return [UIMenu menuWithTitle:menuTitle children:actions]; |
| }]; |
| |
| completionHandler(configuration); |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| contextMenuWillCommitWithAnimator: |
| (id<UIContextMenuInteractionCommitAnimating>)animator { |
| NSLog(@"webView:contextMenuWillCommitWithAnimator:"); |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| runJavaScriptAlertPanelWithMessage:(NSString*)message |
| pageURL:(NSURL*)URL |
| completionHandler:(void (^)(void))handler { |
| UIAlertController* alert = |
| [UIAlertController alertControllerWithTitle:nil |
| message:message |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| [alert addAction:[UIAlertAction actionWithTitle:@"Ok" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| handler(); |
| }]]; |
| |
| [self presentViewController:alert animated:YES completion:nil]; |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| runJavaScriptConfirmPanelWithMessage:(NSString*)message |
| pageURL:(NSURL*)URL |
| completionHandler:(void (^)(BOOL))handler { |
| UIAlertController* alert = |
| [UIAlertController alertControllerWithTitle:nil |
| message:message |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| [alert addAction:[UIAlertAction actionWithTitle:@"Ok" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| handler(YES); |
| }]]; |
| [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:^(UIAlertAction* action) { |
| handler(NO); |
| }]]; |
| |
| [self presentViewController:alert animated:YES completion:nil]; |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt |
| defaultText:(NSString*)defaultText |
| pageURL:(NSURL*)URL |
| completionHandler:(void (^)(NSString*))handler { |
| UIAlertController* alert = |
| [UIAlertController alertControllerWithTitle:nil |
| message:prompt |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| [alert addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.text = defaultText; |
| textField.accessibilityIdentifier = |
| kWebViewShellJavaScriptDialogTextFieldAccessibilityIdentifier; |
| }]; |
| |
| __weak UIAlertController* weakAlert = alert; |
| [alert addAction:[UIAlertAction |
| actionWithTitle:@"Ok" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| NSString* textInput = |
| weakAlert.textFields.firstObject.text; |
| handler(textInput); |
| }]]; |
| [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:^(UIAlertAction* action) { |
| handler(nil); |
| }]]; |
| |
| [self presentViewController:alert animated:YES completion:nil]; |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| didLoadFavicons:(NSArray<CWVFavicon*>*)favIcons { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| } |
| |
| #pragma mark CWVNavigationDelegate methods |
| |
| // Deprecated: this method will not work when `-[CWVNavigationDelegate |
| // webView:decidePolicyForNavigationAction:decisionHandler:]` is implemented |
| - (BOOL)webView:(CWVWebView*)webView |
| shouldStartLoadWithRequest:(NSURLRequest*)request |
| navigationType:(CWVNavigationType)navigationType { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| return YES; |
| } |
| |
| // Deprecated: this method will not work when `-[CWVNavigationDelegate |
| // webView:decidePolicyForNavigationResponse:decisionHandler:]` is implemented |
| - (BOOL)webView:(CWVWebView*)webView |
| shouldContinueLoadWithResponse:(NSURLResponse*)response |
| forMainFrame:(BOOL)forMainFrame { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| return YES; |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| decidePolicyForNavigationAction:(CWVNavigationAction*)navigationAction |
| decisionHandler: |
| (void (^)(CWVNavigationActionPolicy))decisionHandler { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| decisionHandler(CWVNavigationActionPolicyAllow); |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| decidePolicyForNavigationResponse:(CWVNavigationResponse*)navigationResponse |
| decisionHandler:(void (^)(CWVNavigationResponsePolicy)) |
| decisionHandler { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| decisionHandler(CWVNavigationResponsePolicyAllow); |
| } |
| |
| - (void)webViewDidStartNavigation:(CWVWebView*)webView { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| [self updateToolbar]; |
| } |
| |
| - (void)webViewDidCommitNavigation:(CWVWebView*)webView { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| [self updateToolbar]; |
| } |
| |
| - (void)webViewDidFinishNavigation:(CWVWebView*)webView { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| // TODO(crbug.com/41294395): Add some visual indication that the page load has |
| // finished. |
| [self updateToolbar]; |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| didFailNavigationWithError:(NSError*)error { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| [self updateToolbar]; |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| handleSSLErrorWithHandler:(CWVSSLErrorHandler*)handler { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| [handler displayErrorPageWithHTML:handler.error.localizedDescription]; |
| |
| if (!handler.overridable) { |
| return; |
| } |
| |
| UIAlertController* alertController = |
| [self actionSheetWithTitle:@"SSL error encountered" |
| message:@"Would you like to continue anyways?"]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Yes" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [handler overrideErrorAndReloadPage]; |
| }]]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| handleLookalikeURLWithHandler:(CWVLookalikeURLHandler*)handler { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| NSString* html = |
| [NSString stringWithFormat:@"%@ requested, did you mean %@?", |
| handler.requestURL, handler.safeURL]; |
| [handler displayInterstitialPageWithHTML:html]; |
| |
| UIAlertController* alertController = |
| [self actionSheetWithTitle:@"Lookalike URL encountered" |
| message:@"Choose how to proceed."]; |
| [alertController |
| addAction: |
| [UIAlertAction |
| actionWithTitle:@"Proceed to request URL" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| CWVLookalikeURLHandlerDecision decision = |
| CWVLookalikeURLHandlerDecisionProceedToRequestURL; |
| [handler commitDecision:decision]; |
| }]]; |
| [alertController |
| addAction: |
| [UIAlertAction |
| actionWithTitle:@"Proceed to safe URL" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [handler |
| commitDecision: |
| CWVLookalikeURLHandlerDecisionProceedToSafeURL]; |
| }]]; |
| [alertController |
| addAction: |
| [UIAlertAction |
| actionWithTitle:@"Go back or close" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [handler |
| commitDecision: |
| CWVLookalikeURLHandlerDecisionGoBackOrClose]; |
| }]]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| handleUnsafeURLWithHandler:(CWVUnsafeURLHandler*)handler { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| NSString* html = |
| [NSString stringWithFormat:@"%@ requested %@ which might be unsafe.", |
| handler.mainFrameURL, handler.requestURL]; |
| [handler displayInterstitialPageWithHTML:html]; |
| |
| UIAlertController* alertController = |
| [self actionSheetWithTitle:@"Unsafe URL encountered" |
| message:@"Choose how to proceed."]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Proceed" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [handler proceed]; |
| }]]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Go back or close" |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [handler goBack]; |
| }]]; |
| [alertController |
| addAction:[UIAlertAction actionWithTitle:@"Cancel" |
| style:UIAlertActionStyleCancel |
| handler:nil]]; |
| [self presentViewController:alertController animated:YES completion:nil]; |
| } |
| |
| - (void)webViewWebContentProcessDidTerminate:(CWVWebView*)webView { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| } |
| |
| - (void)webView:(CWVWebView*)webView |
| didRequestDownloadWithTask:(CWVDownloadTask*)task { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| self.downloadTask = task; |
| NSString* documentDirectoryPath = NSSearchPathForDirectoriesInDomains( |
| NSDocumentDirectory, NSUserDomainMask, YES)[0]; |
| self.downloadFilePath = [documentDirectoryPath |
| stringByAppendingPathComponent:task.suggestedFileName]; |
| task.delegate = self; |
| [task startDownloadToLocalFileAtPath:self.downloadFilePath]; |
| } |
| |
| #pragma mark CWVAutofillDataManagerObserver |
| |
| - (void)autofillDataManagerDataDidChange: |
| (CWVAutofillDataManager*)autofillDataManager { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| } |
| |
| - (void)autofillDataManager:(CWVAutofillDataManager*)autofillDataManager |
| didChangePasswordsByAdding:(NSArray<CWVPassword*>*)added |
| updating:(NSArray<CWVPassword*>*)updated |
| removing:(NSArray<CWVPassword*>*)removed { |
| NSLog(@"%@: added %@, updated %@, and removed %@ passwords", |
| NSStringFromSelector(_cmd), @(added.count), @(updated.count), |
| @(removed.count)); |
| } |
| |
| #pragma mark CWVDownloadTaskDelegate |
| |
| - (void)downloadTask:(CWVDownloadTask*)downloadTask |
| didFinishWithError:(nullable NSError*)error { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| if (!error) { |
| NSURL* url = [NSURL fileURLWithPath:self.downloadFilePath]; |
| self.documentInteractionController = |
| [UIDocumentInteractionController interactionControllerWithURL:url]; |
| [self.documentInteractionController presentOptionsMenuFromRect:CGRectZero |
| inView:self.view |
| animated:YES]; |
| } |
| self.downloadTask = nil; |
| self.downloadFilePath = nil; |
| } |
| |
| - (void)downloadTaskProgressDidChange:(CWVDownloadTask*)downloadTask { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| } |
| |
| #pragma mark CWVSyncControllerDelegate |
| |
| - (void)syncControllerDidStartSync:(CWVSyncController*)syncController { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| } |
| |
| - (void)syncController:(CWVSyncController*)syncController |
| didFailWithError:(NSError*)error { |
| NSLog(@"%@:%@", NSStringFromSelector(_cmd), error); |
| } |
| |
| - (void)syncControllerDidStopSync:(CWVSyncController*)syncController { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| } |
| |
| - (void)syncControllerDidUpdateState:(CWVSyncController*)syncController { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| } |
| |
| #pragma mark CWVLeakCheckServiceObserver |
| |
| - (void)leakCheckServiceDidChangeState:(CWVLeakCheckService*)leakCheckService { |
| NSLog(@"%@:%d", NSStringFromSelector(_cmd), (int)leakCheckService.state); |
| if (leakCheckService.state != CWVLeakCheckServiceStateRunning) { |
| [self.pendingLeakChecks removeAllObjects]; |
| } |
| } |
| |
| - (void)leakCheckService:(CWVLeakCheckService*)leakCheckService |
| didCheckCredential:(CWVLeakCheckCredential*)credential |
| isLeaked:(BOOL)isLeaked { |
| NSMutableArray<CWVPassword*>* passwordsForCredential = |
| [self.pendingLeakChecks objectForKey:credential]; |
| if (!passwordsForCredential) { |
| NSLog(@"No passwords for CWVLeakCheckCredential!"); |
| return; |
| } |
| |
| [self.pendingLeakChecks removeObjectForKey:credential]; |
| |
| NSMutableArray<NSString*>* passwordDescriptions = [passwordsForCredential |
| valueForKey:NSStringFromSelector(@selector(debugDescription))]; |
| NSString* passwordsDescription = |
| [passwordDescriptions componentsJoinedByString:@"\n\n"]; |
| NSString* message = [NSString |
| stringWithFormat:@"Leak check returned %@ for %@ passwords:\n%@", |
| isLeaked ? @"LEAKED" : @"OK", |
| @(passwordsForCredential.count), passwordsDescription]; |
| NSLog(@"%@", message); |
| NSLog(@"%@ Leak checks remaining...", @(self.pendingLeakChecks.count)); |
| } |
| |
| #pragma mark UIScrollViewDelegate |
| |
| - (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView { |
| NSLog(@"%@", NSStringFromSelector(_cmd)); |
| } |
| |
| @end |