| // Copyright 2017 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/history/history_tab_helper.h" |
| |
| #include "base/memory/ptr_util.h" |
| #include "components/history/core/browser/history_constants.h" |
| #include "components/history/core/browser/history_service.h" |
| #include "components/keyed_service/core/service_access_type.h" |
| #include "components/ntp_snippets/features.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #include "ios/chrome/browser/chrome_url_constants.h" |
| #include "ios/chrome/browser/history/history_service_factory.h" |
| #import "ios/web/public/navigation_item.h" |
| #import "ios/web/public/navigation_manager.h" |
| #import "ios/web/public/web_state/navigation_context.h" |
| #import "ios/web/public/web_state/web_state.h" |
| #include "net/http/http_response_headers.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| base::Optional<base::string16> GetPageTitle(const web::NavigationItem& item) { |
| const base::string16& title = item.GetTitleForDisplay(); |
| if (title.empty() || |
| title == l10n_util::GetStringUTF16(IDS_DEFAULT_TAB_TITLE)) { |
| return base::nullopt; |
| } |
| |
| return base::Optional<base::string16>(title); |
| } |
| |
| } // namespace |
| |
| HistoryTabHelper::~HistoryTabHelper() { |
| DCHECK(!web_state_); |
| } |
| |
| void HistoryTabHelper::UpdateHistoryPageTitle(const web::NavigationItem& item) { |
| DCHECK(!delay_notification_); |
| |
| const base::Optional<base::string16> title = GetPageTitle(item); |
| // Don't update the history if current entry has no title. |
| if (!title) { |
| return; |
| } |
| |
| history::HistoryService* history_service = GetHistoryService(); |
| if (history_service) { |
| history_service->SetPageTitle(item.GetVirtualURL(), title.value()); |
| } |
| } |
| |
| void HistoryTabHelper::SetDelayHistoryServiceNotification( |
| bool delay_notification) { |
| delay_notification_ = delay_notification; |
| if (delay_notification_) { |
| return; |
| } |
| |
| history::HistoryService* history_service = GetHistoryService(); |
| if (history_service) { |
| for (const auto& add_page_args : recorded_navigations_) { |
| history_service->AddPage(add_page_args); |
| } |
| } |
| |
| std::vector<history::HistoryAddPageArgs> empty_vector; |
| std::swap(recorded_navigations_, empty_vector); |
| |
| web::NavigationItem* last_committed_item = |
| web_state_->GetNavigationManager()->GetLastCommittedItem(); |
| if (last_committed_item) { |
| UpdateHistoryPageTitle(*last_committed_item); |
| } |
| } |
| |
| HistoryTabHelper::HistoryTabHelper(web::WebState* web_state) |
| : web_state_(web_state) { |
| web_state_->AddObserver(this); |
| } |
| |
| void HistoryTabHelper::DidFinishNavigation( |
| web::WebState* web_state, |
| web::NavigationContext* navigation_context) { |
| DCHECK_EQ(web_state_, web_state); |
| if (web_state_->GetBrowserState()->IsOffTheRecord()) { |
| return; |
| } |
| |
| // Do not record failed navigation nor 404 to the history (to prevent them |
| // from showing up as Most Visited tiles on NTP). |
| if (navigation_context->GetError()) { |
| return; |
| } |
| |
| if (navigation_context->GetResponseHeaders() && |
| navigation_context->GetResponseHeaders()->response_code() == 404) { |
| return; |
| } |
| |
| if (navigation_context->IsDownload()) { |
| return; |
| } |
| |
| if (!navigation_context->HasCommitted() && |
| !navigation_context->IsSameDocument()) { |
| // Navigation was replaced. |
| return; |
| } |
| |
| DCHECK(web_state->GetNavigationManager()->GetVisibleItem()); |
| web::NavigationItem* visible_item = |
| web_state_->GetNavigationManager()->GetVisibleItem(); |
| DCHECK(!visible_item->GetTimestamp().is_null()); |
| |
| // Do not update the history database for back/forward navigations. |
| // TODO(crbug.com/661667): on iOS the navigation is not currently tagged with |
| // a ui::PAGE_TRANSITION_FORWARD_BACK transition. |
| const ui::PageTransition transition = visible_item->GetTransitionType(); |
| if (transition & ui::PAGE_TRANSITION_FORWARD_BACK) { |
| return; |
| } |
| |
| // Do not update the history database for data: urls. This diverges from |
| // desktop, but prevents dumping huge view-source urls into the history |
| // database. Keep it NDEBUG only because view-source:// URLs are enabled |
| // on NDEBUG builds only. |
| const GURL& url = visible_item->GetURL(); |
| #ifndef NDEBUG |
| if (url.SchemeIs(url::kDataScheme)) { |
| return; |
| } |
| #endif |
| |
| num_title_changes_ = 0; |
| |
| history::RedirectList redirects; |
| const GURL& original_url = visible_item->GetOriginalRequestURL(); |
| const GURL& referrer_url = visible_item->GetReferrer().url; |
| if (original_url != url) { |
| // Simulate a valid redirect chain in case of URLs that have been modified |
| // by CRWWebController -finishHistoryNavigationFromEntry:. |
| if (transition & ui::PAGE_TRANSITION_CLIENT_REDIRECT || |
| url.EqualsIgnoringRef(original_url)) { |
| redirects.push_back(referrer_url); |
| } |
| // TODO(crbug.com/703872): the redirect chain is not constructed the same |
| // way as desktop so this part needs to be revised. |
| redirects.push_back(original_url); |
| redirects.push_back(url); |
| } |
| |
| // Navigations originating from New Tab Page or Reading List should not |
| // contribute to Most Visited. |
| const bool consider_for_ntp_most_visited = |
| referrer_url != ntp_snippets::GetContentSuggestionsReferrerURL() && |
| referrer_url != kReadingListReferrerURL; |
| |
| // Top-level frame navigations are visible; everything else is hidden. |
| // Also hide top-level navigations that result in an error in order to |
| // prevent the omnibox from suggesting URLs that have never been navigated |
| // to successfully. (If a top-level navigation to the URL succeeds at some |
| // point, the URL will be unhidden and thus eligible to be suggested by the |
| // omnibox.) |
| const bool hidden = |
| navigation_context->GetError() || |
| (navigation_context->GetResponseHeaders() && |
| navigation_context->GetResponseHeaders()->response_code() >= 400 && |
| navigation_context->GetResponseHeaders()->response_code() > 600) || |
| !ui::PageTransitionIsMainFrame(navigation_context->GetPageTransition()); |
| history::HistoryAddPageArgs add_page_args( |
| url, visible_item->GetTimestamp(), this, visible_item->GetUniqueID(), |
| referrer_url, redirects, transition, hidden, history::SOURCE_BROWSED, |
| /*did_replace_entry=*/false, consider_for_ntp_most_visited, |
| navigation_context->IsSameDocument() ? GetPageTitle(*visible_item) |
| : base::nullopt); |
| |
| if (delay_notification_) { |
| recorded_navigations_.push_back(std::move(add_page_args)); |
| } else { |
| DCHECK(recorded_navigations_.empty()); |
| |
| history::HistoryService* history_service = GetHistoryService(); |
| if (history_service) { |
| history_service->AddPage(add_page_args); |
| UpdateHistoryPageTitle(*visible_item); |
| } |
| } |
| } |
| |
| void HistoryTabHelper::PageLoaded( |
| web::WebState* web_state, |
| web::PageLoadCompletionStatus load_completion_status) { |
| last_load_completion_ = base::TimeTicks::Now(); |
| } |
| |
| void HistoryTabHelper::TitleWasSet(web::WebState* web_state) { |
| DCHECK_EQ(web_state_, web_state); |
| if (delay_notification_) { |
| return; |
| } |
| |
| // Protect against pages changing their title too often during page load. |
| if (num_title_changes_ >= history::kMaxTitleChanges) |
| return; |
| |
| // Only store page titles into history if they were set while the page was |
| // loading or during a brief span after load is complete. This fixes the case |
| // where a page uses a title change to alert a user of a situation but that |
| // title change ends up saved in history. |
| if (web_state->IsLoading() || |
| (base::TimeTicks::Now() - last_load_completion_ < |
| history::GetTitleSettingWindow())) { |
| web::NavigationItem* last_committed_item = |
| web_state_->GetNavigationManager()->GetLastCommittedItem(); |
| if (last_committed_item) { |
| UpdateHistoryPageTitle(*last_committed_item); |
| ++num_title_changes_; |
| } |
| } |
| } |
| |
| void HistoryTabHelper::WebStateDestroyed(web::WebState* web_state) { |
| DCHECK_EQ(web_state_, web_state); |
| web_state_->RemoveObserver(this); |
| web_state_ = nullptr; |
| } |
| |
| history::HistoryService* HistoryTabHelper::GetHistoryService() { |
| ios::ChromeBrowserState* browser_state = |
| ios::ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState()); |
| if (browser_state->IsOffTheRecord()) |
| return nullptr; |
| |
| return ios::HistoryServiceFactory::GetForBrowserState( |
| browser_state, ServiceAccessType::IMPLICIT_ACCESS); |
| } |