diff options
author | avi@chromium.org <avi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-04-10 21:26:37 +0000 |
---|---|---|
committer | avi@chromium.org <avi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-04-10 21:26:37 +0000 |
commit | f9e4daec200c22fb09df67bcd7f542a2d52fc9ed (patch) | |
tree | 5016666f8a1aea4d60dc911e1bcef4feef167e12 /content/browser/web_contents | |
parent | e5e84e275e9ef65d280d66f2e8ee3ccb666a1cdb (diff) | |
download | chromium_src-f9e4daec200c22fb09df67bcd7f542a2d52fc9ed.zip chromium_src-f9e4daec200c22fb09df67bcd7f542a2d52fc9ed.tar.gz chromium_src-f9e4daec200c22fb09df67bcd7f542a2d52fc9ed.tar.bz2 |
TabContents -> WebContentsImpl, part 3.
BUG=105875
TEST=no change
Review URL: http://codereview.chromium.org/9960071
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@131634 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'content/browser/web_contents')
-rw-r--r-- | content/browser/web_contents/debug_urls.cc | 49 | ||||
-rw-r--r-- | content/browser/web_contents/debug_urls.h | 21 | ||||
-rw-r--r-- | content/browser/web_contents/drag_utils_gtk.cc | 38 | ||||
-rw-r--r-- | content/browser/web_contents/drag_utils_gtk.h | 25 | ||||
-rw-r--r-- | content/browser/web_contents/interstitial_page_impl.cc | 750 | ||||
-rw-r--r-- | content/browser/web_contents/interstitial_page_impl.h | 198 | ||||
-rw-r--r-- | content/browser/web_contents/navigation_controller_impl.cc | 1441 | ||||
-rw-r--r-- | content/browser/web_contents/navigation_controller_impl.h | 332 | ||||
-rw-r--r-- | content/browser/web_contents/navigation_controller_impl_unittest.cc | 2592 | ||||
-rw-r--r-- | content/browser/web_contents/navigation_entry_impl.cc | 221 | ||||
-rw-r--r-- | content/browser/web_contents/navigation_entry_impl.h | 217 | ||||
-rw-r--r-- | content/browser/web_contents/navigation_entry_impl_unittest.cc | 178 | ||||
-rw-r--r-- | content/browser/web_contents/web_drag_dest_gtk.cc | 2 | ||||
-rw-r--r-- | content/browser/web_contents/web_drag_source_gtk.cc | 2 |
14 files changed, 6064 insertions, 2 deletions
diff --git a/content/browser/web_contents/debug_urls.cc b/content/browser/web_contents/debug_urls.cc new file mode 100644 index 0000000..05f9c41 --- /dev/null +++ b/content/browser/web_contents/debug_urls.cc @@ -0,0 +1,49 @@ +// Copyright (c) 2012 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. + +#include "content/browser/web_contents/debug_urls.h" + +#include "content/browser/gpu/gpu_process_host_ui_shim.h" +#include "content/public/common/url_constants.h" +#include "googleurl/src/gurl.h" + +namespace content { + +bool HandleDebugURL(const GURL& url, content::PageTransition transition) { + content::PageTransition base_transition = + content::PageTransitionStripQualifier(transition); + if (base_transition != content::PAGE_TRANSITION_TYPED) + return false; + + if (url.host() == chrome::kChromeUIBrowserCrashHost) { + // Induce an intentional crash in the browser process. + CHECK(false); + return true; + } + + if (url == GURL(chrome::kChromeUIGpuCleanURL)) { + GpuProcessHostUIShim* shim = GpuProcessHostUIShim::GetOneInstance(); + if (shim) + shim->SimulateRemoveAllContext(); + return true; + } + + if (url == GURL(chrome::kChromeUIGpuCrashURL)) { + GpuProcessHostUIShim* shim = GpuProcessHostUIShim::GetOneInstance(); + if (shim) + shim->SimulateCrash(); + return true; + } + + if (url == GURL(chrome::kChromeUIGpuHangURL)) { + GpuProcessHostUIShim* shim = GpuProcessHostUIShim::GetOneInstance(); + if (shim) + shim->SimulateHang(); + return true; + } + + return false; +} + +} // namespace content diff --git a/content/browser/web_contents/debug_urls.h b/content/browser/web_contents/debug_urls.h new file mode 100644 index 0000000..e148675 --- /dev/null +++ b/content/browser/web_contents/debug_urls.h @@ -0,0 +1,21 @@ +// Copyright (c) 2012 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. + +#ifndef CONTENT_BROWSER_WEB_CONTENTS_DEBUG_URLS_H_ +#define CONTENT_BROWSER_WEB_CONTENTS_DEBUG_URLS_H_ +#pragma once + +#include "content/public/common/page_transition_types.h" + +class GURL; + +namespace content { + +// Checks if the given url is a url used for debugging purposes, and if so +// handles it and returns true. +bool HandleDebugURL(const GURL& url, content::PageTransition transition); + +} // namespace content + +#endif // CONTENT_BROWSER_WEB_CONTENTS_DEBUG_URLS_H_ diff --git a/content/browser/web_contents/drag_utils_gtk.cc b/content/browser/web_contents/drag_utils_gtk.cc new file mode 100644 index 0000000..8164c53 --- /dev/null +++ b/content/browser/web_contents/drag_utils_gtk.cc @@ -0,0 +1,38 @@ +// Copyright (c) 2012 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. + +#include "content/browser/web_contents/drag_utils_gtk.h" + +using WebKit::WebDragOperationsMask; +using WebKit::WebDragOperation; +using WebKit::WebDragOperationNone; +using WebKit::WebDragOperationCopy; +using WebKit::WebDragOperationLink; +using WebKit::WebDragOperationMove; + +namespace content { + +GdkDragAction WebDragOpToGdkDragAction(WebDragOperationsMask op) { + GdkDragAction action = static_cast<GdkDragAction>(0); + if (op & WebDragOperationCopy) + action = static_cast<GdkDragAction>(action | GDK_ACTION_COPY); + if (op & WebDragOperationLink) + action = static_cast<GdkDragAction>(action | GDK_ACTION_LINK); + if (op & WebDragOperationMove) + action = static_cast<GdkDragAction>(action | GDK_ACTION_MOVE); + return action; +} + +WebDragOperationsMask GdkDragActionToWebDragOp(GdkDragAction action) { + WebDragOperationsMask op = WebDragOperationNone; + if (action & GDK_ACTION_COPY) + op = static_cast<WebDragOperationsMask>(op | WebDragOperationCopy); + if (action & GDK_ACTION_LINK) + op = static_cast<WebDragOperationsMask>(op | WebDragOperationLink); + if (action & GDK_ACTION_MOVE) + op = static_cast<WebDragOperationsMask>(op | WebDragOperationMove); + return op; +} + +} // namespace content diff --git a/content/browser/web_contents/drag_utils_gtk.h b/content/browser/web_contents/drag_utils_gtk.h new file mode 100644 index 0000000..f94eeda --- /dev/null +++ b/content/browser/web_contents/drag_utils_gtk.h @@ -0,0 +1,25 @@ +// Copyright (c) 2012 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. + +#ifndef CONTENT_BROWSER_WEB_CONTENTS_DRAG_UTILS_GTK_H_ +#define CONTENT_BROWSER_WEB_CONTENTS_DRAG_UTILS_GTK_H_ +#pragma once + +#include <gtk/gtk.h> + +#include "content/common/content_export.h" +#include "third_party/WebKit/Source/WebKit/chromium/public/WebDragOperation.h" + +namespace content { + +// Convenience methods for converting between web drag operations and the GDK +// equivalent. +CONTENT_EXPORT GdkDragAction WebDragOpToGdkDragAction( + WebKit::WebDragOperationsMask op); +CONTENT_EXPORT WebKit::WebDragOperationsMask GdkDragActionToWebDragOp( + GdkDragAction action); + +} // namespace content + +#endif // CONTENT_BROWSER_WEB_CONTENTS_DRAG_UTILS_GTK_H_ diff --git a/content/browser/web_contents/interstitial_page_impl.cc b/content/browser/web_contents/interstitial_page_impl.cc new file mode 100644 index 0000000..0c47523 --- /dev/null +++ b/content/browser/web_contents/interstitial_page_impl.cc @@ -0,0 +1,750 @@ +// Copyright (c) 2012 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. + +#include "content/browser/web_contents/interstitial_page_impl.h" + +#include <vector> + +#include "base/bind.h" +#include "base/compiler_specific.h" +#include "base/message_loop.h" +#include "base/string_util.h" +#include "base/threading/thread.h" +#include "base/utf_string_conversions.h" +#include "content/browser/renderer_host/render_process_host_impl.h" +#include "content/browser/renderer_host/render_view_host_impl.h" +#include "content/browser/renderer_host/resource_dispatcher_host_impl.h" +#include "content/browser/site_instance_impl.h" +#include "content/browser/tab_contents/tab_contents.h" +#include "content/browser/web_contents/navigation_controller_impl.h" +#include "content/browser/web_contents/navigation_entry_impl.h" +#include "content/common/dom_storage_common.h" +#include "content/common/view_messages.h" +#include "content/port/browser/render_widget_host_view_port.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/dom_operation_notification_details.h" +#include "content/public/browser/interstitial_page_delegate.h" +#include "content/public/browser/invalidate_type.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_source.h" +#include "content/public/browser/web_contents_view.h" +#include "content/public/common/bindings_policy.h" +#include "content/public/common/page_transition_types.h" +#include "content/public/common/view_type.h" +#include "net/base/escape.h" +#include "net/url_request/url_request_context_getter.h" + +using content::BrowserThread; +using content::DomOperationNotificationDetails; +using content::InterstitialPageDelegate; +using content::NavigationController; +using content::NavigationEntry; +using content::NavigationEntryImpl; +using content::RenderViewHost; +using content::RenderViewHostImpl; +using content::RenderViewHostDelegate; +using content::RenderWidgetHost; +using content::RenderWidgetHostImpl; +using content::RenderWidgetHostView; +using content::RenderWidgetHostViewPort; +using content::ResourceDispatcherHostImpl; +using content::SiteInstance; +using content::WebContents; +using content::WebContentsView; +using WebKit::WebDragOperation; +using WebKit::WebDragOperationsMask; + +namespace { + +void ResourceRequestHelper(ResourceDispatcherHostImpl* rdh, + int process_id, + int render_view_host_id, + ResourceRequestAction action) { + switch (action) { + case BLOCK: + rdh->BlockRequestsForRoute(process_id, render_view_host_id); + break; + case RESUME: + rdh->ResumeBlockedRequestsForRoute(process_id, render_view_host_id); + break; + case CANCEL: + rdh->CancelBlockedRequestsForRoute(process_id, render_view_host_id); + break; + default: + NOTREACHED(); + } +} + +} // namespace + +class InterstitialPageImpl::InterstitialPageRVHViewDelegate + : public RenderViewHostDelegate::View { + public: + explicit InterstitialPageRVHViewDelegate(InterstitialPageImpl* page); + + // RenderViewHostDelegate::View implementation: + virtual void CreateNewWindow( + int route_id, + const ViewHostMsg_CreateWindow_Params& params); + virtual void CreateNewWidget(int route_id, + WebKit::WebPopupType popup_type); + virtual void CreateNewFullscreenWidget(int route_id); + virtual void ShowCreatedWindow(int route_id, + WindowOpenDisposition disposition, + const gfx::Rect& initial_pos, + bool user_gesture); + virtual void ShowCreatedWidget(int route_id, + const gfx::Rect& initial_pos); + virtual void ShowCreatedFullscreenWidget(int route_id); + virtual void ShowContextMenu(const content::ContextMenuParams& params); + virtual void ShowPopupMenu(const gfx::Rect& bounds, + int item_height, + double item_font_size, + int selected_item, + const std::vector<WebMenuItem>& items, + bool right_aligned); + virtual void StartDragging(const WebDropData& drop_data, + WebDragOperationsMask operations_allowed, + const SkBitmap& image, + const gfx::Point& image_offset); + virtual void UpdateDragCursor(WebDragOperation operation); + virtual void GotFocus(); + virtual void TakeFocus(bool reverse); + virtual void OnFindReply(int request_id, + int number_of_matches, + const gfx::Rect& selection_rect, + int active_match_ordinal, + bool final_update); + + private: + InterstitialPageImpl* interstitial_page_; + + DISALLOW_COPY_AND_ASSIGN(InterstitialPageRVHViewDelegate); +}; + + +// We keep a map of the various blocking pages shown as the UI tests need to +// be able to retrieve them. +typedef std::map<WebContents*, InterstitialPageImpl*> InterstitialPageMap; +static InterstitialPageMap* g_tab_to_interstitial_page; + +// Initializes g_tab_to_interstitial_page in a thread-safe manner. +// Should be called before accessing g_tab_to_interstitial_page. +static void InitInterstitialPageMap() { + if (!g_tab_to_interstitial_page) + g_tab_to_interstitial_page = new InterstitialPageMap; +} + +namespace content { + +InterstitialPage* InterstitialPage::Create(WebContents* tab, + bool new_navigation, + const GURL& url, + InterstitialPageDelegate* delegate) { + return new InterstitialPageImpl(tab, new_navigation, url, delegate); +} + +InterstitialPage* InterstitialPage::GetInterstitialPage( + WebContents* web_contents) { + InitInterstitialPageMap(); + InterstitialPageMap::const_iterator iter = + g_tab_to_interstitial_page->find(web_contents); + if (iter == g_tab_to_interstitial_page->end()) + return NULL; + + return iter->second; +} + +} // namespace content + +InterstitialPageImpl::InterstitialPageImpl(WebContents* tab, + bool new_navigation, + const GURL& url, + InterstitialPageDelegate* delegate) + : tab_(static_cast<TabContents*>(tab)), + url_(url), + new_navigation_(new_navigation), + should_discard_pending_nav_entry_(new_navigation), + reload_on_dont_proceed_(false), + enabled_(true), + action_taken_(NO_ACTION), + render_view_host_(NULL), + original_child_id_(tab->GetRenderProcessHost()->GetID()), + original_rvh_id_(tab->GetRenderViewHost()->GetRoutingID()), + should_revert_tab_title_(false), + tab_was_loading_(false), + resource_dispatcher_host_notified_(false), + ALLOW_THIS_IN_INITIALIZER_LIST(rvh_view_delegate_( + new InterstitialPageRVHViewDelegate(this))), + create_view_(true), + delegate_(delegate) { + InitInterstitialPageMap(); + // It would be inconsistent to create an interstitial with no new navigation + // (which is the case when the interstitial was triggered by a sub-resource on + // a page) when we have a pending entry (in the process of loading a new top + // frame). + DCHECK(new_navigation || !tab->GetController().GetPendingEntry()); +} + +InterstitialPageImpl::~InterstitialPageImpl() { +} + +void InterstitialPageImpl::Show() { + // If an interstitial is already showing or about to be shown, close it before + // showing the new one. + // Be careful not to take an action on the old interstitial more than once. + InterstitialPageMap::const_iterator iter = + g_tab_to_interstitial_page->find(tab_); + if (iter != g_tab_to_interstitial_page->end()) { + InterstitialPageImpl* interstitial = iter->second; + if (interstitial->action_taken_ != NO_ACTION) { + interstitial->Hide(); + delete interstitial; + } else { + // If we are currently showing an interstitial page for which we created + // a transient entry and a new interstitial is shown as the result of a + // new browser initiated navigation, then that transient entry has already + // been discarded and a new pending navigation entry created. + // So we should not discard that new pending navigation entry. + // See http://crbug.com/9791 + if (new_navigation_ && interstitial->new_navigation_) + interstitial->should_discard_pending_nav_entry_= false; + interstitial->DontProceed(); + } + } + + // Block the resource requests for the render view host while it is hidden. + TakeActionOnResourceDispatcher(BLOCK); + // We need to be notified when the RenderViewHost is destroyed so we can + // cancel the blocked requests. We cannot do that on + // NOTIFY_TAB_CONTENTS_DESTROYED as at that point the RenderViewHost has + // already been destroyed. + notification_registrar_.Add( + this, content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED, + content::Source<RenderWidgetHost>(tab_->GetRenderViewHost())); + + // Update the g_tab_to_interstitial_page map. + iter = g_tab_to_interstitial_page->find(tab_); + DCHECK(iter == g_tab_to_interstitial_page->end()); + (*g_tab_to_interstitial_page)[tab_] = this; + + if (new_navigation_) { + NavigationEntryImpl* entry = new NavigationEntryImpl; + entry->SetURL(url_); + entry->SetVirtualURL(url_); + entry->set_page_type(content::PAGE_TYPE_INTERSTITIAL); + + // Give delegates a chance to set some states on the navigation entry. + delegate_->OverrideEntry(entry); + + tab_->GetControllerImpl().AddTransientEntry(entry); + } + + DCHECK(!render_view_host_); + render_view_host_ = static_cast<RenderViewHostImpl*>(CreateRenderViewHost()); + CreateWebContentsView(); + + std::string data_url = "data:text/html;charset=utf-8," + + net::EscapePath(delegate_->GetHTMLContents()); + render_view_host_->NavigateToURL(GURL(data_url)); + + notification_registrar_.Add(this, + content::NOTIFICATION_WEB_CONTENTS_DESTROYED, + content::Source<WebContents>(tab_)); + notification_registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED, + content::Source<NavigationController>(&tab_->GetController())); + notification_registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_PENDING, + content::Source<NavigationController>(&tab_->GetController())); + notification_registrar_.Add( + this, content::NOTIFICATION_DOM_OPERATION_RESPONSE, + content::Source<RenderViewHost>(render_view_host_)); +} + +void InterstitialPageImpl::Hide() { + RenderWidgetHostView* old_view = tab_->GetRenderViewHost()->GetView(); + if (tab_->GetInterstitialPage() == this && + old_view && !old_view->IsShowing()) { + // Show the original RVH since we're going away. Note it might not exist if + // the renderer crashed while the interstitial was showing. + // Note that it is important that we don't call Show() if the view is + // already showing. That would result in bad things (unparented HWND on + // Windows for example) happening. + old_view->Show(); + } + + // If the focus was on the interstitial, let's keep it to the page. + // (Note that in unit-tests the RVH may not have a view). + if (render_view_host_->GetView() && + render_view_host_->GetView()->HasFocus() && + tab_->GetRenderViewHost()->GetView()) { + RenderWidgetHostViewPort::FromRWHV( + tab_->GetRenderViewHost()->GetView())->Focus(); + } + + render_view_host_->Shutdown(); + render_view_host_ = NULL; + if (tab_->GetInterstitialPage()) + tab_->remove_interstitial_page(); + // Let's revert to the original title if necessary. + NavigationEntry* entry = tab_->GetController().GetActiveEntry(); + if (!new_navigation_ && should_revert_tab_title_) { + entry->SetTitle(original_tab_title_); + tab_->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TITLE); + } + + content::NotificationService::current()->Notify( + content::NOTIFICATION_INTERSTITIAL_DETACHED, + content::Source<WebContents>(tab_), + content::NotificationService::NoDetails()); + + InterstitialPageMap::iterator iter = g_tab_to_interstitial_page->find(tab_); + DCHECK(iter != g_tab_to_interstitial_page->end()); + if (iter != g_tab_to_interstitial_page->end()) + g_tab_to_interstitial_page->erase(iter); +} + +void InterstitialPageImpl::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + switch (type) { + case content::NOTIFICATION_NAV_ENTRY_PENDING: + // We are navigating away from the interstitial (the user has typed a URL + // in the location bar or clicked a bookmark). Make sure clicking on the + // interstitial will have no effect. Also cancel any blocked requests + // on the ResourceDispatcherHost. Note that when we get this notification + // the RenderViewHost has not yet navigated so we'll unblock the + // RenderViewHost before the resource request for the new page we are + // navigating arrives in the ResourceDispatcherHost. This ensures that + // request won't be blocked if the same RenderViewHost was used for the + // new navigation. + Disable(); + TakeActionOnResourceDispatcher(CANCEL); + break; + case content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED: + if (action_taken_ == NO_ACTION) { + // The RenderViewHost is being destroyed (as part of the tab being + // closed); make sure we clear the blocked requests. + RenderViewHost* rvh = static_cast<RenderViewHost*>( + static_cast<RenderViewHostImpl*>( + RenderWidgetHostImpl::From( + content::Source<RenderWidgetHost>(source).ptr()))); + DCHECK(rvh->GetProcess()->GetID() == original_child_id_ && + rvh->GetRoutingID() == original_rvh_id_); + TakeActionOnResourceDispatcher(CANCEL); + } + break; + case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: + case content::NOTIFICATION_NAV_ENTRY_COMMITTED: + if (action_taken_ == NO_ACTION) { + // We are navigating away from the interstitial or closing a tab with an + // interstitial. Default to DontProceed(). We don't just call Hide as + // subclasses will almost certainly override DontProceed to do some work + // (ex: close pending connections). + DontProceed(); + } else { + // User decided to proceed and either the navigation was committed or + // the tab was closed before that. + Hide(); + delete this; + } + break; + case content::NOTIFICATION_DOM_OPERATION_RESPONSE: + if (enabled()) { + content::Details<DomOperationNotificationDetails> dom_op_details( + details); + delegate_->CommandReceived(dom_op_details->json); + } + break; + default: + NOTREACHED(); + } +} + +RenderViewHostDelegate::View* InterstitialPageImpl::GetViewDelegate() { + return rvh_view_delegate_.get(); +} + +const GURL& InterstitialPageImpl::GetURL() const { + return url_; +} + +void InterstitialPageImpl::RenderViewGone(RenderViewHost* render_view_host, + base::TerminationStatus status, + int error_code) { + // Our renderer died. This should not happen in normal cases. + // Just dismiss the interstitial. + DontProceed(); +} + +void InterstitialPageImpl::DidNavigate( + RenderViewHost* render_view_host, + const ViewHostMsg_FrameNavigate_Params& params) { + // A fast user could have navigated away from the page that triggered the + // interstitial while the interstitial was loading, that would have disabled + // us. In that case we can dismiss ourselves. + if (!enabled_) { + DontProceed(); + return; + } + if (params.transition == content::PAGE_TRANSITION_AUTO_SUBFRAME) { + // No need to handle navigate message from iframe in the interstitial page. + return; + } + + // The RenderViewHost has loaded its contents, we can show it now. + render_view_host_->GetView()->Show(); + tab_->set_interstitial_page(this); + + // This notification hides the bookmark bar. Note that this has to happen + // after the interstitial page was registered with |tab_|, since there will be + // a callback to |tab_| testing if an interstitial page is showing before + // hiding the bookmark bar. + content::NotificationService::current()->Notify( + content::NOTIFICATION_INTERSTITIAL_ATTACHED, + content::Source<WebContents>(tab_), + content::NotificationService::NoDetails()); + + RenderWidgetHostView* rwh_view = tab_->GetRenderViewHost()->GetView(); + + // The RenderViewHost may already have crashed before we even get here. + if (rwh_view) { + // If the page has focus, focus the interstitial. + if (rwh_view->HasFocus()) + Focus(); + + // Hide the original RVH since we're showing the interstitial instead. + rwh_view->Hide(); + } + + // Notify the tab we are not loading so the throbber is stopped. It also + // causes a NOTIFY_LOAD_STOP notification, that the AutomationProvider (used + // by the UI tests) expects to consider a navigation as complete. Without + // this, navigating in a UI test to a URL that triggers an interstitial would + // hang. + tab_was_loading_ = tab_->IsLoading(); + tab_->SetIsLoading(false, NULL); +} + +void InterstitialPageImpl::UpdateTitle( + RenderViewHost* render_view_host, + int32 page_id, + const string16& title, + base::i18n::TextDirection title_direction) { + DCHECK(render_view_host == render_view_host_); + NavigationEntry* entry = tab_->GetController().GetActiveEntry(); + if (!entry) { + // Crash reports from the field indicate this can be NULL. + // This is unexpected as InterstitialPages constructed with the + // new_navigation flag set to true create a transient navigation entry + // (that is returned as the active entry). And the only case so far of + // interstitial created with that flag set to false is with the + // SafeBrowsingBlockingPage, when the resource triggering the interstitial + // is a sub-resource, meaning the main page has already been loaded and a + // navigation entry should have been created. + NOTREACHED(); + return; + } + + // If this interstitial is shown on an existing navigation entry, we'll need + // to remember its title so we can revert to it when hidden. + if (!new_navigation_ && !should_revert_tab_title_) { + original_tab_title_ = entry->GetTitle(); + should_revert_tab_title_ = true; + } + // TODO(evan): make use of title_direction. + // http://code.google.com/p/chromium/issues/detail?id=27094 + entry->SetTitle(title); + tab_->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TITLE); +} + +content::RendererPreferences InterstitialPageImpl::GetRendererPrefs( + content::BrowserContext* browser_context) const { + delegate_->OverrideRendererPrefs(&renderer_preferences_); + return renderer_preferences_; +} + +WebPreferences InterstitialPageImpl::GetWebkitPrefs() { + return TabContents::GetWebkitPrefs(render_view_host_, url_); +} + +bool InterstitialPageImpl::PreHandleKeyboardEvent( + const NativeWebKeyboardEvent& event, + bool* is_keyboard_shortcut) { + return tab_->PreHandleKeyboardEvent(event, is_keyboard_shortcut); +} + +void InterstitialPageImpl::HandleKeyboardEvent( + const NativeWebKeyboardEvent& event) { + return tab_->HandleKeyboardEvent(event); +} + +WebContents* InterstitialPageImpl::tab() const { + return tab_; +} + +RenderViewHost* InterstitialPageImpl::CreateRenderViewHost() { + RenderViewHostImpl* render_view_host = new RenderViewHostImpl( + SiteInstance::Create(tab()->GetBrowserContext()), + this, MSG_ROUTING_NONE, kInvalidSessionStorageNamespaceId); + return render_view_host; +} + +WebContentsView* InterstitialPageImpl::CreateWebContentsView() { + if (!create_view_) + return NULL; + WebContentsView* web_contents_view = tab()->GetView(); + RenderWidgetHostView* view = + web_contents_view->CreateViewForWidget(render_view_host_); + render_view_host_->SetView(view); + render_view_host_->AllowBindings(content::BINDINGS_POLICY_DOM_AUTOMATION); + + int32 max_page_id = + tab()->GetMaxPageIDForSiteInstance(render_view_host_->GetSiteInstance()); + render_view_host_->CreateRenderView(string16(), max_page_id); + view->SetSize(web_contents_view->GetContainerSize()); + // Don't show the interstitial until we have navigated to it. + view->Hide(); + return web_contents_view; +} + +void InterstitialPageImpl::Proceed() { + if (action_taken_ != NO_ACTION) { + NOTREACHED(); + return; + } + Disable(); + action_taken_ = PROCEED_ACTION; + + // Resumes the throbber, if applicable. + if (tab_was_loading_) + tab_->SetIsLoading(true, NULL); + + // If this is a new navigation, the old page is going away, so we cancel any + // blocked requests for it. If it is not a new navigation, then it means the + // interstitial was shown as a result of a resource loading in the page. + // Since the user wants to proceed, we'll let any blocked request go through. + if (new_navigation_) + TakeActionOnResourceDispatcher(CANCEL); + else + TakeActionOnResourceDispatcher(RESUME); + + // No need to hide if we are a new navigation, we'll get hidden when the + // navigation is committed. + if (!new_navigation_) { + Hide(); + delegate_->OnProceed(); + delete this; + return; + } + + delegate_->OnProceed(); +} + +void InterstitialPageImpl::DontProceed() { + DCHECK(action_taken_ != DONT_PROCEED_ACTION); + + Disable(); + action_taken_ = DONT_PROCEED_ACTION; + + // If this is a new navigation, we are returning to the original page, so we + // resume blocked requests for it. If it is not a new navigation, then it + // means the interstitial was shown as a result of a resource loading in the + // page and we won't return to the original page, so we cancel blocked + // requests in that case. + if (new_navigation_) + TakeActionOnResourceDispatcher(RESUME); + else + TakeActionOnResourceDispatcher(CANCEL); + + if (should_discard_pending_nav_entry_) { + // Since no navigation happens we have to discard the transient entry + // explicitely. Note that by calling DiscardNonCommittedEntries() we also + // discard the pending entry, which is what we want, since the navigation is + // cancelled. + tab_->GetController().DiscardNonCommittedEntries(); + } + + if (reload_on_dont_proceed_) + tab_->GetController().Reload(true); + + Hide(); + delegate_->OnDontProceed(); + delete this; +} + +void InterstitialPageImpl::CancelForNavigation() { + // The user is trying to navigate away. We should unblock the renderer and + // disable the interstitial, but keep it visible until the navigation + // completes. + Disable(); + // If this interstitial was shown for a new navigation, allow any navigations + // on the original page to resume (e.g., subresource requests, XHRs, etc). + // Otherwise, cancel the pending, possibly dangerous navigations. + if (new_navigation_) + TakeActionOnResourceDispatcher(RESUME); + else + TakeActionOnResourceDispatcher(CANCEL); +} + +void InterstitialPageImpl::SetSize(const gfx::Size& size) { +#if !defined(OS_MACOSX) + // When a tab is closed, we might be resized after our view was NULLed + // (typically if there was an info-bar). + if (render_view_host_->GetView()) + render_view_host_->GetView()->SetSize(size); +#else + // TODO(port): Does Mac need to SetSize? + NOTIMPLEMENTED(); +#endif +} + +void InterstitialPageImpl::Focus() { + // Focus the native window. + RenderWidgetHostViewPort::FromRWHV(render_view_host_->GetView())->Focus(); +} + +void InterstitialPageImpl::FocusThroughTabTraversal(bool reverse) { + render_view_host_->SetInitialFocus(reverse); +} + +RenderViewHost* InterstitialPageImpl::GetRenderViewHostForTesting() const { + return render_view_host_; +} + +InterstitialPageDelegate* InterstitialPageImpl::GetDelegateForTesting() { + return delegate_.get(); +} + +void InterstitialPageImpl::DontCreateViewForTesting() { + create_view_ = false; +} + +content::ViewType InterstitialPageImpl::GetRenderViewType() const { + return content::VIEW_TYPE_INTERSTITIAL_PAGE; +} + +gfx::Rect InterstitialPageImpl::GetRootWindowResizerRect() const { + return gfx::Rect(); +} + +void InterstitialPageImpl::Disable() { + enabled_ = false; +} + +void InterstitialPageImpl::TakeActionOnResourceDispatcher( + ResourceRequestAction action) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)) << + "TakeActionOnResourceDispatcher should be called on the main thread."; + + if (action == CANCEL || action == RESUME) { + if (resource_dispatcher_host_notified_) + return; + resource_dispatcher_host_notified_ = true; + } + + // The tab might not have a render_view_host if it was closed (in which case, + // we have taken care of the blocked requests when processing + // NOTIFY_RENDER_WIDGET_HOST_DESTROYED. + // Also we need to test there is a ResourceDispatcherHostImpl, as when unit- + // tests we don't have one. + RenderViewHostImpl* rvh = RenderViewHostImpl::FromID(original_child_id_, + original_rvh_id_); + if (!rvh || !ResourceDispatcherHostImpl::Get()) + return; + + BrowserThread::PostTask( + BrowserThread::IO, + FROM_HERE, + base::Bind( + &ResourceRequestHelper, + ResourceDispatcherHostImpl::Get(), + original_child_id_, + original_rvh_id_, + action)); +} + +InterstitialPageImpl::InterstitialPageRVHViewDelegate:: + InterstitialPageRVHViewDelegate(InterstitialPageImpl* page) + : interstitial_page_(page) { +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::CreateNewWindow( + int route_id, + const ViewHostMsg_CreateWindow_Params& params) { + NOTREACHED() << "InterstitialPage does not support showing popups yet."; +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::CreateNewWidget( + int route_id, WebKit::WebPopupType popup_type) { + NOTREACHED() << "InterstitialPage does not support showing drop-downs yet."; +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate:: + CreateNewFullscreenWidget(int route_id) { + NOTREACHED() + << "InterstitialPage does not support showing full screen popups."; +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::ShowCreatedWindow( + int route_id, WindowOpenDisposition disposition, + const gfx::Rect& initial_pos, bool user_gesture) { + NOTREACHED() << "InterstitialPage does not support showing popups yet."; +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::ShowCreatedWidget( + int route_id, const gfx::Rect& initial_pos) { + NOTREACHED() << "InterstitialPage does not support showing drop-downs yet."; +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate:: + ShowCreatedFullscreenWidget(int route_id) { + NOTREACHED() + << "InterstitialPage does not support showing full screen popups."; +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::ShowContextMenu( + const content::ContextMenuParams& params) { +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::ShowPopupMenu( + const gfx::Rect& bounds, + int item_height, + double item_font_size, + int selected_item, + const std::vector<WebMenuItem>& items, + bool right_aligned) { +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::StartDragging( + const WebDropData& drop_data, + WebDragOperationsMask allowed_operations, + const SkBitmap& image, + const gfx::Point& image_offset) { + NOTREACHED() << "InterstitialPage does not support dragging yet."; +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::UpdateDragCursor( + WebDragOperation) { + NOTREACHED() << "InterstitialPage does not support dragging yet."; +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::GotFocus() { +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::TakeFocus( + bool reverse) { + if (!interstitial_page_->tab()) + return; + TabContents* tab = static_cast<TabContents*>(interstitial_page_->tab()); + if (!tab->GetViewDelegate()) + return; + + tab->GetViewDelegate()->TakeFocus(reverse); +} + +void InterstitialPageImpl::InterstitialPageRVHViewDelegate::OnFindReply( + int request_id, int number_of_matches, const gfx::Rect& selection_rect, + int active_match_ordinal, bool final_update) { +} diff --git a/content/browser/web_contents/interstitial_page_impl.h b/content/browser/web_contents/interstitial_page_impl.h new file mode 100644 index 0000000..2b6c026 --- /dev/null +++ b/content/browser/web_contents/interstitial_page_impl.h @@ -0,0 +1,198 @@ +// Copyright (c) 2012 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. + +#ifndef CONTENT_BROWSER_WEB_CONTENTS_INTERSTITIAL_PAGE_IMPL_H_ +#define CONTENT_BROWSER_WEB_CONTENTS_INTERSTITIAL_PAGE_IMPL_H_ +#pragma once + +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/process_util.h" +#include "content/public/browser/interstitial_page.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "content/public/browser/render_view_host_delegate.h" +#include "content/public/common/renderer_preferences.h" +#include "googleurl/src/gurl.h" + +class WebContentsImpl; + +namespace content { +class NavigationEntry; +class RenderViewHostImpl; +class WebContentsView; +} + +enum ResourceRequestAction { + BLOCK, + RESUME, + CANCEL +}; + +class CONTENT_EXPORT InterstitialPageImpl + : public NON_EXPORTED_BASE(content::InterstitialPage), + public content::NotificationObserver, + public content::RenderViewHostDelegate { + public: + // The different state of actions the user can take in an interstitial. + enum ActionState { + NO_ACTION, // No action has been taken yet. + PROCEED_ACTION, // "Proceed" was selected. + DONT_PROCEED_ACTION // "Don't proceed" was selected. + }; + + InterstitialPageImpl(content::WebContents* tab, + bool new_navigation, + const GURL& url, + content::InterstitialPageDelegate* delegate); + virtual ~InterstitialPageImpl(); + + // InterstitialPage implementation: + virtual void Show() OVERRIDE; + virtual void Hide() OVERRIDE; + virtual void DontProceed() OVERRIDE; + virtual void Proceed() OVERRIDE; + virtual content::RenderViewHost* GetRenderViewHostForTesting() const OVERRIDE; + virtual content::InterstitialPageDelegate* GetDelegateForTesting() OVERRIDE; + virtual void DontCreateViewForTesting() OVERRIDE; + virtual void SetSize(const gfx::Size& size) OVERRIDE; + virtual void Focus() OVERRIDE; + + // Allows the user to navigate away by disabling the interstitial, canceling + // the pending request, and unblocking the hidden renderer. The interstitial + // will stay visible until the navigation completes. + void CancelForNavigation(); + + // Focus the first (last if reverse is true) element in the interstitial page. + // Called when tab traversing. + void FocusThroughTabTraversal(bool reverse); + + // See description above field. + void set_reload_on_dont_proceed(bool value) { + reload_on_dont_proceed_ = value; + } + bool reload_on_dont_proceed() const { return reload_on_dont_proceed_; } + + protected: + // content::NotificationObserver method: + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + // RenderViewHostDelegate implementation: + virtual View* GetViewDelegate() OVERRIDE; + virtual const GURL& GetURL() const OVERRIDE; + virtual void RenderViewGone(content::RenderViewHost* render_view_host, + base::TerminationStatus status, + int error_code) OVERRIDE; + virtual void DidNavigate( + content::RenderViewHost* render_view_host, + const ViewHostMsg_FrameNavigate_Params& params) OVERRIDE; + virtual void UpdateTitle(content::RenderViewHost* render_view_host, + int32 page_id, + const string16& title, + base::i18n::TextDirection title_direction) OVERRIDE; + virtual content::RendererPreferences GetRendererPrefs( + content::BrowserContext* browser_context) const OVERRIDE; + virtual WebPreferences GetWebkitPrefs() OVERRIDE; + virtual bool PreHandleKeyboardEvent(const NativeWebKeyboardEvent& event, + bool* is_keyboard_shortcut) OVERRIDE; + virtual void HandleKeyboardEvent( + const NativeWebKeyboardEvent& event) OVERRIDE; + virtual content::ViewType GetRenderViewType() const OVERRIDE; + virtual gfx::Rect GetRootWindowResizerRect() const OVERRIDE; + + bool enabled() const { return enabled_; } + content::WebContents* tab() const; + const GURL& url() const { return url_; } + + // Creates the RenderViewHost containing the interstitial content. + // Overriden in unit tests. + virtual content::RenderViewHost* CreateRenderViewHost(); + + // Creates the WebContentsView that shows the interstitial RVH. + // Overriden in unit tests. + virtual content::WebContentsView* CreateWebContentsView(); + + // Notification magic. + content::NotificationRegistrar notification_registrar_; + + private: + class InterstitialPageRVHViewDelegate; + + // Disable the interstitial: + // - if it is not yet showing, then it won't be shown. + // - any command sent by the RenderViewHost will be ignored. + void Disable(); + + // Executes the passed action on the ResourceDispatcher (on the IO thread). + // Used to block/resume/cancel requests for the RenderViewHost hidden by this + // interstitial. + void TakeActionOnResourceDispatcher(ResourceRequestAction action); + + // The contents in which we are displayed. + WebContentsImpl* tab_; + + // The URL that is shown when the interstitial is showing. + GURL url_; + + // Whether this interstitial is shown as a result of a new navigation (in + // which case a transient navigation entry is created). + bool new_navigation_; + + // Whether we should discard the pending navigation entry when not proceeding. + // This is to deal with cases where |new_navigation_| is true but a new + // pending entry was created since this interstitial was shown and we should + // not discard it. + bool should_discard_pending_nav_entry_; + + // If true and the user chooses not to proceed the target NavigationController + // is reloaded. This is used when two NavigationControllers are merged + // (CopyStateFromAndPrune). + // The default is false. + bool reload_on_dont_proceed_; + + // Whether this interstitial is enabled. See Disable() for more info. + bool enabled_; + + // Whether the Proceed or DontProceed methods have been called yet. + ActionState action_taken_; + + // The RenderViewHost displaying the interstitial contents. + content::RenderViewHostImpl* render_view_host_; + + // The IDs for the Render[View|Process]Host hidden by this interstitial. + int original_child_id_; + int original_rvh_id_; + + // Whether or not we should change the title of the tab when hidden (to revert + // it to its original value). + bool should_revert_tab_title_; + + // Whether or not the tab was loading resources when the interstitial was + // shown. We restore this state if the user proceeds from the interstitial. + bool tab_was_loading_; + + // Whether the ResourceDispatcherHost has been notified to cancel/resume the + // resource requests blocked for the RenderViewHost. + bool resource_dispatcher_host_notified_; + + // The original title of the tab that should be reverted to when the + // interstitial is hidden. + string16 original_tab_title_; + + // Our RenderViewHostViewDelegate, necessary for accelerators to work. + scoped_ptr<InterstitialPageRVHViewDelegate> rvh_view_delegate_; + + // Settings passed to the renderer. + mutable content::RendererPreferences renderer_preferences_; + + bool create_view_; + + scoped_ptr<content::InterstitialPageDelegate> delegate_; + + DISALLOW_COPY_AND_ASSIGN(InterstitialPageImpl); +}; + +#endif // CONTENT_BROWSER_WEB_CONTENTS_INTERSTITIAL_PAGE_IMPL_H_ diff --git a/content/browser/web_contents/navigation_controller_impl.cc b/content/browser/web_contents/navigation_controller_impl.cc new file mode 100644 index 0000000..6b43023 --- /dev/null +++ b/content/browser/web_contents/navigation_controller_impl.cc @@ -0,0 +1,1441 @@ +// Copyright (c) 2012 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. + +#include "content/browser/web_contents/navigation_controller_impl.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/string_number_conversions.h" // Temporary +#include "base/string_util.h" +#include "base/time.h" +#include "base/utf_string_conversions.h" +#include "content/browser/browser_url_handler_impl.h" +#include "content/browser/child_process_security_policy_impl.h" +#include "content/browser/in_process_webkit/dom_storage_context_impl.h" +#include "content/browser/in_process_webkit/session_storage_namespace_impl.h" +#include "content/browser/renderer_host/render_view_host_impl.h" // Temporary +#include "content/browser/site_instance_impl.h" +#include "content/browser/tab_contents/tab_contents.h" +#include "content/browser/web_contents/debug_urls.h" +#include "content/browser/web_contents/interstitial_page_impl.h" +#include "content/browser/web_contents/navigation_entry_impl.h" +#include "content/common/view_messages.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/invalidate_type.h" +#include "content/public/browser/navigation_details.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/user_metrics.h" +#include "content/public/browser/web_contents_delegate.h" +#include "content/public/common/content_constants.h" +#include "net/base/escape.h" +#include "net/base/mime_util.h" +#include "net/base/net_util.h" +#include "webkit/glue/webkit_glue.h" + +using content::BrowserContext; +using content::DOMStorageContext; +using content::GlobalRequestID; +using content::NavigationController; +using content::NavigationEntry; +using content::NavigationEntryImpl; +using content::RenderViewHostImpl; +using content::SessionStorageNamespace; +using content::SiteInstance; +using content::UserMetricsAction; +using content::WebContents; + +namespace { + +const int kInvalidateAll = 0xFFFFFFFF; + +// Invoked when entries have been pruned, or removed. For example, if the +// current entries are [google, digg, yahoo], with the current entry google, +// and the user types in cnet, then digg and yahoo are pruned. +void NotifyPrunedEntries(NavigationControllerImpl* nav_controller, + bool from_front, + int count) { + content::PrunedDetails details; + details.from_front = from_front; + details.count = count; + content::NotificationService::current()->Notify( + content::NOTIFICATION_NAV_LIST_PRUNED, + content::Source<NavigationController>(nav_controller), + content::Details<content::PrunedDetails>(&details)); +} + +// Ensure the given NavigationEntry has a valid state, so that WebKit does not +// get confused if we navigate back to it. +// +// An empty state is treated as a new navigation by WebKit, which would mean +// losing the navigation entries and generating a new navigation entry after +// this one. We don't want that. To avoid this we create a valid state which +// WebKit will not treat as a new navigation. +void SetContentStateIfEmpty(NavigationEntryImpl* entry) { + if (entry->GetContentState().empty()) { + entry->SetContentState( + webkit_glue::CreateHistoryStateForURL(entry->GetURL())); + } +} + +// Configure all the NavigationEntries in entries for restore. This resets +// the transition type to reload and makes sure the content state isn't empty. +void ConfigureEntriesForRestore( + std::vector<linked_ptr<NavigationEntryImpl> >* entries, + bool from_last_session) { + for (size_t i = 0; i < entries->size(); ++i) { + // Use a transition type of reload so that we don't incorrectly increase + // the typed count. + (*entries)[i]->SetTransitionType(content::PAGE_TRANSITION_RELOAD); + (*entries)[i]->set_restore_type(from_last_session ? + NavigationEntryImpl::RESTORE_LAST_SESSION : + NavigationEntryImpl::RESTORE_CURRENT_SESSION); + // NOTE(darin): This code is only needed for backwards compat. + SetContentStateIfEmpty((*entries)[i].get()); + } +} + +// See NavigationController::IsURLInPageNavigation for how this works and why. +bool AreURLsInPageNavigation(const GURL& existing_url, const GURL& new_url) { + if (existing_url == new_url || !new_url.has_ref()) { + // TODO(jcampan): what about when navigating back from a ref URL to the top + // non ref URL? Nothing is loaded in that case but we return false here. + // The user could also navigate from the ref URL to the non ref URL by + // entering the non ref URL in the location bar or through a bookmark, in + // which case there would be a load. I am not sure if the non-load/load + // scenarios can be differentiated with the TransitionType. + return false; + } + + url_canon::Replacements<char> replacements; + replacements.ClearRef(); + return existing_url.ReplaceComponents(replacements) == + new_url.ReplaceComponents(replacements); +} + +} // namespace + +// NavigationControllerImpl ---------------------------------------------------- + +const size_t kMaxEntryCountForTestingNotSet = -1; + +// static +size_t NavigationControllerImpl::max_entry_count_for_testing_ = + kMaxEntryCountForTestingNotSet; + + // Should Reload check for post data? The default is true, but is set to false +// when testing. +static bool g_check_for_repost = true; + +namespace content { +// static +NavigationEntry* NavigationController::CreateNavigationEntry( + const GURL& url, + const Referrer& referrer, + PageTransition transition, + bool is_renderer_initiated, + const std::string& extra_headers, + BrowserContext* browser_context) { + // Allow the browser URL handler to rewrite the URL. This will, for example, + // remove "view-source:" from the beginning of the URL to get the URL that + // will actually be loaded. This real URL won't be shown to the user, just + // used internally. + GURL loaded_url(url); + bool reverse_on_redirect = false; + BrowserURLHandlerImpl::GetInstance()->RewriteURLIfNecessary( + &loaded_url, browser_context, &reverse_on_redirect); + + NavigationEntryImpl* entry = new NavigationEntryImpl( + NULL, // The site instance for tabs is sent on navigation + // (TabContents::GetSiteInstance). + -1, + loaded_url, + referrer, + string16(), + transition, + is_renderer_initiated); + entry->SetVirtualURL(url); + entry->set_user_typed_url(url); + entry->set_update_virtual_url_with_url(reverse_on_redirect); + entry->set_extra_headers(extra_headers); + return entry; +} + +// static +void NavigationController::DisablePromptOnRepost() { + g_check_for_repost = false; +} + +} // namespace content + +NavigationControllerImpl::NavigationControllerImpl( + TabContents* contents, + BrowserContext* browser_context, + SessionStorageNamespaceImpl* session_storage_namespace) + : browser_context_(browser_context), + pending_entry_(NULL), + last_committed_entry_index_(-1), + pending_entry_index_(-1), + transient_entry_index_(-1), + tab_contents_(contents), + max_restored_page_id_(-1), + ALLOW_THIS_IN_INITIALIZER_LIST(ssl_manager_(this)), + needs_reload_(false), + session_storage_namespace_(session_storage_namespace), + pending_reload_(NO_RELOAD) { + DCHECK(browser_context_); + if (!session_storage_namespace_) { + session_storage_namespace_ = new SessionStorageNamespaceImpl( + static_cast<DOMStorageContextImpl*>( + BrowserContext::GetDOMStorageContext(browser_context_))); + } +} + +NavigationControllerImpl::~NavigationControllerImpl() { + DiscardNonCommittedEntriesInternal(); + + content::NotificationService::current()->Notify( + content::NOTIFICATION_TAB_CLOSED, + content::Source<NavigationController>(this), + content::NotificationService::NoDetails()); +} + +WebContents* NavigationControllerImpl::GetWebContents() const { + return tab_contents_; +} + +BrowserContext* NavigationControllerImpl::GetBrowserContext() const { + return browser_context_; +} + +void NavigationControllerImpl::SetBrowserContext( + BrowserContext* browser_context) { + browser_context_ = browser_context; +} + +void NavigationControllerImpl::Restore( + int selected_navigation, + bool from_last_session, + std::vector<NavigationEntry*>* entries) { + // Verify that this controller is unused and that the input is valid. + DCHECK(GetEntryCount() == 0 && !GetPendingEntry()); + DCHECK(selected_navigation >= 0 && + selected_navigation < static_cast<int>(entries->size())); + + needs_reload_ = true; + for (size_t i = 0; i < entries->size(); ++i) { + NavigationEntryImpl* entry = + NavigationEntryImpl::FromNavigationEntry((*entries)[i]); + entries_.push_back(linked_ptr<NavigationEntryImpl>(entry)); + } + entries->clear(); + + // And finish the restore. + FinishRestore(selected_navigation, from_last_session); +} + +void NavigationControllerImpl::Reload(bool check_for_repost) { + ReloadInternal(check_for_repost, RELOAD); +} +void NavigationControllerImpl::ReloadIgnoringCache(bool check_for_repost) { + ReloadInternal(check_for_repost, RELOAD_IGNORING_CACHE); +} + +void NavigationControllerImpl::ReloadInternal(bool check_for_repost, + ReloadType reload_type) { + // Reloading a transient entry does nothing. + if (transient_entry_index_ != -1) + return; + + DiscardNonCommittedEntriesInternal(); + int current_index = GetCurrentEntryIndex(); + // If we are no where, then we can't reload. TODO(darin): We should add a + // CanReload method. + if (current_index == -1) { + return; + } + + if (g_check_for_repost && check_for_repost && + GetEntryAtIndex(current_index)->GetHasPostData()) { + // The user is asking to reload a page with POST data. Prompt to make sure + // they really want to do this. If they do, the dialog will call us back + // with check_for_repost = false. + content::NotificationService::current()->Notify( + content::NOTIFICATION_REPOST_WARNING_SHOWN, + content::Source<NavigationController>(this), + content::NotificationService::NoDetails()); + + pending_reload_ = reload_type; + tab_contents_->Activate(); + tab_contents_->GetDelegate()->ShowRepostFormWarningDialog(tab_contents_); + } else { + DiscardNonCommittedEntriesInternal(); + + NavigationEntryImpl* entry = entries_[current_index].get(); + SiteInstanceImpl* site_instance = entry->site_instance(); + DCHECK(site_instance); + + // If we are reloading an entry that no longer belongs to the current + // site instance (for example, refreshing a page for just installed app), + // the reload must happen in a new process. + // The new entry must have a new page_id and site instance, so it behaves + // as new navigation (which happens to clear forward history). + // Tabs that are discarded due to low memory conditions may not have a site + // instance, and should not be treated as a cross-site reload. + if (site_instance && + site_instance->HasWrongProcessForURL(entry->GetURL())) { + // Create a navigation entry that resembles the current one, but do not + // copy page id, site instance, and content state. + NavigationEntryImpl* nav_entry = NavigationEntryImpl::FromNavigationEntry( + CreateNavigationEntry( + entry->GetURL(), entry->GetReferrer(), entry->GetTransitionType(), + false, entry->extra_headers(), browser_context_)); + + // Mark the reload type as NO_RELOAD, so navigation will not be considered + // a reload in the renderer. + reload_type = NavigationController::NO_RELOAD; + + nav_entry->set_is_cross_site_reload(true); + pending_entry_ = nav_entry; + } else { + pending_entry_index_ = current_index; + + // The title of the page being reloaded might have been removed in the + // meanwhile, so we need to revert to the default title upon reload and + // invalidate the previously cached title (SetTitle will do both). + // See Chromium issue 96041. + entries_[pending_entry_index_]->SetTitle(string16()); + + entries_[pending_entry_index_]->SetTransitionType( + content::PAGE_TRANSITION_RELOAD); + } + + NavigateToPendingEntry(reload_type); + } +} + +void NavigationControllerImpl::CancelPendingReload() { + DCHECK(pending_reload_ != NO_RELOAD); + pending_reload_ = NO_RELOAD; +} + +void NavigationControllerImpl::ContinuePendingReload() { + if (pending_reload_ == NO_RELOAD) { + NOTREACHED(); + } else { + ReloadInternal(false, pending_reload_); + pending_reload_ = NO_RELOAD; + } +} + +bool NavigationControllerImpl::IsInitialNavigation() { + return last_document_loaded_.is_null(); +} + +NavigationEntryImpl* NavigationControllerImpl::GetEntryWithPageID( + SiteInstance* instance, int32 page_id) const { + int index = GetEntryIndexWithPageID(instance, page_id); + return (index != -1) ? entries_[index].get() : NULL; +} + +void NavigationControllerImpl::LoadEntry(NavigationEntryImpl* entry) { + // Don't navigate to URLs disabled by policy. This prevents showing the URL + // on the Omnibar when it is also going to be blocked by + // ChildProcessSecurityPolicy::CanRequestURL. + ChildProcessSecurityPolicyImpl* policy = + ChildProcessSecurityPolicyImpl::GetInstance(); + if (policy->IsDisabledScheme(entry->GetURL().scheme()) || + policy->IsDisabledScheme(entry->GetVirtualURL().scheme())) { + VLOG(1) << "URL not loaded because the scheme is blocked by policy: " + << entry->GetURL(); + delete entry; + return; + } + + // When navigating to a new page, we don't know for sure if we will actually + // end up leaving the current page. The new page load could for example + // result in a download or a 'no content' response (e.g., a mailto: URL). + DiscardNonCommittedEntriesInternal(); + pending_entry_ = entry; + content::NotificationService::current()->Notify( + content::NOTIFICATION_NAV_ENTRY_PENDING, + content::Source<NavigationController>(this), + content::Details<NavigationEntry>(entry)); + NavigateToPendingEntry(NO_RELOAD); +} + +NavigationEntry* NavigationControllerImpl::GetActiveEntry() const { + if (transient_entry_index_ != -1) + return entries_[transient_entry_index_].get(); + if (pending_entry_) + return pending_entry_; + return GetLastCommittedEntry(); +} + +NavigationEntry* NavigationControllerImpl::GetVisibleEntry() const { + if (transient_entry_index_ != -1) + return entries_[transient_entry_index_].get(); + // Only return the pending_entry for new (non-history), browser-initiated + // navigations, in order to prevent URL spoof attacks. + // Ideally we would also show the pending entry's URL for new renderer- + // initiated navigations with no last committed entry (e.g., a link opening + // in a new tab), but an attacker can insert content into the about:blank + // page while the pending URL loads in that case. + if (pending_entry_ && + pending_entry_->GetPageID() == -1 && + !pending_entry_->is_renderer_initiated()) + return pending_entry_; + return GetLastCommittedEntry(); +} + +int NavigationControllerImpl::GetCurrentEntryIndex() const { + if (transient_entry_index_ != -1) + return transient_entry_index_; + if (pending_entry_index_ != -1) + return pending_entry_index_; + return last_committed_entry_index_; +} + +NavigationEntry* NavigationControllerImpl::GetLastCommittedEntry() const { + if (last_committed_entry_index_ == -1) + return NULL; + return entries_[last_committed_entry_index_].get(); +} + +bool NavigationControllerImpl::CanViewSource() const { + bool is_supported_mime_type = net::IsSupportedNonImageMimeType( + tab_contents_->GetContentsMimeType().c_str()); + NavigationEntry* active_entry = GetActiveEntry(); + return active_entry && !active_entry->IsViewSourceMode() && + is_supported_mime_type && !tab_contents_->GetInterstitialPage(); +} + +int NavigationControllerImpl::GetLastCommittedEntryIndex() const { + return last_committed_entry_index_; +} + +int NavigationControllerImpl::GetEntryCount() const { + DCHECK(entries_.size() <= max_entry_count()); + return static_cast<int>(entries_.size()); +} + +NavigationEntry* NavigationControllerImpl::GetEntryAtIndex( + int index) const { + return entries_.at(index).get(); +} + +NavigationEntry* NavigationControllerImpl::GetEntryAtOffset( + int offset) const { + int index = (transient_entry_index_ != -1) ? + transient_entry_index_ + offset : + last_committed_entry_index_ + offset; + if (index < 0 || index >= GetEntryCount()) + return NULL; + + return entries_[index].get(); +} + +bool NavigationControllerImpl::CanGoBack() const { + return entries_.size() > 1 && GetCurrentEntryIndex() > 0; +} + +bool NavigationControllerImpl::CanGoForward() const { + int index = GetCurrentEntryIndex(); + return index >= 0 && index < (static_cast<int>(entries_.size()) - 1); +} + +void NavigationControllerImpl::GoBack() { + if (!CanGoBack()) { + NOTREACHED(); + return; + } + + // Base the navigation on where we are now... + int current_index = GetCurrentEntryIndex(); + + DiscardNonCommittedEntries(); + + pending_entry_index_ = current_index - 1; + entries_[pending_entry_index_]->SetTransitionType( + content::PageTransitionFromInt( + entries_[pending_entry_index_]->GetTransitionType() | + content::PAGE_TRANSITION_FORWARD_BACK)); + NavigateToPendingEntry(NO_RELOAD); +} + +void NavigationControllerImpl::GoForward() { + if (!CanGoForward()) { + NOTREACHED(); + return; + } + + bool transient = (transient_entry_index_ != -1); + + // Base the navigation on where we are now... + int current_index = GetCurrentEntryIndex(); + + DiscardNonCommittedEntries(); + + pending_entry_index_ = current_index; + // If there was a transient entry, we removed it making the current index + // the next page. + if (!transient) + pending_entry_index_++; + + entries_[pending_entry_index_]->SetTransitionType( + content::PageTransitionFromInt( + entries_[pending_entry_index_]->GetTransitionType() | + content::PAGE_TRANSITION_FORWARD_BACK)); + NavigateToPendingEntry(NO_RELOAD); +} + +void NavigationControllerImpl::GoToIndex(int index) { + if (index < 0 || index >= static_cast<int>(entries_.size())) { + NOTREACHED(); + return; + } + + if (transient_entry_index_ != -1) { + if (index == transient_entry_index_) { + // Nothing to do when navigating to the transient. + return; + } + if (index > transient_entry_index_) { + // Removing the transient is goint to shift all entries by 1. + index--; + } + } + + DiscardNonCommittedEntries(); + + pending_entry_index_ = index; + entries_[pending_entry_index_]->SetTransitionType( + content::PageTransitionFromInt( + entries_[pending_entry_index_]->GetTransitionType() | + content::PAGE_TRANSITION_FORWARD_BACK)); + NavigateToPendingEntry(NO_RELOAD); +} + +void NavigationControllerImpl::GoToOffset(int offset) { + int index = (transient_entry_index_ != -1) ? + transient_entry_index_ + offset : + last_committed_entry_index_ + offset; + if (index < 0 || index >= GetEntryCount()) + return; + + GoToIndex(index); +} + +void NavigationControllerImpl::RemoveEntryAtIndex(int index) { + if (index == last_committed_entry_index_) + return; + + RemoveEntryAtIndexInternal(index); +} + +void NavigationControllerImpl::UpdateVirtualURLToURL( + NavigationEntryImpl* entry, const GURL& new_url) { + GURL new_virtual_url(new_url); + if (BrowserURLHandlerImpl::GetInstance()->ReverseURLRewrite( + &new_virtual_url, entry->GetVirtualURL(), browser_context_)) { + entry->SetVirtualURL(new_virtual_url); + } +} + +void NavigationControllerImpl::AddTransientEntry(NavigationEntryImpl* entry) { + // Discard any current transient entry, we can only have one at a time. + int index = 0; + if (last_committed_entry_index_ != -1) + index = last_committed_entry_index_ + 1; + DiscardTransientEntry(); + entries_.insert( + entries_.begin() + index, linked_ptr<NavigationEntryImpl>(entry)); + transient_entry_index_ = index; + tab_contents_->NotifyNavigationStateChanged(kInvalidateAll); +} + +void NavigationControllerImpl::TransferURL( + const GURL& url, + const content::Referrer& referrer, + content::PageTransition transition, + const std::string& extra_headers, + const GlobalRequestID& transferred_global_request_id, + bool is_renderer_initiated) { + // The user initiated a load, we don't need to reload anymore. + needs_reload_ = false; + + NavigationEntryImpl* entry = NavigationEntryImpl::FromNavigationEntry( + CreateNavigationEntry( + url, referrer, transition, is_renderer_initiated, extra_headers, + browser_context_)); + entry->set_transferred_global_request_id(transferred_global_request_id); + + LoadEntry(entry); +} + +void NavigationControllerImpl::LoadURL( + const GURL& url, + const content::Referrer& referrer, + content::PageTransition transition, + const std::string& extra_headers) { + if (content::HandleDebugURL(url, transition)) + return; + + // The user initiated a load, we don't need to reload anymore. + needs_reload_ = false; + + NavigationEntryImpl* entry = NavigationEntryImpl::FromNavigationEntry( + CreateNavigationEntry( + url, referrer, transition, false, extra_headers, browser_context_)); + + LoadEntry(entry); +} + +void NavigationControllerImpl::LoadURLFromRenderer( + const GURL& url, + const content::Referrer& referrer, + content::PageTransition transition, + const std::string& extra_headers) { + // The user initiated a load, we don't need to reload anymore. + needs_reload_ = false; + + NavigationEntryImpl* entry = NavigationEntryImpl::FromNavigationEntry( + CreateNavigationEntry( + url, referrer, transition, true, extra_headers, browser_context_)); + + LoadEntry(entry); +} + +void NavigationControllerImpl::DocumentLoadedInFrame() { + last_document_loaded_ = base::TimeTicks::Now(); +} + +bool NavigationControllerImpl::RendererDidNavigate( + const ViewHostMsg_FrameNavigate_Params& params, + content::LoadCommittedDetails* details) { + + // Save the previous state before we clobber it. + if (GetLastCommittedEntry()) { + details->previous_url = GetLastCommittedEntry()->GetURL(); + details->previous_entry_index = GetLastCommittedEntryIndex(); + } else { + details->previous_url = GURL(); + details->previous_entry_index = -1; + } + + // If we have a pending entry at this point, it should have a SiteInstance. + // Restored entries start out with a null SiteInstance, but we should have + // assigned one in NavigateToPendingEntry. + DCHECK(pending_entry_index_ == -1 || pending_entry_->site_instance()); + + // If we are doing a cross-site reload, we need to replace the existing + // navigation entry, not add another entry to the history. This has the side + // effect of removing forward browsing history, if such existed. + if (pending_entry_ != NULL) { + details->did_replace_entry = pending_entry_->is_cross_site_reload(); + } + + // is_in_page must be computed before the entry gets committed. + details->is_in_page = IsURLInPageNavigation(params.url); + + // Do navigation-type specific actions. These will make and commit an entry. + details->type = ClassifyNavigation(params); + + switch (details->type) { + case content::NAVIGATION_TYPE_NEW_PAGE: + RendererDidNavigateToNewPage(params, &(details->did_replace_entry)); + break; + case content::NAVIGATION_TYPE_EXISTING_PAGE: + RendererDidNavigateToExistingPage(params); + break; + case content::NAVIGATION_TYPE_SAME_PAGE: + RendererDidNavigateToSamePage(params); + break; + case content::NAVIGATION_TYPE_IN_PAGE: + RendererDidNavigateInPage(params, &(details->did_replace_entry)); + break; + case content::NAVIGATION_TYPE_NEW_SUBFRAME: + RendererDidNavigateNewSubframe(params); + break; + case content::NAVIGATION_TYPE_AUTO_SUBFRAME: + if (!RendererDidNavigateAutoSubframe(params)) + return false; + break; + case content::NAVIGATION_TYPE_NAV_IGNORE: + // If a pending navigation was in progress, this canceled it. We should + // discard it and make sure it is removed from the URL bar. After that, + // there is nothing we can do with this navigation, so we just return to + // the caller that nothing has happened. + if (pending_entry_) { + DiscardNonCommittedEntries(); + tab_contents_->NotifyNavigationStateChanged( + content::INVALIDATE_TYPE_URL); + } + return false; + default: + NOTREACHED(); + } + + // All committed entries should have nonempty content state so WebKit doesn't + // get confused when we go back to them (see the function for details). + DCHECK(!params.content_state.empty()); + NavigationEntryImpl* active_entry = + NavigationEntryImpl::FromNavigationEntry(GetActiveEntry()); + active_entry->SetContentState(params.content_state); + + // Once committed, we do not need to track if the entry was initiated by + // the renderer. + active_entry->set_is_renderer_initiated(false); + + // The active entry's SiteInstance should match our SiteInstance. + DCHECK(active_entry->site_instance() == tab_contents_->GetSiteInstance()); + + // Now prep the rest of the details for the notification and broadcast. + details->entry = active_entry; + details->is_main_frame = + content::PageTransitionIsMainFrame(params.transition); + details->serialized_security_info = params.security_info; + details->http_status_code = params.http_status_code; + NotifyNavigationEntryCommitted(details); + + return true; +} + +content::NavigationType NavigationControllerImpl::ClassifyNavigation( + const ViewHostMsg_FrameNavigate_Params& params) const { + if (params.page_id == -1) { + // The renderer generates the page IDs, and so if it gives us the invalid + // page ID (-1) we know it didn't actually navigate. This happens in a few + // cases: + // + // - If a page makes a popup navigated to about blank, and then writes + // stuff like a subframe navigated to a real page. We'll get the commit + // for the subframe, but there won't be any commit for the outer page. + // + // - We were also getting these for failed loads (for example, bug 21849). + // The guess is that we get a "load commit" for the alternate error page, + // but that doesn't affect the page ID, so we get the "old" one, which + // could be invalid. This can also happen for a cross-site transition + // that causes us to swap processes. Then the error page load will be in + // a new process with no page IDs ever assigned (and hence a -1 value), + // yet the navigation controller still might have previous pages in its + // list. + // + // In these cases, there's nothing we can do with them, so ignore. + return content::NAVIGATION_TYPE_NAV_IGNORE; + } + + if (params.page_id > tab_contents_->GetMaxPageID()) { + // Greater page IDs than we've ever seen before are new pages. We may or may + // not have a pending entry for the page, and this may or may not be the + // main frame. + if (content::PageTransitionIsMainFrame(params.transition)) + return content::NAVIGATION_TYPE_NEW_PAGE; + + // When this is a new subframe navigation, we should have a committed page + // for which it's a suframe in. This may not be the case when an iframe is + // navigated on a popup navigated to about:blank (the iframe would be + // written into the popup by script on the main page). For these cases, + // there isn't any navigation stuff we can do, so just ignore it. + if (!GetLastCommittedEntry()) + return content::NAVIGATION_TYPE_NAV_IGNORE; + + // Valid subframe navigation. + return content::NAVIGATION_TYPE_NEW_SUBFRAME; + } + + // Now we know that the notification is for an existing page. Find that entry. + int existing_entry_index = GetEntryIndexWithPageID( + tab_contents_->GetSiteInstance(), + params.page_id); + if (existing_entry_index == -1) { + // The page was not found. It could have been pruned because of the limit on + // back/forward entries (not likely since we'll usually tell it to navigate + // to such entries). It could also mean that the renderer is smoking crack. + NOTREACHED(); + + // Because the unknown entry has committed, we risk showing the wrong URL in + // release builds. Instead, we'll kill the renderer process to be safe. + LOG(ERROR) << "terminating renderer for bad navigation: " << params.url; + content::RecordAction(UserMetricsAction("BadMessageTerminate_NC")); + + // Temporary code so we can get more information. Format: + // http://url/foo.html#page1#max3#frame1#ids:2_Nx,1_1x,3_2 + std::string temp = params.url.spec(); + temp.append("#page"); + temp.append(base::IntToString(params.page_id)); + temp.append("#max"); + temp.append(base::IntToString(tab_contents_->GetMaxPageID())); + temp.append("#frame"); + temp.append(base::IntToString(params.frame_id)); + temp.append("#ids"); + for (int i = 0; i < static_cast<int>(entries_.size()); ++i) { + // Append entry metadata (e.g., 3_7x): + // 3: page_id + // 7: SiteInstance ID, or N for null + // x: appended if not from the current SiteInstance + temp.append(base::IntToString(entries_[i]->GetPageID())); + temp.append("_"); + if (entries_[i]->site_instance()) + temp.append(base::IntToString(entries_[i]->site_instance()->GetId())); + else + temp.append("N"); + if (entries_[i]->site_instance() != tab_contents_->GetSiteInstance()) + temp.append("x"); + temp.append(","); + } + GURL url(temp); + static_cast<RenderViewHostImpl*>( + tab_contents_->GetRenderViewHost())->Send( + new ViewMsg_TempCrashWithData(url)); + return content::NAVIGATION_TYPE_NAV_IGNORE; + } + NavigationEntryImpl* existing_entry = entries_[existing_entry_index].get(); + + if (!content::PageTransitionIsMainFrame(params.transition)) { + // All manual subframes would get new IDs and were handled above, so we + // know this is auto. Since the current page was found in the navigation + // entry list, we're guaranteed to have a last committed entry. + DCHECK(GetLastCommittedEntry()); + return content::NAVIGATION_TYPE_AUTO_SUBFRAME; + } + + // Anything below here we know is a main frame navigation. + if (pending_entry_ && + existing_entry != pending_entry_ && + pending_entry_->GetPageID() == -1 && + existing_entry == GetLastCommittedEntry()) { + // In this case, we have a pending entry for a URL but WebCore didn't do a + // new navigation. This happens when you press enter in the URL bar to + // reload. We will create a pending entry, but WebKit will convert it to + // a reload since it's the same page and not create a new entry for it + // (the user doesn't want to have a new back/forward entry when they do + // this). If this matches the last committed entry, we want to just ignore + // the pending entry and go back to where we were (the "existing entry"). + return content::NAVIGATION_TYPE_SAME_PAGE; + } + + // Any toplevel navigations with the same base (minus the reference fragment) + // are in-page navigations. We weeded out subframe navigations above. Most of + // the time this doesn't matter since WebKit doesn't tell us about subframe + // navigations that don't actually navigate, but it can happen when there is + // an encoding override (it always sends a navigation request). + if (AreURLsInPageNavigation(existing_entry->GetURL(), params.url)) + return content::NAVIGATION_TYPE_IN_PAGE; + + // Since we weeded out "new" navigations above, we know this is an existing + // (back/forward) navigation. + return content::NAVIGATION_TYPE_EXISTING_PAGE; +} + +bool NavigationControllerImpl::IsRedirect( + const ViewHostMsg_FrameNavigate_Params& params) { + // For main frame transition, we judge by params.transition. + // Otherwise, by params.redirects. + if (content::PageTransitionIsMainFrame(params.transition)) { + return content::PageTransitionIsRedirect(params.transition); + } + return params.redirects.size() > 1; +} + +void NavigationControllerImpl::RendererDidNavigateToNewPage( + const ViewHostMsg_FrameNavigate_Params& params, bool* did_replace_entry) { + NavigationEntryImpl* new_entry; + bool update_virtual_url; + if (pending_entry_) { + // TODO(brettw) this assumes that the pending entry is appropriate for the + // new page that was just loaded. I don't think this is necessarily the + // case! We should have some more tracking to know for sure. + new_entry = new NavigationEntryImpl(*pending_entry_); + + // Don't use the page type from the pending entry. Some interstitial page + // may have set the type to interstitial. Once we commit, however, the page + // type must always be normal. + new_entry->set_page_type(content::PAGE_TYPE_NORMAL); + update_virtual_url = new_entry->update_virtual_url_with_url(); + } else { + new_entry = new NavigationEntryImpl; + // When navigating to a new page, give the browser URL handler a chance to + // update the virtual URL based on the new URL. For example, this is needed + // to show chrome://bookmarks/#1 when the bookmarks webui extension changes + // the URL. + update_virtual_url = true; + } + + new_entry->SetURL(params.url); + if (update_virtual_url) + UpdateVirtualURLToURL(new_entry, params.url); + new_entry->SetReferrer(params.referrer); + new_entry->SetPageID(params.page_id); + new_entry->SetTransitionType(params.transition); + new_entry->set_site_instance( + static_cast<SiteInstanceImpl*>(tab_contents_->GetSiteInstance())); + new_entry->SetHasPostData(params.is_post); + new_entry->SetPostID(params.post_id); + + InsertOrReplaceEntry(new_entry, *did_replace_entry); +} + +void NavigationControllerImpl::RendererDidNavigateToExistingPage( + const ViewHostMsg_FrameNavigate_Params& params) { + // We should only get here for main frame navigations. + DCHECK(content::PageTransitionIsMainFrame(params.transition)); + + // This is a back/forward navigation. The existing page for the ID is + // guaranteed to exist by ClassifyNavigation, and we just need to update it + // with new information from the renderer. + int entry_index = GetEntryIndexWithPageID(tab_contents_->GetSiteInstance(), + params.page_id); + DCHECK(entry_index >= 0 && + entry_index < static_cast<int>(entries_.size())); + NavigationEntryImpl* entry = entries_[entry_index].get(); + + // The URL may have changed due to redirects. The site instance will normally + // be the same except during session restore, when no site instance will be + // assigned. + entry->SetURL(params.url); + if (entry->update_virtual_url_with_url()) + UpdateVirtualURLToURL(entry, params.url); + DCHECK(entry->site_instance() == NULL || + entry->site_instance() == tab_contents_->GetSiteInstance()); + entry->set_site_instance( + static_cast<SiteInstanceImpl*>(tab_contents_->GetSiteInstance())); + + entry->SetHasPostData(params.is_post); + entry->SetPostID(params.post_id); + + // The entry we found in the list might be pending if the user hit + // back/forward/reload. This load should commit it (since it's already in the + // list, we can just discard the pending pointer). We should also discard the + // pending entry if it corresponds to a different navigation, since that one + // is now likely canceled. If it is not canceled, we will treat it as a new + // navigation when it arrives, which is also ok. + // + // Note that we need to use the "internal" version since we don't want to + // actually change any other state, just kill the pointer. + if (pending_entry_) + DiscardNonCommittedEntriesInternal(); + + // If a transient entry was removed, the indices might have changed, so we + // have to query the entry index again. + last_committed_entry_index_ = + GetEntryIndexWithPageID(tab_contents_->GetSiteInstance(), params.page_id); +} + +void NavigationControllerImpl::RendererDidNavigateToSamePage( + const ViewHostMsg_FrameNavigate_Params& params) { + // This mode implies we have a pending entry that's the same as an existing + // entry for this page ID. This entry is guaranteed to exist by + // ClassifyNavigation. All we need to do is update the existing entry. + NavigationEntryImpl* existing_entry = GetEntryWithPageID( + tab_contents_->GetSiteInstance(), params.page_id); + + // We assign the entry's unique ID to be that of the new one. Since this is + // always the result of a user action, we want to dismiss infobars, etc. like + // a regular user-initiated navigation. + existing_entry->set_unique_id(pending_entry_->GetUniqueID()); + + // The URL may have changed due to redirects. + if (existing_entry->update_virtual_url_with_url()) + UpdateVirtualURLToURL(existing_entry, params.url); + existing_entry->SetURL(params.url); + + DiscardNonCommittedEntries(); +} + +void NavigationControllerImpl::RendererDidNavigateInPage( + const ViewHostMsg_FrameNavigate_Params& params, bool* did_replace_entry) { + DCHECK(content::PageTransitionIsMainFrame(params.transition)) << + "WebKit should only tell us about in-page navs for the main frame."; + // We're guaranteed to have an entry for this one. + NavigationEntryImpl* existing_entry = GetEntryWithPageID( + tab_contents_->GetSiteInstance(), params.page_id); + + // Reference fragment navigation. We're guaranteed to have the last_committed + // entry and it will be the same page as the new navigation (minus the + // reference fragments, of course). We'll update the URL of the existing + // entry without pruning the forward history. + existing_entry->SetURL(params.url); + if (existing_entry->update_virtual_url_with_url()) + UpdateVirtualURLToURL(existing_entry, params.url); + + // This replaces the existing entry since the page ID didn't change. + *did_replace_entry = true; + + if (pending_entry_) + DiscardNonCommittedEntriesInternal(); + + // If a transient entry was removed, the indices might have changed, so we + // have to query the entry index again. + last_committed_entry_index_ = + GetEntryIndexWithPageID(tab_contents_->GetSiteInstance(), params.page_id); +} + +void NavigationControllerImpl::RendererDidNavigateNewSubframe( + const ViewHostMsg_FrameNavigate_Params& params) { + if (content::PageTransitionStripQualifier(params.transition) == + content::PAGE_TRANSITION_AUTO_SUBFRAME) { + // This is not user-initiated. Ignore. + return; + } + + // Manual subframe navigations just get the current entry cloned so the user + // can go back or forward to it. The actual subframe information will be + // stored in the page state for each of those entries. This happens out of + // band with the actual navigations. + DCHECK(GetLastCommittedEntry()) << "ClassifyNavigation should guarantee " + << "that a last committed entry exists."; + NavigationEntryImpl* new_entry = new NavigationEntryImpl( + *NavigationEntryImpl::FromNavigationEntry(GetLastCommittedEntry())); + new_entry->SetPageID(params.page_id); + InsertOrReplaceEntry(new_entry, false); +} + +bool NavigationControllerImpl::RendererDidNavigateAutoSubframe( + const ViewHostMsg_FrameNavigate_Params& params) { + // We're guaranteed to have a previously committed entry, and we now need to + // handle navigation inside of a subframe in it without creating a new entry. + DCHECK(GetLastCommittedEntry()); + + // Handle the case where we're navigating back/forward to a previous subframe + // navigation entry. This is case "2." in NAV_AUTO_SUBFRAME comment in the + // header file. In case "1." this will be a NOP. + int entry_index = GetEntryIndexWithPageID( + tab_contents_->GetSiteInstance(), + params.page_id); + if (entry_index < 0 || + entry_index >= static_cast<int>(entries_.size())) { + NOTREACHED(); + return false; + } + + // Update the current navigation entry in case we're going back/forward. + if (entry_index != last_committed_entry_index_) { + last_committed_entry_index_ = entry_index; + return true; + } + return false; +} + +int NavigationControllerImpl::GetIndexOfEntry( + const NavigationEntryImpl* entry) const { + const NavigationEntries::const_iterator i(std::find( + entries_.begin(), + entries_.end(), + entry)); + return (i == entries_.end()) ? -1 : static_cast<int>(i - entries_.begin()); +} + +bool NavigationControllerImpl::IsURLInPageNavigation(const GURL& url) const { + NavigationEntry* last_committed = GetLastCommittedEntry(); + if (!last_committed) + return false; + return AreURLsInPageNavigation(last_committed->GetURL(), url); +} + +void NavigationControllerImpl::CopyStateFrom( + const NavigationController& temp) { + const NavigationControllerImpl& source = + static_cast<const NavigationControllerImpl&>(temp); + // Verify that we look new. + DCHECK(GetEntryCount() == 0 && !GetPendingEntry()); + + if (source.GetEntryCount() == 0) + return; // Nothing new to do. + + needs_reload_ = true; + InsertEntriesFrom(source, source.GetEntryCount()); + + session_storage_namespace_ = source.session_storage_namespace_->Clone(); + + FinishRestore(source.last_committed_entry_index_, false); + + // Copy the max page id map from the old tab to the new tab. This ensures + // that new and existing navigations in the tab's current SiteInstances + // are identified properly. + tab_contents_->CopyMaxPageIDsFrom(source.tab_contents()); +} + +void NavigationControllerImpl::CopyStateFromAndPrune( + NavigationController* temp) { + NavigationControllerImpl* source = + static_cast<NavigationControllerImpl*>(temp); + // The SiteInstance and page_id of the last committed entry needs to be + // remembered at this point, in case there is only one committed entry + // and it is pruned. We use a scoped_refptr to ensure the SiteInstance + // can't be freed during this time period. + NavigationEntryImpl* last_committed = + NavigationEntryImpl::FromNavigationEntry(GetLastCommittedEntry()); + scoped_refptr<SiteInstance> site_instance( + last_committed ? last_committed->site_instance() : NULL); + int32 minimum_page_id = last_committed ? last_committed->GetPageID() : -1; + int32 max_page_id = last_committed ? + tab_contents_->GetMaxPageIDForSiteInstance(site_instance.get()) : -1; + + // This code is intended for use when the last entry is the active entry. + DCHECK( + (transient_entry_index_ != -1 && + transient_entry_index_ == GetEntryCount() - 1) || + (pending_entry_ && (pending_entry_index_ == -1 || + pending_entry_index_ == GetEntryCount() - 1)) || + (!pending_entry_ && last_committed_entry_index_ == GetEntryCount() - 1)); + + // Remove all the entries leaving the active entry. + PruneAllButActive(); + + // We now have zero or one entries. Ensure that adding the entries from + // source won't put us over the limit. + DCHECK(GetEntryCount() == 0 || GetEntryCount() == 1); + if (GetEntryCount() > 0) + source->PruneOldestEntryIfFull(); + + // Insert the entries from source. Don't use source->GetCurrentEntryIndex as + // we don't want to copy over the transient entry. + int max_source_index = source->pending_entry_index_ != -1 ? + source->pending_entry_index_ : source->last_committed_entry_index_; + if (max_source_index == -1) + max_source_index = source->GetEntryCount(); + else + max_source_index++; + InsertEntriesFrom(*source, max_source_index); + + // Adjust indices such that the last entry and pending are at the end now. + last_committed_entry_index_ = GetEntryCount() - 1; + if (pending_entry_index_ != -1) + pending_entry_index_ = GetEntryCount() - 1; + if (transient_entry_index_ != -1) { + // There's a transient entry. In this case we want the last committed to + // point to the previous entry. + transient_entry_index_ = GetEntryCount() - 1; + if (last_committed_entry_index_ != -1) + last_committed_entry_index_--; + } + + tab_contents_->SetHistoryLengthAndPrune(site_instance.get(), + max_source_index, + minimum_page_id); + + // Copy the max page id map from the old tab to the new tab. This ensures + // that new and existing navigations in the tab's current SiteInstances + // are identified properly. + tab_contents_->CopyMaxPageIDsFrom(source->tab_contents()); + + // If there is a last committed entry, be sure to include it in the new + // max page ID map. + if (max_page_id > -1) { + tab_contents_->UpdateMaxPageIDForSiteInstance(site_instance.get(), + max_page_id); + } +} + +void NavigationControllerImpl::PruneAllButActive() { + if (transient_entry_index_ != -1) { + // There is a transient entry. Prune up to it. + DCHECK_EQ(GetEntryCount() - 1, transient_entry_index_); + entries_.erase(entries_.begin(), entries_.begin() + transient_entry_index_); + transient_entry_index_ = 0; + last_committed_entry_index_ = -1; + pending_entry_index_ = -1; + } else if (!pending_entry_) { + // There's no pending entry. Leave the last entry (if there is one). + if (!GetEntryCount()) + return; + + DCHECK(last_committed_entry_index_ >= 0); + entries_.erase(entries_.begin(), + entries_.begin() + last_committed_entry_index_); + entries_.erase(entries_.begin() + 1, entries_.end()); + last_committed_entry_index_ = 0; + } else if (pending_entry_index_ != -1) { + entries_.erase(entries_.begin(), entries_.begin() + pending_entry_index_); + entries_.erase(entries_.begin() + 1, entries_.end()); + pending_entry_index_ = 0; + last_committed_entry_index_ = 0; + } else { + // There is a pending_entry, but it's not in entries_. + pending_entry_index_ = -1; + last_committed_entry_index_ = -1; + entries_.clear(); + } + + if (tab_contents_->GetInterstitialPage()) { + // Normally the interstitial page hides itself if the user doesn't proceeed. + // This would result in showing a NavigationEntry we just removed. Set this + // so the interstitial triggers a reload if the user doesn't proceed. + static_cast<InterstitialPageImpl*>(tab_contents_->GetInterstitialPage())-> + set_reload_on_dont_proceed(true); + } +} + +void NavigationControllerImpl::SetMaxRestoredPageID(int32 max_id) { + max_restored_page_id_ = max_id; +} + +int32 NavigationControllerImpl::GetMaxRestoredPageID() const { + return max_restored_page_id_; +} + +SessionStorageNamespace* + NavigationControllerImpl::GetSessionStorageNamespace() const { + return session_storage_namespace_; +} + +bool NavigationControllerImpl::NeedsReload() const { + return needs_reload_; +} + +void NavigationControllerImpl::RemoveEntryAtIndexInternal(int index) { + DCHECK(index < GetEntryCount()); + DCHECK(index != last_committed_entry_index_); + + DiscardNonCommittedEntries(); + + entries_.erase(entries_.begin() + index); + if (last_committed_entry_index_ > index) + last_committed_entry_index_--; +} + +void NavigationControllerImpl::DiscardNonCommittedEntries() { + bool transient = transient_entry_index_ != -1; + DiscardNonCommittedEntriesInternal(); + + // If there was a transient entry, invalidate everything so the new active + // entry state is shown. + if (transient) { + tab_contents_->NotifyNavigationStateChanged(kInvalidateAll); + } +} + +NavigationEntry* NavigationControllerImpl::GetPendingEntry() const { + return pending_entry_; +} + +int NavigationControllerImpl::GetPendingEntryIndex() const { + return pending_entry_index_; +} + +void NavigationControllerImpl::InsertOrReplaceEntry(NavigationEntryImpl* entry, + bool replace) { + DCHECK(entry->GetTransitionType() != content::PAGE_TRANSITION_AUTO_SUBFRAME); + + // Copy the pending entry's unique ID to the committed entry. + // I don't know if pending_entry_index_ can be other than -1 here. + const NavigationEntryImpl* const pending_entry = + (pending_entry_index_ == -1) ? + pending_entry_ : entries_[pending_entry_index_].get(); + if (pending_entry) + entry->set_unique_id(pending_entry->GetUniqueID()); + + DiscardNonCommittedEntriesInternal(); + + int current_size = static_cast<int>(entries_.size()); + + if (current_size > 0) { + // Prune any entries which are in front of the current entry. + // Also prune the current entry if we are to replace the current entry. + // last_committed_entry_index_ must be updated here since calls to + // NotifyPrunedEntries() below may re-enter and we must make sure + // last_committed_entry_index_ is not left in an invalid state. + if (replace) + --last_committed_entry_index_; + + int num_pruned = 0; + while (last_committed_entry_index_ < (current_size - 1)) { + num_pruned++; + entries_.pop_back(); + current_size--; + } + if (num_pruned > 0) // Only notify if we did prune something. + NotifyPrunedEntries(this, false, num_pruned); + } + + PruneOldestEntryIfFull(); + + entries_.push_back(linked_ptr<NavigationEntryImpl>(entry)); + last_committed_entry_index_ = static_cast<int>(entries_.size()) - 1; + + // This is a new page ID, so we need everybody to know about it. + tab_contents_->UpdateMaxPageID(entry->GetPageID()); +} + +void NavigationControllerImpl::PruneOldestEntryIfFull() { + if (entries_.size() >= max_entry_count()) { + DCHECK_EQ(max_entry_count(), entries_.size()); + DCHECK(last_committed_entry_index_ > 0); + RemoveEntryAtIndex(0); + NotifyPrunedEntries(this, true, 1); + } +} + +void NavigationControllerImpl::NavigateToPendingEntry(ReloadType reload_type) { + needs_reload_ = false; + + // If we were navigating to a slow-to-commit page, and the user performs + // a session history navigation to the last committed page, RenderViewHost + // will force the throbber to start, but WebKit will essentially ignore the + // navigation, and won't send a message to stop the throbber. To prevent this + // from happening, we drop the navigation here and stop the slow-to-commit + // page from loading (which would normally happen during the navigation). + if (pending_entry_index_ != -1 && + pending_entry_index_ == last_committed_entry_index_ && + (entries_[pending_entry_index_]->restore_type() == + NavigationEntryImpl::RESTORE_NONE) && + (entries_[pending_entry_index_]->GetTransitionType() & + content::PAGE_TRANSITION_FORWARD_BACK)) { + tab_contents_->Stop(); + + // If an interstitial page is showing, we want to close it to get back + // to what was showing before. + if (tab_contents_->GetInterstitialPage()) + tab_contents_->GetInterstitialPage()->DontProceed(); + + DiscardNonCommittedEntries(); + return; + } + + // If an interstitial page is showing, the previous renderer is blocked and + // cannot make new requests. Unblock (and disable) it to allow this + // navigation to succeed. The interstitial will stay visible until the + // resulting DidNavigate. + if (tab_contents_->GetInterstitialPage()) { + static_cast<InterstitialPageImpl*>(tab_contents_->GetInterstitialPage())-> + CancelForNavigation(); + } + + // For session history navigations only the pending_entry_index_ is set. + if (!pending_entry_) { + DCHECK_NE(pending_entry_index_, -1); + pending_entry_ = entries_[pending_entry_index_].get(); + } + + if (!tab_contents_->NavigateToPendingEntry(reload_type)) + DiscardNonCommittedEntries(); + + // If the entry is being restored and doesn't have a SiteInstance yet, fill + // it in now that we know. This allows us to find the entry when it commits. + // This works for browser-initiated navigations. We handle renderer-initiated + // navigations to restored entries in TabContents::OnGoToEntryAtOffset. + if (pending_entry_ && !pending_entry_->site_instance() && + pending_entry_->restore_type() != NavigationEntryImpl::RESTORE_NONE) { + pending_entry_->set_site_instance(static_cast<SiteInstanceImpl*>( + tab_contents_->GetPendingSiteInstance())); + pending_entry_->set_restore_type(NavigationEntryImpl::RESTORE_NONE); + } +} + +void NavigationControllerImpl::NotifyNavigationEntryCommitted( + content::LoadCommittedDetails* details) { + details->entry = GetActiveEntry(); + content::NotificationDetails notification_details = + content::Details<content::LoadCommittedDetails>(details); + + // We need to notify the ssl_manager_ before the tab_contents_ so the + // location bar will have up-to-date information about the security style + // when it wants to draw. See http://crbug.com/11157 + ssl_manager_.DidCommitProvisionalLoad(notification_details); + + // TODO(pkasting): http://b/1113079 Probably these explicit notification paths + // should be removed, and interested parties should just listen for the + // notification below instead. + tab_contents_->NotifyNavigationStateChanged(kInvalidateAll); + + content::NotificationService::current()->Notify( + content::NOTIFICATION_NAV_ENTRY_COMMITTED, + content::Source<NavigationController>(this), + notification_details); +} + +// static +size_t NavigationControllerImpl::max_entry_count() { + if (max_entry_count_for_testing_ != kMaxEntryCountForTestingNotSet) + return max_entry_count_for_testing_; + return content::kMaxSessionHistoryEntries; +} + +void NavigationControllerImpl::SetActive(bool is_active) { + if (is_active && needs_reload_) + LoadIfNecessary(); +} + +void NavigationControllerImpl::LoadIfNecessary() { + if (!needs_reload_) + return; + + // Calling Reload() results in ignoring state, and not loading. + // Explicitly use NavigateToPendingEntry so that the renderer uses the + // cached state. + pending_entry_index_ = last_committed_entry_index_; + NavigateToPendingEntry(NO_RELOAD); +} + +void NavigationControllerImpl::NotifyEntryChanged(const NavigationEntry* entry, + int index) { + content::EntryChangedDetails det; + det.changed_entry = entry; + det.index = index; + content::NotificationService::current()->Notify( + content::NOTIFICATION_NAV_ENTRY_CHANGED, + content::Source<NavigationController>(this), + content::Details<content::EntryChangedDetails>(&det)); +} + +void NavigationControllerImpl::FinishRestore(int selected_index, + bool from_last_session) { + DCHECK(selected_index >= 0 && selected_index < GetEntryCount()); + ConfigureEntriesForRestore(&entries_, from_last_session); + + SetMaxRestoredPageID(static_cast<int32>(GetEntryCount())); + + last_committed_entry_index_ = selected_index; +} + +void NavigationControllerImpl::DiscardNonCommittedEntriesInternal() { + if (pending_entry_index_ == -1) + delete pending_entry_; + pending_entry_ = NULL; + pending_entry_index_ = -1; + + DiscardTransientEntry(); +} + +void NavigationControllerImpl::DiscardTransientEntry() { + if (transient_entry_index_ == -1) + return; + entries_.erase(entries_.begin() + transient_entry_index_); + if (last_committed_entry_index_ > transient_entry_index_) + last_committed_entry_index_--; + transient_entry_index_ = -1; +} + +int NavigationControllerImpl::GetEntryIndexWithPageID( + SiteInstance* instance, int32 page_id) const { + for (int i = static_cast<int>(entries_.size()) - 1; i >= 0; --i) { + if ((entries_[i]->site_instance() == instance) && + (entries_[i]->GetPageID() == page_id)) + return i; + } + return -1; +} + +NavigationEntry* NavigationControllerImpl::GetTransientEntry() const { + if (transient_entry_index_ == -1) + return NULL; + return entries_[transient_entry_index_].get(); +} + +void NavigationControllerImpl::InsertEntriesFrom( + const NavigationControllerImpl& source, + int max_index) { + DCHECK_LE(max_index, source.GetEntryCount()); + size_t insert_index = 0; + for (int i = 0; i < max_index; i++) { + // When cloning a tab, copy all entries except interstitial pages + if (source.entries_[i].get()->GetPageType() != + content::PAGE_TYPE_INTERSTITIAL) { + entries_.insert(entries_.begin() + insert_index++, + linked_ptr<NavigationEntryImpl>( + new NavigationEntryImpl(*source.entries_[i]))); + } + } +} diff --git a/content/browser/web_contents/navigation_controller_impl.h b/content/browser/web_contents/navigation_controller_impl.h new file mode 100644 index 0000000..71f1f52 --- /dev/null +++ b/content/browser/web_contents/navigation_controller_impl.h @@ -0,0 +1,332 @@ +// Copyright (c) 2012 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. + +#ifndef CONTENT_BROWSER_WEB_CONTENTS_NAVIGATION_CONTROLLER_IMPL_H_ +#define CONTENT_BROWSER_WEB_CONTENTS_NAVIGATION_CONTROLLER_IMPL_H_ +#pragma once + +#include "build/build_config.h" +#include "base/compiler_specific.h" +#include "base/memory/linked_ptr.h" +#include "base/time.h" +#include "content/browser/ssl/ssl_manager.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/navigation_type.h" + +class SessionStorageNamespaceImpl; +struct ViewHostMsg_FrameNavigate_Params; +class WebContentsImpl; + +namespace content { +class NavigationEntryImpl; +struct LoadCommittedDetails; +class SiteInstance; +} + +class CONTENT_EXPORT NavigationControllerImpl + : public NON_EXPORTED_BASE(content::NavigationController) { + public: + NavigationControllerImpl( + WebContentsImpl* tab_contents, + content::BrowserContext* browser_context, + SessionStorageNamespaceImpl* session_storage_namespace); + virtual ~NavigationControllerImpl(); + + // NavigationController implementation: + virtual content::WebContents* GetWebContents() const OVERRIDE; + virtual content::BrowserContext* GetBrowserContext() const OVERRIDE; + virtual void SetBrowserContext( + content::BrowserContext* browser_context) OVERRIDE; + virtual void Restore( + int selected_navigation, + bool from_last_session, + std::vector<content::NavigationEntry*>* entries) OVERRIDE; + virtual content::NavigationEntry* GetActiveEntry() const OVERRIDE; + virtual content::NavigationEntry* GetVisibleEntry() const OVERRIDE; + virtual int GetCurrentEntryIndex() const OVERRIDE; + virtual content::NavigationEntry* GetLastCommittedEntry() const OVERRIDE; + virtual int GetLastCommittedEntryIndex() const OVERRIDE; + virtual bool CanViewSource() const OVERRIDE; + virtual int GetEntryCount() const OVERRIDE; + virtual content::NavigationEntry* GetEntryAtIndex(int index) const OVERRIDE; + virtual content::NavigationEntry* GetEntryAtOffset(int offset) const OVERRIDE; + virtual void DiscardNonCommittedEntries() OVERRIDE; + virtual content::NavigationEntry* GetPendingEntry() const OVERRIDE; + virtual int GetPendingEntryIndex() const OVERRIDE; + virtual content::NavigationEntry* GetTransientEntry() const OVERRIDE; + virtual void LoadURL(const GURL& url, + const content::Referrer& referrer, + content::PageTransition type, + const std::string& extra_headers) OVERRIDE; + virtual void LoadURLFromRenderer(const GURL& url, + const content::Referrer& referrer, + content::PageTransition type, + const std::string& extra_headers) OVERRIDE; + virtual void TransferURL( + const GURL& url, + const content::Referrer& referrer, + content::PageTransition transition, + const std::string& extra_headers, + const content::GlobalRequestID& transferred_global_request_id, + bool is_renderer_initiated) OVERRIDE; + virtual void LoadIfNecessary() OVERRIDE; + virtual bool CanGoBack() const OVERRIDE; + virtual bool CanGoForward() const OVERRIDE; + virtual void GoBack() OVERRIDE; + virtual void GoForward() OVERRIDE; + virtual void GoToIndex(int index) OVERRIDE; + virtual void GoToOffset(int offset) OVERRIDE; + virtual void RemoveEntryAtIndex(int index) OVERRIDE; + virtual content::SessionStorageNamespace* + GetSessionStorageNamespace() const OVERRIDE; + virtual void SetMaxRestoredPageID(int32 max_id) OVERRIDE; + virtual int32 GetMaxRestoredPageID() const OVERRIDE; + virtual bool NeedsReload() const OVERRIDE; + virtual void CancelPendingReload() OVERRIDE; + virtual void ContinuePendingReload() OVERRIDE; + virtual bool IsInitialNavigation() OVERRIDE; + virtual void Reload(bool check_for_repost) OVERRIDE; + virtual void ReloadIgnoringCache(bool check_for_repost) OVERRIDE; + virtual void NotifyEntryChanged(const content::NavigationEntry* entry, + int index) OVERRIDE; + virtual void CopyStateFrom( + const content::NavigationController& source) OVERRIDE; + virtual void CopyStateFromAndPrune( + content::NavigationController* source) OVERRIDE; + virtual void PruneAllButActive() OVERRIDE; + + // Returns the index of the specified entry, or -1 if entry is not contained + // in this NavigationController. + int GetIndexOfEntry(const content::NavigationEntryImpl* entry) const; + + // Return the index of the entry with the corresponding instance and page_id, + // or -1 if not found. + int GetEntryIndexWithPageID(content::SiteInstance* instance, + int32 page_id) const; + + // Return the entry with the corresponding instance and page_id, or NULL if + // not found. + content::NavigationEntryImpl* GetEntryWithPageID( + content::SiteInstance* instance, + int32 page_id) const; + + // Transient entry ----------------------------------------------------------- + + // Adds an entry that is returned by GetActiveEntry(). The entry is + // transient: any navigation causes it to be removed and discarded. + // The NavigationController becomes the owner of |entry| and deletes it when + // it discards it. This is useful with interstitial page that need to be + // represented as an entry, but should go away when the user navigates away + // from them. + // Note that adding a transient entry does not change the active contents. + void AddTransientEntry(content::NavigationEntryImpl* entry); + + // WebContentsImpl ----------------------------------------------------------- + + WebContentsImpl* tab_contents() const { + // This currently returns the active tab contents which should be renamed to + // tab_contents. + return tab_contents_; + } + + // Called when a document has been loaded in a frame. + void DocumentLoadedInFrame(); + + // For use by WebContentsImpl ------------------------------------------------ + + // Handles updating the navigation state after the renderer has navigated. + // This is used by the WebContentsImpl. + // + // If a new entry is created, it will return true and will have filled the + // given details structure and broadcast the NOTIFY_NAV_ENTRY_COMMITTED + // notification. The caller can then use the details without worrying about + // listening for the notification. + // + // In the case that nothing has changed, the details structure is undefined + // and it will return false. + bool RendererDidNavigate(const ViewHostMsg_FrameNavigate_Params& params, + content::LoadCommittedDetails* details); + + // Notifies us that we just became active. This is used by the WebContentsImpl + // so that we know to load URLs that were pending as "lazy" loads. + void SetActive(bool is_active); + + // Returns true if the given URL would be an in-page navigation (i.e. only + // the reference fragment is different) from the "last committed entry". We do + // not compare it against the "active entry" since the active entry can be + // pending and in page navigations only happen on committed pages. If there + // is no last committed entry, then nothing will be in-page. + // + // Special note: if the URLs are the same, it does NOT count as an in-page + // navigation. Neither does an input URL that has no ref, even if the rest is + // the same. This may seem weird, but when we're considering whether a + // navigation happened without loading anything, the same URL would be a + // reload, while only a different ref would be in-page (pages can't clear + // refs without reload, only change to "#" which we don't count as empty). + bool IsURLInPageNavigation(const GURL& url) const; + + // Random data --------------------------------------------------------------- + + SSLManager* ssl_manager() { return &ssl_manager_; } + + // Maximum number of entries before we start removing entries from the front. + static void set_max_entry_count_for_testing(size_t max_entry_count) { + max_entry_count_for_testing_ = max_entry_count; + } + static size_t max_entry_count(); + + private: + class RestoreHelper; + friend class RestoreHelper; + friend class WebContentsImpl; // For invoking OnReservedPageIDRange. + + // Classifies the given renderer navigation (see the NavigationType enum). + content::NavigationType ClassifyNavigation( + const ViewHostMsg_FrameNavigate_Params& params) const; + + // Causes the controller to load the specified entry. The function assumes + // ownership of the pointer since it is put in the navigation list. + // NOTE: Do not pass an entry that the controller already owns! + void LoadEntry(content::NavigationEntryImpl* entry); + + // Handlers for the different types of navigation types. They will actually + // handle the navigations corresponding to the different NavClasses above. + // They will NOT broadcast the commit notification, that should be handled by + // the caller. + // + // RendererDidNavigateAutoSubframe is special, it may not actually change + // anything if some random subframe is loaded. It will return true if anything + // changed, or false if not. + // + // The functions taking |did_replace_entry| will fill into the given variable + // whether the last entry has been replaced or not. + // See LoadCommittedDetails.did_replace_entry. + void RendererDidNavigateToNewPage( + const ViewHostMsg_FrameNavigate_Params& params, bool* did_replace_entry); + void RendererDidNavigateToExistingPage( + const ViewHostMsg_FrameNavigate_Params& params); + void RendererDidNavigateToSamePage( + const ViewHostMsg_FrameNavigate_Params& params); + void RendererDidNavigateInPage( + const ViewHostMsg_FrameNavigate_Params& params, bool* did_replace_entry); + void RendererDidNavigateNewSubframe( + const ViewHostMsg_FrameNavigate_Params& params); + bool RendererDidNavigateAutoSubframe( + const ViewHostMsg_FrameNavigate_Params& params); + + // Helper function for code shared between Reload() and ReloadIgnoringCache(). + void ReloadInternal(bool check_for_repost, ReloadType reload_type); + + // Actually issues the navigation held in pending_entry. + void NavigateToPendingEntry(ReloadType reload_type); + + // Allows the derived class to issue notifications that a load has been + // committed. This will fill in the active entry to the details structure. + void NotifyNavigationEntryCommitted(content::LoadCommittedDetails* details); + + // Updates the virtual URL of an entry to match a new URL, for cases where + // the real renderer URL is derived from the virtual URL, like view-source: + void UpdateVirtualURLToURL(content::NavigationEntryImpl* entry, + const GURL& new_url); + + // Invoked after session/tab restore or cloning a tab. Resets the transition + // type of the entries, updates the max page id and creates the active + // contents. See RestoreFromState for a description of from_last_session. + void FinishRestore(int selected_index, bool from_last_session); + + // Inserts a new entry or replaces the current entry with a new one, removing + // all entries after it. The new entry will become the active one. + void InsertOrReplaceEntry(content::NavigationEntryImpl* entry, bool replace); + + // Removes the entry at |index|, as long as it is not the current entry. + void RemoveEntryAtIndexInternal(int index); + + // Discards the pending and transient entries. + void DiscardNonCommittedEntriesInternal(); + + // Discards the transient entry. + void DiscardTransientEntry(); + + // If we have the maximum number of entries, remove the oldest one in + // preparation to add another. + void PruneOldestEntryIfFull(); + + // Returns true if the navigation is redirect. + bool IsRedirect(const ViewHostMsg_FrameNavigate_Params& params); + + // Returns true if the navigation is likley to be automatic rather than + // user-initiated. + bool IsLikelyAutoNavigation(base::TimeTicks now); + + // Inserts up to |max_index| entries from |source| into this. This does NOT + // adjust any of the members that reference entries_ + // (last_committed_entry_index_, pending_entry_index_ or + // transient_entry_index_). + void InsertEntriesFrom(const NavigationControllerImpl& source, int max_index); + + // --------------------------------------------------------------------------- + + // The user browser context associated with this controller. + content::BrowserContext* browser_context_; + + // List of NavigationEntry for this tab + typedef std::vector<linked_ptr<content::NavigationEntryImpl> > + NavigationEntries; + NavigationEntries entries_; + + // An entry we haven't gotten a response for yet. This will be discarded + // when we navigate again. It's used only so we know what the currently + // displayed tab is. + // + // This may refer to an item in the entries_ list if the pending_entry_index_ + // == -1, or it may be its own entry that should be deleted. Be careful with + // the memory management. + content::NavigationEntryImpl* pending_entry_; + + // currently visible entry + int last_committed_entry_index_; + + // index of pending entry if it is in entries_, or -1 if pending_entry_ is a + // new entry (created by LoadURL). + int pending_entry_index_; + + // The index for the entry that is shown until a navigation occurs. This is + // used for interstitial pages. -1 if there are no such entry. + // Note that this entry really appears in the list of entries, but only + // temporarily (until the next navigation). Any index pointing to an entry + // after the transient entry will become invalid if you navigate forward. + int transient_entry_index_; + + // The tab contents associated with the controller. Possibly NULL during + // setup. + WebContentsImpl* tab_contents_; + + // The max restored page ID in this controller, if it was restored. We must + // store this so that WebContentsImpl can tell any renderer in charge of one + // of the restored entries to update its max page ID. + int32 max_restored_page_id_; + + // Manages the SSL security UI + SSLManager ssl_manager_; + + // Whether we need to be reloaded when made active. + bool needs_reload_; + + // The time ticks at which the last document was loaded. + base::TimeTicks last_document_loaded_; + + // The session storage id that any (indirectly) owned RenderView should use. + scoped_refptr<SessionStorageNamespaceImpl> session_storage_namespace_; + + // The maximum number of entries that a navigation controller can store. + static size_t max_entry_count_for_testing_; + + // If a repost is pending, its type (RELOAD or RELOAD_IGNORING_CACHE), + // NO_RELOAD otherwise. + ReloadType pending_reload_; + + DISALLOW_COPY_AND_ASSIGN(NavigationControllerImpl); +}; + +#endif // CONTENT_BROWSER_WEB_CONTENTS_NAVIGATION_CONTROLLER_IMPL_H_ diff --git a/content/browser/web_contents/navigation_controller_impl_unittest.cc b/content/browser/web_contents/navigation_controller_impl_unittest.cc new file mode 100644 index 0000000..023e83d --- /dev/null +++ b/content/browser/web_contents/navigation_controller_impl_unittest.cc @@ -0,0 +1,2592 @@ +// Copyright (c) 2012 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. + +#include "base/file_util.h" +#include "base/memory/scoped_ptr.h" +#include "base/path_service.h" +#include "base/stl_util.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +// These are only used for commented out tests. If someone wants to enable +// them, they should be moved to chrome first. +// #include "chrome/browser/history/history.h" +// #include "chrome/browser/profiles/profile_manager.h" +// #include "chrome/browser/sessions/session_service.h" +// #include "chrome/browser/sessions/session_service_factory.h" +// #include "chrome/browser/sessions/session_service_test_helper.h" +// #include "chrome/browser/sessions/session_types.h" +#include "content/browser/renderer_host/test_render_view_host.h" +#include "content/browser/site_instance_impl.h" +#include "content/browser/tab_contents/tab_contents.h" +#include "content/browser/tab_contents/test_web_contents.h" +#include "content/browser/web_contents/navigation_controller_impl.h" +#include "content/browser/web_contents/navigation_entry_impl.h" +#include "content/common/view_messages.h" +#include "content/public/browser/navigation_details.h" +#include "content/public/browser/notification_registrar.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/web_contents_delegate.h" +#include "content/test/mock_render_process_host.h" +#include "content/test/test_browser_context.h" +#include "content/test/test_notification_tracker.h" +#include "net/base/net_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "webkit/glue/webkit_glue.h" + +using base::Time; +using content::NavigationController; +using content::NavigationEntry; +using content::NavigationEntryImpl; +using content::RenderViewHostImplTestHarness; +using content::SiteInstance; +using content::TestRenderViewHost; +using content::TestWebContents; +using content::WebContents; + +// NavigationControllerTest ---------------------------------------------------- + +class NavigationControllerTest : public RenderViewHostImplTestHarness { + public: + NavigationControllerTest() {} + + NavigationControllerImpl& controller_impl() { + return static_cast<NavigationControllerImpl&>(controller()); + } +}; + +void RegisterForAllNavNotifications(TestNotificationTracker* tracker, + NavigationController* controller) { + tracker->ListenFor(content::NOTIFICATION_NAV_ENTRY_COMMITTED, + content::Source<NavigationController>( + controller)); + tracker->ListenFor(content::NOTIFICATION_NAV_LIST_PRUNED, + content::Source<NavigationController>( + controller)); + tracker->ListenFor(content::NOTIFICATION_NAV_ENTRY_CHANGED, + content::Source<NavigationController>( + controller)); +} + +SiteInstance* GetSiteInstanceFromEntry(NavigationEntry* entry) { + return NavigationEntryImpl::FromNavigationEntry(entry)->site_instance(); +} + +class TestWebContentsDelegate : public content::WebContentsDelegate { + public: + explicit TestWebContentsDelegate() : + navigation_state_change_count_(0) {} + + int navigation_state_change_count() { + return navigation_state_change_count_; + } + + // Keep track of whether the tab has notified us of a navigation state change. + virtual void NavigationStateChanged(const WebContents* source, + unsigned changed_flags) { + navigation_state_change_count_++; + } + + private: + // The number of times NavigationStateChanged has been called. + int navigation_state_change_count_; +}; + +// ----------------------------------------------------------------------------- + +TEST_F(NavigationControllerTest, Defaults) { + NavigationControllerImpl& controller = controller_impl(); + + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.GetLastCommittedEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), -1); + EXPECT_EQ(controller.GetEntryCount(), 0); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +TEST_F(NavigationControllerTest, LoadURL) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + // Creating a pending notification should not have issued any of the + // notifications we're listening for. + EXPECT_EQ(0U, notifications.size()); + + // The load should now be pending. + EXPECT_EQ(controller.GetEntryCount(), 0); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), -1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_FALSE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), -1); + + // We should have gotten no notifications from the preceeding checks. + EXPECT_EQ(0U, notifications.size()); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The load should now be committed. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 0); + + // Load another... + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + // The load should now be pending. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + // TODO(darin): maybe this should really be true? + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 0); + + // Simulate the beforeunload ack for the cross-site transition, and then the + // commit. + test_rvh()->SendShouldCloseACK(true); + static_cast<TestRenderViewHost*>( + contents()->GetPendingRenderViewHost())->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The load should now be committed. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 1); +} + +// Tests what happens when the same page is loaded again. Should not create a +// new session history entry. This is what happens when you press enter in the +// URL bar to reload: a pending entry is created and then it is discarded when +// the load commits (because WebCore didn't actually make a new entry). +TEST_F(NavigationControllerTest, LoadURL_SamePage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // We should not have produced a new session history entry. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests loading a URL but discarding it before the load commits. +TEST_F(NavigationControllerTest, LoadURL_Discarded) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + controller.DiscardNonCommittedEntries(); + EXPECT_EQ(0U, notifications.size()); + + // Should not have produced a new session history entry. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests navigations that come in unrequested. This happens when the user +// navigates from the web page, and here we test that there is no pending entry. +TEST_F(NavigationControllerTest, LoadURL_NoPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make an existing committed entry. + const GURL kExistingURL1("http://eh"); + controller.LoadURL(kExistingURL1, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Do a new navigation without making a pending one. + const GURL kNewURL("http://see"); + test_rvh()->SendNavigate(99, kNewURL); + + // There should no longer be any pending entry, and the third navigation we + // just made should be committed. + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetActiveEntry()->GetURL()); +} + +// Tests navigating to a new URL when there is a new pending navigation that is +// not the one that just loaded. This will happen if the user types in a URL to +// somewhere slow, and then navigates the current page before the typed URL +// commits. +TEST_F(NavigationControllerTest, LoadURL_NewPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make an existing committed entry. + const GURL kExistingURL1("http://eh"); + controller.LoadURL(kExistingURL1, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Make a pending entry to somewhere new. + const GURL kExistingURL2("http://bee"); + controller.LoadURL(kExistingURL2, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + + // After the beforeunload but before it commits, do a new navigation. + test_rvh()->SendShouldCloseACK(true); + const GURL kNewURL("http://see"); + static_cast<TestRenderViewHost*>( + contents()->GetPendingRenderViewHost())->SendNavigate(3, kNewURL); + + // There should no longer be any pending entry, and the third navigation we + // just made should be committed. + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetActiveEntry()->GetURL()); +} + +// Tests navigating to a new URL when there is a pending back/forward +// navigation. This will happen if the user hits back, but before that commits, +// they navigate somewhere new. +TEST_F(NavigationControllerTest, LoadURL_ExistingPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make some history. + const GURL kExistingURL1("http://foo/eh"); + controller.LoadURL(kExistingURL1, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL kExistingURL2("http://foo/bee"); + controller.LoadURL(kExistingURL2, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, kExistingURL2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now make a pending back/forward navigation. The zeroth entry should be + // pending. + controller.GoBack(); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(0, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + + // Before that commits, do a new navigation. + const GURL kNewURL("http://foo/see"); + content::LoadCommittedDetails details; + test_rvh()->SendNavigate(3, kNewURL); + + // There should no longer be any pending entry, and the third navigation we + // just made should be committed. + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(2, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetActiveEntry()->GetURL()); +} + +// Tests navigating to an existing URL when there is a pending new navigation. +// This will happen if the user enters a URL, but before that commits, the +// current page fires history.back(). +TEST_F(NavigationControllerTest, LoadURL_BackPreemptsPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make some history. + const GURL kExistingURL1("http://foo/eh"); + controller.LoadURL(kExistingURL1, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL kExistingURL2("http://foo/bee"); + controller.LoadURL(kExistingURL2, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, kExistingURL2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now make a pending new navigation. + const GURL kNewURL("http://foo/see"); + controller.LoadURL( + kNewURL, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + + // Before that commits, a back navigation from the renderer commits. + test_rvh()->SendNavigate(0, kExistingURL1); + + // There should no longer be any pending entry, and the back navigation we + // just made should be committed. + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kExistingURL1, controller.GetActiveEntry()->GetURL()); +} + +// Tests an ignored navigation when there is a pending new navigation. +// This will happen if the user enters a URL, but before that commits, the +// current blank page reloads. See http://crbug.com/77507. +TEST_F(NavigationControllerTest, LoadURL_IgnorePreemptsPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Set a WebContentsDelegate to listen for state changes. + scoped_ptr<TestWebContentsDelegate> delegate(new TestWebContentsDelegate()); + EXPECT_FALSE(contents()->GetDelegate()); + contents()->SetDelegate(delegate.get()); + + // Without any navigations, the renderer starts at about:blank. + const GURL kExistingURL("about:blank"); + + // Now make a pending new navigation. + const GURL kNewURL("http://eh"); + controller.LoadURL( + kNewURL, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(1, delegate->navigation_state_change_count()); + + // Before that commits, a document.write and location.reload can cause the + // renderer to send a FrameNavigate with page_id -1. + test_rvh()->SendNavigate(-1, kExistingURL); + + // This should clear the pending entry and notify of a navigation state + // change, so that we do not keep displaying kNewURL. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(2, delegate->navigation_state_change_count()); + + contents()->SetDelegate(NULL); +} + +// Tests that the pending entry state is correct after an abort. +TEST_F(NavigationControllerTest, LoadURL_AbortCancelsPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Set a WebContentsDelegate to listen for state changes. + scoped_ptr<TestWebContentsDelegate> delegate(new TestWebContentsDelegate()); + EXPECT_FALSE(contents()->GetDelegate()); + contents()->SetDelegate(delegate.get()); + + // Without any navigations, the renderer starts at about:blank. + const GURL kExistingURL("about:blank"); + + // Now make a pending new navigation. + const GURL kNewURL("http://eh"); + controller.LoadURL( + kNewURL, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(1, delegate->navigation_state_change_count()); + + // It may abort before committing, if it's a download or due to a stop or + // a new navigation from the user. + ViewHostMsg_DidFailProvisionalLoadWithError_Params params; + params.frame_id = 1; + params.is_main_frame = true; + params.error_code = net::ERR_ABORTED; + params.error_description = string16(); + params.url = kNewURL; + params.showing_repost_interstitial = false; + test_rvh()->OnMessageReceived( + ViewHostMsg_DidFailProvisionalLoadWithError(0, // routing_id + params)); + + // This should clear the pending entry and notify of a navigation state + // change, so that we do not keep displaying kNewURL. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(2, delegate->navigation_state_change_count()); + + contents()->SetDelegate(NULL); +} + +// Tests that the pending entry state is correct after a redirect and abort. +// http://crbug.com/83031. +TEST_F(NavigationControllerTest, LoadURL_RedirectAbortCancelsPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Set a WebContentsDelegate to listen for state changes. + scoped_ptr<TestWebContentsDelegate> delegate(new TestWebContentsDelegate()); + EXPECT_FALSE(contents()->GetDelegate()); + contents()->SetDelegate(delegate.get()); + + // Without any navigations, the renderer starts at about:blank. + const GURL kExistingURL("about:blank"); + + // Now make a pending new navigation. + const GURL kNewURL("http://eh"); + controller.LoadURL( + kNewURL, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(1, delegate->navigation_state_change_count()); + + // Now the navigation redirects. + const GURL kRedirectURL("http://bee"); + test_rvh()->OnMessageReceived( + ViewHostMsg_DidRedirectProvisionalLoad(0, // routing_id + -1, // pending page_id + GURL(), // opener + kNewURL, // old url + kRedirectURL)); // new url + + // We don't want to change the NavigationEntry's url, in case it cancels. + // Prevents regression of http://crbug.com/77786. + EXPECT_EQ(kNewURL, controller.GetPendingEntry()->GetURL()); + + // It may abort before committing, if it's a download or due to a stop or + // a new navigation from the user. + ViewHostMsg_DidFailProvisionalLoadWithError_Params params; + params.frame_id = 1; + params.is_main_frame = true; + params.error_code = net::ERR_ABORTED; + params.error_description = string16(); + params.url = kRedirectURL; + params.showing_repost_interstitial = false; + test_rvh()->OnMessageReceived( + ViewHostMsg_DidFailProvisionalLoadWithError(0, // routing_id + params)); + + // This should clear the pending entry and notify of a navigation state + // change, so that we do not keep displaying kNewURL. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(2, delegate->navigation_state_change_count()); + + contents()->SetDelegate(NULL); +} + +TEST_F(NavigationControllerTest, Reload) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + controller.GetActiveEntry()->SetTitle(ASCIIToUTF16("Title")); + controller.Reload(true); + EXPECT_EQ(0U, notifications.size()); + + // The reload is pending. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + // Make sure the title has been cleared (will be redrawn just after reload). + // Avoids a stale cached title when the new page being reloaded has no title. + // See http://crbug.com/96041. + EXPECT_TRUE(controller.GetActiveEntry()->GetTitle().empty()); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now the reload is committed. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests what happens when a reload navigation produces a new page. +TEST_F(NavigationControllerTest, Reload_GeneratesNewPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.Reload(true); + EXPECT_EQ(0U, notifications.size()); + + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now the reload is committed. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests what happens when we navigate back successfully +TEST_F(NavigationControllerTest, Back) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL url2("http://foo2"); + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoBack(); + EXPECT_EQ(0U, notifications.size()); + + // We should now have a pending navigation to go back. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_TRUE(controller.CanGoForward()); + + test_rvh()->SendNavigate(0, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The back navigation completed successfully. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_TRUE(controller.CanGoForward()); +} + +// Tests what happens when a back navigation produces a new page. +TEST_F(NavigationControllerTest, Back_GeneratesNewPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoBack(); + EXPECT_EQ(0U, notifications.size()); + + // We should now have a pending navigation to go back. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_TRUE(controller.CanGoForward()); + + test_rvh()->SendNavigate(2, url3); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The back navigation resulted in a completely new navigation. + // TODO(darin): perhaps this behavior will be confusing to users? + EXPECT_EQ(controller.GetEntryCount(), 3); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 2); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Receives a back message when there is a new pending navigation entry. +TEST_F(NavigationControllerTest, Back_NewPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL kUrl1("http://foo1"); + const GURL kUrl2("http://foo2"); + const GURL kUrl3("http://foo3"); + + // First navigate two places so we have some back history. + test_rvh()->SendNavigate(0, kUrl1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // controller.LoadURL(kUrl2, content::PAGE_TRANSITION_TYPED); + test_rvh()->SendNavigate(1, kUrl2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now start a new pending navigation and go back before it commits. + controller.LoadURL( + kUrl3, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(kUrl3, controller.GetPendingEntry()->GetURL()); + controller.GoBack(); + + // The pending navigation should now be the "back" item and the new one + // should be gone. + EXPECT_EQ(0, controller.GetPendingEntryIndex()); + EXPECT_EQ(kUrl1, controller.GetPendingEntry()->GetURL()); +} + +// Receives a back message when there is a different renavigation already +// pending. +TEST_F(NavigationControllerTest, Back_OtherBackPending) { + NavigationControllerImpl& controller = controller_impl(); + const GURL kUrl1("http://foo/1"); + const GURL kUrl2("http://foo/2"); + const GURL kUrl3("http://foo/3"); + + // First navigate three places so we have some back history. + test_rvh()->SendNavigate(0, kUrl1); + test_rvh()->SendNavigate(1, kUrl2); + test_rvh()->SendNavigate(2, kUrl3); + + // With nothing pending, say we get a navigation to the second entry. + test_rvh()->SendNavigate(1, kUrl2); + + // We know all the entries have the same site instance, so we can just grab + // a random one for looking up other entries. + SiteInstance* site_instance = + NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->site_instance(); + + // That second URL should be the last committed and it should have gotten the + // new title. + EXPECT_EQ(kUrl2, controller.GetEntryWithPageID(site_instance, 1)->GetURL()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + + // Now go forward to the last item again and say it was committed. + controller.GoForward(); + test_rvh()->SendNavigate(2, kUrl3); + + // Now start going back one to the second page. It will be pending. + controller.GoBack(); + EXPECT_EQ(1, controller.GetPendingEntryIndex()); + EXPECT_EQ(2, controller.GetLastCommittedEntryIndex()); + + // Not synthesize a totally new back event to the first page. This will not + // match the pending one. + test_rvh()->SendNavigate(0, kUrl1); + + // The committed navigation should clear the pending entry. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + + // But the navigated entry should be the last committed. + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kUrl1, controller.GetLastCommittedEntry()->GetURL()); +} + +// Tests what happens when we navigate forward successfully. +TEST_F(NavigationControllerTest, Forward) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoBack(); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoForward(); + + // We should now have a pending navigation to go forward. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), 1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The forward navigation completed successfully. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests what happens when a forward navigation produces a new page. +TEST_F(NavigationControllerTest, Forward_GeneratesNewPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const GURL url3("http://foo3"); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoBack(); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoForward(); + EXPECT_EQ(0U, notifications.size()); + + // Should now have a pending navigation to go forward. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), 1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + + test_rvh()->SendNavigate(2, url3); + EXPECT_TRUE(notifications.Check2AndReset( + content::NOTIFICATION_NAV_LIST_PRUNED, + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Two consequent navigation for the same URL entered in should be considered +// as SAME_PAGE navigation even when we are redirected to some other page. +TEST_F(NavigationControllerTest, Redirect) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); // Redirection target + + // First request + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Second request + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = content::PAGE_TRANSITION_SERVER_REDIRECT; + params.redirects.push_back(GURL("http://foo1")); + params.redirects.push_back(GURL("http://foo2")); + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + content::LoadCommittedDetails details; + + EXPECT_EQ(0U, notifications.size()); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + EXPECT_TRUE(details.type == content::NAVIGATION_TYPE_SAME_PAGE); + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Similar to Redirect above, but the first URL is requested by POST, +// the second URL is requested by GET. NavigationEntry::has_post_data_ +// must be cleared. http://crbug.com/21245 +TEST_F(NavigationControllerTest, PostThenRedirect) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); // Redirection target + + // First request as POST + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + controller.GetActiveEntry()->SetHasPostData(true); + + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Second request + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = content::PAGE_TRANSITION_SERVER_REDIRECT; + params.redirects.push_back(GURL("http://foo1")); + params.redirects.push_back(GURL("http://foo2")); + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + content::LoadCommittedDetails details; + + EXPECT_EQ(0U, notifications.size()); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + EXPECT_TRUE(details.type == content::NAVIGATION_TYPE_SAME_PAGE); + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + EXPECT_FALSE(controller.GetActiveEntry()->GetHasPostData()); + + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// A redirect right off the bat should be a NEW_PAGE. +TEST_F(NavigationControllerTest, ImmediateRedirect) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); // Redirection target + + // First request + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = content::PAGE_TRANSITION_SERVER_REDIRECT; + params.redirects.push_back(GURL("http://foo1")); + params.redirects.push_back(GURL("http://foo2")); + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + content::LoadCommittedDetails details; + + EXPECT_EQ(0U, notifications.size()); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + EXPECT_TRUE(details.type == content::NAVIGATION_TYPE_NEW_PAGE); + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests navigation via link click within a subframe. A new navigation entry +// should be created. +TEST_F(NavigationControllerTest, NewSubframe) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL url2("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url2; + params.transition = content::PAGE_TRANSITION_MANUAL_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(url1, details.previous_url); + EXPECT_FALSE(details.is_in_page); + EXPECT_FALSE(details.is_main_frame); + + // The new entry should be appended. + EXPECT_EQ(2, controller.GetEntryCount()); + + // New entry should refer to the new page, but the old URL (entries only + // reflect the toplevel URL). + EXPECT_EQ(url1, details.entry->GetURL()); + EXPECT_EQ(params.page_id, details.entry->GetPageID()); +} + +// Some pages create a popup, then write an iframe into it. This causes a +// subframe navigation without having any committed entry. Such navigations +// just get thrown on the ground, but we shouldn't crash. +TEST_F(NavigationControllerTest, SubframeOnEmptyPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Navigation controller currently has no entries. + const GURL url("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url; + params.transition = content::PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + + content::LoadCommittedDetails details; + EXPECT_FALSE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(0U, notifications.size()); +} + +// Auto subframes are ones the page loads automatically like ads. They should +// not create new navigation entries. +TEST_F(NavigationControllerTest, AutoSubframe) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL url2("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = content::PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + // Navigating should do nothing. + content::LoadCommittedDetails details; + EXPECT_FALSE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(0U, notifications.size()); + + // There should still be only one entry. + EXPECT_EQ(1, controller.GetEntryCount()); +} + +// Tests navigation and then going back to a subframe navigation. +TEST_F(NavigationControllerTest, BackSubframe) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Main page. + const GURL url1("http://foo1"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // First manual subframe navigation. + const GURL url2("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url2; + params.transition = content::PAGE_TRANSITION_MANUAL_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + // This should generate a new entry. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(2, controller.GetEntryCount()); + + // Second manual subframe navigation should also make a new entry. + const GURL url3("http://foo3"); + params.page_id = 2; + params.url = url3; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(2, controller.GetCurrentEntryIndex()); + + // Go back one. + controller.GoBack(); + params.url = url2; + params.page_id = 1; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(1, controller.GetCurrentEntryIndex()); + + // Go back one more. + controller.GoBack(); + params.url = url1; + params.page_id = 0; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(0, controller.GetCurrentEntryIndex()); +} + +TEST_F(NavigationControllerTest, LinkClick) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Should not have produced a new session history entry. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +TEST_F(NavigationControllerTest, InPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Main page. + const GURL url1("http://foo"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // First navigation. + const GURL url2("http://foo#a"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url2; + params.transition = content::PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + // This should generate a new entry. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_TRUE(details.is_in_page); + EXPECT_FALSE(details.did_replace_entry); + EXPECT_EQ(2, controller.GetEntryCount()); + + // Go back one. + ViewHostMsg_FrameNavigate_Params back_params(params); + controller.GoBack(); + back_params.url = url1; + back_params.page_id = 0; + EXPECT_TRUE(controller.RendererDidNavigate(back_params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + // is_in_page is false in that case but should be true. + // See comment in AreURLsInPageNavigation() in navigation_controller.cc + // EXPECT_TRUE(details.is_in_page); + EXPECT_EQ(2, controller.GetEntryCount()); + EXPECT_EQ(0, controller.GetCurrentEntryIndex()); + EXPECT_EQ(back_params.url, controller.GetActiveEntry()->GetURL()); + + // Go forward + ViewHostMsg_FrameNavigate_Params forward_params(params); + controller.GoForward(); + forward_params.url = url2; + forward_params.page_id = 1; + EXPECT_TRUE(controller.RendererDidNavigate(forward_params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_TRUE(details.is_in_page); + EXPECT_EQ(2, controller.GetEntryCount()); + EXPECT_EQ(1, controller.GetCurrentEntryIndex()); + EXPECT_EQ(forward_params.url, + controller.GetActiveEntry()->GetURL()); + + // Now go back and forward again. This is to work around a bug where we would + // compare the incoming URL with the last committed entry rather than the + // one identified by an existing page ID. This would result in the second URL + // losing the reference fragment when you navigate away from it and then back. + controller.GoBack(); + EXPECT_TRUE(controller.RendererDidNavigate(back_params, &details)); + controller.GoForward(); + EXPECT_TRUE(controller.RendererDidNavigate(forward_params, &details)); + EXPECT_EQ(forward_params.url, + controller.GetActiveEntry()->GetURL()); + + // Finally, navigate to an unrelated URL to make sure in_page is not sticky. + const GURL url3("http://bar"); + params.page_id = 2; + params.url = url3; + notifications.Reset(); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_FALSE(details.is_in_page); +} + +TEST_F(NavigationControllerTest, InPage_Replace) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Main page. + const GURL url1("http://foo"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // First navigation. + const GURL url2("http://foo#a"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; // Same page_id + params.url = url2; + params.transition = content::PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + // This should NOT generate a new entry, nor prune the list. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_TRUE(details.is_in_page); + EXPECT_TRUE(details.did_replace_entry); + EXPECT_EQ(1, controller.GetEntryCount()); +} + +// Tests for http://crbug.com/40395 +// Simulates this: +// <script> +// window.location.replace("#a"); +// window.location='http://foo3/'; +// </script> +TEST_F(NavigationControllerTest, ClientRedirectAfterInPageNavigation) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Load an initial page. + { + const GURL url("http://foo/"); + test_rvh()->SendNavigate(0, url); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + } + + // Navigate to a new page. + { + const GURL url("http://foo2/"); + test_rvh()->SendNavigate(1, url); + controller.DocumentLoadedInFrame(); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + } + + // Navigate within the page. + { + const GURL url("http://foo2/#a"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; // Same page_id + params.url = url; + params.transition = content::PAGE_TRANSITION_LINK; + params.redirects.push_back(url); + params.should_update_history = true; + params.gesture = NavigationGestureUnknown; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + + // This should NOT generate a new entry, nor prune the list. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_TRUE(details.is_in_page); + EXPECT_TRUE(details.did_replace_entry); + EXPECT_EQ(2, controller.GetEntryCount()); + } + + // Perform a client redirect to a new page. + { + const GURL url("http://foo3/"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 2; // New page_id + params.url = url; + params.transition = content::PAGE_TRANSITION_CLIENT_REDIRECT; + params.redirects.push_back(GURL("http://foo2/#a")); + params.redirects.push_back(url); + params.should_update_history = true; + params.gesture = NavigationGestureUnknown; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + + // This SHOULD generate a new entry. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_FALSE(details.is_in_page); + EXPECT_EQ(3, controller.GetEntryCount()); + } + + // Verify that BACK brings us back to http://foo2/. + { + const GURL url("http://foo2/"); + controller.GoBack(); + test_rvh()->SendNavigate(1, url); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(url, controller.GetActiveEntry()->GetURL()); + } +} + +// NotificationObserver implementation used in verifying we've received the +// content::NOTIFICATION_NAV_LIST_PRUNED method. +class PrunedListener : public content::NotificationObserver { + public: + explicit PrunedListener(NavigationControllerImpl* controller) + : notification_count_(0) { + registrar_.Add(this, content::NOTIFICATION_NAV_LIST_PRUNED, + content::Source<NavigationController>(controller)); + } + + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + if (type == content::NOTIFICATION_NAV_LIST_PRUNED) { + notification_count_++; + details_ = *(content::Details<content::PrunedDetails>(details).ptr()); + } + } + + // Number of times NAV_LIST_PRUNED has been observed. + int notification_count_; + + // Details from the last NAV_LIST_PRUNED. + content::PrunedDetails details_; + + private: + content::NotificationRegistrar registrar_; + + DISALLOW_COPY_AND_ASSIGN(PrunedListener); +}; + +// Tests that we limit the number of navigation entries created correctly. +TEST_F(NavigationControllerTest, EnforceMaxNavigationCount) { + NavigationControllerImpl& controller = controller_impl(); + size_t original_count = NavigationControllerImpl::max_entry_count(); + const int kMaxEntryCount = 5; + + NavigationControllerImpl::set_max_entry_count_for_testing(kMaxEntryCount); + + int url_index; + // Load up to the max count, all entries should be there. + for (url_index = 0; url_index < kMaxEntryCount; url_index++) { + GURL url(StringPrintf("http://www.a.com/%d", url_index)); + controller.LoadURL( + url, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + test_rvh()->SendNavigate(url_index, url); + } + + EXPECT_EQ(controller.GetEntryCount(), kMaxEntryCount); + + // Created a PrunedListener to observe prune notifications. + PrunedListener listener(&controller); + + // Navigate some more. + GURL url(StringPrintf("http://www.a.com/%d", url_index)); + controller.LoadURL( + url, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(url_index, url); + url_index++; + + // We should have got a pruned navigation. + EXPECT_EQ(1, listener.notification_count_); + EXPECT_TRUE(listener.details_.from_front); + EXPECT_EQ(1, listener.details_.count); + + // We expect http://www.a.com/0 to be gone. + EXPECT_EQ(controller.GetEntryCount(), kMaxEntryCount); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), + GURL("http:////www.a.com/1")); + + // More navigations. + for (int i = 0; i < 3; i++) { + url = GURL(StringPrintf("http:////www.a.com/%d", url_index)); + controller.LoadURL( + url, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + test_rvh()->SendNavigate(url_index, url); + url_index++; + } + EXPECT_EQ(controller.GetEntryCount(), kMaxEntryCount); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), + GURL("http:////www.a.com/4")); + + NavigationControllerImpl::set_max_entry_count_for_testing(original_count); +} + +// Tests that we can do a restore and navigate to the restored entries and +// everything is updated properly. This can be tricky since there is no +// SiteInstance for the entries created initially. +TEST_F(NavigationControllerTest, RestoreNavigate) { + // Create a NavigationController with a restored set of tabs. + GURL url("http://foo"); + std::vector<NavigationEntry*> entries; + NavigationEntry* entry = NavigationControllerImpl::CreateNavigationEntry( + url, content::Referrer(), content::PAGE_TRANSITION_RELOAD, false, + std::string(), browser_context()); + entry->SetPageID(0); + entry->SetTitle(ASCIIToUTF16("Title")); + entry->SetContentState("state"); + entries.push_back(entry); + TabContents our_contents( + browser_context(), NULL, MSG_ROUTING_NONE, NULL, NULL); + NavigationControllerImpl& our_controller = our_contents.GetControllerImpl(); + our_controller.Restore(0, true, &entries); + ASSERT_EQ(0u, entries.size()); + + // Before navigating to the restored entry, it should have a restore_type + // and no SiteInstance. + EXPECT_EQ(NavigationEntryImpl::RESTORE_LAST_SESSION, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); + EXPECT_FALSE(NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->site_instance()); + + // After navigating, we should have one entry, and it should be "pending". + // It should now have a SiteInstance and no restore_type. + our_controller.GoToIndex(0); + EXPECT_EQ(1, our_controller.GetEntryCount()); + EXPECT_EQ(our_controller.GetEntryAtIndex(0), + our_controller.GetPendingEntry()); + EXPECT_EQ(0, our_controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, + NavigationEntryImpl::FromNavigationEntry + (our_controller.GetEntryAtIndex(0))->restore_type()); + EXPECT_TRUE(NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->site_instance()); + + // Say we navigated to that entry. + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url; + params.transition = content::PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + content::LoadCommittedDetails details; + our_controller.RendererDidNavigate(params, &details); + + // There should be no longer any pending entry and one committed one. This + // means that we were able to locate the entry, assign its site instance, and + // commit it properly. + EXPECT_EQ(1, our_controller.GetEntryCount()); + EXPECT_EQ(0, our_controller.GetLastCommittedEntryIndex()); + EXPECT_FALSE(our_controller.GetPendingEntry()); + EXPECT_EQ(url, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetLastCommittedEntry())->site_instance()-> + GetSite()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); +} + +// Tests that we can still navigate to a restored entry after a different +// navigation fails and clears the pending entry. http://crbug.com/90085 +TEST_F(NavigationControllerTest, RestoreNavigateAfterFailure) { + // Create a NavigationController with a restored set of tabs. + GURL url("http://foo"); + std::vector<NavigationEntry*> entries; + NavigationEntry* entry = NavigationControllerImpl::CreateNavigationEntry( + url, content::Referrer(), content::PAGE_TRANSITION_RELOAD, false, + std::string(), browser_context()); + entry->SetPageID(0); + entry->SetTitle(ASCIIToUTF16("Title")); + entry->SetContentState("state"); + entries.push_back(entry); + TabContents our_contents( + browser_context(), NULL, MSG_ROUTING_NONE, NULL, NULL); + NavigationControllerImpl& our_controller = our_contents.GetControllerImpl(); + our_controller.Restore(0, true, &entries); + ASSERT_EQ(0u, entries.size()); + + // Before navigating to the restored entry, it should have a restore_type + // and no SiteInstance. + EXPECT_EQ(NavigationEntryImpl::RESTORE_LAST_SESSION, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); + EXPECT_FALSE(NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->site_instance()); + + // After navigating, we should have one entry, and it should be "pending". + // It should now have a SiteInstance and no restore_type. + our_controller.GoToIndex(0); + EXPECT_EQ(1, our_controller.GetEntryCount()); + EXPECT_EQ(our_controller.GetEntryAtIndex(0), + our_controller.GetPendingEntry()); + EXPECT_EQ(0, our_controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); + EXPECT_TRUE(NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->site_instance()); + + // This pending navigation may have caused a different navigation to fail, + // which causes the pending entry to be cleared. + TestRenderViewHost* rvh = + static_cast<TestRenderViewHost*>(our_contents.GetRenderViewHost()); + ViewHostMsg_DidFailProvisionalLoadWithError_Params fail_load_params; + fail_load_params.frame_id = 1; + fail_load_params.is_main_frame = true; + fail_load_params.error_code = net::ERR_ABORTED; + fail_load_params.error_description = string16(); + fail_load_params.url = url; + fail_load_params.showing_repost_interstitial = false; + rvh->OnMessageReceived( + ViewHostMsg_DidFailProvisionalLoadWithError(0, // routing_id + fail_load_params)); + + // Now the pending restored entry commits. + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url; + params.transition = content::PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + content::LoadCommittedDetails details; + our_controller.RendererDidNavigate(params, &details); + + // There should be no pending entry and one committed one. + EXPECT_EQ(1, our_controller.GetEntryCount()); + EXPECT_EQ(0, our_controller.GetLastCommittedEntryIndex()); + EXPECT_FALSE(our_controller.GetPendingEntry()); + EXPECT_EQ(url, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetLastCommittedEntry())->site_instance()-> + GetSite()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); +} + +// Make sure that the page type and stuff is correct after an interstitial. +TEST_F(NavigationControllerTest, Interstitial) { + NavigationControllerImpl& controller = controller_impl(); + // First navigate somewhere normal. + const GURL url1("http://foo"); + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + + // Now navigate somewhere with an interstitial. + const GURL url2("http://bar"); + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + set_page_type(content::PAGE_TYPE_INTERSTITIAL); + + // At this point the interstitial will be displayed and the load will still + // be pending. If the user continues, the load will commit. + test_rvh()->SendNavigate(1, url2); + + // The page should be a normal page again. + EXPECT_EQ(url2, controller.GetLastCommittedEntry()->GetURL()); + EXPECT_EQ(content::PAGE_TYPE_NORMAL, + controller.GetLastCommittedEntry()->GetPageType()); +} + +TEST_F(NavigationControllerTest, RemoveEntry) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + const GURL url4("http://foo/4"); + const GURL url5("http://foo/5"); + const GURL pending_url("http://foo/pending"); + const GURL default_url("http://foo/default"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url2); + controller.LoadURL( + url3, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(2, url3); + controller.LoadURL( + url4, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(3, url4); + controller.LoadURL( + url5, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(4, url5); + + // Try to remove the last entry. Will fail because it is the current entry. + controller.RemoveEntryAtIndex(controller.GetEntryCount() - 1); + EXPECT_EQ(5, controller.GetEntryCount()); + EXPECT_EQ(4, controller.GetLastCommittedEntryIndex()); + + // Go back and remove the last entry. + controller.GoBack(); + test_rvh()->SendNavigate(3, url4); + controller.RemoveEntryAtIndex(controller.GetEntryCount() - 1); + EXPECT_EQ(4, controller.GetEntryCount()); + EXPECT_EQ(3, controller.GetLastCommittedEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + + // Remove an entry which is not the last committed one. + controller.RemoveEntryAtIndex(0); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(2, controller.GetLastCommittedEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + + // Remove the 2 remaining entries. + controller.RemoveEntryAtIndex(1); + controller.RemoveEntryAtIndex(0); + + // This should leave us with only the last committed entry. + EXPECT_EQ(1, controller.GetEntryCount()); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); +} + +// Tests the transient entry, making sure it goes away with all navigations. +TEST_F(NavigationControllerTest, TransientEntry) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url0("http://foo/0"); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + const GURL url4("http://foo/4"); + const GURL transient_url("http://foo/transient"); + + controller.LoadURL( + url0, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url0); + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url1); + + notifications.Reset(); + + // Adding a transient with no pending entry. + NavigationEntryImpl* transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + + // We should not have received any notifications. + EXPECT_EQ(0U, notifications.size()); + + // Check our state. + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 3); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 1); + + // Navigate. + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(2, url2); + + // We should have navigated, transient entry should be gone. + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 3); + + // Add a transient again, then navigate with no pending entry this time. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + test_rvh()->SendNavigate(3, url3); + // Transient entry should be gone. + EXPECT_EQ(url3, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 4); + + // Initiate a navigation, add a transient then commit navigation. + controller.LoadURL( + url4, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + test_rvh()->SendNavigate(4, url4); + EXPECT_EQ(url4, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 5); + + // Add a transient and go back. This should simply remove the transient. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + controller.GoBack(); + // Transient entry should be gone. + EXPECT_EQ(url4, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 5); + test_rvh()->SendNavigate(3, url3); + + // Add a transient and go to an entry before the current one. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + controller.GoToIndex(1); + // The navigation should have been initiated, transient entry should be gone. + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + // Visible entry does not update for history navigations until commit. + EXPECT_EQ(url3, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(1, url1); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + + // Add a transient and go to an entry after the current one. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + controller.GoToIndex(3); + // The navigation should have been initiated, transient entry should be gone. + // Because of the transient entry that is removed, going to index 3 makes us + // land on url2 (which is visible after the commit). + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(2, url2); + EXPECT_EQ(url2, controller.GetVisibleEntry()->GetURL()); + + // Add a transient and go forward. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + EXPECT_TRUE(controller.CanGoForward()); + controller.GoForward(); + // We should have navigated, transient entry should be gone. + EXPECT_EQ(url3, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url2, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(3, url3); + EXPECT_EQ(url3, controller.GetVisibleEntry()->GetURL()); + + // Ensure the URLS are correct. + EXPECT_EQ(controller.GetEntryCount(), 5); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url0); + EXPECT_EQ(controller.GetEntryAtIndex(1)->GetURL(), url1); + EXPECT_EQ(controller.GetEntryAtIndex(2)->GetURL(), url2); + EXPECT_EQ(controller.GetEntryAtIndex(3)->GetURL(), url3); + EXPECT_EQ(controller.GetEntryAtIndex(4)->GetURL(), url4); +} + +// Tests that the URLs for renderer-initiated navigations are not displayed to +// the user until the navigation commits, to prevent URL spoof attacks. +// See http://crbug.com/99016. +TEST_F(NavigationControllerTest, DontShowRendererURLUntilCommit) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url0("http://foo/0"); + const GURL url1("http://foo/1"); + + // For typed navigations (browser-initiated), both active and visible entries + // should update before commit. + controller.LoadURL(url0, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(url0, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url0, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(0, url0); + + // For link clicks (renderer-initiated navigations), the active entry should + // update before commit but the visible should not. + controller.LoadURLFromRenderer(url1, content::Referrer(), + content::PAGE_TRANSITION_LINK, + std::string()); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url0, controller.GetVisibleEntry()->GetURL()); + EXPECT_TRUE( + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + is_renderer_initiated()); + + // After commit, both should be updated, and we should no longer treat the + // entry as renderer-initiated. + test_rvh()->SendNavigate(1, url1); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + EXPECT_FALSE( + NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->is_renderer_initiated()); + + notifications.Reset(); +} + +// Tests that IsInPageNavigation returns appropriate results. Prevents +// regression for bug 1126349. +TEST_F(NavigationControllerTest, IsInPageNavigation) { + NavigationControllerImpl& controller = controller_impl(); + // Navigate to URL with no refs. + const GURL url("http://www.google.com/home.html"); + test_rvh()->SendNavigate(0, url); + + // Reloading the page is not an in-page navigation. + EXPECT_FALSE(controller.IsURLInPageNavigation(url)); + const GURL other_url("http://www.google.com/add.html"); + EXPECT_FALSE(controller.IsURLInPageNavigation(other_url)); + const GURL url_with_ref("http://www.google.com/home.html#my_ref"); + EXPECT_TRUE(controller.IsURLInPageNavigation(url_with_ref)); + + // Navigate to URL with refs. + test_rvh()->SendNavigate(1, url_with_ref); + + // Reloading the page is not an in-page navigation. + EXPECT_FALSE(controller.IsURLInPageNavigation(url_with_ref)); + EXPECT_FALSE(controller.IsURLInPageNavigation(url)); + EXPECT_FALSE(controller.IsURLInPageNavigation(other_url)); + const GURL other_url_with_ref("http://www.google.com/home.html#my_other_ref"); + EXPECT_TRUE(controller.IsURLInPageNavigation( + other_url_with_ref)); +} + +// Some pages can have subframes with the same base URL (minus the reference) as +// the main page. Even though this is hard, it can happen, and we don't want +// these subframe navigations to affect the toplevel document. They should +// instead be ignored. http://crbug.com/5585 +TEST_F(NavigationControllerTest, SameSubframe) { + NavigationControllerImpl& controller = controller_impl(); + // Navigate the main frame. + const GURL url("http://www.google.com/"); + test_rvh()->SendNavigate(0, url); + + // We should be at the first navigation entry. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + + // Navigate a subframe that would normally count as in-page. + const GURL subframe("http://www.google.com/#"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = subframe; + params.transition = content::PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(subframe)); + content::LoadCommittedDetails details; + EXPECT_FALSE(controller.RendererDidNavigate(params, &details)); + + // Nothing should have changed. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); +} + +// Make sure that on cloning a tabcontents and going back needs_reload is false. +TEST_F(NavigationControllerTest, CloneAndGoBack) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + + scoped_ptr<WebContents> clone(controller.GetWebContents()->Clone()); + + ASSERT_EQ(2, clone->GetController().GetEntryCount()); + EXPECT_TRUE(clone->GetController().NeedsReload()); + clone->GetController().GoBack(); + // Navigating back should have triggered needs_reload_ to go false. + EXPECT_FALSE(clone->GetController().NeedsReload()); +} + +// Make sure that cloning a tabcontents doesn't copy interstitials. +TEST_F(NavigationControllerTest, CloneOmitsInterstitials) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + + // Add an interstitial entry. Should be deleted with controller. + NavigationEntryImpl* interstitial_entry = new NavigationEntryImpl(); + interstitial_entry->set_page_type(content::PAGE_TYPE_INTERSTITIAL); + controller.AddTransientEntry(interstitial_entry); + + scoped_ptr<WebContents> clone(controller.GetWebContents()->Clone()); + + ASSERT_EQ(2, clone->GetController().GetEntryCount()); +} + +// Tests a subframe navigation while a toplevel navigation is pending. +// http://crbug.com/43967 +TEST_F(NavigationControllerTest, SubframeWhilePending) { + NavigationControllerImpl& controller = controller_impl(); + // Load the first page. + const GURL url1("http://foo/"); + NavigateAndCommit(url1); + + // Now start a pending load to a totally different page, but don't commit it. + const GURL url2("http://bar/"); + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + // Send a subframe update from the first page, as if one had just + // automatically loaded. Auto subframes don't increment the page ID. + const GURL url1_sub("http://foo/subframe"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = controller.GetLastCommittedEntry()->GetPageID(); + params.url = url1_sub; + params.transition = content::PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url1_sub)); + content::LoadCommittedDetails details; + + // This should return false meaning that nothing was actually updated. + EXPECT_FALSE(controller.RendererDidNavigate(params, &details)); + + // The notification should have updated the last committed one, and not + // the pending load. + EXPECT_EQ(url1, controller.GetLastCommittedEntry()->GetURL()); + + // The active entry should be unchanged by the subframe load. + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); +} + +// Tests CopyStateFromAndPrune with 2 urls in source, 1 in dest. +TEST_F(NavigationControllerTest, CopyStateFromAndPrune) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + + // First two entries should have the same SiteInstance. + SiteInstance* instance1 = + GetSiteInstanceFromEntry(controller.GetEntryAtIndex(0)); + SiteInstance* instance2 = + GetSiteInstanceFromEntry(controller.GetEntryAtIndex(1)); + EXPECT_EQ(instance1, instance2); + EXPECT_EQ(0, controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(1, controller.GetEntryAtIndex(1)->GetPageID()); + EXPECT_EQ(1, contents()->GetMaxPageIDForSiteInstance(instance1)); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = + other_contents->GetControllerImpl(); + other_contents->NavigateAndCommit(url3); + other_contents->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)), 2, + other_controller.GetEntryAtIndex(0)->GetPageID()); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain the 3 urls: url1, url2 and url3. + + ASSERT_EQ(3, other_controller.GetEntryCount()); + + ASSERT_EQ(2, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(url2, other_controller.GetEntryAtIndex(1)->GetURL()); + EXPECT_EQ(url3, other_controller.GetEntryAtIndex(2)->GetURL()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(1, other_controller.GetEntryAtIndex(1)->GetPageID()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(2)->GetPageID()); + + // A new SiteInstance should be used for the new tab. + SiteInstance* instance3 = + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(2)); + EXPECT_NE(instance3, instance1); + + // The max page ID map should be copied over and updated with the max page ID + // from the current tab. + EXPECT_EQ(1, other_contents->GetMaxPageIDForSiteInstance(instance1)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance3)); +} + +// Test CopyStateFromAndPrune with 2 urls, the first selected and nothing in +// the target. +TEST_F(NavigationControllerTest, CopyStateFromAndPrune2) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const GURL url3("http://foo3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + controller.GoBack(); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = + other_contents->GetControllerImpl(); + other_contents->ExpectSetHistoryLengthAndPrune(NULL, 1, -1); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain the 1 url: url1. + + ASSERT_EQ(1, other_controller.GetEntryCount()); + + ASSERT_EQ(0, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(0)->GetPageID()); + + // The max page ID map should be copied over and updated with the max page ID + // from the current tab. + SiteInstance* instance1 = + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance1)); +} + +// Test CopyStateFromAndPrune with 2 urls, the first selected and nothing in +// the target. +TEST_F(NavigationControllerTest, CopyStateFromAndPrune3) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const GURL url3("http://foo3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + controller.GoBack(); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = + other_contents->GetControllerImpl(); + other_controller.LoadURL( + url3, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + other_contents->ExpectSetHistoryLengthAndPrune(NULL, 1, -1); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain 1 entry for url1, and a pending entry + // for url3. + + ASSERT_EQ(1, other_controller.GetEntryCount()); + + EXPECT_EQ(0, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + + // And there should be a pending entry for url3. + ASSERT_TRUE(other_controller.GetPendingEntry()); + + EXPECT_EQ(url3, other_controller.GetPendingEntry()->GetURL()); + + // The max page ID map should be copied over and updated with the max page ID + // from the current tab. + SiteInstance* instance1 = + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance1)); +} + +// Tests CopyStateFromAndPrune with 3 urls in source, 1 in dest, +// when the max entry count is 3. We should prune one entry. +TEST_F(NavigationControllerTest, CopyStateFromAndPruneMaxEntries) { + NavigationControllerImpl& controller = controller_impl(); + size_t original_count = NavigationControllerImpl::max_entry_count(); + const int kMaxEntryCount = 3; + + NavigationControllerImpl::set_max_entry_count_for_testing(kMaxEntryCount); + + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + const GURL url4("http://foo/4"); + + // Create a PrunedListener to observe prune notifications. + PrunedListener listener(&controller); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = + other_contents->GetControllerImpl(); + other_contents->NavigateAndCommit(url4); + other_contents->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)), 2, + other_controller.GetEntryAtIndex(0)->GetPageID()); + other_controller.CopyStateFromAndPrune(&controller); + + // We should have received a pruned notification. + EXPECT_EQ(1, listener.notification_count_); + EXPECT_TRUE(listener.details_.from_front); + EXPECT_EQ(1, listener.details_.count); + + // other_controller should now contain only 3 urls: url2, url3 and url4. + + ASSERT_EQ(3, other_controller.GetEntryCount()); + + ASSERT_EQ(2, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url2, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(url3, other_controller.GetEntryAtIndex(1)->GetURL()); + EXPECT_EQ(url4, other_controller.GetEntryAtIndex(2)->GetURL()); + EXPECT_EQ(1, other_controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(2, other_controller.GetEntryAtIndex(1)->GetPageID()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(2)->GetPageID()); + + NavigationControllerImpl::set_max_entry_count_for_testing(original_count); +} + +// Tests that navigations initiated from the page (with the history object) +// work as expected without navigation entries. +TEST_F(NavigationControllerTest, HistoryNavigate) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + controller.GoBack(); + contents()->CommitPendingNavigation(); + + // Simulate the page calling history.back(), it should not create a pending + // entry. + contents()->OnGoToEntryAtOffset(-1); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + // The actual cross-navigation is suspended until the current RVH tells us + // it unloaded, simulate that. + contents()->ProceedWithCrossSiteNavigation(); + // Also make sure we told the page to navigate. + const IPC::Message* message = + process()->sink().GetFirstMessageMatching(ViewMsg_Navigate::ID); + ASSERT_TRUE(message != NULL); + Tuple1<ViewMsg_Navigate_Params> nav_params; + ViewMsg_Navigate::Read(message, &nav_params); + EXPECT_EQ(url1, nav_params.a.url); + process()->sink().ClearMessages(); + + // Now test history.forward() + contents()->OnGoToEntryAtOffset(1); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + // The actual cross-navigation is suspended until the current RVH tells us + // it unloaded, simulate that. + contents()->ProceedWithCrossSiteNavigation(); + message = process()->sink().GetFirstMessageMatching(ViewMsg_Navigate::ID); + ASSERT_TRUE(message != NULL); + ViewMsg_Navigate::Read(message, &nav_params); + EXPECT_EQ(url3, nav_params.a.url); + process()->sink().ClearMessages(); + + // Make sure an extravagant history.go() doesn't break. + contents()->OnGoToEntryAtOffset(120); // Out of bounds. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + message = process()->sink().GetFirstMessageMatching(ViewMsg_Navigate::ID); + EXPECT_TRUE(message == NULL); +} + +// Test call to PruneAllButActive for the only entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForSingle) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + NavigateAndCommit(url1); + controller.PruneAllButActive(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url1); +} + +// Test call to PruneAllButActive for last entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForLast) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + controller.GoBack(); + controller.GoBack(); + contents()->CommitPendingNavigation(); + + controller.PruneAllButActive(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url1); +} + +// Test call to PruneAllButActive for intermediate entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForIntermediate) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + controller.GoBack(); + contents()->CommitPendingNavigation(); + + controller.PruneAllButActive(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url2); +} + +// Test call to PruneAllButActive for intermediate entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForPending) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + controller.GoBack(); + + controller.PruneAllButActive(); + + EXPECT_EQ(0, controller.GetPendingEntryIndex()); +} + +// Test call to PruneAllButActive for transient entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForTransient) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url0("http://foo/0"); + const GURL url1("http://foo/1"); + const GURL transient_url("http://foo/transient"); + + controller.LoadURL( + url0, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url0); + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url1); + + // Adding a transient with no pending entry. + NavigationEntryImpl* transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + + controller.PruneAllButActive(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetTransientEntry()->GetURL(), transient_url); +} + +// Test to ensure that when we do a history navigation back to the current +// committed page (e.g., going forward to a slow-loading page, then pressing +// the back button), we just stop the navigation to prevent the throbber from +// running continuously. Otherwise, the RenderViewHost forces the throbber to +// start, but WebKit essentially ignores the navigation and never sends a +// message to stop the throbber. +TEST_F(NavigationControllerTest, StopOnHistoryNavigationToCurrentPage) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url0("http://foo/0"); + const GURL url1("http://foo/1"); + + NavigateAndCommit(url0); + NavigateAndCommit(url1); + + // Go back to the original page, then forward to the slow page, then back + controller.GoBack(); + contents()->CommitPendingNavigation(); + + controller.GoForward(); + EXPECT_EQ(1, controller.GetPendingEntryIndex()); + + controller.GoBack(); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); +} + +/* TODO(brettw) These test pass on my local machine but fail on the XP buildbot + (but not Vista) cleaning up the directory after they run. + This should be fixed. + +// NavigationControllerHistoryTest --------------------------------------------- + +class NavigationControllerHistoryTest : public NavigationControllerTest { + public: + NavigationControllerHistoryTest() + : url0("http://foo1"), + url1("http://foo1"), + url2("http://foo1"), + profile_manager_(NULL) { + } + + virtual ~NavigationControllerHistoryTest() { + // Prevent our base class from deleting the profile since profile's + // lifetime is managed by profile_manager_. + STLDeleteElements(&windows_); + } + + // testing::Test overrides. + virtual void SetUp() { + NavigationControllerTest::SetUp(); + + // Force the session service to be created. + SessionService* service = new SessionService(profile()); + SessionServiceFactory::SetForTestProfile(profile(), service); + service->SetWindowType(window_id, Browser::TYPE_TABBED); + service->SetWindowBounds(window_id, gfx::Rect(0, 1, 2, 3), false); + service->SetTabIndexInWindow(window_id, + controller.session_id(), 0); + controller.SetWindowID(window_id); + + session_helper_.set_service(service); + } + + virtual void TearDown() { + // Release profile's reference to the session service. Otherwise the file + // will still be open and we won't be able to delete the directory below. + session_helper_.ReleaseService(); // profile owns this + SessionServiceFactory::SetForTestProfile(profile(), NULL); + + // Make sure we wait for history to shut down before continuing. The task + // we add will cause our message loop to quit once it is destroyed. + HistoryService* history = + profile()->GetHistoryService(Profile::IMPLICIT_ACCESS); + if (history) { + history->SetOnBackendDestroyTask(MessageLoop::QuitClosure()); + MessageLoop::current()->Run(); + } + + // Do normal cleanup before deleting the profile directory below. + NavigationControllerTest::TearDown(); + + ASSERT_TRUE(file_util::Delete(test_dir_, true)); + ASSERT_FALSE(file_util::PathExists(test_dir_)); + } + + // Deletes the current profile manager and creates a new one. Indirectly this + // shuts down the history database and reopens it. + void ReopenDatabase() { + session_helper_.set_service(NULL); + SessionServiceFactory::SetForTestProfile(profile(), NULL); + + SessionService* service = new SessionService(profile()); + SessionServiceFactory::SetForTestProfile(profile(), service); + session_helper_.set_service(service); + } + + void GetLastSession() { + SessionServiceFactory::GetForProfile(profile())->TabClosed( + controller.window_id(), controller.session_id(), false); + + ReopenDatabase(); + Time close_time; + + session_helper_.ReadWindows(&windows_); + } + + CancelableRequestConsumer consumer; + + // URLs for testing. + const GURL url0; + const GURL url1; + const GURL url2; + + std::vector<SessionWindow*> windows_; + + SessionID window_id; + + SessionServiceTestHelper session_helper_; + + private: + ProfileManager* profile_manager_; + FilePath test_dir_; +}; + +// A basic test case. Navigates to a single url, and make sure the history +// db matches. +TEST_F(NavigationControllerHistoryTest, Basic) { + NavigationControllerImpl& controller = controller_impl(); + controller.LoadURL(url0, GURL(), content::PAGE_TRANSITION_LINK); + test_rvh()->SendNavigate(0, url0); + + GetLastSession(); + + session_helper_.AssertSingleWindowWithSingleTab(windows_, 1); + session_helper_.AssertTabEquals(0, 0, 1, *(windows_[0]->tabs[0])); + TabNavigation nav1(0, url0, GURL(), string16(), + webkit_glue::CreateHistoryStateForURL(url0), + content::PAGE_TRANSITION_LINK); + session_helper_.AssertNavigationEquals(nav1, + windows_[0]->tabs[0]->navigations[0]); +} + +// Navigates to three urls, then goes back and make sure the history database +// is in sync. +TEST_F(NavigationControllerHistoryTest, NavigationThenBack) { + NavigationControllerImpl& controller = controller_impl(); + test_rvh()->SendNavigate(0, url0); + test_rvh()->SendNavigate(1, url1); + test_rvh()->SendNavigate(2, url2); + + controller.GoBack(); + test_rvh()->SendNavigate(1, url1); + + GetLastSession(); + + session_helper_.AssertSingleWindowWithSingleTab(windows_, 3); + session_helper_.AssertTabEquals(0, 1, 3, *(windows_[0]->tabs[0])); + + TabNavigation nav(0, url0, GURL(), string16(), + webkit_glue::CreateHistoryStateForURL(url0), + content::PAGE_TRANSITION_LINK); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[0]); + nav.set_url(url1); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[1]); + nav.set_url(url2); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[2]); +} + +// Navigates to three urls, then goes back twice, then loads a new url. +TEST_F(NavigationControllerHistoryTest, NavigationPruning) { + NavigationControllerImpl& controller = controller_impl(); + test_rvh()->SendNavigate(0, url0); + test_rvh()->SendNavigate(1, url1); + test_rvh()->SendNavigate(2, url2); + + controller.GoBack(); + test_rvh()->SendNavigate(1, url1); + + controller.GoBack(); + test_rvh()->SendNavigate(0, url0); + + test_rvh()->SendNavigate(3, url2); + + // Now have url0, and url2. + + GetLastSession(); + + session_helper_.AssertSingleWindowWithSingleTab(windows_, 2); + session_helper_.AssertTabEquals(0, 1, 2, *(windows_[0]->tabs[0])); + + TabNavigation nav(0, url0, GURL(), string16(), + webkit_glue::CreateHistoryStateForURL(url0), + content::PAGE_TRANSITION_LINK); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[0]); + nav.set_url(url2); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[1]); +} +*/ diff --git a/content/browser/web_contents/navigation_entry_impl.cc b/content/browser/web_contents/navigation_entry_impl.cc new file mode 100644 index 0000000..60f58b9 --- /dev/null +++ b/content/browser/web_contents/navigation_entry_impl.cc @@ -0,0 +1,221 @@ +// Copyright (c) 2012 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. + +#include "content/browser/web_contents/navigation_entry_impl.h" + +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "content/public/common/content_constants.h" +#include "content/public/common/url_constants.h" +#include "net/base/net_util.h" +#include "ui/base/text/text_elider.h" + +using content::SiteInstance; + +// Use this to get a new unique ID for a NavigationEntry during construction. +// The returned ID is guaranteed to be nonzero (which is the "no ID" indicator). +static int GetUniqueIDInConstructor() { + static int unique_id_counter = 0; + return ++unique_id_counter; +} + +namespace content { + +NavigationEntry* NavigationEntry::Create() { + return new NavigationEntryImpl(); +} + +NavigationEntry* NavigationEntry::Create(const NavigationEntry& copy) { + return new NavigationEntryImpl(static_cast<const NavigationEntryImpl&>(copy)); +} + +NavigationEntryImpl* NavigationEntryImpl::FromNavigationEntry( + NavigationEntry* entry) { + return static_cast<NavigationEntryImpl*>(entry); +} + +NavigationEntryImpl::NavigationEntryImpl() + : unique_id_(GetUniqueIDInConstructor()), + site_instance_(NULL), + page_type_(PAGE_TYPE_NORMAL), + update_virtual_url_with_url_(false), + page_id_(-1), + transition_type_(PAGE_TRANSITION_LINK), + has_post_data_(false), + post_id_(-1), + restore_type_(RESTORE_NONE), + is_renderer_initiated_(false), + is_cross_site_reload_(false) { +} + +NavigationEntryImpl::NavigationEntryImpl(SiteInstanceImpl* instance, + int page_id, + const GURL& url, + const Referrer& referrer, + const string16& title, + PageTransition transition_type, + bool is_renderer_initiated) + : unique_id_(GetUniqueIDInConstructor()), + site_instance_(instance), + page_type_(PAGE_TYPE_NORMAL), + url_(url), + referrer_(referrer), + update_virtual_url_with_url_(false), + title_(title), + page_id_(page_id), + transition_type_(transition_type), + has_post_data_(false), + post_id_(-1), + restore_type_(RESTORE_NONE), + is_renderer_initiated_(is_renderer_initiated), + is_cross_site_reload_(false) { +} + +NavigationEntryImpl::~NavigationEntryImpl() { +} + +int NavigationEntryImpl::GetUniqueID() const { + return unique_id_; +} + +PageType NavigationEntryImpl::GetPageType() const { + return page_type_; +} + +void NavigationEntryImpl::SetURL(const GURL& url) { + url_ = url; + cached_display_title_.clear(); +} + +const GURL& NavigationEntryImpl::GetURL() const { + return url_; +} + +void NavigationEntryImpl::SetReferrer(const Referrer& referrer) { + referrer_ = referrer; +} + +const Referrer& NavigationEntryImpl::GetReferrer() const { + return referrer_; +} + +void NavigationEntryImpl::SetVirtualURL(const GURL& url) { + virtual_url_ = (url == url_) ? GURL() : url; + cached_display_title_.clear(); +} + +const GURL& NavigationEntryImpl::GetVirtualURL() const { + return virtual_url_.is_empty() ? url_ : virtual_url_; +} + +void NavigationEntryImpl::SetTitle(const string16& title) { + title_ = title; + cached_display_title_.clear(); +} + +const string16& NavigationEntryImpl::GetTitle() const { + return title_; +} + +void NavigationEntryImpl::SetContentState(const std::string& state) { + content_state_ = state; +} + +const std::string& NavigationEntryImpl::GetContentState() const { + return content_state_; +} + +void NavigationEntryImpl::SetPageID(int page_id) { + page_id_ = page_id; +} + +int32 NavigationEntryImpl::GetPageID() const { + return page_id_; +} + +void NavigationEntryImpl::set_site_instance(SiteInstanceImpl* site_instance) { + site_instance_ = site_instance; +} + +const string16& NavigationEntryImpl::GetTitleForDisplay( + const std::string& languages) const { + // Most pages have real titles. Don't even bother caching anything if this is + // the case. + if (!title_.empty()) + return title_; + + // More complicated cases will use the URLs as the title. This result we will + // cache since it's more complicated to compute. + if (!cached_display_title_.empty()) + return cached_display_title_; + + // Use the virtual URL first if any, and fall back on using the real URL. + string16 title; + if (!virtual_url_.is_empty()) { + title = net::FormatUrl(virtual_url_, languages); + } else if (!url_.is_empty()) { + title = net::FormatUrl(url_, languages); + } + + // For file:// URLs use the filename as the title, not the full path. + if (url_.SchemeIsFile()) { + string16::size_type slashpos = title.rfind('/'); + if (slashpos != string16::npos) + title = title.substr(slashpos + 1); + } + + ui::ElideString(title, kMaxTitleChars, &cached_display_title_); + return cached_display_title_; +} + +bool NavigationEntryImpl::IsViewSourceMode() const { + return virtual_url_.SchemeIs(chrome::kViewSourceScheme); +} + +void NavigationEntryImpl::SetTransitionType( + PageTransition transition_type) { + transition_type_ = transition_type; +} + +PageTransition NavigationEntryImpl::GetTransitionType() const { + return transition_type_; +} + +const GURL& NavigationEntryImpl::GetUserTypedURL() const { + return user_typed_url_; +} + +void NavigationEntryImpl::SetHasPostData(bool has_post_data) { + has_post_data_ = has_post_data; +} + +bool NavigationEntryImpl::GetHasPostData() const { + return has_post_data_; +} + +void NavigationEntryImpl::SetPostID(int64 post_id) { + post_id_ = post_id; +} + +int64 NavigationEntryImpl::GetPostID() const { + return post_id_; +} + +const FaviconStatus& NavigationEntryImpl::GetFavicon() const { + return favicon_; +} + +FaviconStatus& NavigationEntryImpl::GetFavicon() { + return favicon_; +} + +const SSLStatus& NavigationEntryImpl::GetSSL() const { + return ssl_; +} + +SSLStatus& NavigationEntryImpl::GetSSL() { + return ssl_; +} + +} // namespace content diff --git a/content/browser/web_contents/navigation_entry_impl.h b/content/browser/web_contents/navigation_entry_impl.h new file mode 100644 index 0000000..ec194fd --- /dev/null +++ b/content/browser/web_contents/navigation_entry_impl.h @@ -0,0 +1,217 @@ +// Copyright (c) 2012 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. + +#ifndef CONTENT_BROWSER_WEB_CONTENTS_NAVIGATION_ENTRY_IMPL_H_ +#define CONTENT_BROWSER_WEB_CONTENTS_NAVIGATION_ENTRY_IMPL_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "content/browser/site_instance_impl.h" +#include "content/public/browser/favicon_status.h" +#include "content/public/browser/global_request_id.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/common/ssl_status.h" + +namespace content { + +class CONTENT_EXPORT NavigationEntryImpl + : public NON_EXPORTED_BASE(NavigationEntry) { + public: + static NavigationEntryImpl* FromNavigationEntry(NavigationEntry* entry); + + NavigationEntryImpl(); + NavigationEntryImpl(SiteInstanceImpl* instance, + int page_id, + const GURL& url, + const Referrer& referrer, + const string16& title, + PageTransition transition_type, + bool is_renderer_initiated); + virtual ~NavigationEntryImpl(); + + // NavigationEntry implementation: + virtual int GetUniqueID() const OVERRIDE; + virtual PageType GetPageType() const OVERRIDE; + virtual void SetURL(const GURL& url) OVERRIDE; + virtual const GURL& GetURL() const OVERRIDE; + virtual void SetReferrer(const Referrer& referrer) OVERRIDE; + virtual const Referrer& GetReferrer() const OVERRIDE; + virtual void SetVirtualURL(const GURL& url) OVERRIDE; + virtual const GURL& GetVirtualURL() const OVERRIDE; + virtual void SetTitle(const string16& title) OVERRIDE; + virtual const string16& GetTitle() const OVERRIDE; + virtual void SetContentState(const std::string& state) OVERRIDE; + virtual const std::string& GetContentState() const OVERRIDE; + virtual void SetPageID(int page_id) OVERRIDE; + virtual int32 GetPageID() const OVERRIDE; + virtual const string16& GetTitleForDisplay( + const std::string& languages) const OVERRIDE; + virtual bool IsViewSourceMode() const OVERRIDE; + virtual void SetTransitionType(PageTransition transition_type) OVERRIDE; + virtual PageTransition GetTransitionType() const OVERRIDE; + virtual const GURL& GetUserTypedURL() const OVERRIDE; + virtual void SetHasPostData(bool has_post_data) OVERRIDE; + virtual bool GetHasPostData() const OVERRIDE; + virtual void SetPostID(int64 post_id) OVERRIDE; + virtual int64 GetPostID() const OVERRIDE; + virtual const FaviconStatus& GetFavicon() const OVERRIDE; + virtual FaviconStatus& GetFavicon() OVERRIDE; + virtual const SSLStatus& GetSSL() const OVERRIDE; + virtual SSLStatus& GetSSL() OVERRIDE; + + void set_unique_id(int unique_id) { + unique_id_ = unique_id; + } + + // The SiteInstance tells us how to share sub-processes when the tab type is + // TAB_CONTENTS_WEB. This will be NULL otherwise. This is a reference counted + // pointer to a shared site instance. + // + // Note that the SiteInstance should usually not be changed after it is set, + // but this may happen if the NavigationEntry was cloned and needs to use a + // different SiteInstance. + void set_site_instance(SiteInstanceImpl* site_instance); + SiteInstanceImpl* site_instance() const { + return site_instance_.get(); + } + + void set_page_type(PageType page_type) { + page_type_ = page_type; + } + + bool has_virtual_url() const { + return !virtual_url_.is_empty(); + } + + bool update_virtual_url_with_url() const { + return update_virtual_url_with_url_; + } + void set_update_virtual_url_with_url(bool update) { + update_virtual_url_with_url_ = update; + } + + // Extra headers (separated by \n) to send during the request. + void set_extra_headers(const std::string& extra_headers) { + extra_headers_ = extra_headers; + } + const std::string& extra_headers() const { + return extra_headers_; + } + + // Whether this (pending) navigation is renderer-initiated. Resets to false + // for all types of navigations after commit. + void set_is_renderer_initiated(bool is_renderer_initiated) { + is_renderer_initiated_ = is_renderer_initiated; + } + bool is_renderer_initiated() const { + return is_renderer_initiated_; + } + + void set_user_typed_url(const GURL& user_typed_url) { + user_typed_url_ = user_typed_url; + } + + // Enumerations of the possible restore types. + enum RestoreType { + // The entry has been restored is from the last session. + RESTORE_LAST_SESSION, + + // The entry has been restored from the current session. This is used when + // the user issues 'reopen closed tab'. + RESTORE_CURRENT_SESSION, + + // The entry was not restored. + RESTORE_NONE + }; + + // The RestoreType for this entry. This is set if the entry was retored. This + // is set to RESTORE_NONE once the entry is loaded. + void set_restore_type(RestoreType type) { + restore_type_ = type; + } + RestoreType restore_type() const { + return restore_type_; + } + + void set_transferred_global_request_id( + const GlobalRequestID& transferred_global_request_id) { + transferred_global_request_id_ = transferred_global_request_id; + } + + GlobalRequestID transferred_global_request_id() const { + return transferred_global_request_id_; + } + + // Whether this (pending) navigation is reload across site instances. + // Resets to false after commit. + void set_is_cross_site_reload(bool is_cross_site_reload) { + is_cross_site_reload_ = is_cross_site_reload; + } + bool is_cross_site_reload() const { + return is_cross_site_reload_; + } + + private: + // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING + // Session/Tab restore save portions of this class so that it can be recreated + // later. If you add a new field that needs to be persisted you'll have to + // update SessionService/TabRestoreService appropriately. + // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING + + // See the accessors above for descriptions. + int unique_id_; + scoped_refptr<SiteInstanceImpl> site_instance_; + PageType page_type_; + GURL url_; + Referrer referrer_; + GURL virtual_url_; + bool update_virtual_url_with_url_; + string16 title_; + FaviconStatus favicon_; + std::string content_state_; + int32 page_id_; + SSLStatus ssl_; + PageTransition transition_type_; + GURL user_typed_url_; + bool has_post_data_; + int64 post_id_; + RestoreType restore_type_; + + // This member is not persisted with sesssion restore. + std::string extra_headers_; + + // Whether the entry, while loading, was created for a renderer-initiated + // navigation. This dictates whether the URL should be displayed before the + // navigation commits. It is cleared on commit and not persisted. + bool is_renderer_initiated_; + + // This is a cached version of the result of GetTitleForDisplay. It prevents + // us from having to do URL formatting on the URL every time the title is + // displayed. When the URL, virtual URL, or title is set, this should be + // cleared to force a refresh. + mutable string16 cached_display_title_; + + // In case a navigation is transferred to a new RVH but the request has + // been generated in the renderer already, this identifies the old request so + // that it can be resumed. The old request is stored until the + // ResourceDispatcher receives the navigation from the renderer which + // carries this |transferred_global_request_id_| annotation. Once the request + // is transferred to the new process, this is cleared and the request + // continues as normal. + GlobalRequestID transferred_global_request_id_; + + // This is set to true when this entry is being reloaded and due to changes in + // the state of the URL, it has to be reloaded in a different site instance. + // In such case, we must treat it as an existing navigation in the new site + // instance, instead of a new navigation. This value should not be persisted + // and is not needed after the entry commits. + bool is_cross_site_reload_; + + // Copy and assignment is explicitly allowed for this class. +}; + +} // namespace content + +#endif // CONTENT_BROWSER_WEB_CONTENTS_NAVIGATION_ENTRY_IMPL_H_ diff --git a/content/browser/web_contents/navigation_entry_impl_unittest.cc b/content/browser/web_contents/navigation_entry_impl_unittest.cc new file mode 100644 index 0000000..f8fca43 --- /dev/null +++ b/content/browser/web_contents/navigation_entry_impl_unittest.cc @@ -0,0 +1,178 @@ +// Copyright (c) 2012 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. + +#include "base/string16.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "content/browser/site_instance_impl.h" +#include "content/browser/web_contents/navigation_entry_impl.h" +#include "content/public/common/ssl_status.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +class NavigationEntryTest : public testing::Test { + public: + NavigationEntryTest() : instance_(NULL) { + } + + virtual void SetUp() { + entry1_.reset(new NavigationEntryImpl); + + instance_ = static_cast<SiteInstanceImpl*>(SiteInstance::Create(NULL)); + entry2_.reset(new NavigationEntryImpl( + instance_, 3, + GURL("test:url"), + Referrer(GURL("from"), WebKit::WebReferrerPolicyDefault), + ASCIIToUTF16("title"), + PAGE_TRANSITION_TYPED, + false)); + } + + virtual void TearDown() { + } + + protected: + scoped_ptr<NavigationEntryImpl> entry1_; + scoped_ptr<NavigationEntryImpl> entry2_; + // SiteInstances are deleted when their NavigationEntries are gone. + SiteInstanceImpl* instance_; +}; + +// Test unique ID accessors +TEST_F(NavigationEntryTest, NavigationEntryUniqueIDs) { + // Two entries should have different IDs by default + EXPECT_NE(entry1_.get()->GetUniqueID(), entry2_.get()->GetUniqueID()); + + // Can set an entry to have the same ID as another + entry2_.get()->set_unique_id(entry1_.get()->GetUniqueID()); + EXPECT_EQ(entry1_.get()->GetUniqueID(), entry2_.get()->GetUniqueID()); +} + +// Test URL accessors +TEST_F(NavigationEntryTest, NavigationEntryURLs) { + // Start with no virtual_url (even if a url is set) + EXPECT_FALSE(entry1_.get()->has_virtual_url()); + EXPECT_FALSE(entry2_.get()->has_virtual_url()); + + EXPECT_EQ(GURL(), entry1_.get()->GetURL()); + EXPECT_EQ(GURL(), entry1_.get()->GetVirtualURL()); + EXPECT_TRUE(entry1_.get()->GetTitleForDisplay("").empty()); + + // Setting URL affects virtual_url and GetTitleForDisplay + entry1_.get()->SetURL(GURL("http://www.google.com")); + EXPECT_EQ(GURL("http://www.google.com"), entry1_.get()->GetURL()); + EXPECT_EQ(GURL("http://www.google.com"), entry1_.get()->GetVirtualURL()); + EXPECT_EQ(ASCIIToUTF16("www.google.com"), + entry1_.get()->GetTitleForDisplay("")); + + // file:/// URLs should only show the filename. + entry1_.get()->SetURL(GURL("file:///foo/bar baz.txt")); + EXPECT_EQ(ASCIIToUTF16("bar baz.txt"), + entry1_.get()->GetTitleForDisplay("")); + + // Title affects GetTitleForDisplay + entry1_.get()->SetTitle(ASCIIToUTF16("Google")); + EXPECT_EQ(ASCIIToUTF16("Google"), entry1_.get()->GetTitleForDisplay("")); + + // Setting virtual_url doesn't affect URL + entry2_.get()->SetVirtualURL(GURL("display:url")); + EXPECT_TRUE(entry2_.get()->has_virtual_url()); + EXPECT_EQ(GURL("test:url"), entry2_.get()->GetURL()); + EXPECT_EQ(GURL("display:url"), entry2_.get()->GetVirtualURL()); + + // Having a title set in constructor overrides virtual URL + EXPECT_EQ(ASCIIToUTF16("title"), entry2_.get()->GetTitleForDisplay("")); + + // User typed URL is independent of the others + EXPECT_EQ(GURL(), entry1_.get()->GetUserTypedURL()); + EXPECT_EQ(GURL(), entry2_.get()->GetUserTypedURL()); + entry2_.get()->set_user_typed_url(GURL("typedurl")); + EXPECT_EQ(GURL("typedurl"), entry2_.get()->GetUserTypedURL()); +} + +// Test Favicon inner class construction. +TEST_F(NavigationEntryTest, NavigationEntryFavicons) { + EXPECT_EQ(GURL(), entry1_.get()->GetFavicon().url); + EXPECT_FALSE(entry1_.get()->GetFavicon().valid); +} + +// Test SSLStatus inner class +TEST_F(NavigationEntryTest, NavigationEntrySSLStatus) { + // Default (unknown) + EXPECT_EQ(SECURITY_STYLE_UNKNOWN, entry1_.get()->GetSSL().security_style); + EXPECT_EQ(SECURITY_STYLE_UNKNOWN, entry2_.get()->GetSSL().security_style); + EXPECT_EQ(0, entry1_.get()->GetSSL().cert_id); + EXPECT_EQ(0U, entry1_.get()->GetSSL().cert_status); + EXPECT_EQ(-1, entry1_.get()->GetSSL().security_bits); + int content_status = entry1_.get()->GetSSL().content_status; + EXPECT_FALSE(!!(content_status & SSLStatus::DISPLAYED_INSECURE_CONTENT)); + EXPECT_FALSE(!!(content_status & SSLStatus::RAN_INSECURE_CONTENT)); +} + +// Test other basic accessors +TEST_F(NavigationEntryTest, NavigationEntryAccessors) { + // SiteInstance + EXPECT_TRUE(entry1_.get()->site_instance() == NULL); + EXPECT_EQ(instance_, entry2_.get()->site_instance()); + entry1_.get()->set_site_instance(instance_); + EXPECT_EQ(instance_, entry1_.get()->site_instance()); + + // Page type + EXPECT_EQ(PAGE_TYPE_NORMAL, entry1_.get()->GetPageType()); + EXPECT_EQ(PAGE_TYPE_NORMAL, entry2_.get()->GetPageType()); + entry2_.get()->set_page_type(PAGE_TYPE_INTERSTITIAL); + EXPECT_EQ(PAGE_TYPE_INTERSTITIAL, entry2_.get()->GetPageType()); + + // Referrer + EXPECT_EQ(GURL(), entry1_.get()->GetReferrer().url); + EXPECT_EQ(GURL("from"), entry2_.get()->GetReferrer().url); + entry2_.get()->SetReferrer( + Referrer(GURL("from2"), WebKit::WebReferrerPolicyDefault)); + EXPECT_EQ(GURL("from2"), entry2_.get()->GetReferrer().url); + + // Title + EXPECT_EQ(string16(), entry1_.get()->GetTitle()); + EXPECT_EQ(ASCIIToUTF16("title"), entry2_.get()->GetTitle()); + entry2_.get()->SetTitle(ASCIIToUTF16("title2")); + EXPECT_EQ(ASCIIToUTF16("title2"), entry2_.get()->GetTitle()); + + // State + EXPECT_EQ(std::string(), entry1_.get()->GetContentState()); + EXPECT_EQ(std::string(), entry2_.get()->GetContentState()); + entry2_.get()->SetContentState("state"); + EXPECT_EQ("state", entry2_.get()->GetContentState()); + + // Page ID + EXPECT_EQ(-1, entry1_.get()->GetPageID()); + EXPECT_EQ(3, entry2_.get()->GetPageID()); + entry2_.get()->SetPageID(2); + EXPECT_EQ(2, entry2_.get()->GetPageID()); + + // Transition type + EXPECT_EQ(PAGE_TRANSITION_LINK, entry1_.get()->GetTransitionType()); + EXPECT_EQ(PAGE_TRANSITION_TYPED, entry2_.get()->GetTransitionType()); + entry2_.get()->SetTransitionType(PAGE_TRANSITION_RELOAD); + EXPECT_EQ(PAGE_TRANSITION_RELOAD, entry2_.get()->GetTransitionType()); + + // Is renderer initiated + EXPECT_FALSE(entry1_.get()->is_renderer_initiated()); + EXPECT_FALSE(entry2_.get()->is_renderer_initiated()); + entry2_.get()->set_is_renderer_initiated(true); + EXPECT_TRUE(entry2_.get()->is_renderer_initiated()); + + // Post Data + EXPECT_FALSE(entry1_.get()->GetHasPostData()); + EXPECT_FALSE(entry2_.get()->GetHasPostData()); + entry2_.get()->SetHasPostData(true); + EXPECT_TRUE(entry2_.get()->GetHasPostData()); + + // Restored + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, entry1_->restore_type()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, entry2_->restore_type()); + entry2_->set_restore_type(NavigationEntryImpl::RESTORE_LAST_SESSION); + EXPECT_EQ(NavigationEntryImpl::RESTORE_LAST_SESSION, entry2_->restore_type()); +} + +} // namespace content diff --git a/content/browser/web_contents/web_drag_dest_gtk.cc b/content/browser/web_contents/web_drag_dest_gtk.cc index 572a135c..3b6572e 100644 --- a/content/browser/web_contents/web_drag_dest_gtk.cc +++ b/content/browser/web_contents/web_drag_dest_gtk.cc @@ -11,8 +11,8 @@ #include "base/message_loop.h" #include "base/utf_string_conversions.h" #include "content/browser/renderer_host/render_view_host_impl.h" -#include "content/browser/tab_contents/drag_utils_gtk.h" #include "content/browser/tab_contents/tab_contents.h" +#include "content/browser/web_contents/drag_utils_gtk.h" #include "content/public/browser/web_drag_dest_delegate.h" #include "content/public/common/url_constants.h" #include "net/base/net_util.h" diff --git a/content/browser/web_contents/web_drag_source_gtk.cc b/content/browser/web_contents/web_drag_source_gtk.cc index 31e9c03..c18d4a2 100644 --- a/content/browser/web_contents/web_drag_source_gtk.cc +++ b/content/browser/web_contents/web_drag_source_gtk.cc @@ -13,8 +13,8 @@ #include "content/browser/download/drag_download_file.h" #include "content/browser/download/drag_download_util.h" #include "content/browser/renderer_host/render_view_host_impl.h" -#include "content/browser/tab_contents/drag_utils_gtk.h" #include "content/browser/tab_contents/tab_contents.h" +#include "content/browser/web_contents/drag_utils_gtk.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/render_view_host_delegate.h" #include "content/public/browser/web_contents_view.h" |