| // Copyright 2019 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/navigation/crw_js_navigation_handler.h" |
| |
| #import "base/json/string_escape.h" |
| #import "base/logging.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "ios/web/history_state_util.h" |
| #import "ios/web/navigation/navigation_context_impl.h" |
| #import "ios/web/navigation/navigation_item_impl.h" |
| #import "ios/web/navigation/navigation_manager_impl.h" |
| #import "ios/web/web_state/user_interaction_state.h" |
| #import "ios/web/web_state/web_state_impl.h" |
| #import "net/base/apple/url_conversions.h" |
| |
| namespace { |
| |
| // URLs that are fed into WKWebView as history push/replace get escaped, |
| // potentially changing their format. Code that attempts to determine whether a |
| // URL hasn't changed can be confused by those differences though, so method |
| // will round-trip a URL through the escaping process so that it can be adjusted |
| // pre-storing, to allow later comparisons to work as expected. |
| GURL URLEscapedForHistory(const GURL& url) { |
| // TODO(crbug.com/41464782): This is a very large hammer; see if limited |
| // unicode escaping would be sufficient. |
| return net::GURLWithNSURL(net::NSURLWithGURL(url)); |
| } |
| |
| } // namespace |
| |
| @implementation CRWJSNavigationHandler |
| |
| #pragma mark - Public |
| |
| - (void)handleNavigationWillChangeState { |
| self.changingHistoryState = YES; |
| } |
| |
| - (void)handleNavigationDidPushStateMessage:(base::Value::Dict*)dict |
| webState:(web::WebStateImpl*)webStateImpl |
| hasUserGesture:(BOOL)hasUserGesture |
| userInteractionState: |
| (web::UserInteractionState*)userInteractionState |
| currentURL:(GURL)currentURL { |
| if (!webStateImpl || webStateImpl->IsBeingDestroyed()) { |
| // Ignore messages received after WebState is being destroyed. |
| return; |
| } |
| |
| DCHECK(self.changingHistoryState); |
| self.changingHistoryState = NO; |
| |
| const web::NavigationManagerImpl& navigationManagerImpl = |
| webStateImpl->GetNavigationManagerImpl(); |
| |
| // If there is a pending entry, a new navigation has been registered but |
| // hasn't begun loading. Since the pushState message is coming from the |
| // previous page, ignore it and allow the previously registered navigation to |
| // continue. This can ocur if a pushState is issued from an anchor tag |
| // onClick event, as the click would have already been registered. |
| if (navigationManagerImpl.GetPendingItem()) { |
| return; |
| } |
| |
| const std::string* pageURL = dict->FindString("pageUrl"); |
| const std::string* baseURL = dict->FindString("baseUrl"); |
| if (!pageURL || !baseURL) { |
| DLOG(WARNING) << "JS message parameter not found: pageUrl or baseUrl"; |
| return; |
| } |
| GURL pushURL = web::history_state_util::GetHistoryStateChangeUrl( |
| currentURL, GURL(*baseURL), *pageURL); |
| // UIWebView seems to choke on unicode characters that haven't been |
| // escaped; escape the URL now so the expected load URL is correct. |
| pushURL = URLEscapedForHistory(pushURL); |
| if (!pushURL.is_valid()) { |
| return; |
| } |
| |
| web::NavigationItemImpl* navItem = navigationManagerImpl.GetCurrentItemImpl(); |
| // PushState happened before first navigation entry or called when the |
| // navigation entry does not contain a valid URL. |
| if (!navItem || !navItem->GetURL().is_valid()) { |
| return; |
| } |
| if (!web::history_state_util::IsHistoryStateChangeValid(navItem->GetURL(), |
| pushURL)) { |
| // If the current session entry URL origin still doesn't match pushURL's |
| // origin, ignore the pushState. This can happen if a new URL is loaded |
| // just before the pushState. |
| return; |
| } |
| const std::string* stateObjectJSON = dict->FindString("stateObject"); |
| if (!stateObjectJSON) { |
| DLOG(WARNING) << "JS message parameter not found: stateObject"; |
| return; |
| } |
| NSString* stateObject = base::SysUTF8ToNSString(*stateObjectJSON); |
| |
| int currentIndex = navigationManagerImpl.GetIndexOfItem(navItem); |
| if (currentIndex > 0) { |
| web::NavigationItem* previousItem = |
| navigationManagerImpl.GetItemAtIndex(currentIndex - 1); |
| web::UserAgentType userAgent = previousItem->GetUserAgentType(); |
| if (userAgent != web::UserAgentType::NONE) { |
| navItem->SetUserAgentType(userAgent); |
| } |
| } |
| // If the user interacted with the page, categorize it as a link navigation. |
| // If not, categorize it is a client redirect as it occurred without user |
| // input and should not be added to the history stack. |
| // TODO(crbug.com/41213462): Improve transition detection. |
| ui::PageTransition transition = |
| userInteractionState->UserInteractionRegisteredSincePageLoaded() |
| ? ui::PAGE_TRANSITION_LINK |
| : ui::PAGE_TRANSITION_CLIENT_REDIRECT; |
| [self pushStateWithPageURL:pushURL |
| stateObject:stateObject |
| transition:transition |
| hasUserGesture:hasUserGesture |
| userInteractionState:userInteractionState |
| webState:webStateImpl]; |
| } |
| |
| - (void)handleNavigationDidReplaceStateMessage:(base::Value::Dict*)dict |
| webState:(web::WebStateImpl*)webStateImpl |
| hasUserGesture:(BOOL)hasUserGesture |
| userInteractionState: |
| (web::UserInteractionState*)userInteractionState |
| currentURL:(GURL)currentURL { |
| if (!webStateImpl || webStateImpl->IsBeingDestroyed()) { |
| // Ignore messages received after WebState is being destroyed. |
| return; |
| } |
| |
| DCHECK(self.changingHistoryState); |
| self.changingHistoryState = NO; |
| |
| const std::string* pageURL = dict->FindString("pageUrl"); |
| const std::string* baseURL = dict->FindString("baseUrl"); |
| if (!pageURL || !baseURL) { |
| DLOG(WARNING) << "JS message parameter not found: pageUrl or baseUrl"; |
| return; |
| } |
| GURL replaceURL = web::history_state_util::GetHistoryStateChangeUrl( |
| currentURL, GURL(*baseURL), *pageURL); |
| // UIWebView seems to choke on unicode characters that haven't been |
| // escaped; escape the URL now so the expected load URL is correct. |
| replaceURL = URLEscapedForHistory(replaceURL); |
| if (!replaceURL.is_valid()) { |
| return; |
| } |
| |
| const web::NavigationManagerImpl& navigationManagerImpl = |
| webStateImpl->GetNavigationManagerImpl(); |
| web::NavigationItemImpl* navItem = navigationManagerImpl.GetCurrentItemImpl(); |
| // ReplaceState happened before first navigation entry or called right |
| // after window.open when the url is empty/not valid. |
| if (!navItem || (navigationManagerImpl.GetItemCount() <= 1 && |
| navItem->GetURL().is_empty())) { |
| return; |
| } |
| if (!web::history_state_util::IsHistoryStateChangeValid(navItem->GetURL(), |
| replaceURL)) { |
| // If the current session entry URL origin still doesn't match |
| // replaceURL's origin, ignore the replaceState. This can happen if a |
| // new URL is loaded just before the replaceState. |
| return; |
| } |
| const std::string* stateObjectJSON = dict->FindString("stateObject"); |
| if (!stateObjectJSON) { |
| DLOG(WARNING) << "JS message parameter not found: stateObject"; |
| return; |
| } |
| NSString* stateObject = base::SysUTF8ToNSString(*stateObjectJSON); |
| [self replaceStateWithPageURL:replaceURL |
| stateObject:stateObject |
| hasUserGesture:hasUserGesture |
| webState:webStateImpl]; |
| } |
| |
| #pragma mark - Private |
| |
| // Adds a new NavigationItem with the given URL and state object to the |
| // history stack. A state object is a serialized generic JavaScript object |
| // that contains details of the UI's state for a given NavigationItem/URL. |
| // TODO(crbug.com/40624624): Move the pushState/replaceState logic into |
| // NavigationManager. |
| - (void)pushStateWithPageURL:(const GURL&)pageURL |
| stateObject:(NSString*)stateObject |
| transition:(ui::PageTransition)transition |
| hasUserGesture:(BOOL)hasUserGesture |
| userInteractionState:(web::UserInteractionState*)userInteractionState |
| webState:(web::WebStateImpl*)webStateImpl { |
| std::unique_ptr<web::NavigationContextImpl> context = |
| web::NavigationContextImpl::CreateNavigationContext( |
| webStateImpl, pageURL, hasUserGesture, transition, |
| /*is_renderer_initiated=*/true); |
| context->SetIsSameDocument(true); |
| webStateImpl->OnNavigationStarted(context.get()); |
| context->SetHasCommitted(true); |
| webStateImpl->OnNavigationFinished(context.get()); |
| userInteractionState->SetUserInteractionRegisteredSincePageLoaded(false); |
| } |
| |
| // Assigns the given URL and state object to the current NavigationItem. |
| - (void)replaceStateWithPageURL:(const GURL&)pageURL |
| stateObject:(NSString*)stateObject |
| hasUserGesture:(BOOL)hasUserGesture |
| webState:(web::WebStateImpl*)webStateImpl { |
| std::unique_ptr<web::NavigationContextImpl> context = |
| web::NavigationContextImpl::CreateNavigationContext( |
| webStateImpl, pageURL, hasUserGesture, |
| ui::PageTransition::PAGE_TRANSITION_CLIENT_REDIRECT, |
| /*is_renderer_initiated=*/true); |
| context->SetIsSameDocument(true); |
| webStateImpl->OnNavigationStarted(context.get()); |
| web::NavigationManagerImpl& navigationManagerImpl = |
| webStateImpl->GetNavigationManagerImpl(); |
| navigationManagerImpl.UpdateCurrentItemForReplaceState(pageURL, stateObject); |
| context->SetHasCommitted(true); |
| webStateImpl->OnNavigationFinished(context.get()); |
| } |
| |
| @end |