| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/ui/dialogs/dialog_presenter.h" |
| |
| #include <deque> |
| #include <map> |
| |
| #import "base/ios/block_types.h" |
| #include "base/logging.h" |
| #import "base/mac/scoped_nsobject.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "components/strings/grit/components_strings.h" |
| #import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h" |
| #import "ios/chrome/browser/ui/alert_coordinator/alert_coordinator.h" |
| #import "ios/chrome/browser/ui/alert_coordinator/input_alert_coordinator.h" |
| #import "ios/chrome/browser/ui/dialogs/javascript_dialog_blocking_util.h" |
| #import "ios/chrome/browser/ui/dialogs/nsurl_protection_space_util.h" |
| #include "ios/chrome/browser/ui/ui_util.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #include "ios/web/public/web_state/web_state.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| // Externed accessibility identifier. |
| NSString* const kJavaScriptDialogTextFieldAccessibiltyIdentifier = |
| @"JavaScriptDialogTextFieldAccessibiltyIdentifier"; |
| |
| namespace { |
| // The hostname to use for JavaScript alerts when there is no valid hostname in |
| // the URL passed to |+localizedTitleForJavaScriptAlertFromPage:type:|. |
| const char kAboutNullHostname[] = "about:null"; |
| } // namespace |
| |
| @interface DialogPresenter () { |
| // Queue of WebStates which correspond to the keys in |
| // |_dialogCoordinatorsForWebStates|. |
| std::deque<web::WebState*> _queuedWebStates; |
| // A map associating queued webStates with their coordinators. |
| std::map<web::WebState*, base::scoped_nsobject<AlertCoordinator>> |
| _dialogCoordinatorsForWebStates; |
| } |
| |
| // The delegate passed on initialization. |
| @property(weak, nonatomic, readonly) id<DialogPresenterDelegate> delegate; |
| |
| // The presenting view controller passed on initialization. |
| @property(weak, nonatomic, readonly) UIViewController* viewController; |
| |
| // Whether a modal dialog is currently being shown. |
| @property(nonatomic, readonly, getter=isShowingDialog) BOOL showingDialog; |
| |
| // The webState for |presentedDialog|. |
| @property(nonatomic) web::WebState* presentedDialogWebState; |
| |
| // The dialog that's currently being shown, if any. |
| @property(nonatomic, strong) AlertCoordinator* presentedDialogCoordinator; |
| |
| // The JavaScript dialog blocking confirmation action sheet being shown, if any. |
| @property(nonatomic, strong) AlertCoordinator* blockingConfirmationCoordinator; |
| |
| // Adds |context| and |coordinator| to the queue. If a dialog is not already |
| // being shown, |coordinator| will be presented. Otherwise, |coordinator| will |
| // be displayed once the previously shown dialog is dismissed. |
| - (void)addDialogCoordinator:(AlertCoordinator*)coordinator |
| forWebState:(web::WebState*)webState; |
| |
| // Shows the dialog associated with the next context in |contextQueue|. |
| - (void)showNextDialog; |
| |
| // Called when a button in |coordinator| is tapped. |
| - (void)buttonWasTappedForCoordinator:(AlertCoordinator*)coordinator; |
| |
| // Adds buttons to |alertCoordinator|. A confirmation button with |label| as |
| // the text will be added for |confirmAction|, and a cancel button will be added |
| // for |cancelAction|. |
| - (void)setUpAlertCoordinator:(AlertCoordinator*)alertCoordinator |
| confirmAction:(ProceduralBlock)confirmAction |
| cancelAction:(ProceduralBlock)cancelAction |
| OKLabel:(NSString*)label; |
| |
| // Sets up the JavaScript dialog blocking option for |alertCoordinator|. |
| // Overrides |alertCoordinator|'s |startAction| to call |
| // JavaScriptDialogWasShown(). Depending on the value of |
| // ShouldShowDialogBlockingOption() for |webState|, optionally adds a button to |
| // |alertCoordinator| allowing for the blocking of future dialogs. In addition |
| // to blocking dialogs for the WebState, the added button will call |
| // |alertCoordinator|'s |cancelAction|. |
| - (void)setUpBlockingOptionForCoordinator:(AlertCoordinator*)alertCoordinator |
| webState:(web::WebState*)webState; |
| |
| // The block to use for the JavaScript dialog blocking option for |coordinator|. |
| - (ProceduralBlock)blockingActionForCoordinator:(AlertCoordinator*)coordinator; |
| |
| @end |
| |
| @implementation DialogPresenter |
| |
| @synthesize active = _active; |
| @synthesize delegate = _delegate; |
| @synthesize viewController = _viewController; |
| @synthesize presentedDialogCoordinator = _presentedDialogCoordinator; |
| @synthesize blockingConfirmationCoordinator = _blockingConfirmationCoordinator; |
| @synthesize presentedDialogWebState = _presentedDialogWebState; |
| |
| - (instancetype)initWithDelegate:(id<DialogPresenterDelegate>)delegate |
| presentingViewController:(UIViewController*)viewController { |
| if ((self = [super init])) { |
| DCHECK(delegate); |
| DCHECK(viewController); |
| _delegate = delegate; |
| _viewController = viewController; |
| } |
| return self; |
| } |
| |
| #pragma mark - Accessors |
| |
| - (void)setActive:(BOOL)active { |
| if (_active != active) { |
| _active = active; |
| [self tryToPresent]; |
| } |
| } |
| |
| - (BOOL)isShowingDialog { |
| DCHECK_EQ(self.presentedDialogWebState != nullptr, |
| self.presentedDialogCoordinator != nil); |
| return self.presentedDialogCoordinator != nil; |
| } |
| |
| #pragma mark - Public |
| |
| - (void)runJavaScriptAlertPanelWithMessage:(NSString*)message |
| requestURL:(const GURL&)requestURL |
| webState:(web::WebState*)webState |
| completionHandler:(void (^)(void))completionHandler { |
| NSString* title = |
| [DialogPresenter localizedTitleForJavaScriptAlertFromPage:requestURL]; |
| AlertCoordinator* alertCoordinator = |
| [[AlertCoordinator alloc] initWithBaseViewController:self.viewController |
| title:title |
| message:message]; |
| |
| // Handler. |
| __weak DialogPresenter* weakSelf = self; |
| __weak AlertCoordinator* weakCoordinator = alertCoordinator; |
| ProceduralBlock OKHandler = ^{ |
| if (completionHandler) |
| completionHandler(); |
| [weakSelf buttonWasTappedForCoordinator:weakCoordinator]; |
| }; |
| |
| // Add button. |
| [alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_OK) |
| action:OKHandler |
| style:UIAlertActionStyleDefault]; |
| |
| // Add cancel handler. |
| alertCoordinator.cancelAction = completionHandler; |
| alertCoordinator.noInteractionAction = completionHandler; |
| |
| // Blocking option setup. |
| [self setUpBlockingOptionForCoordinator:alertCoordinator webState:webState]; |
| |
| [self addDialogCoordinator:alertCoordinator forWebState:webState]; |
| } |
| |
| - (void)runJavaScriptConfirmPanelWithMessage:(NSString*)message |
| requestURL:(const GURL&)requestURL |
| webState:(web::WebState*)webState |
| completionHandler: |
| (void (^)(BOOL isConfirmed))completionHandler { |
| NSString* title = |
| [DialogPresenter localizedTitleForJavaScriptAlertFromPage:requestURL]; |
| AlertCoordinator* alertCoordinator = |
| [[AlertCoordinator alloc] initWithBaseViewController:self.viewController |
| title:title |
| message:message]; |
| |
| // Actions. |
| ProceduralBlock confirmAction = ^{ |
| if (completionHandler) |
| completionHandler(YES); |
| }; |
| |
| ProceduralBlock cancelAction = ^{ |
| if (completionHandler) |
| completionHandler(NO); |
| }; |
| |
| // Coordinator Setup. |
| NSString* OKLabel = l10n_util::GetNSString(IDS_OK); |
| [self setUpAlertCoordinator:alertCoordinator |
| confirmAction:confirmAction |
| cancelAction:cancelAction |
| OKLabel:OKLabel]; |
| |
| // Blocking option setup. |
| [self setUpBlockingOptionForCoordinator:alertCoordinator webState:webState]; |
| |
| [self addDialogCoordinator:alertCoordinator forWebState:webState]; |
| } |
| |
| - (void)runJavaScriptTextInputPanelWithPrompt:(NSString*)message |
| defaultText:(NSString*)defaultText |
| requestURL:(const GURL&)requestURL |
| webState:(web::WebState*)webState |
| completionHandler: |
| (void (^)(NSString* input))completionHandler { |
| NSString* title = |
| [DialogPresenter localizedTitleForJavaScriptAlertFromPage:requestURL]; |
| InputAlertCoordinator* alertCoordinator = [[InputAlertCoordinator alloc] |
| initWithBaseViewController:self.viewController |
| title:title |
| message:message]; |
| |
| // Actions. |
| __weak InputAlertCoordinator* weakCoordinator = alertCoordinator; |
| ProceduralBlock confirmAction = ^{ |
| if (completionHandler) { |
| NSString* textInput = [weakCoordinator textFields].firstObject.text; |
| completionHandler(textInput ? textInput : @""); |
| } |
| }; |
| |
| ProceduralBlock cancelAction = ^{ |
| if (completionHandler) |
| completionHandler(nil); |
| }; |
| |
| // Coordinator Setup. |
| NSString* OKLabel = l10n_util::GetNSString(IDS_OK); |
| [self setUpAlertCoordinator:alertCoordinator |
| confirmAction:confirmAction |
| cancelAction:cancelAction |
| OKLabel:OKLabel]; |
| |
| // Blocking option setup. |
| [self setUpBlockingOptionForCoordinator:alertCoordinator webState:webState]; |
| |
| // Add text field. |
| [alertCoordinator |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.text = defaultText; |
| textField.accessibilityIdentifier = |
| kJavaScriptDialogTextFieldAccessibiltyIdentifier; |
| }]; |
| |
| [self addDialogCoordinator:alertCoordinator forWebState:webState]; |
| } |
| |
| - (void)runAuthDialogForProtectionSpace:(NSURLProtectionSpace*)protectionSpace |
| proposedCredential:(NSURLCredential*)credential |
| webState:(web::WebState*)webState |
| completionHandler:(void (^)(NSString* user, |
| NSString* password))handler { |
| NSString* title = l10n_util::GetNSStringWithFixup(IDS_LOGIN_DIALOG_TITLE); |
| NSString* message = |
| ios_internal::nsurlprotectionspace_util::MessageForHTTPAuth( |
| protectionSpace); |
| |
| InputAlertCoordinator* alertCoordinator = [[InputAlertCoordinator alloc] |
| initWithBaseViewController:self.viewController |
| title:title |
| message:message]; |
| |
| // Actions. |
| __weak InputAlertCoordinator* weakCoordinator = alertCoordinator; |
| ProceduralBlock confirmAction = ^{ |
| if (handler) { |
| NSString* username = [[weakCoordinator textFields] objectAtIndex:0].text; |
| NSString* password = [[weakCoordinator textFields] objectAtIndex:1].text; |
| handler(username, password); |
| } |
| }; |
| |
| ProceduralBlock cancelAction = ^{ |
| if (handler) |
| handler(nil, nil); |
| }; |
| |
| // Coordinator Setup. |
| NSString* OKLabel = |
| l10n_util::GetNSStringWithFixup(IDS_LOGIN_DIALOG_OK_BUTTON_LABEL); |
| [self setUpAlertCoordinator:alertCoordinator |
| confirmAction:confirmAction |
| cancelAction:cancelAction |
| OKLabel:OKLabel]; |
| |
| // Add text fields. |
| NSString* username = credential.user ? credential.user : @""; |
| [alertCoordinator |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.text = username; |
| textField.placeholder = l10n_util::GetNSString( |
| IDS_IOS_HTTP_LOGIN_DIALOG_USERNAME_PLACEHOLDER); |
| }]; |
| [alertCoordinator |
| addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
| textField.placeholder = l10n_util::GetNSString( |
| IDS_IOS_HTTP_LOGIN_DIALOG_PASSWORD_PLACEHOLDER); |
| textField.secureTextEntry = YES; |
| }]; |
| |
| [self addDialogCoordinator:alertCoordinator forWebState:webState]; |
| } |
| |
| - (void)cancelDialogForWebState:(web::WebState*)webState { |
| DCHECK_NE(webState, self.presentedDialogWebState); |
| AlertCoordinator* dialogToCancel = _dialogCoordinatorsForWebStates[webState]; |
| if (dialogToCancel) { |
| auto it = |
| std::find(_queuedWebStates.begin(), _queuedWebStates.end(), webState); |
| DCHECK(it != _queuedWebStates.end()); |
| _queuedWebStates.erase(it); |
| [dialogToCancel executeCancelHandler]; |
| [dialogToCancel stop]; |
| _dialogCoordinatorsForWebStates.erase(webState); |
| } |
| } |
| |
| - (void)cancelAllDialogs { |
| [self.presentedDialogCoordinator executeCancelHandler]; |
| [self.presentedDialogCoordinator stop]; |
| self.presentedDialogCoordinator = nil; |
| self.presentedDialogWebState = nil; |
| while (!_queuedWebStates.empty()) { |
| [self cancelDialogForWebState:_queuedWebStates.front()]; |
| } |
| } |
| |
| - (void)tryToPresent { |
| // Don't try to present if a JavaScript dialog blocking confirmation sheet is |
| // displayed. |
| if (self.blockingConfirmationCoordinator) |
| return; |
| // The active TabModel can't be changed while a JavaScript dialog is shown. |
| DCHECK(!self.showingDialog); |
| if (_active && !_queuedWebStates.empty() && !self.delegate.presenting) |
| [self showNextDialog]; |
| } |
| |
| + (NSString*)localizedTitleForJavaScriptAlertFromPage:(const GURL&)pageURL { |
| NSString* hostname = base::SysUTF8ToNSString(pageURL.host()); |
| if (!hostname.length) |
| hostname = base::SysUTF8ToNSString(kAboutNullHostname); |
| return l10n_util::GetNSStringF(IDS_JAVASCRIPT_MESSAGEBOX_TITLE, |
| base::SysNSStringToUTF16(hostname)); |
| } |
| |
| #pragma mark - Private methods. |
| |
| - (void)addDialogCoordinator:(AlertCoordinator*)coordinator |
| forWebState:(web::WebState*)webState { |
| DCHECK(coordinator); |
| DCHECK(webState); |
| DCHECK_NE(webState, self.presentedDialogWebState); |
| DCHECK(!_dialogCoordinatorsForWebStates[webState]); |
| _queuedWebStates.push_back(webState); |
| _dialogCoordinatorsForWebStates[webState] = |
| base::scoped_nsobject<AlertCoordinator>(coordinator); |
| |
| if (self.active && !self.showingDialog && !self.delegate.presenting) |
| [self showNextDialog]; |
| } |
| |
| - (void)showNextDialog { |
| DCHECK(self.active); |
| DCHECK(!self.showingDialog); |
| DCHECK(!_queuedWebStates.empty()); |
| // Update properties and remove context and the dialog from queue. |
| self.presentedDialogWebState = _queuedWebStates.front(); |
| _queuedWebStates.pop_front(); |
| self.presentedDialogCoordinator = |
| _dialogCoordinatorsForWebStates[self.presentedDialogWebState]; |
| _dialogCoordinatorsForWebStates.erase(self.presentedDialogWebState); |
| // Notify the delegate and display the dialog. |
| [self.delegate dialogPresenter:self |
| willShowDialogForWebState:self.presentedDialogWebState]; |
| [self.presentedDialogCoordinator start]; |
| } |
| |
| - (void)buttonWasTappedForCoordinator:(AlertCoordinator*)coordinator { |
| if (coordinator != self.presentedDialogCoordinator) |
| return; |
| self.presentedDialogWebState = nil; |
| self.presentedDialogCoordinator = nil; |
| self.blockingConfirmationCoordinator = nil; |
| if (!_queuedWebStates.empty() && !self.delegate.presenting) |
| [self showNextDialog]; |
| } |
| |
| - (void)setUpAlertCoordinator:(AlertCoordinator*)alertCoordinator |
| confirmAction:(ProceduralBlock)confirmAction |
| cancelAction:(ProceduralBlock)cancelAction |
| OKLabel:(NSString*)label { |
| // Handlers. |
| __weak DialogPresenter* weakSelf = self; |
| __weak AlertCoordinator* weakCoordinator = alertCoordinator; |
| |
| ProceduralBlock confirmHandler = ^{ |
| if (confirmAction) |
| confirmAction(); |
| [weakSelf buttonWasTappedForCoordinator:weakCoordinator]; |
| }; |
| |
| ProceduralBlock cancelHandler = ^{ |
| if (cancelAction) |
| cancelAction(); |
| [weakSelf buttonWasTappedForCoordinator:weakCoordinator]; |
| }; |
| |
| // Add buttons. |
| [alertCoordinator addItemWithTitle:label |
| action:confirmHandler |
| style:UIAlertActionStyleDefault]; |
| [alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
| action:cancelHandler |
| style:UIAlertActionStyleCancel]; |
| |
| // Add cancel handler. |
| alertCoordinator.cancelAction = cancelAction; |
| alertCoordinator.noInteractionAction = cancelAction; |
| } |
| |
| - (void)setUpBlockingOptionForCoordinator:(AlertCoordinator*)alertCoordinator |
| webState:(web::WebState*)webState { |
| DCHECK(alertCoordinator); |
| DCHECK(webState); |
| |
| // Set up the start action. |
| ProceduralBlock originalStartAction = alertCoordinator.startAction; |
| alertCoordinator.startAction = ^{ |
| if (originalStartAction) |
| originalStartAction(); |
| JavaScriptDialogWasShown(webState); |
| }; |
| |
| // Early return if a blocking option should not be added. |
| if (!ShouldShowDialogBlockingOption(webState)) |
| return; |
| |
| ProceduralBlock blockingAction = |
| [self blockingActionForCoordinator:alertCoordinator]; |
| NSString* blockingOptionTitle = |
| l10n_util::GetNSString(IDS_IOS_JAVA_SCRIPT_DIALOG_BLOCKING_BUTTON_TEXT); |
| [alertCoordinator addItemWithTitle:blockingOptionTitle |
| action:blockingAction |
| style:UIAlertActionStyleDefault]; |
| } |
| |
| - (ProceduralBlock)blockingActionForCoordinator:(AlertCoordinator*)coordinator { |
| __weak DialogPresenter* weakSelf = self; |
| __weak AlertCoordinator* weakCoordinator = coordinator; |
| __weak UIViewController* weakBaseViewController = |
| coordinator.baseViewController; |
| ProceduralBlock cancelAction = coordinator.cancelAction; |
| return [^{ |
| // Create the confirmation coordinator. Use an action sheet on iPhone and |
| // an alert on iPhone. |
| NSString* confirmMessage = |
| l10n_util::GetNSString(IDS_JAVASCRIPT_MESSAGEBOX_SUPPRESS_OPTION); |
| AlertCoordinator* confirmationCoordinator = |
| IsIPadIdiom() ? [[AlertCoordinator alloc] |
| initWithBaseViewController:weakBaseViewController |
| title:nil |
| message:confirmMessage] |
| : [[ActionSheetCoordinator alloc] |
| initWithBaseViewController:weakBaseViewController |
| title:nil |
| message:confirmMessage |
| rect:CGRectZero |
| view:nil]; |
| // Set up button actions. |
| ProceduralBlock confirmHandler = ^{ |
| if (cancelAction) |
| cancelAction(); |
| DialogPresenter* strongSelf = weakSelf; |
| if (!strongSelf) |
| return; |
| DialogBlockingOptionSelected([strongSelf presentedDialogWebState]); |
| [strongSelf buttonWasTappedForCoordinator:weakCoordinator]; |
| }; |
| ProceduralBlock cancelHandler = ^{ |
| if (cancelAction) |
| cancelAction(); |
| [weakSelf buttonWasTappedForCoordinator:weakCoordinator]; |
| }; |
| NSString* blockingOptionTitle = |
| l10n_util::GetNSString(IDS_IOS_JAVA_SCRIPT_DIALOG_BLOCKING_BUTTON_TEXT); |
| [confirmationCoordinator addItemWithTitle:blockingOptionTitle |
| action:confirmHandler |
| style:UIAlertActionStyleDestructive]; |
| [confirmationCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
| action:cancelHandler |
| style:UIAlertActionStyleCancel]; |
| [weakSelf setBlockingConfirmationCoordinator:confirmationCoordinator]; |
| [[weakSelf blockingConfirmationCoordinator] start]; |
| } copy]; |
| } |
| |
| @end |