diff options
author | nasko@chromium.org <nasko@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-30 21:06:40 +0000 |
---|---|---|
committer | nasko@chromium.org <nasko@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-30 21:06:40 +0000 |
commit | d4a8ca488bb83c9a77c2e8b9a8c8fb7408b6faf6 (patch) | |
tree | da0f92e87f8655795cb2f2861e2bc964920671f4 /content/browser/frame_host | |
parent | 9004e635f04bbba6012ecf7e525eccc65ef9b33c (diff) | |
download | chromium_src-d4a8ca488bb83c9a77c2e8b9a8c8fb7408b6faf6.zip chromium_src-d4a8ca488bb83c9a77c2e8b9a8c8fb7408b6faf6.tar.gz chromium_src-d4a8ca488bb83c9a77c2e8b9a8c8fb7408b6faf6.tar.bz2 |
Move navigation and frame tree classes to a new frame_host/ directory.
This CL moves all frame tree classes, Navigation* and its dependencies from web_contents/ to a separate frame_host/ directory.
BUG=304341
Review URL: https://codereview.chromium.org/49823002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@231921 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'content/browser/frame_host')
26 files changed, 12260 insertions, 0 deletions
diff --git a/content/browser/frame_host/DEPS b/content/browser/frame_host/DEPS new file mode 100644 index 0000000..85fb015 --- /dev/null +++ b/content/browser/frame_host/DEPS @@ -0,0 +1,22 @@ +include_rules = [ + # The frame_host files should only call upwards in the layering via the + # delegate interfaces. + "-content/browser/web_contents", + "-content/public/browser/web_contents.h", + "-content/public/browser/web_contents_delegate.h", + "-content/public/browser/web_contents_view.h", +] + +specific_include_rules = { + ".*_(unit|browser)test\.cc": [ + "+content/browser/web_contents", + "+content/public/browser/web_contents_delegate.h", + ], + ".*interstitial_page_impl\.cc": [ + # TODO(nasko): This should be removed once we remove + # WebContentsObserver as the method of telling interstitial pages to + # clean themselves up. + "+content/browser/web_contents", + "+content/public/browser/web_contents_delegate.h", + ], +} diff --git a/content/browser/frame_host/debug_urls.cc b/content/browser/frame_host/debug_urls.cc new file mode 100644 index 0000000..7cb11e8 --- /dev/null +++ b/content/browser/frame_host/debug_urls.cc @@ -0,0 +1,82 @@ +// Copyright 2013 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/frame_host/debug_urls.h" + +#include <vector> + +#include "base/strings/utf_string_conversions.h" +#include "content/browser/gpu/gpu_process_host_ui_shim.h" +#include "content/browser/ppapi_plugin_process_host.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/common/content_constants.h" +#include "content/public/common/url_constants.h" +#include "ppapi/proxy/ppapi_messages.h" +#include "url/gurl.h" + +namespace content { + +namespace { + +void HandlePpapiFlashDebugURL(const GURL& url) { +#if defined(ENABLE_PLUGINS) + bool crash = url == GURL(kChromeUIPpapiFlashCrashURL); + + std::vector<PpapiPluginProcessHost*> hosts; + PpapiPluginProcessHost::FindByName(UTF8ToUTF16(kFlashPluginName), &hosts); + for (std::vector<PpapiPluginProcessHost*>::iterator iter = hosts.begin(); + iter != hosts.end(); ++iter) { + if (crash) + (*iter)->Send(new PpapiMsg_Crash()); + else + (*iter)->Send(new PpapiMsg_Hang()); + } +#endif +} + +} // namespace + +bool HandleDebugURL(const GURL& url, PageTransition transition) { + // Ensure that the user explicitly navigated to this URL. + if (!(transition & PAGE_TRANSITION_FROM_ADDRESS_BAR)) + return false; + + if (url.host() == kChromeUIBrowserCrashHost) { + // Induce an intentional crash in the browser process. + CHECK(false); + return true; + } + + if (url == GURL(kChromeUIGpuCleanURL)) { + GpuProcessHostUIShim* shim = GpuProcessHostUIShim::GetOneInstance(); + if (shim) + shim->SimulateRemoveAllContext(); + return true; + } + + if (url == GURL(kChromeUIGpuCrashURL)) { + GpuProcessHostUIShim* shim = GpuProcessHostUIShim::GetOneInstance(); + if (shim) + shim->SimulateCrash(); + return true; + } + + if (url == GURL(kChromeUIGpuHangURL)) { + GpuProcessHostUIShim* shim = GpuProcessHostUIShim::GetOneInstance(); + if (shim) + shim->SimulateHang(); + return true; + } + + if (url == GURL(kChromeUIPpapiFlashCrashURL) || + url == GURL(kChromeUIPpapiFlashHangURL)) { + BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, + base::Bind(&HandlePpapiFlashDebugURL, url)); + return true; + } + + return false; +} + +} // namespace content diff --git a/content/browser/frame_host/debug_urls.h b/content/browser/frame_host/debug_urls.h new file mode 100644 index 0000000..1985253 --- /dev/null +++ b/content/browser/frame_host/debug_urls.h @@ -0,0 +1,20 @@ +// Copyright 2013 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_FRAME_HOST_DEBUG_URLS_H_ +#define CONTENT_BROWSER_FRAME_HOST_DEBUG_URLS_H_ + +#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, PageTransition transition); + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_DEBUG_URLS_H_ diff --git a/content/browser/frame_host/frame_tree.cc b/content/browser/frame_host/frame_tree.cc new file mode 100644 index 0000000..0cb9d5e --- /dev/null +++ b/content/browser/frame_host/frame_tree.cc @@ -0,0 +1,138 @@ +// Copyright 2013 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/frame_host/frame_tree.h" + +#include <queue> + +#include "base/bind.h" +#include "base/callback.h" +#include "content/browser/frame_host/frame_tree_node.h" +#include "content/browser/frame_host/render_frame_host_impl.h" + +namespace content { + +namespace { +// Used with FrameTree::ForEach() to search for the FrameTreeNode +// corresponding to |frame_id|. +bool FrameTreeNodeForId(int64 frame_id, FrameTreeNode** out_node, + FrameTreeNode* node) { + if (node->frame_id() == frame_id) { + *out_node = node; + // Terminate iteration once the node has been found. + return false; + } + return true; +} + +} // namespace + +FrameTree::FrameTree() + : root_(new FrameTreeNode(FrameTreeNode::kInvalidFrameId, std::string(), + scoped_ptr<RenderFrameHostImpl>())) { +} + +FrameTree::~FrameTree() { +} + +FrameTreeNode* FrameTree::FindByID(int64 frame_id) { + FrameTreeNode* node = NULL; + ForEach(base::Bind(&FrameTreeNodeForId, frame_id, &node)); + return node; +} + +void FrameTree::ForEach( + const base::Callback<bool(FrameTreeNode*)>& on_node) const { + std::queue<FrameTreeNode*> queue; + queue.push(root_.get()); + + while (!queue.empty()) { + FrameTreeNode* node = queue.front(); + queue.pop(); + if (!on_node.Run(node)) + break; + + for (size_t i = 0; i < node->child_count(); ++i) + queue.push(node->child_at(i)); + } +} + +bool FrameTree::IsFirstNavigationAfterSwap() const { + return root_->frame_id() == FrameTreeNode::kInvalidFrameId; +} + +void FrameTree::OnFirstNavigationAfterSwap(int main_frame_id) { + root_->set_frame_id(main_frame_id); +} + +void FrameTree::AddFrame(int render_frame_host_id, int64 parent_frame_id, + int64 frame_id, const std::string& frame_name) { + FrameTreeNode* parent = FindByID(parent_frame_id); + // TODO(ajwong): Should the renderer be killed here? Would there be a race on + // shutdown that might make this case possible? + if (!parent) + return; + + parent->AddChild(CreateNode(frame_id, frame_name, render_frame_host_id, + parent->render_frame_host()->GetProcess())); +} + +void FrameTree::RemoveFrame(int64 parent_frame_id, int64 frame_id) { + // If switches::kSitePerProcess is not specified, then the FrameTree only + // contains a node for the root element. However, even in this case + // frame detachments need to be broadcast outwards. + // + // TODO(ajwong): Move this below the |parent| check after the FrameTree is + // guaranteed to be correctly populated even without the + // switches::kSitePerProcess flag. + FrameTreeNode* parent = FindByID(parent_frame_id); + if (!on_frame_removed_.is_null()) { + on_frame_removed_.Run( + root_->render_frame_host()->render_view_host(), frame_id); + } + + // TODO(ajwong): Should the renderer be killed here? Would there be a race on + // shutdown that might make this case possible? + if (!parent) + return; + + parent->RemoveChild(frame_id); +} + +void FrameTree::SetFrameUrl(int64 frame_id, const GURL& url) { + FrameTreeNode* node = FindByID(frame_id); + // TODO(ajwong): Should the renderer be killed here? Would there be a race on + // shutdown that might make this case possible? + if (!node) + return; + + if (node) + node->set_current_url(url); +} + +void FrameTree::SwapMainFrame(RenderFrameHostImpl* render_frame_host) { + return root_->ResetForMainFrame(render_frame_host); +} + +RenderFrameHostImpl* FrameTree::GetMainFrame() const { + return root_->render_frame_host(); +} + +void FrameTree::SetFrameRemoveListener( + const base::Callback<void(RenderViewHostImpl*, int64)>& on_frame_removed) { + on_frame_removed_ = on_frame_removed; +} + +scoped_ptr<FrameTreeNode> FrameTree::CreateNode( + int64 frame_id, const std::string& frame_name, int render_frame_host_id, + RenderProcessHost* render_process_host) { + scoped_ptr<RenderFrameHostImpl> render_frame_host( + new RenderFrameHostImpl(root_->render_frame_host()->render_view_host(), + this, render_frame_host_id, false)); + + return make_scoped_ptr(new FrameTreeNode(frame_id, frame_name, + render_frame_host.Pass())); +} + +} // namespace content diff --git a/content/browser/frame_host/frame_tree.h b/content/browser/frame_host/frame_tree.h new file mode 100644 index 0000000..f9bf809 --- /dev/null +++ b/content/browser/frame_host/frame_tree.h @@ -0,0 +1,103 @@ +// Copyright 2013 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_FRAME_HOST_FRAME_TREE_H_ +#define CONTENT_BROWSER_FRAME_HOST_FRAME_TREE_H_ + +#include <string> + +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "content/browser/frame_host/frame_tree_node.h" +#include "content/common/content_export.h" + +namespace content { + +class FrameTreeNode; +class RenderProcessHost; +class RenderViewHostImpl; + +// Represents the frame tree for a page. With the exception of the main frame, +// all FrameTreeNodes will be created/deleted in response to frame attach and +// detach events in the DOM. +// +// The main frame's FrameTreeNode is special in that it is reused. This allows +// it to serve as an anchor for state that needs to persist across top-level +// page navigations. +// +// TODO(ajwong): Move NavigationController ownership to the main frame +// FrameTreeNode. Possibly expose access to it from here. +// +// TODO(ajwong): Currently this class only contains FrameTreeNodes for +// subframes if the --site-per-process flag is enabled. +// +// This object is only used on the UI thread. +class CONTENT_EXPORT FrameTree { + public: + FrameTree(); + ~FrameTree(); + + // Returns the FrameTreeNode with the given |frame_id|. + FrameTreeNode* FindByID(int64 frame_id); + + // Executes |on_node| on each node in the frame tree. If |on_node| returns + // false, terminates the iteration immediately. Returning false is useful + // if |on_node| is just doing a search over the tree. + void ForEach(const base::Callback<bool(FrameTreeNode*)>& on_node) const; + + // After the FrameTree is created, or after SwapMainFrame() has been called, + // the root node does not yet have a frame id. This is allocated by the + // renderer and is published to the browser process on the first navigation + // after a swap. These two functions are used to set the root node's frame + // id. + // + // TODO(ajwong): Remove these once RenderFrameHost's routing id replaces + // frame_id. + bool IsFirstNavigationAfterSwap() const; + void OnFirstNavigationAfterSwap(int main_frame_id); + + // Frame tree manipulation routines. + void AddFrame(int render_frame_host_id, int64 parent_frame_id, + int64 frame_id, const std::string& frame_name); + void RemoveFrame(int64 parent_frame_id, int64 frame_id); + void SetFrameUrl(int64 frame_id, const GURL& url); + + // Resets the FrameTree and changes RenderFrameHost for the main frame. + // This destroys most of the frame tree but retains the root node so that + // navigation state may be kept on it between process swaps. Used to + // support bookkeeping for top-level navigations. + // + // If |main_frame| is NULL, reset tree to initially constructed state. + // + // TODO(ajwong): This function should not be given a |main_frame|. This is + // required currently because the RenderViewHost owns its main frame. When + // that relation is fixed, the FrameTree should be responsible for + // created/destroying the main frame on the swap. + void SwapMainFrame(RenderFrameHostImpl* main_frame); + + // Convenience accessor for the main frame's RenderFrameHostImpl. + RenderFrameHostImpl* GetMainFrame() const; + + // Allows a client to listen for frame removal. + void SetFrameRemoveListener( + const base::Callback<void(RenderViewHostImpl*, int64)>& on_frame_removed); + + FrameTreeNode* GetRootForTesting() { return root_.get(); } + + private: + scoped_ptr<FrameTreeNode> CreateNode(int64 frame_id, + const std::string& frame_name, + int render_frame_host_id, + RenderProcessHost* render_process_host); + + scoped_ptr<FrameTreeNode> root_; + + base::Callback<void(RenderViewHostImpl*, int64)> on_frame_removed_; + + DISALLOW_COPY_AND_ASSIGN(FrameTree); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_FRAME_TREE_H_ diff --git a/content/browser/frame_host/frame_tree_node.cc b/content/browser/frame_host/frame_tree_node.cc new file mode 100644 index 0000000..9bed9f6 --- /dev/null +++ b/content/browser/frame_host/frame_tree_node.cc @@ -0,0 +1,57 @@ +// Copyright 2013 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/frame_host/frame_tree_node.h" + +#include <queue> + +#include "base/stl_util.h" +#include "content/browser/frame_host/render_frame_host_impl.h" + +namespace content { + +const int64 FrameTreeNode::kInvalidFrameId = -1; + +FrameTreeNode::FrameTreeNode(int64 frame_id, const std::string& name, + scoped_ptr<RenderFrameHostImpl> render_frame_host) + : frame_id_(frame_id), + frame_name_(name), + owns_render_frame_host_(true), + render_frame_host_(render_frame_host.release()) { +} + +FrameTreeNode::~FrameTreeNode() { + if (owns_render_frame_host_) + delete render_frame_host_; +} + +void FrameTreeNode::AddChild(scoped_ptr<FrameTreeNode> child) { + children_.push_back(child.release()); +} + +void FrameTreeNode::RemoveChild(int64 child_id) { + std::vector<FrameTreeNode*>::iterator iter; + + for (iter = children_.begin(); iter != children_.end(); ++iter) { + if ((*iter)->frame_id() == child_id) + break; + } + + if (iter != children_.end()) + children_.erase(iter); +} + +void FrameTreeNode::ResetForMainFrame( + RenderFrameHostImpl* new_render_frame_host) { + DCHECK_EQ(0UL, children_.size()); + + owns_render_frame_host_ = false; + frame_id_ = kInvalidFrameId; + current_url_ = GURL(); + children_.clear(); + + render_frame_host_ = new_render_frame_host; +} + +} // namespace content diff --git a/content/browser/frame_host/frame_tree_node.h b/content/browser/frame_host/frame_tree_node.h new file mode 100644 index 0000000..63030a6 --- /dev/null +++ b/content/browser/frame_host/frame_tree_node.h @@ -0,0 +1,116 @@ +// Copyright 2013 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_FRAME_HOST_FRAME_TREE_NODE_H_ +#define CONTENT_BROWSER_FRAME_HOST_FRAME_TREE_NODE_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "content/common/content_export.h" +#include "url/gurl.h" + +namespace content { + +class RenderFrameHostImpl; + +// When a page contains iframes, its renderer process maintains a tree structure +// of those frames. We are mirroring this tree in the browser process. This +// class represents a node in this tree and is a wrapper for all objects that +// are frame-specific (as opposed to page-specific). +class CONTENT_EXPORT FrameTreeNode { + public: + static const int64 kInvalidFrameId; + + FrameTreeNode(int64 frame_id, const std::string& name, + scoped_ptr<RenderFrameHostImpl> render_frame_host); + ~FrameTreeNode(); + + void AddChild(scoped_ptr<FrameTreeNode> child); + void RemoveChild(int64 child_id); + + // Transitional API allowing the RenderFrameHost of a FrameTreeNode + // representing the main frame to be provided by someone else. After + // this is called, the FrameTreeNode no longer owns its RenderFrameHost. + // + // This should only be used for the main frame (aka root) in a frame tree. + // + // TODO(ajwong): Remove this method once the main frame RenderFrameHostImpl is + // no longer owned by the RenderViewHostImpl. + void ResetForMainFrame(RenderFrameHostImpl* new_render_frame_host); + + void set_frame_id(int64 frame_id) { + DCHECK_EQ(frame_id_, kInvalidFrameId); + frame_id_ = frame_id; + } + + int64 frame_id() const { + return frame_id_; + } + + const std::string& frame_name() const { + return frame_name_; + } + + size_t child_count() const { + return children_.size(); + } + + FrameTreeNode* child_at(size_t index) const { + return children_[index]; + } + + const GURL& current_url() const { + return current_url_; + } + + void set_current_url(const GURL& url) { + current_url_ = url; + } + + RenderFrameHostImpl* render_frame_host() const { + return render_frame_host_; + } + + private: + // The unique identifier for the frame in the page. + int64 frame_id_; + + // The assigned name of the frame. This name can be empty, unlike the unique + // name generated internally in the DOM tree. + std::string frame_name_; + + // The immediate children of this specific frame. + ScopedVector<FrameTreeNode> children_; + + // When ResetForMainFrame() is called, this is set to false and the + // |render_frame_host_| below is not deleted on destruction. + // + // For the mainframe, the FrameTree does not own the |render_frame_host_|. + // This is a transitional wart because RenderViewHostManager does not yet + // have the bookkeeping logic to handle creating a pending RenderFrameHost + // along with a pending RenderViewHost. Thus, for the main frame, the + // RenderViewHost currently retains ownership and the FrameTreeNode should + // not delete it on destruction. + bool owns_render_frame_host_; + + // The active RenderFrameHost for this frame. The FrameTreeNode does not + // always own this pointer. See comments above |owns_render_frame_host_|. + // TODO(ajwong): Replace with RenderFrameHostManager. + RenderFrameHostImpl* render_frame_host_; + + // Track the current frame's last committed URL, so we can estimate the + // process impact of out-of-process iframes. + // TODO(creis): Remove this when we can store subframe URLs in the + // NavigationController. + GURL current_url_; + + DISALLOW_COPY_AND_ASSIGN(FrameTreeNode); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_FRAME_TREE_NODE_H_ diff --git a/content/browser/frame_host/frame_tree_unittest.cc b/content/browser/frame_host/frame_tree_unittest.cc new file mode 100644 index 0000000..20295dc --- /dev/null +++ b/content/browser/frame_host/frame_tree_unittest.cc @@ -0,0 +1,173 @@ +// Copyright 2013 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/frame_host/frame_tree.h" + +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "content/browser/frame_host/render_frame_host_impl.h" +#include "content/browser/renderer_host/render_view_host_impl.h" +#include "content/public/test/mock_render_process_host.h" +#include "content/public/test/test_browser_context.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "content/public/test/test_renderer_host.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { +namespace { + +class FrameTreeTest : public RenderViewHostTestHarness { + protected: + // Prints a FrameTree, for easy assertions of the tree hierarchy. + std::string GetTreeState(FrameTree* frame_tree) { + std::string result; + AppendTreeNodeState(frame_tree->GetRootForTesting(), &result); + return result; + } + + private: + void AppendTreeNodeState(FrameTreeNode* node, std::string* result) { + result->append(base::Int64ToString(node->frame_id())); + if (!node->frame_name().empty()) { + result->append(" '"); + result->append(node->frame_name()); + result->append("'"); + } + result->append(": ["); + const char* separator = ""; + for (size_t i = 0; i < node->child_count(); i++) { + result->append(separator); + AppendTreeNodeState(node->child_at(i), result); + separator = ", "; + } + result->append("]"); + } +}; + +// The root node never changes during navigation even though its +// RenderFrameHost does. +// - Swapping main frame doesn't change root node. +// - Swapping back to NULL doesn't crash (easier tear-down for interstitials). +// - Main frame does not own RenderFrameHost. +TEST_F(FrameTreeTest, RootNode) { + FrameTree frame_tree; + + // Initial state has empty node. + FrameTreeNode* root = frame_tree.GetRootForTesting(); + ASSERT_TRUE(root); + EXPECT_FALSE(frame_tree.GetMainFrame()); + + // Swap in main frame. + RenderFrameHostImpl* dummy = reinterpret_cast<RenderFrameHostImpl*>(0x1); + frame_tree.SwapMainFrame(dummy); + EXPECT_EQ(root, frame_tree.GetRootForTesting()); + EXPECT_EQ(dummy, frame_tree.GetMainFrame()); + + // Move back to NULL. + frame_tree.SwapMainFrame(NULL); + EXPECT_EQ(root, frame_tree.GetRootForTesting()); + EXPECT_FALSE(frame_tree.GetMainFrame()); + + // Move back to an invalid pointer, let the FrameTree go out of scope. Test + // should not crash because the main frame isn't owned. + frame_tree.SwapMainFrame(dummy); +} + +// Test that swapping the main frame resets the renderer-assigned frame id. +// - On creation, frame id is unassigned. +// - After a swap, frame id is unassigned. +TEST_F(FrameTreeTest, FirstNavigationAfterSwap) { + FrameTree frame_tree; + + EXPECT_TRUE(frame_tree.IsFirstNavigationAfterSwap()); + EXPECT_EQ(FrameTreeNode::kInvalidFrameId, + frame_tree.GetRootForTesting()->frame_id()); + frame_tree.OnFirstNavigationAfterSwap(1); + EXPECT_FALSE(frame_tree.IsFirstNavigationAfterSwap()); + EXPECT_EQ(1, frame_tree.GetRootForTesting()->frame_id()); + + frame_tree.SwapMainFrame(NULL); + EXPECT_TRUE(frame_tree.IsFirstNavigationAfterSwap()); + EXPECT_EQ(FrameTreeNode::kInvalidFrameId, + frame_tree.GetRootForTesting()->frame_id()); +} + +// Exercise tree manipulation routines. +// - Add a series of nodes and verify tree structure. +// - Remove a series of nodes and verify tree structure. +TEST_F(FrameTreeTest, Shape) { + FrameTree frame_tree; + std::string no_children_node("no children node"); + std::string deep_subtree("node with deep subtree"); + + // Ensure the top-level node of the FrameTree is initialized by simulating a + // main frame swap here. + RenderFrameHostImpl render_frame_host(static_cast<RenderViewHostImpl*>(rvh()), + &frame_tree, + process()->GetNextRoutingID(), false); + frame_tree.SwapMainFrame(&render_frame_host); + frame_tree.OnFirstNavigationAfterSwap(5); + + ASSERT_EQ("5: []", GetTreeState(&frame_tree)); + + // Simulate attaching a series of frames to build the frame tree. + frame_tree.AddFrame(process()->GetNextRoutingID(), 5, 14, std::string()); + frame_tree.AddFrame(process()->GetNextRoutingID(), 5, 15, std::string()); + frame_tree.AddFrame(process()->GetNextRoutingID(), 5, 16, std::string()); + + frame_tree.AddFrame(process()->GetNextRoutingID(), 14, 244, std::string()); + frame_tree.AddFrame(process()->GetNextRoutingID(), 15, 255, no_children_node); + frame_tree.AddFrame(process()->GetNextRoutingID(), 14, 245, std::string()); + + ASSERT_EQ("5: [14: [244: [], 245: []], " + "15: [255 'no children node': []], " + "16: []]", + GetTreeState(&frame_tree)); + + frame_tree.AddFrame(process()->GetNextRoutingID(), 16, 264, std::string()); + frame_tree.AddFrame(process()->GetNextRoutingID(), 16, 265, std::string()); + frame_tree.AddFrame(process()->GetNextRoutingID(), 16, 266, std::string()); + frame_tree.AddFrame(process()->GetNextRoutingID(), 16, 267, deep_subtree); + frame_tree.AddFrame(process()->GetNextRoutingID(), 16, 268, std::string()); + + frame_tree.AddFrame(process()->GetNextRoutingID(), 267, 365, std::string()); + frame_tree.AddFrame(process()->GetNextRoutingID(), 365, 455, std::string()); + frame_tree.AddFrame(process()->GetNextRoutingID(), 455, 555, std::string()); + frame_tree.AddFrame(process()->GetNextRoutingID(), 555, 655, std::string()); + + // Now that's it's fully built, verify the tree structure is as expected. + ASSERT_EQ("5: [14: [244: [], 245: []], " + "15: [255 'no children node': []], " + "16: [264: [], 265: [], 266: [], " + "267 'node with deep subtree': " + "[365: [455: [555: [655: []]]]], 268: []]]", + GetTreeState(&frame_tree)); + + // Test removing of nodes. + frame_tree.RemoveFrame(555, 655); + ASSERT_EQ("5: [14: [244: [], 245: []], " + "15: [255 'no children node': []], " + "16: [264: [], 265: [], 266: [], " + "267 'node with deep subtree': " + "[365: [455: [555: []]]], 268: []]]", + GetTreeState(&frame_tree)); + + frame_tree.RemoveFrame(16, 265); + ASSERT_EQ("5: [14: [244: [], 245: []], " + "15: [255 'no children node': []], " + "16: [264: [], 266: [], " + "267 'node with deep subtree': " + "[365: [455: [555: []]]], 268: []]]", + GetTreeState(&frame_tree)); + + frame_tree.RemoveFrame(5, 15); + ASSERT_EQ("5: [14: [244: [], 245: []], " + "16: [264: [], 266: [], " + "267 'node with deep subtree': " + "[365: [455: [555: []]]], 268: []]]", + GetTreeState(&frame_tree)); +} + +} // namespace +} // namespace content diff --git a/content/browser/frame_host/interstitial_page_impl.cc b/content/browser/frame_host/interstitial_page_impl.cc new file mode 100644 index 0000000..4bcee69 --- /dev/null +++ b/content/browser/frame_host/interstitial_page_impl.cc @@ -0,0 +1,843 @@ +// Copyright 2013 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/frame_host/interstitial_page_impl.h" + +#include <vector> + +#include "base/bind.h" +#include "base/compiler_specific.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/thread.h" +#include "content/browser/dom_storage/dom_storage_context_wrapper.h" +#include "content/browser/dom_storage/session_storage_namespace_impl.h" +#include "content/browser/frame_host/navigation_controller_impl.h" +#include "content/browser/frame_host/navigation_entry_impl.h" +#include "content/browser/loader/resource_dispatcher_host_impl.h" +#include "content/browser/renderer_host/render_process_host_impl.h" +#include "content/browser/renderer_host/render_view_host_factory.h" +#include "content/browser/renderer_host/render_view_host_impl.h" +#include "content/browser/site_instance_impl.h" +#include "content/browser/web_contents/web_contents_impl.h" +#include "content/common/view_messages.h" +#include "content/port/browser/render_view_host_delegate_view.h" +#include "content/port/browser/render_widget_host_view_port.h" +#include "content/port/browser/web_contents_view_port.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/content_browser_client.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/storage_partition.h" +#include "content/public/browser/web_contents_delegate.h" +#include "content/public/common/bindings_policy.h" +#include "content/public/common/page_transition_types.h" +#include "net/base/escape.h" +#include "net/url_request/url_request_context_getter.h" + +using WebKit::WebDragOperation; +using WebKit::WebDragOperationsMask; + +namespace content { +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::InterstitialPageRVHDelegateView + : public RenderViewHostDelegateView { + public: + explicit InterstitialPageRVHDelegateView(InterstitialPageImpl* page); + + // RenderViewHostDelegateView implementation: + virtual void ShowPopupMenu(const gfx::Rect& bounds, + int item_height, + double item_font_size, + int selected_item, + const std::vector<MenuItem>& items, + bool right_aligned, + bool allow_multiple_selection) OVERRIDE; + virtual void StartDragging(const DropData& drop_data, + WebDragOperationsMask operations_allowed, + const gfx::ImageSkia& image, + const gfx::Vector2d& image_offset, + const DragEventSourceInfo& event_info) OVERRIDE; + virtual void UpdateDragCursor(WebDragOperation operation) OVERRIDE; + virtual void GotFocus() OVERRIDE; + virtual void TakeFocus(bool reverse) OVERRIDE; + 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(InterstitialPageRVHDelegateView); +}; + + +// 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_web_contents_to_interstitial_page; + +// Initializes g_web_contents_to_interstitial_page in a thread-safe manner. +// Should be called before accessing g_web_contents_to_interstitial_page. +static void InitInterstitialPageMap() { + if (!g_web_contents_to_interstitial_page) + g_web_contents_to_interstitial_page = new InterstitialPageMap; +} + +InterstitialPage* InterstitialPage::Create(WebContents* web_contents, + bool new_navigation, + const GURL& url, + InterstitialPageDelegate* delegate) { + return new InterstitialPageImpl( + web_contents, + static_cast<RenderWidgetHostDelegate*>( + static_cast<WebContentsImpl*>(web_contents)), + new_navigation, url, delegate); +} + +InterstitialPage* InterstitialPage::GetInterstitialPage( + WebContents* web_contents) { + InitInterstitialPageMap(); + InterstitialPageMap::const_iterator iter = + g_web_contents_to_interstitial_page->find(web_contents); + if (iter == g_web_contents_to_interstitial_page->end()) + return NULL; + + return iter->second; +} + +InterstitialPageImpl::InterstitialPageImpl( + WebContents* web_contents, + RenderWidgetHostDelegate* render_widget_host_delegate, + bool new_navigation, + const GURL& url, + InterstitialPageDelegate* delegate) + : WebContentsObserver(web_contents), + web_contents_(web_contents), + controller_(static_cast<NavigationControllerImpl*>( + &web_contents->GetController())), + render_widget_host_delegate_(render_widget_host_delegate), + 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_(web_contents->GetRenderProcessHost()->GetID()), + original_rvh_id_(web_contents->GetRenderViewHost()->GetRoutingID()), + should_revert_web_contents_title_(false), + web_contents_was_loading_(false), + resource_dispatcher_host_notified_(false), + rvh_delegate_view_(new InterstitialPageRVHDelegateView(this)), + create_view_(true), + delegate_(delegate), + weak_ptr_factory_(this) { + 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 || !web_contents->GetController().GetPendingEntry()); +} + +InterstitialPageImpl::~InterstitialPageImpl() { +} + +void InterstitialPageImpl::Show() { + if (!enabled()) + return; + + // 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_web_contents_to_interstitial_page->find(web_contents_); + if (iter != g_web_contents_to_interstitial_page->end()) { + InterstitialPageImpl* interstitial = iter->second; + if (interstitial->action_taken_ != NO_ACTION) { + interstitial->Hide(); + } 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_WEB_CONTENTS_DESTROYED as at that point the RenderViewHost has + // already been destroyed. + notification_registrar_.Add( + this, NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED, + Source<RenderWidgetHost>(controller_->delegate()->GetRenderViewHost())); + + // Update the g_web_contents_to_interstitial_page map. + iter = g_web_contents_to_interstitial_page->find(web_contents_); + DCHECK(iter == g_web_contents_to_interstitial_page->end()); + (*g_web_contents_to_interstitial_page)[web_contents_] = this; + + if (new_navigation_) { + NavigationEntryImpl* entry = new NavigationEntryImpl; + entry->SetURL(url_); + entry->SetVirtualURL(url_); + entry->set_page_type(PAGE_TYPE_INTERSTITIAL); + + // Give delegates a chance to set some states on the navigation entry. + delegate_->OverrideEntry(entry); + + controller_->SetTransientEntry(entry); + } + + DCHECK(!render_view_host_); + render_view_host_ = static_cast<RenderViewHostImpl*>(CreateRenderViewHost()); + render_view_host_->AttachToFrameTree(); + 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, NOTIFICATION_NAV_ENTRY_PENDING, + Source<NavigationController>(controller_)); + notification_registrar_.Add( + this, NOTIFICATION_DOM_OPERATION_RESPONSE, + Source<RenderViewHost>(render_view_host_)); +} + +void InterstitialPageImpl::Hide() { + // We may have already been hidden, and are just waiting to be deleted. + // We can't check for enabled() here, because some callers have already + // called Disable. + if (!render_view_host_) + return; + + Disable(); + + RenderWidgetHostView* old_view = + controller_->delegate()->GetRenderViewHost()->GetView(); + if (controller_->delegate()->GetInterstitialPage() == this && + old_view && + !old_view->IsShowing() && + !controller_->delegate()->IsHidden()) { + // 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() && + controller_->delegate()->GetRenderViewHost()->GetView()) { + RenderWidgetHostViewPort::FromRWHV( + controller_->delegate()->GetRenderViewHost()->GetView())->Focus(); + } + + // Shutdown the RVH asynchronously, as we may have been called from a RVH + // delegate method, and we can't delete the RVH out from under itself. + base::MessageLoop::current()->PostNonNestableTask( + FROM_HERE, + base::Bind(&InterstitialPageImpl::Shutdown, + weak_ptr_factory_.GetWeakPtr(), + render_view_host_)); + render_view_host_ = NULL; + frame_tree_.SwapMainFrame(NULL); + controller_->delegate()->DetachInterstitialPage(); + // Let's revert to the original title if necessary. + NavigationEntry* entry = controller_->GetVisibleEntry(); + if (!new_navigation_ && should_revert_web_contents_title_) { + entry->SetTitle(original_web_contents_title_); + controller_->delegate()->NotifyNavigationStateChanged( + INVALIDATE_TYPE_TITLE); + } + + InterstitialPageMap::iterator iter = + g_web_contents_to_interstitial_page->find(web_contents_); + DCHECK(iter != g_web_contents_to_interstitial_page->end()); + if (iter != g_web_contents_to_interstitial_page->end()) + g_web_contents_to_interstitial_page->erase(iter); + + // Clear the WebContents pointer, because it may now be deleted. + // This signifies that we are in the process of shutting down. + web_contents_ = NULL; +} + +void InterstitialPageImpl::Observe( + int type, + const NotificationSource& source, + const NotificationDetails& details) { + switch (type) { + case 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 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( + Source<RenderWidgetHost>(source).ptr()))); + DCHECK(rvh->GetProcess()->GetID() == original_child_id_ && + rvh->GetRoutingID() == original_rvh_id_); + TakeActionOnResourceDispatcher(CANCEL); + } + break; + case NOTIFICATION_DOM_OPERATION_RESPONSE: + if (enabled()) { + Details<DomOperationNotificationDetails> dom_op_details( + details); + delegate_->CommandReceived(dom_op_details->json); + } + break; + default: + NOTREACHED(); + } +} + +void InterstitialPageImpl::NavigationEntryCommitted( + const LoadCommittedDetails& load_details) { + OnNavigatingAwayOrTabClosing(); +} + +void InterstitialPageImpl::WebContentsDestroyed(WebContents* web_contents) { + OnNavigatingAwayOrTabClosing(); +} + +RenderViewHostDelegateView* InterstitialPageImpl::GetDelegateView() { + return rvh_delegate_view_.get(); +} + +const GURL& InterstitialPageImpl::GetURL() const { + return url_; +} + +void InterstitialPageImpl::RenderViewTerminated( + RenderViewHost* render_view_host, + base::TerminationStatus status, + int error_code) { + // Our renderer died. This should not happen in normal cases. + // If we haven't already started shutdown, just dismiss the interstitial. + // We cannot check for enabled() here, because we may have called Disable + // without calling Hide. + if (render_view_host_) + 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 (PageTransitionCoreTypeIs(params.transition, + 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. + if (!controller_->delegate()->IsHidden()) + render_view_host_->GetView()->Show(); + controller_->delegate()->AttachInterstitialPage(this); + + RenderWidgetHostView* rwh_view = + controller_->delegate()->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 WebContentsObserver::DidStopLoading callback 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. + web_contents_was_loading_ = controller_->delegate()->IsLoading(); + controller_->delegate()->SetIsLoading( + controller_->delegate()->GetRenderViewHost(), false, NULL); +} + +void InterstitialPageImpl::UpdateTitle( + RenderViewHost* render_view_host, + int32 page_id, + const string16& title, + base::i18n::TextDirection title_direction) { + if (!enabled()) + return; + + DCHECK(render_view_host == render_view_host_); + NavigationEntry* entry = controller_->GetVisibleEntry(); + 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_web_contents_title_) { + original_web_contents_title_ = entry->GetTitle(); + should_revert_web_contents_title_ = true; + } + // TODO(evan): make use of title_direction. + // http://code.google.com/p/chromium/issues/detail?id=27094 + entry->SetTitle(title); + controller_->delegate()->NotifyNavigationStateChanged(INVALIDATE_TYPE_TITLE); +} + +RendererPreferences InterstitialPageImpl::GetRendererPrefs( + BrowserContext* browser_context) const { + delegate_->OverrideRendererPrefs(&renderer_preferences_); + return renderer_preferences_; +} + +WebPreferences InterstitialPageImpl::GetWebkitPrefs() { + if (!enabled()) + return WebPreferences(); + + return render_view_host_->GetWebkitPrefs(url_); +} + +void InterstitialPageImpl::RenderWidgetDeleted( + RenderWidgetHostImpl* render_widget_host) { + delete this; +} + +bool InterstitialPageImpl::PreHandleKeyboardEvent( + const NativeWebKeyboardEvent& event, + bool* is_keyboard_shortcut) { + if (!enabled()) + return false; + return render_widget_host_delegate_->PreHandleKeyboardEvent( + event, is_keyboard_shortcut); +} + +void InterstitialPageImpl::HandleKeyboardEvent( + const NativeWebKeyboardEvent& event) { + if (enabled()) + render_widget_host_delegate_->HandleKeyboardEvent(event); +} + +#if defined(OS_WIN) && defined(USE_AURA) +gfx::NativeViewAccessible +InterstitialPageImpl::GetParentNativeViewAccessible() { + return render_widget_host_delegate_->GetParentNativeViewAccessible(); +} +#endif + +WebContents* InterstitialPageImpl::web_contents() const { + return web_contents_; +} + +RenderViewHost* InterstitialPageImpl::CreateRenderViewHost() { + if (!enabled()) + return NULL; + + // Interstitial pages don't want to share the session storage so we mint a + // new one. + BrowserContext* browser_context = web_contents()->GetBrowserContext(); + scoped_refptr<SiteInstance> site_instance = + SiteInstance::Create(browser_context); + DOMStorageContextWrapper* dom_storage_context = + static_cast<DOMStorageContextWrapper*>( + BrowserContext::GetStoragePartition( + browser_context, site_instance.get())->GetDOMStorageContext()); + session_storage_namespace_ = + new SessionStorageNamespaceImpl(dom_storage_context); + + return RenderViewHostFactory::Create(site_instance.get(), + this, + this, + MSG_ROUTING_NONE, + MSG_ROUTING_NONE, + false, + false); +} + +WebContentsView* InterstitialPageImpl::CreateWebContentsView() { + if (!enabled() || !create_view_) + return NULL; + WebContentsView* web_contents_view = web_contents()->GetView(); + WebContentsViewPort* web_contents_view_port = + static_cast<WebContentsViewPort*>(web_contents_view); + RenderWidgetHostView* view = + web_contents_view_port->CreateViewForWidget(render_view_host_); + render_view_host_->SetView(view); + render_view_host_->AllowBindings(BINDINGS_POLICY_DOM_AUTOMATION); + + int32 max_page_id = web_contents()-> + GetMaxPageIDForSiteInstance(render_view_host_->GetSiteInstance()); + render_view_host_->CreateRenderView(string16(), + MSG_ROUTING_NONE, + max_page_id); + controller_->delegate()->RenderViewForInterstitialPageCreated( + render_view_host_); + 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() { + // Don't repeat this if we are already shutting down. We cannot check for + // enabled() here, because we may have called Disable without calling Hide. + if (!render_view_host_) + return; + + if (action_taken_ != NO_ACTION) { + NOTREACHED(); + return; + } + Disable(); + action_taken_ = PROCEED_ACTION; + + // Resumes the throbber, if applicable. + if (web_contents_was_loading_) + controller_->delegate()->SetIsLoading( + controller_->delegate()->GetRenderViewHost(), 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(); + return; + } + + delegate_->OnProceed(); +} + +void InterstitialPageImpl::DontProceed() { + // Don't repeat this if we are already shutting down. We cannot check for + // enabled() here, because we may have called Disable without calling Hide. + if (!render_view_host_) + return; + 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. + controller_->DiscardNonCommittedEntries(); + } + + if (reload_on_dont_proceed_) + controller_->Reload(true); + + Hide(); + delegate_->OnDontProceed(); +} + +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 (!enabled()) + return; +#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. + if (!enabled()) + return; + RenderWidgetHostViewPort::FromRWHV(render_view_host_->GetView())->Focus(); +} + +void InterstitialPageImpl::FocusThroughTabTraversal(bool reverse) { + if (!enabled()) + return; + render_view_host_->SetInitialFocus(reverse); +} + +RenderWidgetHostView* InterstitialPageImpl::GetView() { + return render_view_host_->GetView(); +} + +RenderViewHost* InterstitialPageImpl::GetRenderViewHostForTesting() const { + return render_view_host_; +} + +#if defined(OS_ANDROID) +RenderViewHost* InterstitialPageImpl::GetRenderViewHost() const { + return render_view_host_; +} +#endif + +InterstitialPageDelegate* InterstitialPageImpl::GetDelegateForTesting() { + return delegate_.get(); +} + +void InterstitialPageImpl::DontCreateViewForTesting() { + create_view_ = false; +} + +gfx::Rect InterstitialPageImpl::GetRootWindowResizerRect() const { + return gfx::Rect(); +} + +void InterstitialPageImpl::CreateNewWindow( + int route_id, + int main_frame_route_id, + const ViewHostMsg_CreateWindow_Params& params, + SessionStorageNamespace* session_storage_namespace) { + NOTREACHED() << "InterstitialPage does not support showing popups yet."; +} + +void InterstitialPageImpl::CreateNewWidget(int route_id, + WebKit::WebPopupType popup_type) { + NOTREACHED() << "InterstitialPage does not support showing drop-downs yet."; +} + +void InterstitialPageImpl::CreateNewFullscreenWidget(int route_id) { + NOTREACHED() + << "InterstitialPage does not support showing full screen popups."; +} + +void InterstitialPageImpl::ShowCreatedWindow(int route_id, + WindowOpenDisposition disposition, + const gfx::Rect& initial_pos, + bool user_gesture) { + NOTREACHED() << "InterstitialPage does not support showing popups yet."; +} + +void InterstitialPageImpl::ShowCreatedWidget(int route_id, + const gfx::Rect& initial_pos) { + NOTREACHED() << "InterstitialPage does not support showing drop-downs yet."; +} + +void InterstitialPageImpl::ShowCreatedFullscreenWidget(int route_id) { + NOTREACHED() + << "InterstitialPage does not support showing full screen popups."; +} + +SessionStorageNamespace* InterstitialPageImpl::GetSessionStorageNamespace( + SiteInstance* instance) { + return session_storage_namespace_.get(); +} + +FrameTree* InterstitialPageImpl::GetFrameTree() { + return &frame_tree_; +} + +void InterstitialPageImpl::Disable() { + enabled_ = false; +} + +void InterstitialPageImpl::Shutdown(RenderViewHostImpl* render_view_host) { + render_view_host->Shutdown(); + // We are deleted now. +} + +void InterstitialPageImpl::OnNavigatingAwayOrTabClosing() { + 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(); + } +} + +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::InterstitialPageRVHDelegateView:: + InterstitialPageRVHDelegateView(InterstitialPageImpl* page) + : interstitial_page_(page) { +} + +void InterstitialPageImpl::InterstitialPageRVHDelegateView::ShowPopupMenu( + const gfx::Rect& bounds, + int item_height, + double item_font_size, + int selected_item, + const std::vector<MenuItem>& items, + bool right_aligned, + bool allow_multiple_selection) { + NOTREACHED() << "InterstitialPage does not support showing popup menus."; +} + +void InterstitialPageImpl::InterstitialPageRVHDelegateView::StartDragging( + const DropData& drop_data, + WebDragOperationsMask allowed_operations, + const gfx::ImageSkia& image, + const gfx::Vector2d& image_offset, + const DragEventSourceInfo& event_info) { + NOTREACHED() << "InterstitialPage does not support dragging yet."; +} + +void InterstitialPageImpl::InterstitialPageRVHDelegateView::UpdateDragCursor( + WebDragOperation) { + NOTREACHED() << "InterstitialPage does not support dragging yet."; +} + +void InterstitialPageImpl::InterstitialPageRVHDelegateView::GotFocus() { + WebContents* web_contents = interstitial_page_->web_contents(); + if (web_contents && web_contents->GetDelegate()) + web_contents->GetDelegate()->WebContentsFocused(web_contents); +} + +void InterstitialPageImpl::InterstitialPageRVHDelegateView::TakeFocus( + bool reverse) { + if (!interstitial_page_->web_contents()) + return; + WebContentsImpl* web_contents = + static_cast<WebContentsImpl*>(interstitial_page_->web_contents()); + if (!web_contents->GetDelegateView()) + return; + + web_contents->GetDelegateView()->TakeFocus(reverse); +} + +void InterstitialPageImpl::InterstitialPageRVHDelegateView::OnFindReply( + int request_id, int number_of_matches, const gfx::Rect& selection_rect, + int active_match_ordinal, bool final_update) { +} + +} // namespace content diff --git a/content/browser/frame_host/interstitial_page_impl.h b/content/browser/frame_host/interstitial_page_impl.h new file mode 100644 index 0000000..66c0ca9 --- /dev/null +++ b/content/browser/frame_host/interstitial_page_impl.h @@ -0,0 +1,267 @@ +// Copyright 2013 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_FRAME_HOST_INTERSTITIAL_PAGE_IMPL_H_ +#define CONTENT_BROWSER_FRAME_HOST_INTERSTITIAL_PAGE_IMPL_H_ + +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "content/browser/frame_host/frame_tree.h" +#include "content/browser/renderer_host/render_view_host_delegate.h" +#include "content/browser/renderer_host/render_widget_host_delegate.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/web_contents_observer.h" +#include "content/public/common/renderer_preferences.h" +#include "url/gurl.h" + +namespace content { +class NavigationEntry; +class NavigationControllerImpl; +class RenderViewHostImpl; +class RenderWidgetHostView; +class WebContentsView; +class WebContentsImpl; + +enum ResourceRequestAction { + BLOCK, + RESUME, + CANCEL +}; + +class CONTENT_EXPORT InterstitialPageImpl + : public NON_EXPORTED_BASE(InterstitialPage), + public NotificationObserver, + public WebContentsObserver, + public RenderViewHostDelegate, + public RenderWidgetHostDelegate { + 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(WebContents* web_contents, + RenderWidgetHostDelegate* render_widget_host_delegate, + bool new_navigation, + const GURL& url, + InterstitialPageDelegate* delegate); + virtual ~InterstitialPageImpl(); + + // InterstitialPage implementation: + virtual void Show() OVERRIDE; + virtual void Hide() OVERRIDE; + virtual void DontProceed() OVERRIDE; + virtual void Proceed() OVERRIDE; + virtual RenderViewHost* GetRenderViewHostForTesting() const OVERRIDE; + virtual 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); + + RenderWidgetHostView* GetView(); + + // 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_; } + +#if defined(OS_ANDROID) + // Android shares a single platform window for all tabs, so we need to expose + // the RenderViewHost to properly route gestures to the interstitial. + RenderViewHost* GetRenderViewHost() const; +#endif + + protected: + // NotificationObserver method: + virtual void Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) OVERRIDE; + + // WebContentsObserver implementation: + virtual void WebContentsDestroyed(WebContents* web_contents) OVERRIDE; + virtual void NavigationEntryCommitted( + const LoadCommittedDetails& load_details) OVERRIDE; + + // RenderViewHostDelegate implementation: + virtual RenderViewHostDelegateView* GetDelegateView() OVERRIDE; + virtual const GURL& GetURL() const OVERRIDE; + virtual void RenderViewTerminated(RenderViewHost* render_view_host, + base::TerminationStatus status, + int error_code) OVERRIDE; + virtual void DidNavigate( + RenderViewHost* render_view_host, + const ViewHostMsg_FrameNavigate_Params& params) OVERRIDE; + virtual void UpdateTitle(RenderViewHost* render_view_host, + int32 page_id, + const string16& title, + base::i18n::TextDirection title_direction) OVERRIDE; + virtual RendererPreferences GetRendererPrefs( + BrowserContext* browser_context) const OVERRIDE; + virtual WebPreferences GetWebkitPrefs() OVERRIDE; + virtual gfx::Rect GetRootWindowResizerRect() const OVERRIDE; + virtual void CreateNewWindow( + int route_id, + int main_frame_route_id, + const ViewHostMsg_CreateWindow_Params& params, + SessionStorageNamespace* session_storage_namespace) OVERRIDE; + virtual void CreateNewWidget(int route_id, + WebKit::WebPopupType popup_type) OVERRIDE; + virtual void CreateNewFullscreenWidget(int route_id) OVERRIDE; + virtual void ShowCreatedWindow(int route_id, + WindowOpenDisposition disposition, + const gfx::Rect& initial_pos, + bool user_gesture) OVERRIDE; + virtual void ShowCreatedWidget(int route_id, + const gfx::Rect& initial_pos) OVERRIDE; + virtual void ShowCreatedFullscreenWidget(int route_id) OVERRIDE; + + virtual SessionStorageNamespace* GetSessionStorageNamespace( + SiteInstance* instance) OVERRIDE; + + virtual FrameTree* GetFrameTree() OVERRIDE; + + // RenderWidgetHostDelegate implementation: + virtual void RenderWidgetDeleted( + RenderWidgetHostImpl* render_widget_host) OVERRIDE; + virtual bool PreHandleKeyboardEvent( + const NativeWebKeyboardEvent& event, + bool* is_keyboard_shortcut) OVERRIDE; + virtual void HandleKeyboardEvent( + const NativeWebKeyboardEvent& event) OVERRIDE; +#if defined(OS_WIN) && defined(USE_AURA) + virtual gfx::NativeViewAccessible GetParentNativeViewAccessible() OVERRIDE; +#endif + + bool enabled() const { return enabled_; } + WebContents* web_contents() const; + const GURL& url() const { return url_; } + + // Creates the RenderViewHost containing the interstitial content. + // Overriden in unit tests. + virtual RenderViewHost* CreateRenderViewHost(); + + // Creates the WebContentsView that shows the interstitial RVH. + // Overriden in unit tests. + virtual WebContentsView* CreateWebContentsView(); + + // Notification magic. + NotificationRegistrar notification_registrar_; + + private: + class InterstitialPageRVHDelegateView; + + // 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(); + + // Shutdown the RVH. We will be deleted by the time this method returns. + void Shutdown(RenderViewHostImpl* render_view_host); + + void OnNavigatingAwayOrTabClosing(); + + // 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. This is valid until Hide is + // called, at which point it will be set to NULL because the WebContents + // itself may be deleted. + WebContents* web_contents_; + + // The NavigationController for the content this page is being displayed over. + NavigationControllerImpl* controller_; + + // Delegate for dispatching keyboard events and accessing the native view. + RenderWidgetHostDelegate* render_widget_host_delegate_; + + // 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. This is valid + // until Hide is called, at which point it will be set to NULL, signifying + // that shutdown has started. + RenderViewHostImpl* render_view_host_; + + // The frame tree structure of the current page. + FrameTree frame_tree_; + + // 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 contents when hidden (to + // revert it to its original value). + bool should_revert_web_contents_title_; + + // Whether or not the contents was loading resources when the interstitial was + // shown. We restore this state if the user proceeds from the interstitial. + bool web_contents_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 contents that should be reverted to when the + // interstitial is hidden. + string16 original_web_contents_title_; + + // Our RenderViewHostViewDelegate, necessary for accelerators to work. + scoped_ptr<InterstitialPageRVHDelegateView> rvh_delegate_view_; + + // Settings passed to the renderer. + mutable RendererPreferences renderer_preferences_; + + bool create_view_; + + scoped_ptr<InterstitialPageDelegate> delegate_; + + base::WeakPtrFactory<InterstitialPageImpl> weak_ptr_factory_; + + scoped_refptr<SessionStorageNamespace> session_storage_namespace_; + + DISALLOW_COPY_AND_ASSIGN(InterstitialPageImpl); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_INTERSTITIAL_PAGE_IMPL_H_ diff --git a/content/browser/frame_host/navigation_controller_delegate.h b/content/browser/frame_host/navigation_controller_delegate.h new file mode 100644 index 0000000..cb3204c --- /dev/null +++ b/content/browser/frame_host/navigation_controller_delegate.h @@ -0,0 +1,79 @@ +// Copyright 2013 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_FRAME_HOST_NAVIGATION_CONTROLLER_DELEGATE_H_ +#define CONTENT_BROWSER_FRAME_HOST_NAVIGATION_CONTROLLER_DELEGATE_H_ + +#include <string> +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/navigation_details.h" + +namespace content { + +struct LoadCommittedDetails; +struct LoadNotificationDetails; +struct NativeWebKeyboardEvent; +class InterstitialPage; +class InterstitialPageImpl; +class RenderViewHost; +class SiteInstance; +class WebContents; +class WebContentsDelegate; + +// Interface for objects embedding a NavigationController to provide the +// functionality NavigationController needs. +// TODO(nasko): This interface should exist for short amount of time, while +// we transition navigation code from WebContents to Navigator. +class NavigationControllerDelegate { + public: + virtual ~NavigationControllerDelegate() {} + + // Duplicates of WebContents methods. + virtual RenderViewHost* GetRenderViewHost() const = 0; + virtual InterstitialPage* GetInterstitialPage() const = 0; + virtual const std::string& GetContentsMimeType() const = 0; + virtual void NotifyNavigationStateChanged(unsigned changed_flags) = 0; + virtual void Stop() = 0; + virtual SiteInstance* GetSiteInstance() const = 0; + virtual SiteInstance* GetPendingSiteInstance() const = 0; + virtual int32 GetMaxPageID() = 0; + virtual int32 GetMaxPageIDForSiteInstance(SiteInstance* site_instance) = 0; + virtual bool IsLoading() const = 0; + + // Methods from WebContentsImpl that NavigationControllerImpl needs to + // call. + virtual void NotifyBeforeFormRepostWarningShow() = 0; + virtual void NotifyNavigationEntryCommitted( + const LoadCommittedDetails& load_details) = 0; + virtual bool NavigateToPendingEntry( + NavigationController::ReloadType reload_type) = 0; + virtual void SetHistoryLengthAndPrune( + const SiteInstance* site_instance, + int merge_history_length, + int32 minimum_page_id) = 0; + virtual void CopyMaxPageIDsFrom(WebContents* web_contents) = 0; + virtual void UpdateMaxPageID(int32 page_id) = 0; + virtual void UpdateMaxPageIDForSiteInstance(SiteInstance* site_instance, + int32 page_id) = 0; + virtual void ActivateAndShowRepostFormWarningDialog() = 0; + + // This method is needed, since we are no longer guaranteed that the + // embedder for NavigationController will be a WebContents object. + virtual WebContents* GetWebContents() = 0; + + // Methods needed by InterstitialPageImpl. + virtual bool IsHidden() = 0; + virtual void RenderViewForInterstitialPageCreated( + RenderViewHost* render_view_host) = 0; + virtual void AttachInterstitialPage( + InterstitialPageImpl* interstitial_page) = 0; + virtual void DetachInterstitialPage() = 0; + virtual void SetIsLoading(RenderViewHost* render_view_host, + bool is_loading, + LoadNotificationDetails* details) = 0; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_NAVIGATION_CONTROLLER_DELEGATE_H_ diff --git a/content/browser/frame_host/navigation_controller_impl.cc b/content/browser/frame_host/navigation_controller_impl.cc new file mode 100644 index 0000000..04c229a --- /dev/null +++ b/content/browser/frame_host/navigation_controller_impl.cc @@ -0,0 +1,1700 @@ +// Copyright 2013 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/frame_host/navigation_controller_impl.h" + +#include "base/bind.h" +#include "base/debug/trace_event.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" // Temporary +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "content/browser/browser_url_handler_impl.h" +#include "content/browser/dom_storage/dom_storage_context_wrapper.h" +#include "content/browser/dom_storage/session_storage_namespace_impl.h" +#include "content/browser/frame_host/debug_urls.h" +#include "content/browser/frame_host/interstitial_page_impl.h" +#include "content/browser/frame_host/navigation_entry_impl.h" +#include "content/browser/frame_host/web_contents_screenshot_manager.h" +#include "content/browser/renderer_host/render_view_host_impl.h" // Temporary +#include "content/browser/site_instance_impl.h" +#include "content/common/view_messages.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/content_browser_client.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/render_widget_host.h" +#include "content/public/browser/render_widget_host_view.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/browser/user_metrics.h" +#include "content/public/common/content_client.h" +#include "content/public/common/content_constants.h" +#include "content/public/common/url_constants.h" +#include "net/base/escape.h" +#include "net/base/mime_util.h" +#include "net/base/net_util.h" +#include "skia/ext/platform_canvas.h" + +namespace content { +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) { + PrunedDetails details; + details.from_front = from_front; + details.count = count; + NotificationService::current()->Notify( + NOTIFICATION_NAV_LIST_PRUNED, + Source<NavigationController>(nav_controller), + Details<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 SetPageStateIfEmpty(NavigationEntryImpl* entry) { + if (!entry->GetPageState().IsValid()) + entry->SetPageState(PageState::CreateFromURL(entry->GetURL())); +} + +NavigationEntryImpl::RestoreType ControllerRestoreTypeToEntryType( + NavigationController::RestoreType type) { + switch (type) { + case NavigationController::RESTORE_CURRENT_SESSION: + return NavigationEntryImpl::RESTORE_CURRENT_SESSION; + case NavigationController::RESTORE_LAST_SESSION_EXITED_CLEANLY: + return NavigationEntryImpl::RESTORE_LAST_SESSION_EXITED_CLEANLY; + case NavigationController::RESTORE_LAST_SESSION_CRASHED: + return NavigationEntryImpl::RESTORE_LAST_SESSION_CRASHED; + } + NOTREACHED(); + return NavigationEntryImpl::RESTORE_CURRENT_SESSION; +} + +// 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, + NavigationController::RestoreType type) { + 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(PAGE_TRANSITION_RELOAD); + (*entries)[i]->set_restore_type(ControllerRestoreTypeToEntryType(type)); + // NOTE(darin): This code is only needed for backwards compat. + SetPageStateIfEmpty((*entries)[i].get()); + } +} + +// See NavigationController::IsURLInPageNavigation for how this works and why. +bool AreURLsInPageNavigation(const GURL& existing_url, + const GURL& new_url, + bool renderer_says_in_page, + NavigationType navigation_type) { + if (existing_url == new_url) + return renderer_says_in_page; + + if (!new_url.has_ref()) { + // When going back from the ref URL to the non ref one the navigation type + // is IN_PAGE. + return navigation_type == NAVIGATION_TYPE_IN_PAGE; + } + + url_canon::Replacements<char> replacements; + replacements.ClearRef(); + return existing_url.ReplaceComponents(replacements) == + new_url.ReplaceComponents(replacements); +} + +// Determines whether or not we should be carrying over a user agent override +// between two NavigationEntries. +bool ShouldKeepOverride(const NavigationEntry* last_entry) { + return last_entry && last_entry->GetIsOverridingUserAgent(); +} + +} // 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; + +// 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 + // (WebContents::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; +} + +base::Time NavigationControllerImpl::TimeSmoother::GetSmoothedTime( + base::Time t) { + // If |t| is between the water marks, we're in a run of duplicates + // or just getting out of it, so increase the high-water mark to get + // a time that probably hasn't been used before and return it. + if (low_water_mark_ <= t && t <= high_water_mark_) { + high_water_mark_ += base::TimeDelta::FromMicroseconds(1); + return high_water_mark_; + } + + // Otherwise, we're clear of the last duplicate run, so reset the + // water marks. + low_water_mark_ = high_water_mark_ = t; + return t; +} + +NavigationControllerImpl::NavigationControllerImpl( + NavigationControllerDelegate* delegate, + BrowserContext* browser_context) + : browser_context_(browser_context), + pending_entry_(NULL), + last_committed_entry_index_(-1), + pending_entry_index_(-1), + transient_entry_index_(-1), + delegate_(delegate), + max_restored_page_id_(-1), + ssl_manager_(this), + needs_reload_(false), + is_initial_navigation_(true), + pending_reload_(NO_RELOAD), + get_timestamp_callback_(base::Bind(&base::Time::Now)), + screenshot_manager_(new WebContentsScreenshotManager(this)) { + DCHECK(browser_context_); +} + +NavigationControllerImpl::~NavigationControllerImpl() { + DiscardNonCommittedEntriesInternal(); +} + +WebContents* NavigationControllerImpl::GetWebContents() const { + return delegate_->GetWebContents(); +} + +BrowserContext* NavigationControllerImpl::GetBrowserContext() const { + return browser_context_; +} + +void NavigationControllerImpl::SetBrowserContext( + BrowserContext* browser_context) { + browser_context_ = browser_context; +} + +void NavigationControllerImpl::Restore( + int selected_navigation, + RestoreType type, + 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, type); +} + +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::ReloadOriginalRequestURL(bool check_for_repost) { + ReloadInternal(check_for_repost, RELOAD_ORIGINAL_REQUEST_URL); +} + +void NavigationControllerImpl::ReloadInternal(bool check_for_repost, + ReloadType reload_type) { + if (transient_entry_index_ != -1) { + // If an interstitial is showing, treat a reload as a navigation to the + // transient entry's URL. + NavigationEntryImpl* transient_entry = + NavigationEntryImpl::FromNavigationEntry(GetTransientEntry()); + if (!transient_entry) + return; + LoadURL(transient_entry->GetURL(), + Referrer(), + PAGE_TRANSITION_RELOAD, + transient_entry->extra_headers()); + return; + } + + NavigationEntryImpl* entry = NULL; + int current_index = -1; + + // If we are reloading the initial navigation, just use the current + // pending entry. Otherwise look up the current entry. + if (IsInitialNavigation() && pending_entry_) { + entry = pending_entry_; + // The pending entry might be in entries_ (e.g., after a Clone), so we + // should also update the current_index. + current_index = pending_entry_index_; + } else { + DiscardNonCommittedEntriesInternal(); + current_index = GetCurrentEntryIndex(); + if (current_index != -1) { + entry = NavigationEntryImpl::FromNavigationEntry( + GetEntryAtIndex(current_index)); + } + } + + // If we are no where, then we can't reload. TODO(darin): We should add a + // CanReload method. + if (!entry) + return; + + if (reload_type == NavigationControllerImpl::RELOAD_ORIGINAL_REQUEST_URL && + entry->GetOriginalRequestURL().is_valid() && !entry->GetHasPostData()) { + // We may have been redirected when navigating to the current URL. + // Use the URL the user originally intended to visit, if it's valid and if a + // POST wasn't involved; the latter case avoids issues with sending data to + // the wrong page. + entry->SetURL(entry->GetOriginalRequestURL()); + } + + if (g_check_for_repost && check_for_repost && + entry->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. + delegate_->NotifyBeforeFormRepostWarningShow(); + + pending_reload_ = reload_type; + delegate_->ActivateAndShowRepostFormWarningDialog(); + } else { + if (!IsInitialNavigation()) + DiscardNonCommittedEntriesInternal(); + + // 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. + SiteInstanceImpl* site_instance = entry->site_instance(); + 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, content state, or timestamp. + 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_should_replace_entry(true); + pending_entry_ = nav_entry; + } else { + pending_entry_ = entry; + 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. + pending_entry_->SetTitle(string16()); + + pending_entry_->SetTransitionType(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() const { + return is_initial_navigation_; +} + +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) { + // 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). + SetPendingEntry(entry); + NavigateToPendingEntry(NO_RELOAD); +} + +void NavigationControllerImpl::SetPendingEntry(NavigationEntryImpl* entry) { + DiscardNonCommittedEntriesInternal(); + pending_entry_ = entry; + NotificationService::current()->Notify( + NOTIFICATION_NAV_ENTRY_PENDING, + Source<NavigationController>(this), + Details<NavigationEntry>(entry)); +} + +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(); + // The pending entry is safe to return for new (non-history), browser- + // initiated navigations. Most renderer-initiated navigations should not + // show the pending entry, to prevent URL spoof attacks. + // + // We make an exception for renderer-initiated navigations in new tabs, as + // long as no other page has tried to access the initial empty document in + // the new tab. If another page modifies this blank page, a URL spoof is + // possible, so we must stop showing the pending entry. + RenderViewHostImpl* rvh = static_cast<RenderViewHostImpl*>( + delegate_->GetRenderViewHost()); + bool safe_to_show_pending = + pending_entry_ && + // Require a new navigation. + pending_entry_->GetPageID() == -1 && + // Require either browser-initiated or an unmodified new tab. + (!pending_entry_->is_renderer_initiated() || + (IsInitialNavigation() && + !GetLastCommittedEntry() && + !rvh->has_accessed_initial_document())); + + // Also allow showing the pending entry for history navigations in a new tab, + // such as Ctrl+Back. In this case, no existing page is visible and no one + // can script the new tab before it commits. + if (!safe_to_show_pending && + pending_entry_ && + pending_entry_->GetPageID() != -1 && + IsInitialNavigation() && + !pending_entry_->is_renderer_initiated()) + safe_to_show_pending = true; + + if (safe_to_show_pending) + 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 { + const std::string& mime_type = delegate_->GetContentsMimeType(); + bool is_viewable_mime_type = net::IsSupportedNonImageMimeType(mime_type) && + !net::IsSupportedMediaMimeType(mime_type); + NavigationEntry* visible_entry = GetVisibleEntry(); + return visible_entry && !visible_entry->IsViewSourceMode() && + is_viewable_mime_type && !delegate_->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 = GetIndexForOffset(offset); + if (index < 0 || index >= GetEntryCount()) + return NULL; + + return entries_[index].get(); +} + +int NavigationControllerImpl::GetIndexForOffset(int offset) const { + return GetCurrentEntryIndex() + offset; +} + +void NavigationControllerImpl::TakeScreenshot() { + screenshot_manager_->TakeScreenshot(); +} + +void NavigationControllerImpl::SetScreenshotManager( + WebContentsScreenshotManager* manager) { + screenshot_manager_.reset(manager ? manager : + new WebContentsScreenshotManager(this)); +} + +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); +} + +bool NavigationControllerImpl::CanGoToOffset(int offset) const { + int index = GetIndexForOffset(offset); + return index >= 0 && index < GetEntryCount(); +} + +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( + PageTransitionFromInt( + entries_[pending_entry_index_]->GetTransitionType() | + 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( + PageTransitionFromInt( + entries_[pending_entry_index_]->GetTransitionType() | + 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( + PageTransitionFromInt( + entries_[pending_entry_index_]->GetTransitionType() | + PAGE_TRANSITION_FORWARD_BACK)); + NavigateToPendingEntry(NO_RELOAD); +} + +void NavigationControllerImpl::GoToOffset(int offset) { + if (!CanGoToOffset(offset)) + return; + + GoToIndex(GetIndexForOffset(offset)); +} + +bool NavigationControllerImpl::RemoveEntryAtIndex(int index) { + if (index == last_committed_entry_index_ || + index == pending_entry_index_) + return false; + + RemoveEntryAtIndexInternal(index); + return true; +} + +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::LoadURL( + const GURL& url, + const Referrer& referrer, + PageTransition transition, + const std::string& extra_headers) { + LoadURLParams params(url); + params.referrer = referrer; + params.transition_type = transition; + params.extra_headers = extra_headers; + LoadURLWithParams(params); +} + +void NavigationControllerImpl::LoadURLWithParams(const LoadURLParams& params) { + TRACE_EVENT0("browser", "NavigationControllerImpl::LoadURLWithParams"); + if (HandleDebugURL(params.url, params.transition_type)) + return; + + // Checks based on params.load_type. + switch (params.load_type) { + case LOAD_TYPE_DEFAULT: + break; + case LOAD_TYPE_BROWSER_INITIATED_HTTP_POST: + if (!params.url.SchemeIs(kHttpScheme) && + !params.url.SchemeIs(kHttpsScheme)) { + NOTREACHED() << "Http post load must use http(s) scheme."; + return; + } + break; + case LOAD_TYPE_DATA: + if (!params.url.SchemeIs(chrome::kDataScheme)) { + NOTREACHED() << "Data load must use data scheme."; + return; + } + break; + default: + NOTREACHED(); + break; + }; + + // The user initiated a load, we don't need to reload anymore. + needs_reload_ = false; + + bool override = false; + switch (params.override_user_agent) { + case UA_OVERRIDE_INHERIT: + override = ShouldKeepOverride(GetLastCommittedEntry()); + break; + case UA_OVERRIDE_TRUE: + override = true; + break; + case UA_OVERRIDE_FALSE: + override = false; + break; + default: + NOTREACHED(); + break; + } + + NavigationEntryImpl* entry = NavigationEntryImpl::FromNavigationEntry( + CreateNavigationEntry( + params.url, + params.referrer, + params.transition_type, + params.is_renderer_initiated, + params.extra_headers, + browser_context_)); + if (params.redirect_chain.size() > 0) + entry->set_redirect_chain(params.redirect_chain); + if (params.should_replace_current_entry) + entry->set_should_replace_entry(true); + entry->set_should_clear_history_list(params.should_clear_history_list); + entry->SetIsOverridingUserAgent(override); + entry->set_transferred_global_request_id( + params.transferred_global_request_id); + entry->SetFrameToNavigate(params.frame_name); + + switch (params.load_type) { + case LOAD_TYPE_DEFAULT: + break; + case LOAD_TYPE_BROWSER_INITIATED_HTTP_POST: + entry->SetHasPostData(true); + entry->SetBrowserInitiatedPostData( + params.browser_initiated_post_data.get()); + break; + case LOAD_TYPE_DATA: + entry->SetBaseURLForDataURL(params.base_url_for_data_url); + entry->SetVirtualURL(params.virtual_url_for_data_url); + entry->SetCanLoadLocalResources(params.can_load_local_resources); + break; + default: + NOTREACHED(); + break; + }; + + LoadEntry(entry); +} + +bool NavigationControllerImpl::RendererDidNavigate( + const ViewHostMsg_FrameNavigate_Params& params, + LoadCommittedDetails* details) { + is_initial_navigation_ = false; + + // 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. + // Or if we are doing a cross-site redirect navigation, + // we will do a similar thing. + details->did_replace_entry = + pending_entry_ && pending_entry_->should_replace_entry(); + + // Do navigation-type specific actions. These will make and commit an entry. + details->type = ClassifyNavigation(params); + + // is_in_page must be computed before the entry gets committed. + details->is_in_page = IsURLInPageNavigation( + params.url, params.was_within_same_page, details->type); + + switch (details->type) { + case NAVIGATION_TYPE_NEW_PAGE: + RendererDidNavigateToNewPage(params, details->did_replace_entry); + break; + case NAVIGATION_TYPE_EXISTING_PAGE: + RendererDidNavigateToExistingPage(params); + break; + case NAVIGATION_TYPE_SAME_PAGE: + RendererDidNavigateToSamePage(params); + break; + case NAVIGATION_TYPE_IN_PAGE: + RendererDidNavigateInPage(params, &details->did_replace_entry); + break; + case NAVIGATION_TYPE_NEW_SUBFRAME: + RendererDidNavigateNewSubframe(params); + break; + case NAVIGATION_TYPE_AUTO_SUBFRAME: + if (!RendererDidNavigateAutoSubframe(params)) + return false; + break; + case 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(); + delegate_->NotifyNavigationStateChanged(INVALIDATE_TYPE_URL); + } + return false; + default: + NOTREACHED(); + } + + // At this point, we know that the navigation has just completed, so + // record the time. + // + // TODO(akalin): Use "sane time" as described in + // http://www.chromium.org/developers/design-documents/sane-time . + base::Time timestamp = + time_smoother_.GetSmoothedTime(get_timestamp_callback_.Run()); + DVLOG(1) << "Navigation finished at (smoothed) timestamp " + << timestamp.ToInternalValue(); + + // We should not have a pending entry anymore. Clear it again in case any + // error cases above forgot to do so. + DiscardNonCommittedEntriesInternal(); + + // 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.page_state.IsValid()); + NavigationEntryImpl* active_entry = + NavigationEntryImpl::FromNavigationEntry(GetLastCommittedEntry()); + active_entry->SetTimestamp(timestamp); + active_entry->SetHttpStatusCode(params.http_status_code); + active_entry->SetPageState(params.page_state); + + // Once it is committed, we no longer need to track several pieces of state on + // the entry. + active_entry->ResetForCommit(); + + // The active entry's SiteInstance should match our SiteInstance. + CHECK(active_entry->site_instance() == delegate_->GetSiteInstance()); + + // Remember the bindings the renderer process has at this point, so that + // we do not grant this entry additional bindings if we come back to it. + active_entry->SetBindings( + delegate_->GetRenderViewHost()->GetEnabledBindings()); + + // Now prep the rest of the details for the notification and broadcast. + details->entry = active_entry; + details->is_main_frame = + PageTransitionIsMainFrame(params.transition); + details->serialized_security_info = params.security_info; + details->http_status_code = params.http_status_code; + NotifyNavigationEntryCommitted(details); + + return true; +} + +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 NAVIGATION_TYPE_NAV_IGNORE; + } + + if (params.page_id > delegate_->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 (PageTransitionIsMainFrame(params.transition)) + return 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 NAVIGATION_TYPE_NAV_IGNORE; + + // Valid subframe navigation. + return NAVIGATION_TYPE_NEW_SUBFRAME; + } + + // We only clear the session history when navigating to a new page. + DCHECK(!params.history_list_was_cleared); + + // Now we know that the notification is for an existing page. Find that entry. + int existing_entry_index = GetEntryIndexWithPageID( + delegate_->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; + 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(delegate_->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() != delegate_->GetSiteInstance()) + temp.append("x"); + temp.append(","); + } + GURL url(temp); + static_cast<RenderViewHostImpl*>( + delegate_->GetRenderViewHost())->Send( + new ViewMsg_TempCrashWithData(url)); + return NAVIGATION_TYPE_NAV_IGNORE; + } + NavigationEntryImpl* existing_entry = entries_[existing_entry_index].get(); + + if (!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 NAVIGATION_TYPE_AUTO_SUBFRAME; + } + + // Anything below here we know is a main frame navigation. + if (pending_entry_ && + !pending_entry_->is_renderer_initiated() && + 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 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, + params.was_within_same_page, + NAVIGATION_TYPE_UNKNOWN)) { + return NAVIGATION_TYPE_IN_PAGE; + } + + // Since we weeded out "new" navigations above, we know this is an existing + // (back/forward) navigation. + return 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 (PageTransitionIsMainFrame(params.transition)) { + return PageTransitionIsRedirect(params.transition); + } + return params.redirects.size() > 1; +} + +void NavigationControllerImpl::RendererDidNavigateToNewPage( + const ViewHostMsg_FrameNavigate_Params& params, bool replace_entry) { + NavigationEntryImpl* new_entry; + bool update_virtual_url; + // Only make a copy of the pending entry if it is appropriate for the new page + // that was just loaded. We verify this at a coarse grain by checking that + // the SiteInstance hasn't been assigned to something else. + if (pending_entry_ && + (!pending_entry_->site_instance() || + pending_entry_->site_instance() == delegate_->GetSiteInstance())) { + 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(PAGE_TYPE_NORMAL); + update_virtual_url = new_entry->update_virtual_url_with_url(); + } else { + new_entry = new NavigationEntryImpl; + + // Find out whether the new entry needs to update its virtual URL on URL + // change and set up the entry accordingly. This is needed to correctly + // update the virtual URL when replaceState is called after a pushState. + GURL url = params.url; + bool needs_update = false; + BrowserURLHandlerImpl::GetInstance()->RewriteURLIfNecessary( + &url, browser_context_, &needs_update); + new_entry->set_update_virtual_url_with_url(needs_update); + + // 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 = needs_update; + } + + 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*>(delegate_->GetSiteInstance())); + new_entry->SetHasPostData(params.is_post); + new_entry->SetPostID(params.post_id); + new_entry->SetOriginalRequestURL(params.original_request_url); + new_entry->SetIsOverridingUserAgent(params.is_overriding_user_agent); + + DCHECK(!params.history_list_was_cleared || !replace_entry); + // The browser requested to clear the session history when it initiated the + // navigation. Now we know that the renderer has updated its state accordingly + // and it is safe to also clear the browser side history. + if (params.history_list_was_cleared) { + DiscardNonCommittedEntriesInternal(); + entries_.clear(); + last_committed_entry_index_ = -1; + } + + InsertOrReplaceEntry(new_entry, replace_entry); +} + +void NavigationControllerImpl::RendererDidNavigateToExistingPage( + const ViewHostMsg_FrameNavigate_Params& params) { + // We should only get here for main frame navigations. + DCHECK(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(delegate_->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. + entry->SetURL(params.url); + if (entry->update_virtual_url_with_url()) + UpdateVirtualURLToURL(entry, params.url); + + // The redirected to page should not inherit the favicon from the previous + // page. + if (PageTransitionIsRedirect(params.transition)) + entry->GetFavicon() = FaviconStatus(); + + // The site instance will normally be the same except during session restore, + // when no site instance will be assigned. + DCHECK(entry->site_instance() == NULL || + entry->site_instance() == delegate_->GetSiteInstance()); + entry->set_site_instance( + static_cast<SiteInstanceImpl*>(delegate_->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. + 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(delegate_->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( + delegate_->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(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( + delegate_->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; + + 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(delegate_->GetSiteInstance(), params.page_id); +} + +void NavigationControllerImpl::RendererDidNavigateNewSubframe( + const ViewHostMsg_FrameNavigate_Params& params) { + if (PageTransitionCoreTypeIs(params.transition, + PAGE_TRANSITION_AUTO_SUBFRAME)) { + // This is not user-initiated. Ignore. + DiscardNonCommittedEntriesInternal(); + 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( + delegate_->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; + DiscardNonCommittedEntriesInternal(); + return true; + } + + // We do not need to discard the pending entry in this case, since we will + // not generate commit notifications for this auto-subframe navigation. + 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, + bool renderer_says_in_page, + NavigationType navigation_type) const { + NavigationEntry* last_committed = GetLastCommittedEntry(); + return last_committed && AreURLsInPageNavigation( + last_committed->GetURL(), url, renderer_says_in_page, navigation_type); +} + +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()); + + for (SessionStorageNamespaceMap::const_iterator it = + source.session_storage_namespace_map_.begin(); + it != source.session_storage_namespace_map_.end(); + ++it) { + SessionStorageNamespaceImpl* source_namespace = + static_cast<SessionStorageNamespaceImpl*>(it->second.get()); + session_storage_namespace_map_[it->first] = source_namespace->Clone(); + } + + FinishRestore(source.last_committed_entry_index_, RESTORE_CURRENT_SESSION); + + // 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. + delegate_->CopyMaxPageIDsFrom(source.delegate()->GetWebContents()); +} + +void NavigationControllerImpl::CopyStateFromAndPrune( + NavigationController* temp) { + // It is up to callers to check the invariants before calling this. + CHECK(CanPruneAllButVisible()); + + 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->site_instance()); + int32 minimum_page_id = last_committed->GetPageID(); + int32 max_page_id = + delegate_->GetMaxPageIDForSiteInstance(site_instance.get()); + + // Remove all the entries leaving the active entry. + PruneAllButVisibleInternal(); + + // We now have one entry, possibly with a new pending entry. Ensure that + // adding the entries from source won't put us over the limit. + DCHECK_EQ(1, GetEntryCount()); + source->PruneOldestEntryIfFull(); + + // Insert the entries from source. Don't use source->GetCurrentEntryIndex as + // we don't want to copy over the transient entry. Ignore any pending entry, + // since it has not committed in source. + int max_source_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; + + delegate_->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. + delegate_->CopyMaxPageIDsFrom(source->delegate()->GetWebContents()); + + // If there is a last committed entry, be sure to include it in the new + // max page ID map. + if (max_page_id > -1) { + delegate_->UpdateMaxPageIDForSiteInstance(site_instance.get(), + max_page_id); + } +} + +bool NavigationControllerImpl::CanPruneAllButVisible() { + // If there is no last committed entry, we cannot prune. Even if there is a + // pending entry, it may not commit, leaving this WebContents blank, despite + // possibly giving it new entries via CopyStateFromAndPrune. + if (last_committed_entry_index_ == -1) + return false; + + // We cannot prune if there is a pending entry at an existing entry index. + // It may not commit, so we have to keep the last committed entry, and thus + // there is no sensible place to keep the pending entry. It is ok to have + // a new pending entry, which can optionally commit as a new navigation. + if (pending_entry_index_ != -1) + return false; + + // We should not prune if we are currently showing a transient entry. + if (transient_entry_index_ != -1) + return false; + + return true; +} + +void NavigationControllerImpl::PruneAllButVisible() { + PruneAllButVisibleInternal(); + + // We should still have a last committed entry. + DCHECK_NE(-1, last_committed_entry_index_); + + // We pass 0 instead of GetEntryCount() for the history_length parameter of + // SetHistoryLengthAndPrune, because it will create history_length additional + // history entries. + // TODO(jochen): This API is confusing and we should clean it up. + // http://crbug.com/178491 + NavigationEntryImpl* entry = + NavigationEntryImpl::FromNavigationEntry(GetVisibleEntry()); + delegate_->SetHistoryLengthAndPrune( + entry->site_instance(), 0, entry->GetPageID()); +} + +void NavigationControllerImpl::PruneAllButVisibleInternal() { + // It is up to callers to check the invariants before calling this. + CHECK(CanPruneAllButVisible()); + + // Erase all entries but the last committed entry. There may still be a + // new pending entry after this. + entries_.erase(entries_.begin(), + entries_.begin() + last_committed_entry_index_); + entries_.erase(entries_.begin() + 1, entries_.end()); + last_committed_entry_index_ = 0; +} + +void NavigationControllerImpl::ClearAllScreenshots() { + screenshot_manager_->ClearAllScreenshots(); +} + +void NavigationControllerImpl::SetSessionStorageNamespace( + const std::string& partition_id, + SessionStorageNamespace* session_storage_namespace) { + if (!session_storage_namespace) + return; + + // We can't overwrite an existing SessionStorage without violating spec. + // Attempts to do so may give a tab access to another tab's session storage + // so die hard on an error. + bool successful_insert = session_storage_namespace_map_.insert( + make_pair(partition_id, + static_cast<SessionStorageNamespaceImpl*>( + session_storage_namespace))) + .second; + CHECK(successful_insert) << "Cannot replace existing SessionStorageNamespace"; +} + +void NavigationControllerImpl::SetMaxRestoredPageID(int32 max_id) { + max_restored_page_id_ = max_id; +} + +int32 NavigationControllerImpl::GetMaxRestoredPageID() const { + return max_restored_page_id_; +} + +SessionStorageNamespace* +NavigationControllerImpl::GetSessionStorageNamespace(SiteInstance* instance) { + std::string partition_id; + if (instance) { + // TODO(ajwong): When GetDefaultSessionStorageNamespace() goes away, remove + // this if statement so |instance| must not be NULL. + partition_id = + GetContentClient()->browser()->GetStoragePartitionIdForSite( + browser_context_, instance->GetSiteURL()); + } + + SessionStorageNamespaceMap::const_iterator it = + session_storage_namespace_map_.find(partition_id); + if (it != session_storage_namespace_map_.end()) + return it->second.get(); + + // Create one if no one has accessed session storage for this partition yet. + // + // TODO(ajwong): Should this use the |partition_id| directly rather than + // re-lookup via |instance|? http://crbug.com/142685 + StoragePartition* partition = + BrowserContext::GetStoragePartition(browser_context_, instance); + SessionStorageNamespaceImpl* session_storage_namespace = + new SessionStorageNamespaceImpl( + static_cast<DOMStorageContextWrapper*>( + partition->GetDOMStorageContext())); + session_storage_namespace_map_[partition_id] = session_storage_namespace; + + return session_storage_namespace; +} + +SessionStorageNamespace* +NavigationControllerImpl::GetDefaultSessionStorageNamespace() { + // TODO(ajwong): Remove if statement in GetSessionStorageNamespace(). + return GetSessionStorageNamespace(NULL); +} + +const SessionStorageNamespaceMap& +NavigationControllerImpl::GetSessionStorageNamespaceMap() const { + return session_storage_namespace_map_; +} + +bool NavigationControllerImpl::NeedsReload() const { + return needs_reload_; +} + +void NavigationControllerImpl::SetNeedsReload() { + needs_reload_ = true; +} + +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) { + delegate_->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() != 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. + delegate_->UpdateMaxPageID(entry->GetPageID()); +} + +void NavigationControllerImpl::PruneOldestEntryIfFull() { + if (entries_.size() >= max_entry_count()) { + DCHECK_EQ(max_entry_count(), entries_.size()); + DCHECK_GT(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() & + PAGE_TRANSITION_FORWARD_BACK)) { + delegate_->Stop(); + + // If an interstitial page is showing, we want to close it to get back + // to what was showing before. + if (delegate_->GetInterstitialPage()) + delegate_->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 (delegate_->GetInterstitialPage()) { + static_cast<InterstitialPageImpl*>(delegate_->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 (!delegate_->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 WebContentsImpl::OnGoToEntryAtOffset. + if (pending_entry_ && !pending_entry_->site_instance() && + pending_entry_->restore_type() != NavigationEntryImpl::RESTORE_NONE) { + pending_entry_->set_site_instance(static_cast<SiteInstanceImpl*>( + delegate_->GetPendingSiteInstance())); + pending_entry_->set_restore_type(NavigationEntryImpl::RESTORE_NONE); + } +} + +void NavigationControllerImpl::NotifyNavigationEntryCommitted( + LoadCommittedDetails* details) { + details->entry = GetLastCommittedEntry(); + + // We need to notify the ssl_manager_ before the web_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(*details); + + delegate_->NotifyNavigationStateChanged(kInvalidateAll); + delegate_->NotifyNavigationEntryCommitted(*details); + + // TODO(avi): Remove. http://crbug.com/170921 + NotificationDetails notification_details = + Details<LoadCommittedDetails>(details); + NotificationService::current()->Notify( + NOTIFICATION_NAV_ENTRY_COMMITTED, + 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 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) { + EntryChangedDetails det; + det.changed_entry = entry; + det.index = index; + NotificationService::current()->Notify( + NOTIFICATION_NAV_ENTRY_CHANGED, + Source<NavigationController>(this), + Details<EntryChangedDetails>(&det)); +} + +void NavigationControllerImpl::FinishRestore(int selected_index, + RestoreType type) { + DCHECK(selected_index >= 0 && selected_index < GetEntryCount()); + ConfigureEntriesForRestore(&entries_, type); + + SetMaxRestoredPageID(static_cast<int32>(GetEntryCount())); + + last_committed_entry_index_ = selected_index; +} + +void NavigationControllerImpl::DiscardNonCommittedEntriesInternal() { + DiscardPendingEntry(); + DiscardTransientEntry(); +} + +void NavigationControllerImpl::DiscardPendingEntry() { + if (pending_entry_index_ == -1) + delete pending_entry_; + pending_entry_ = NULL; + pending_entry_index_ = -1; +} + +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::SetTransientEntry(NavigationEntry* 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>( + NavigationEntryImpl::FromNavigationEntry(entry))); + transient_entry_index_ = index; + delegate_->NotifyNavigationStateChanged(kInvalidateAll); +} + +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() != + PAGE_TYPE_INTERSTITIAL) { + entries_.insert(entries_.begin() + insert_index++, + linked_ptr<NavigationEntryImpl>( + new NavigationEntryImpl(*source.entries_[i]))); + } + } +} + +void NavigationControllerImpl::SetGetTimestampCallbackForTest( + const base::Callback<base::Time()>& get_timestamp_callback) { + get_timestamp_callback_ = get_timestamp_callback; +} + +} // namespace content diff --git a/content/browser/frame_host/navigation_controller_impl.h b/content/browser/frame_host/navigation_controller_impl.h new file mode 100644 index 0000000..9177914 --- /dev/null +++ b/content/browser/frame_host/navigation_controller_impl.h @@ -0,0 +1,410 @@ +// Copyright 2013 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_FRAME_HOST_NAVIGATION_CONTROLLER_IMPL_H_ +#define CONTENT_BROWSER_FRAME_HOST_NAVIGATION_CONTROLLER_IMPL_H_ + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" +#include "base/memory/linked_ptr.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "content/browser/frame_host/navigation_controller_delegate.h" +#include "content/browser/ssl/ssl_manager.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/navigation_type.h" + +struct ViewHostMsg_FrameNavigate_Params; + +namespace content { +class NavigationEntryImpl; +class RenderViewHost; +class WebContentsScreenshotManager; +class SiteInstance; +struct LoadCommittedDetails; + +class CONTENT_EXPORT NavigationControllerImpl + : public NON_EXPORTED_BASE(NavigationController) { + public: + NavigationControllerImpl( + NavigationControllerDelegate* delegate, + BrowserContext* browser_context); + virtual ~NavigationControllerImpl(); + + // NavigationController implementation: + virtual WebContents* GetWebContents() const OVERRIDE; + virtual BrowserContext* GetBrowserContext() const OVERRIDE; + virtual void SetBrowserContext( + BrowserContext* browser_context) OVERRIDE; + virtual void Restore( + int selected_navigation, + RestoreType type, + std::vector<NavigationEntry*>* entries) OVERRIDE; + virtual NavigationEntry* GetActiveEntry() const OVERRIDE; + virtual NavigationEntry* GetVisibleEntry() const OVERRIDE; + virtual int GetCurrentEntryIndex() const OVERRIDE; + virtual NavigationEntry* GetLastCommittedEntry() const OVERRIDE; + virtual int GetLastCommittedEntryIndex() const OVERRIDE; + virtual bool CanViewSource() const OVERRIDE; + virtual int GetEntryCount() const OVERRIDE; + virtual NavigationEntry* GetEntryAtIndex(int index) const OVERRIDE; + virtual NavigationEntry* GetEntryAtOffset(int offset) const OVERRIDE; + virtual void DiscardNonCommittedEntries() OVERRIDE; + virtual NavigationEntry* GetPendingEntry() const OVERRIDE; + virtual int GetPendingEntryIndex() const OVERRIDE; + virtual NavigationEntry* GetTransientEntry() const OVERRIDE; + virtual void SetTransientEntry(NavigationEntry* entry) OVERRIDE; + virtual void LoadURL(const GURL& url, + const Referrer& referrer, + PageTransition type, + const std::string& extra_headers) OVERRIDE; + virtual void LoadURLWithParams(const LoadURLParams& params) OVERRIDE; + virtual void LoadIfNecessary() OVERRIDE; + virtual bool CanGoBack() const OVERRIDE; + virtual bool CanGoForward() const OVERRIDE; + virtual bool CanGoToOffset(int offset) const OVERRIDE; + virtual void GoBack() OVERRIDE; + virtual void GoForward() OVERRIDE; + virtual void GoToIndex(int index) OVERRIDE; + virtual void GoToOffset(int offset) OVERRIDE; + virtual bool RemoveEntryAtIndex(int index) OVERRIDE; + virtual const SessionStorageNamespaceMap& + GetSessionStorageNamespaceMap() const OVERRIDE; + virtual SessionStorageNamespace* + GetDefaultSessionStorageNamespace() OVERRIDE; + virtual void SetMaxRestoredPageID(int32 max_id) OVERRIDE; + virtual int32 GetMaxRestoredPageID() const OVERRIDE; + virtual bool NeedsReload() const OVERRIDE; + virtual void SetNeedsReload() OVERRIDE; + virtual void CancelPendingReload() OVERRIDE; + virtual void ContinuePendingReload() OVERRIDE; + virtual bool IsInitialNavigation() const OVERRIDE; + virtual void Reload(bool check_for_repost) OVERRIDE; + virtual void ReloadIgnoringCache(bool check_for_repost) OVERRIDE; + virtual void ReloadOriginalRequestURL(bool check_for_repost) OVERRIDE; + virtual void NotifyEntryChanged(const NavigationEntry* entry, + int index) OVERRIDE; + virtual void CopyStateFrom( + const NavigationController& source) OVERRIDE; + virtual void CopyStateFromAndPrune( + NavigationController* source) OVERRIDE; + virtual bool CanPruneAllButVisible() OVERRIDE; + virtual void PruneAllButVisible() OVERRIDE; + virtual void ClearAllScreenshots() OVERRIDE; + + // The session storage namespace that all child RenderViews belonging to + // |instance| should use. + SessionStorageNamespace* GetSessionStorageNamespace( + SiteInstance* instance); + + // Returns the index of the specified entry, or -1 if entry is not contained + // in this NavigationController. + int GetIndexOfEntry(const NavigationEntryImpl* entry) const; + + // Return the index of the entry with the corresponding instance and page_id, + // or -1 if not found. + int GetEntryIndexWithPageID(SiteInstance* instance, + int32 page_id) const; + + // Return the entry with the corresponding instance and page_id, or NULL if + // not found. + NavigationEntryImpl* GetEntryWithPageID( + SiteInstance* instance, + int32 page_id) const; + + NavigationControllerDelegate* delegate() const { + return delegate_; + } + + // For use by WebContentsImpl ------------------------------------------------ + + // Allow renderer-initiated navigations to create a pending entry when the + // provisional load starts. + void SetPendingEntry(content::NavigationEntryImpl* entry); + + // 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, + 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 automatically 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 could + // 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 { + return IsURLInPageNavigation(url, false, NAVIGATION_TYPE_UNKNOWN); + } + + // The situation is made murkier by history.replaceState(), which could + // provide the same URL as part of an in-page navigation, not a reload. So + // we need this form which lets the (untrustworthy) renderer resolve the + // ambiguity, but only when the URLs are equal. This should be safe since the + // origin isn't changing. + bool IsURLInPageNavigation( + const GURL& url, + bool renderer_says_in_page, + NavigationType navigation_type) const; + + // Sets the SessionStorageNamespace for the given |partition_id|. This is + // used during initialization of a new NavigationController to allow + // pre-population of the SessionStorageNamespace objects. Session restore, + // prerendering, and the implementaion of window.open() are the primary users + // of this API. + // + // Calling this function when a SessionStorageNamespace has already been + // associated with a |partition_id| will CHECK() fail. + void SetSessionStorageNamespace( + const std::string& partition_id, + SessionStorageNamespace* session_storage_namespace); + + // 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(); + + void SetGetTimestampCallbackForTest( + const base::Callback<base::Time()>& get_timestamp_callback); + + // Takes a screenshot of the page at the current state. + void TakeScreenshot(); + + // Sets the screenshot manager for this NavigationControllerImpl. The + // controller takes ownership of the screenshot manager and destroys it when + // a new screenshot-manager is set, or when the controller is destroyed. + // Setting a NULL manager recreates the default screenshot manager and uses + // that. + void SetScreenshotManager(WebContentsScreenshotManager* manager); + + // Discards only the pending entry. + void DiscardPendingEntry(); + + private: + friend class RestoreHelper; + + FRIEND_TEST_ALL_PREFIXES(NavigationControllerTest, + PurgeScreenshot); + FRIEND_TEST_ALL_PREFIXES(TimeSmoother, Basic); + FRIEND_TEST_ALL_PREFIXES(TimeSmoother, SingleDuplicate); + FRIEND_TEST_ALL_PREFIXES(TimeSmoother, ManyDuplicates); + FRIEND_TEST_ALL_PREFIXES(TimeSmoother, ClockBackwardsJump); + + // Helper class to smooth out runs of duplicate timestamps while still + // allowing time to jump backwards. + class CONTENT_EXPORT TimeSmoother { + public: + // Returns |t| with possibly some time added on. + base::Time GetSmoothedTime(base::Time t); + + private: + // |low_water_mark_| is the first time in a sequence of adjusted + // times and |high_water_mark_| is the last. + base::Time low_water_mark_; + base::Time high_water_mark_; + }; + + // Classifies the given renderer navigation (see the NavigationType enum). + 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(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 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(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(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. + void FinishRestore(int selected_index, RestoreType type); + + // 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(NavigationEntryImpl* entry, bool replace); + + // Removes the entry at |index|, as long as it is not the current entry. + void RemoveEntryAtIndexInternal(int index); + + // Discards both the pending and transient entries. + void DiscardNonCommittedEntriesInternal(); + + // Discards only the transient entry. + void DiscardTransientEntry(); + + // If we have the maximum number of entries, remove the oldest one in + // preparation to add another. + void PruneOldestEntryIfFull(); + + // Removes all entries except the last committed entry. If there is a new + // pending navigation it is preserved. In contrast to PruneAllButVisible() + // this does not update the session history of the RenderView. Callers + // must ensure that |CanPruneAllButVisible| returns true before calling this. + void PruneAllButVisibleInternal(); + + // 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); + + // Returns the navigation index that differs from the current entry by the + // specified |offset|. The index returned is not guaranteed to be valid. + int GetIndexForOffset(int offset) const; + + // --------------------------------------------------------------------------- + + // The user browser context associated with this controller. + BrowserContext* browser_context_; + + // List of NavigationEntry for this tab + typedef std::vector<linked_ptr<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. + 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 delegate associated with the controller. Possibly NULL during + // setup. + NavigationControllerDelegate* delegate_; + + // 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_; + + // Whether this is the initial navigation. + // Becomes false when initial navigation commits. + bool is_initial_navigation_; + + // Used to find the appropriate SessionStorageNamespace for the storage + // partition of a NavigationEntry. + // + // A NavigationController may contain NavigationEntries that correspond to + // different StoragePartitions. Even though they are part of the same + // NavigationController, only entries in the same StoragePartition may + // share session storage state with one another. + SessionStorageNamespaceMap session_storage_namespace_map_; + + // 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_; + + // Used to get timestamps for newly-created navigation entries. + base::Callback<base::Time()> get_timestamp_callback_; + + // Used to smooth out timestamps from |get_timestamp_callback_|. + // Without this, whenever there is a run of redirects or + // code-generated navigations, those navigations may occur within + // the timer resolution, leading to things sometimes showing up in + // the wrong order in the history view. + TimeSmoother time_smoother_; + + scoped_ptr<WebContentsScreenshotManager> screenshot_manager_; + + DISALLOW_COPY_AND_ASSIGN(NavigationControllerImpl); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_NAVIGATION_CONTROLLER_IMPL_H_ diff --git a/content/browser/frame_host/navigation_controller_impl_unittest.cc b/content/browser/frame_host/navigation_controller_impl_unittest.cc new file mode 100644 index 0000000..9056f85 --- /dev/null +++ b/content/browser/frame_host/navigation_controller_impl_unittest.cc @@ -0,0 +1,3844 @@ +// Copyright 2013 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/basictypes.h" +#include "base/bind.h" +#include "base/file_util.h" +#include "base/memory/scoped_ptr.h" +#include "base/path_service.h" +#include "base/stl_util.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "content/browser/frame_host/navigation_controller_impl.h" +#include "content/browser/frame_host/navigation_entry_impl.h" +#include "content/browser/frame_host/web_contents_screenshot_manager.h" +#include "content/browser/renderer_host/test_render_view_host.h" +#include "content/browser/site_instance_impl.h" +#include "content/browser/web_contents/web_contents_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/render_view_host.h" +#include "content/public/browser/web_contents_delegate.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/common/page_state.h" +#include "content/public/common/url_constants.h" +#include "content/public/test/mock_render_process_host.h" +#include "content/public/test/test_notification_tracker.h" +#include "content/public/test/test_utils.h" +#include "content/test/test_web_contents.h" +#include "net/base/net_util.h" +#include "skia/ext/platform_canvas.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::Time; + +namespace { + +// Creates an image with a 1x1 SkBitmap of the specified |color|. +gfx::Image CreateImage(SkColor color) { + SkBitmap bitmap; + bitmap.setConfig(SkBitmap::kARGB_8888_Config, 1, 1); + bitmap.allocPixels(); + bitmap.eraseColor(color); + return gfx::Image::CreateFrom1xBitmap(bitmap); +} + +// Returns true if images |a| and |b| have the same pixel data. +bool DoImagesMatch(const gfx::Image& a, const gfx::Image& b) { + // Assume that if the 1x bitmaps match, the images match. + SkBitmap a_bitmap = a.AsBitmap(); + SkBitmap b_bitmap = b.AsBitmap(); + + if (a_bitmap.width() != b_bitmap.width() || + a_bitmap.height() != b_bitmap.height()) { + return false; + } + SkAutoLockPixels a_bitmap_lock(a_bitmap); + SkAutoLockPixels b_bitmap_lock(b_bitmap); + return memcmp(a_bitmap.getPixels(), + b_bitmap.getPixels(), + a_bitmap.getSize()) == 0; +} + +class MockScreenshotManager : public content::WebContentsScreenshotManager { + public: + explicit MockScreenshotManager(content::NavigationControllerImpl* owner) + : content::WebContentsScreenshotManager(owner), + encoding_screenshot_in_progress_(false) { + } + + virtual ~MockScreenshotManager() { + } + + void TakeScreenshotFor(content::NavigationEntryImpl* entry) { + SkBitmap bitmap; + bitmap.setConfig(SkBitmap::kARGB_8888_Config, 1, 1); + bitmap.allocPixels(); + bitmap.eraseRGB(0, 0, 0); + encoding_screenshot_in_progress_ = true; + OnScreenshotTaken(entry->GetUniqueID(), true, bitmap); + WaitUntilScreenshotIsReady(); + } + + int GetScreenshotCount() { + return content::WebContentsScreenshotManager::GetScreenshotCount(); + } + + void WaitUntilScreenshotIsReady() { + if (!encoding_screenshot_in_progress_) + return; + message_loop_runner_ = new content::MessageLoopRunner; + message_loop_runner_->Run(); + } + + private: + // Overridden from content::WebContentsScreenshotManager: + virtual void TakeScreenshotImpl( + content::RenderViewHost* host, + content::NavigationEntryImpl* entry) OVERRIDE { + } + + virtual void OnScreenshotSet(content::NavigationEntryImpl* entry) OVERRIDE { + encoding_screenshot_in_progress_ = false; + WebContentsScreenshotManager::OnScreenshotSet(entry); + if (message_loop_runner_.get()) + message_loop_runner_->Quit(); + } + + scoped_refptr<content::MessageLoopRunner> message_loop_runner_; + bool encoding_screenshot_in_progress_; + + DISALLOW_COPY_AND_ASSIGN(MockScreenshotManager); +}; + +} // namespace + +namespace content { + +// TimeSmoother tests ---------------------------------------------------------- + +// With no duplicates, GetSmoothedTime should be the identity +// function. +TEST(TimeSmoother, Basic) { + NavigationControllerImpl::TimeSmoother smoother; + for (int64 i = 1; i < 1000; ++i) { + base::Time t = base::Time::FromInternalValue(i); + EXPECT_EQ(t, smoother.GetSmoothedTime(t)); + } +} + +// With a single duplicate and timestamps thereafter increasing by one +// microsecond, the smoothed time should always be one behind. +TEST(TimeSmoother, SingleDuplicate) { + NavigationControllerImpl::TimeSmoother smoother; + base::Time t = base::Time::FromInternalValue(1); + EXPECT_EQ(t, smoother.GetSmoothedTime(t)); + for (int64 i = 1; i < 1000; ++i) { + base::Time expected_t = base::Time::FromInternalValue(i + 1); + t = base::Time::FromInternalValue(i); + EXPECT_EQ(expected_t, smoother.GetSmoothedTime(t)); + } +} + +// With k duplicates and timestamps thereafter increasing by one +// microsecond, the smoothed time should always be k behind. +TEST(TimeSmoother, ManyDuplicates) { + const int64 kNumDuplicates = 100; + NavigationControllerImpl::TimeSmoother smoother; + base::Time t = base::Time::FromInternalValue(1); + for (int64 i = 0; i < kNumDuplicates; ++i) { + base::Time expected_t = base::Time::FromInternalValue(i + 1); + EXPECT_EQ(expected_t, smoother.GetSmoothedTime(t)); + } + for (int64 i = 1; i < 1000; ++i) { + base::Time expected_t = + base::Time::FromInternalValue(i + kNumDuplicates); + t = base::Time::FromInternalValue(i); + EXPECT_EQ(expected_t, smoother.GetSmoothedTime(t)); + } +} + +// If the clock jumps far back enough after a run of duplicates, it +// should immediately jump to that value. +TEST(TimeSmoother, ClockBackwardsJump) { + const int64 kNumDuplicates = 100; + NavigationControllerImpl::TimeSmoother smoother; + base::Time t = base::Time::FromInternalValue(1000); + for (int64 i = 0; i < kNumDuplicates; ++i) { + base::Time expected_t = base::Time::FromInternalValue(i + 1000); + EXPECT_EQ(expected_t, smoother.GetSmoothedTime(t)); + } + t = base::Time::FromInternalValue(500); + EXPECT_EQ(t, smoother.GetSmoothedTime(t)); +} + +// NavigationControllerTest ---------------------------------------------------- + +class NavigationControllerTest + : public RenderViewHostImplTestHarness, + public WebContentsObserver { + public: + NavigationControllerTest() : navigation_entry_committed_counter_(0) { + } + + virtual void SetUp() OVERRIDE { + RenderViewHostImplTestHarness::SetUp(); + WebContents* web_contents = RenderViewHostImplTestHarness::web_contents(); + ASSERT_TRUE(web_contents); // The WebContents should be created by now. + WebContentsObserver::Observe(web_contents); + } + + // WebContentsObserver: + virtual void NavigateToPendingEntry( + const GURL& url, + NavigationController::ReloadType reload_type) OVERRIDE { + navigated_url_ = url; + } + + virtual void NavigationEntryCommitted( + const LoadCommittedDetails& load_details) OVERRIDE { + navigation_entry_committed_counter_++; + } + + const GURL& navigated_url() const { + return navigated_url_; + } + + NavigationControllerImpl& controller_impl() { + return static_cast<NavigationControllerImpl&>(controller()); + } + + protected: + GURL navigated_url_; + size_t navigation_entry_committed_counter_; +}; + +void RegisterForAllNavNotifications(TestNotificationTracker* tracker, + NavigationController* controller) { + tracker->ListenFor(NOTIFICATION_NAV_LIST_PRUNED, + Source<NavigationController>(controller)); + tracker->ListenFor(NOTIFICATION_NAV_ENTRY_CHANGED, + Source<NavigationController>(controller)); +} + +SiteInstance* GetSiteInstanceFromEntry(NavigationEntry* entry) { + return NavigationEntryImpl::FromNavigationEntry(entry)->site_instance(); +} + +class TestWebContentsDelegate : public 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) OVERRIDE { + 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.GetVisibleEntry()); + 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, GoToOffset) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const int kNumUrls = 5; + std::vector<GURL> urls(kNumUrls); + for (int i = 0; i < kNumUrls; ++i) { + urls[i] = GURL(base::StringPrintf("http://www.a.com/%d", i)); + } + + test_rvh()->SendNavigate(0, urls[0]); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(urls[0], controller.GetVisibleEntry()->GetVirtualURL()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_FALSE(controller.CanGoToOffset(1)); + + for (int i = 1; i <= 4; ++i) { + test_rvh()->SendNavigate(i, urls[i]); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(urls[i], controller.GetVisibleEntry()->GetVirtualURL()); + EXPECT_TRUE(controller.CanGoToOffset(-i)); + EXPECT_FALSE(controller.CanGoToOffset(-(i + 1))); + EXPECT_FALSE(controller.CanGoToOffset(1)); + } + + // We have loaded 5 pages, and are currently at the last-loaded page. + int url_index = 4; + + enum Tests { + GO_TO_MIDDLE_PAGE = -2, + GO_FORWARDS = 1, + GO_BACKWARDS = -1, + GO_TO_BEGINNING = -2, + GO_TO_END = 4, + NUM_TESTS = 5, + }; + + const int test_offsets[NUM_TESTS] = { + GO_TO_MIDDLE_PAGE, + GO_FORWARDS, + GO_BACKWARDS, + GO_TO_BEGINNING, + GO_TO_END + }; + + for (int test = 0; test < NUM_TESTS; ++test) { + int offset = test_offsets[test]; + controller.GoToOffset(offset); + url_index += offset; + // Check that the GoToOffset will land on the expected page. + EXPECT_EQ(urls[url_index], controller.GetPendingEntry()->GetVirtualURL()); + test_rvh()->SendNavigate(url_index, urls[url_index]); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + // Check that we can go to any valid offset into the history. + for (size_t j = 0; j < urls.size(); ++j) + EXPECT_TRUE(controller.CanGoToOffset(j - url_index)); + // Check that we can't go beyond the beginning or end of the history. + EXPECT_FALSE(controller.CanGoToOffset(-(url_index + 1))); + EXPECT_FALSE(controller.CanGoToOffset(urls.size() - url_index)); + } +} + +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, Referrer(), 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()); + ASSERT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntry(), controller.GetVisibleEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), -1); + + // Neither the timestamp nor the status code should have been set yet. + EXPECT_TRUE(controller.GetPendingEntry()->GetTimestamp().is_null()); + EXPECT_EQ(0, controller.GetPendingEntry()->GetHttpStatusCode()); + + // We should have gotten no notifications from the preceeding checks. + EXPECT_EQ(0U, notifications.size()); + + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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()); + ASSERT_TRUE(controller.GetVisibleEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 0); + EXPECT_EQ(0, NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->bindings()); + + // The timestamp should have been set. + EXPECT_FALSE(controller.GetVisibleEntry()->GetTimestamp().is_null()); + + // Load another... + controller.LoadURL(url2, Referrer(), 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()); + ASSERT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntry(), controller.GetVisibleEntry()); + // TODO(darin): maybe this should really be true? + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 0); + + EXPECT_TRUE(controller.GetPendingEntry()->GetTimestamp().is_null()); + + // 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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()); + ASSERT_TRUE(controller.GetVisibleEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 1); + + EXPECT_FALSE(controller.GetVisibleEntry()->GetTimestamp().is_null()); +} + +namespace { + +base::Time GetFixedTime(base::Time time) { + return time; +} + +} // namespace + +TEST_F(NavigationControllerTest, LoadURLSameTime) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Set the clock to always return a timestamp of 1. + controller.SetGetTimestampCallbackForTest( + base::Bind(&GetFixedTime, base::Time::FromInternalValue(1))); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + controller.LoadURL(url1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Load another... + controller.LoadURL(url2, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + + // Simulate the beforeunload ack for the cross-site transition, and then the + // commit. + test_rvh()->SendShouldCloseACK(true); + test_rvh()->SendNavigate(1, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // The two loads should now be committed. + ASSERT_EQ(controller.GetEntryCount(), 2); + + // Timestamps should be distinct despite the clock returning the + // same value. + EXPECT_EQ(1u, + controller.GetEntryAtIndex(0)->GetTimestamp().ToInternalValue()); + EXPECT_EQ(2u, + controller.GetEntryAtIndex(1)->GetTimestamp().ToInternalValue()); +} + +void CheckNavigationEntryMatchLoadParams( + NavigationController::LoadURLParams& load_params, + NavigationEntryImpl* entry) { + EXPECT_EQ(load_params.url, entry->GetURL()); + EXPECT_EQ(load_params.referrer.url, entry->GetReferrer().url); + EXPECT_EQ(load_params.referrer.policy, entry->GetReferrer().policy); + EXPECT_EQ(load_params.transition_type, entry->GetTransitionType()); + EXPECT_EQ(load_params.extra_headers, entry->extra_headers()); + + EXPECT_EQ(load_params.is_renderer_initiated, entry->is_renderer_initiated()); + EXPECT_EQ(load_params.base_url_for_data_url, entry->GetBaseURLForDataURL()); + if (!load_params.virtual_url_for_data_url.is_empty()) { + EXPECT_EQ(load_params.virtual_url_for_data_url, entry->GetVirtualURL()); + } + if (NavigationController::UA_OVERRIDE_INHERIT != + load_params.override_user_agent) { + bool should_override = (NavigationController::UA_OVERRIDE_TRUE == + load_params.override_user_agent); + EXPECT_EQ(should_override, entry->GetIsOverridingUserAgent()); + } + EXPECT_EQ(load_params.browser_initiated_post_data, + entry->GetBrowserInitiatedPostData()); + EXPECT_EQ(load_params.transferred_global_request_id, + entry->transferred_global_request_id()); +} + +TEST_F(NavigationControllerTest, LoadURLWithParams) { + NavigationControllerImpl& controller = controller_impl(); + + NavigationController::LoadURLParams load_params(GURL("http://foo")); + load_params.referrer = + Referrer(GURL("http://referrer"), WebKit::WebReferrerPolicyDefault); + load_params.transition_type = PAGE_TRANSITION_GENERATED; + load_params.extra_headers = "content-type: text/plain"; + load_params.load_type = NavigationController::LOAD_TYPE_DEFAULT; + load_params.is_renderer_initiated = true; + load_params.override_user_agent = NavigationController::UA_OVERRIDE_TRUE; + load_params.transferred_global_request_id = GlobalRequestID(2, 3); + + controller.LoadURLWithParams(load_params); + NavigationEntryImpl* entry = + NavigationEntryImpl::FromNavigationEntry( + controller.GetPendingEntry()); + + // The timestamp should not have been set yet. + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->GetTimestamp().is_null()); + + CheckNavigationEntryMatchLoadParams(load_params, entry); +} + +TEST_F(NavigationControllerTest, LoadURLWithExtraParams_Data) { + NavigationControllerImpl& controller = controller_impl(); + + NavigationController::LoadURLParams load_params( + GURL("data:text/html,dataurl")); + load_params.load_type = NavigationController::LOAD_TYPE_DATA; + load_params.base_url_for_data_url = GURL("http://foo"); + load_params.virtual_url_for_data_url = GURL(kAboutBlankURL); + load_params.override_user_agent = NavigationController::UA_OVERRIDE_FALSE; + + controller.LoadURLWithParams(load_params); + NavigationEntryImpl* entry = + NavigationEntryImpl::FromNavigationEntry( + controller.GetPendingEntry()); + + CheckNavigationEntryMatchLoadParams(load_params, entry); +} + +TEST_F(NavigationControllerTest, LoadURLWithExtraParams_HttpPost) { + NavigationControllerImpl& controller = controller_impl(); + + NavigationController::LoadURLParams load_params(GURL("https://posturl")); + load_params.transition_type = PAGE_TRANSITION_TYPED; + load_params.load_type = + NavigationController::LOAD_TYPE_BROWSER_INITIATED_HTTP_POST; + load_params.override_user_agent = NavigationController::UA_OVERRIDE_TRUE; + + + const unsigned char* raw_data = + reinterpret_cast<const unsigned char*>("d\n\0a2"); + const int length = 5; + std::vector<unsigned char> post_data_vector(raw_data, raw_data+length); + scoped_refptr<base::RefCountedBytes> data = + base::RefCountedBytes::TakeVector(&post_data_vector); + load_params.browser_initiated_post_data = data.get(); + + controller.LoadURLWithParams(load_params); + NavigationEntryImpl* entry = + NavigationEntryImpl::FromNavigationEntry( + controller.GetPendingEntry()); + + CheckNavigationEntryMatchLoadParams(load_params, entry); +} + +// 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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + ASSERT_TRUE(controller.GetVisibleEntry()); + const base::Time timestamp = controller.GetVisibleEntry()->GetTimestamp(); + EXPECT_FALSE(timestamp.is_null()); + + controller.LoadURL(url1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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()); + ASSERT_TRUE(controller.GetVisibleEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + + // The timestamp should have been updated. + // + // TODO(akalin): Change this EXPECT_GE (and other similar ones) to + // EXPECT_GT once we guarantee that timestamps are unique. + EXPECT_GE(controller.GetVisibleEntry()->GetTimestamp(), timestamp); +} + +// 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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + ASSERT_TRUE(controller.GetVisibleEntry()); + const base::Time timestamp = controller.GetVisibleEntry()->GetTimestamp(); + EXPECT_FALSE(timestamp.is_null()); + + controller.LoadURL(url2, Referrer(), 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()); + ASSERT_TRUE(controller.GetVisibleEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + + // Timestamp should not have changed. + EXPECT_EQ(timestamp, controller.GetVisibleEntry()->GetTimestamp()); +} + +// 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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetVisibleEntry()->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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Make a pending entry to somewhere new. + const GURL kExistingURL2("http://bee"); + controller.LoadURL( + kExistingURL2, Referrer(), 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetVisibleEntry()->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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + const GURL kExistingURL2("http://foo/bee"); + controller.LoadURL( + kExistingURL2, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, kExistingURL2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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"); + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(2, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetVisibleEntry()->GetURL()); +} + +// Tests navigating to a new URL when there is a pending back/forward +// navigation to a cross-process, privileged URL. This will happen if the user +// hits back, but before that commits, they navigate somewhere new. +TEST_F(NavigationControllerTest, LoadURL_PrivilegedPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make some history, starting with a privileged URL. + const GURL kExistingURL1("http://privileged"); + controller.LoadURL( + kExistingURL1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + // Pretend it has bindings so we can tell if we incorrectly copy it. + test_rvh()->AllowBindings(2); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Navigate cross-process to a second URL. + const GURL kExistingURL2("http://foo/eh"); + controller.LoadURL( + kExistingURL2, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendShouldCloseACK(true); + TestRenderViewHost* foo_rvh = static_cast<TestRenderViewHost*>( + contents()->GetPendingRenderViewHost()); + foo_rvh->SendNavigate(1, kExistingURL2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Now make a pending back/forward navigation to a privileged entry. + // The zeroth entry should be pending. + controller.GoBack(); + foo_rvh->SendShouldCloseACK(true); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(0, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(2, NavigationEntryImpl::FromNavigationEntry( + controller.GetPendingEntry())->bindings()); + + // Before that commits, do a new navigation. + const GURL kNewURL("http://foo/bee"); + LoadCommittedDetails details; + foo_rvh->SendNavigate(3, kNewURL); + + // There should no longer be any pending entry, and the third navigation we + // just made should be committed. + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(2, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetVisibleEntry()->GetURL()); + EXPECT_EQ(0, NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->bindings()); +} + +// 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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + const GURL kExistingURL2("http://foo/bee"); + controller.LoadURL( + kExistingURL2, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, kExistingURL2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Now make a pending new navigation. + const GURL kNewURL("http://foo/see"); + controller.LoadURL( + kNewURL, Referrer(), 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kExistingURL1, controller.GetVisibleEntry()->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(kAboutBlankURL); + + // Now make a pending new navigation. + const GURL kNewURL("http://eh"); + controller.LoadURL( + kNewURL, Referrer(), 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. +// We do not want to clear the pending entry, so that the user doesn't +// lose a typed URL. (See http://crbug.com/9682.) +TEST_F(NavigationControllerTest, LoadURL_AbortDoesntCancelPending) { + 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()); + + // Start with a pending new navigation. + const GURL kNewURL("http://eh"); + controller.LoadURL( + kNewURL, Referrer(), 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 not clear the pending entry or notify of a navigation state + // change, so that we keep displaying kNewURL (until the user clears it). + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(1, delegate->navigation_state_change_count()); + NavigationEntry* pending_entry = controller.GetPendingEntry(); + + // Ensure that a reload keeps the same pending entry. + controller.Reload(true); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(pending_entry, controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + + contents()->SetDelegate(NULL); +} + +// Tests that the pending URL is not visible during a renderer-initiated +// redirect and abort. See http://crbug.com/83031. +TEST_F(NavigationControllerTest, LoadURL_RedirectAbortDoesntShowPendingURL) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make an existing committed entry. + const GURL kExistingURL("http://foo/eh"); + controller.LoadURL(kExistingURL, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Set a WebContentsDelegate to listen for state changes. + scoped_ptr<TestWebContentsDelegate> delegate(new TestWebContentsDelegate()); + EXPECT_FALSE(contents()->GetDelegate()); + contents()->SetDelegate(delegate.get()); + + // Now make a pending new navigation, initiated by the renderer. + const GURL kNewURL("http://foo/bee"); + NavigationController::LoadURLParams load_url_params(kNewURL); + load_url_params.transition_type = PAGE_TRANSITION_TYPED; + load_url_params.is_renderer_initiated = true; + controller.LoadURLWithParams(load_url_params); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(0, delegate->navigation_state_change_count()); + + // The visible entry should be the last committed URL, not the pending one. + EXPECT_EQ(kExistingURL, controller.GetVisibleEntry()->GetURL()); + + // Now the navigation redirects. + const GURL kRedirectURL("http://foo/see"); + test_rvh()->OnMessageReceived( + ViewHostMsg_DidRedirectProvisionalLoad(0, // routing_id + -1, // pending page_id + 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)); + + // Because the pending entry is renderer initiated and not visible, we + // clear it when it fails. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(0, delegate->navigation_state_change_count()); + + // The visible entry should be the last committed URL, not the pending one, + // so that no spoof is possible. + EXPECT_EQ(kExistingURL, controller.GetVisibleEntry()->GetURL()); + + contents()->SetDelegate(NULL); +} + +// Ensure that NavigationEntries track which bindings their RenderViewHost had +// at the time they committed. http://crbug.com/173672. +TEST_F(NavigationControllerTest, LoadURL_WithBindings) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + // Navigate to a first, unprivileged URL. + controller.LoadURL(url1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(NavigationEntryImpl::kInvalidBindings, + NavigationEntryImpl::FromNavigationEntry( + controller.GetPendingEntry())->bindings()); + + // Commit. + TestRenderViewHost* orig_rvh = static_cast<TestRenderViewHost*>(test_rvh()); + orig_rvh->SendNavigate(0, url1); + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(0, NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->bindings()); + + // Manually increase the number of active views in the SiteInstance + // that orig_rvh belongs to, to prevent it from being destroyed when + // it gets swapped out, so that we can reuse orig_rvh when the + // controller goes back. + static_cast<SiteInstanceImpl*>(orig_rvh->GetSiteInstance())-> + increment_active_view_count(); + + // Navigate to a second URL, simulate the beforeunload ack for the cross-site + // transition, and set bindings on the pending RenderViewHost to simulate a + // privileged url. + controller.LoadURL(url2, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + orig_rvh->SendShouldCloseACK(true); + contents()->GetPendingRenderViewHost()->AllowBindings(1); + static_cast<TestRenderViewHost*>( + contents()->GetPendingRenderViewHost())->SendNavigate(1, url2); + + // The second load should be committed, and bindings should be remembered. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_EQ(1, NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->bindings()); + + // Going back, the first entry should still appear unprivileged. + controller.GoBack(); + orig_rvh->SendNavigate(0, url1); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(0, NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->bindings()); +} + +TEST_F(NavigationControllerTest, Reload) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + + controller.LoadURL(url1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + ASSERT_TRUE(controller.GetVisibleEntry()); + controller.GetVisibleEntry()->SetTitle(ASCIIToUTF16("Title")); + controller.Reload(true); + EXPECT_EQ(0U, notifications.size()); + + const base::Time timestamp = controller.GetVisibleEntry()->GetTimestamp(); + EXPECT_FALSE(timestamp.is_null()); + + // 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.GetVisibleEntry()->GetTitle().empty()); + + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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()); + + // The timestamp should have been updated. + ASSERT_TRUE(controller.GetVisibleEntry()); + EXPECT_GE(controller.GetVisibleEntry()->GetTimestamp(), timestamp); +} + +// 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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + controller.Reload(true); + EXPECT_EQ(0U, notifications.size()); + + test_rvh()->SendNavigate(1, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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()); +} + +#if !defined(OS_ANDROID) // http://crbug.com/157428 +TEST_F(NavigationControllerTest, ReloadOriginalRequestURL) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL original_url("http://foo1"); + const GURL final_url("http://foo2"); + + // Load up the original URL, but get redirected. + controller.LoadURL( + original_url, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigateWithOriginalRequestURL(0, final_url, original_url); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // The NavigationEntry should save both the original URL and the final + // redirected URL. + EXPECT_EQ( + original_url, controller.GetVisibleEntry()->GetOriginalRequestURL()); + EXPECT_EQ(final_url, controller.GetVisibleEntry()->GetURL()); + + // Reload using the original URL. + controller.GetVisibleEntry()->SetTitle(ASCIIToUTF16("Title")); + controller.ReloadOriginalRequestURL(false); + EXPECT_EQ(0U, notifications.size()); + + // The reload is pending. The request should point to the original URL. + EXPECT_EQ(original_url, navigated_url()); + 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.GetVisibleEntry()->GetTitle().empty()); + + // Send that the navigation has proceeded; say it got redirected again. + test_rvh()->SendNavigate(0, final_url); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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()); +} + +#endif // !defined(OS_ANDROID) + +// Test that certain non-persisted NavigationEntryImpl values get reset after +// commit. +TEST_F(NavigationControllerTest, ResetEntryValuesAfterCommit) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + controller.LoadURL(url1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + + // Set up some sample values. + const unsigned char* raw_data = + reinterpret_cast<const unsigned char*>("post\n\n\0data"); + const int length = 11; + std::vector<unsigned char> post_data_vector(raw_data, raw_data+length); + scoped_refptr<base::RefCountedBytes> post_data = + base::RefCountedBytes::TakeVector(&post_data_vector); + GlobalRequestID transfer_id(3, 4); + std::vector<GURL> redirects; + redirects.push_back(GURL("http://foo2")); + + // Set non-persisted values on the pending entry. + NavigationEntryImpl* pending_entry = + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry()); + pending_entry->SetBrowserInitiatedPostData(post_data.get()); + pending_entry->set_is_renderer_initiated(true); + pending_entry->set_transferred_global_request_id(transfer_id); + pending_entry->set_should_replace_entry(true); + pending_entry->set_redirect_chain(redirects); + pending_entry->set_should_clear_history_list(true); + EXPECT_EQ(post_data.get(), pending_entry->GetBrowserInitiatedPostData()); + EXPECT_TRUE(pending_entry->is_renderer_initiated()); + EXPECT_EQ(transfer_id, pending_entry->transferred_global_request_id()); + EXPECT_TRUE(pending_entry->should_replace_entry()); + EXPECT_EQ(1U, pending_entry->redirect_chain().size()); + EXPECT_TRUE(pending_entry->should_clear_history_list()); + + test_rvh()->SendNavigate(0, url1); + + // Certain values that are only used for pending entries get reset after + // commit. + NavigationEntryImpl* committed_entry = + NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry()); + EXPECT_FALSE(committed_entry->GetBrowserInitiatedPostData()); + EXPECT_FALSE(committed_entry->is_renderer_initiated()); + EXPECT_EQ(GlobalRequestID(-1, -1), + committed_entry->transferred_global_request_id()); + EXPECT_FALSE(committed_entry->should_replace_entry()); + EXPECT_EQ(0U, committed_entry->redirect_chain().size()); + EXPECT_FALSE(committed_entry->should_clear_history_list()); +} + +// 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + const GURL url2("http://foo2"); + test_rvh()->SendNavigate(1, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + 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_FALSE(controller.CanGoToOffset(-1)); + EXPECT_TRUE(controller.CanGoForward()); + EXPECT_TRUE(controller.CanGoToOffset(1)); + EXPECT_FALSE(controller.CanGoToOffset(2)); // Cannot go foward 2 steps. + + // Timestamp for entry 1 should be on or after that of entry 0. + EXPECT_FALSE(controller.GetEntryAtIndex(0)->GetTimestamp().is_null()); + EXPECT_GE(controller.GetEntryAtIndex(1)->GetTimestamp(), + controller.GetEntryAtIndex(0)->GetTimestamp()); + + test_rvh()->SendNavigate(0, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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_FALSE(controller.CanGoToOffset(-1)); + EXPECT_TRUE(controller.CanGoForward()); + EXPECT_TRUE(controller.CanGoToOffset(1)); + EXPECT_FALSE(controller.CanGoToOffset(2)); // Cannot go foward 2 steps. + + // Timestamp for entry 0 should be on or after that of entry 1 + // (since we went back to it). + EXPECT_GE(controller.GetEntryAtIndex(0)->GetTimestamp(), + controller.GetEntryAtIndex(1)->GetTimestamp()); +} + +// 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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + controller.LoadURL(url2, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // controller.LoadURL(kUrl2, PAGE_TRANSITION_TYPED); + test_rvh()->SendNavigate(1, kUrl2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Now start a new pending navigation and go back before it commits. + controller.LoadURL(kUrl3, Referrer(), 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + test_rvh()->SendNavigate(1, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + controller.GoBack(); + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + 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_TRUE(controller.CanGoToOffset(-1)); + EXPECT_FALSE(controller.CanGoToOffset(-2)); // Cannot go back 2 steps. + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_FALSE(controller.CanGoToOffset(1)); + + // Timestamp for entry 0 should be on or after that of entry 1 + // (since we went back to it). + EXPECT_FALSE(controller.GetEntryAtIndex(0)->GetTimestamp().is_null()); + EXPECT_GE(controller.GetEntryAtIndex(0)->GetTimestamp(), + controller.GetEntryAtIndex(1)->GetTimestamp()); + + test_rvh()->SendNavigate(1, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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_TRUE(controller.CanGoToOffset(-1)); + EXPECT_FALSE(controller.CanGoToOffset(-2)); // Cannot go back 2 steps. + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_FALSE(controller.CanGoToOffset(1)); + + // Timestamp for entry 1 should be on or after that of entry 0 + // (since we went forward to it). + EXPECT_GE(controller.GetEntryAtIndex(1)->GetTimestamp(), + controller.GetEntryAtIndex(0)->GetTimestamp()); +} + +// 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + test_rvh()->SendNavigate(1, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + controller.GoBack(); + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_TRUE(notifications.Check1AndReset(NOTIFICATION_NAV_LIST_PRUNED)); + + 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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Second request + controller.LoadURL(url1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = 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.page_state = PageState::CreateFromURL(url2); + + LoadCommittedDetails details; + + EXPECT_EQ(0U, notifications.size()); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + EXPECT_TRUE(details.type == 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.GetVisibleEntry()->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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + controller.GetVisibleEntry()->SetHasPostData(true); + + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Second request + controller.LoadURL(url1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = 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.page_state = PageState::CreateFromURL(url2); + + LoadCommittedDetails details; + + EXPECT_EQ(0U, notifications.size()); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + EXPECT_TRUE(details.type == 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.GetVisibleEntry()->GetURL()); + EXPECT_FALSE(controller.GetVisibleEntry()->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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = 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.page_state = PageState::CreateFromURL(url2); + + LoadCommittedDetails details; + + EXPECT_EQ(0U, notifications.size()); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + EXPECT_TRUE(details.type == 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.GetVisibleEntry()->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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + const GURL url2("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url2; + params.transition = PAGE_TRANSITION_MANUAL_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url2); + + LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + 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 = PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url); + + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + const GURL url2("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url2); + + // Navigating should do nothing. + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // First manual subframe navigation. + const GURL url2("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url2; + params.transition = PAGE_TRANSITION_MANUAL_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url2); + + // This should generate a new entry. + LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(1, controller.GetCurrentEntryIndex()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + + // Go back one more. + controller.GoBack(); + params.url = url1; + params.page_id = 0; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(0, controller.GetCurrentEntryIndex()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); +} + +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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + test_rvh()->SendNavigate(1, url2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Ensure main page navigation to same url respects the was_within_same_page + // hint provided in the params. + ViewHostMsg_FrameNavigate_Params self_params; + self_params.page_id = 0; + self_params.url = url1; + self_params.transition = PAGE_TRANSITION_LINK; + self_params.should_update_history = false; + self_params.gesture = NavigationGestureUser; + self_params.is_post = false; + self_params.page_state = PageState::CreateFromURL(url1); + self_params.was_within_same_page = true; + + LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(self_params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_TRUE(details.is_in_page); + EXPECT_TRUE(details.did_replace_entry); + EXPECT_EQ(1, controller.GetEntryCount()); + + // Fragment navigation to a new page_id. + const GURL url2("http://foo#a"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url2; + params.transition = PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url2); + params.was_within_same_page = true; + + // This should generate a new entry. + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_TRUE(details.is_in_page); + EXPECT_EQ(2, controller.GetEntryCount()); + EXPECT_EQ(0, controller.GetCurrentEntryIndex()); + EXPECT_EQ(back_params.url, controller.GetVisibleEntry()->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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_TRUE(details.is_in_page); + EXPECT_EQ(2, controller.GetEntryCount()); + EXPECT_EQ(1, controller.GetCurrentEntryIndex()); + EXPECT_EQ(forward_params.url, + controller.GetVisibleEntry()->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.GetVisibleEntry()->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; + navigation_entry_committed_counter_ = 0; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_FALSE(details.is_in_page); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(2, controller.GetCurrentEntryIndex()); +} + +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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // First navigation. + const GURL url2("http://foo#a"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; // Same page_id + params.url = url2; + params.transition = PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url2); + + // This should NOT generate a new entry, nor prune the list. + LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + } + + // Navigate to a new page. + { + const GURL url("http://foo2/"); + test_rvh()->SendNavigate(1, url); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + } + + // 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 = PAGE_TRANSITION_LINK; + params.redirects.push_back(url); + params.should_update_history = true; + params.gesture = NavigationGestureUnknown; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url); + + // This should NOT generate a new entry, nor prune the list. + LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + 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 = 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.page_state = PageState::CreateFromURL(url); + + // This SHOULD generate a new entry. + LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + 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_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_EQ(url, controller.GetVisibleEntry()->GetURL()); + } +} + +// NotificationObserver implementation used in verifying we've received the +// NOTIFICATION_NAV_LIST_PRUNED method. +class PrunedListener : public NotificationObserver { + public: + explicit PrunedListener(NavigationControllerImpl* controller) + : notification_count_(0) { + registrar_.Add(this, NOTIFICATION_NAV_LIST_PRUNED, + Source<NavigationController>(controller)); + } + + virtual void Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) OVERRIDE { + if (type == NOTIFICATION_NAV_LIST_PRUNED) { + notification_count_++; + details_ = *(Details<PrunedDetails>(details).ptr()); + } + } + + // Number of times NAV_LIST_PRUNED has been observed. + int notification_count_; + + // Details from the last NAV_LIST_PRUNED. + PrunedDetails details_; + + private: + 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(base::StringPrintf("http://www.a.com/%d", url_index)); + controller.LoadURL( + url, Referrer(), 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(base::StringPrintf("http://www.a.com/%d", url_index)); + controller.LoadURL( + url, Referrer(), 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(base::StringPrintf("http:////www.a.com/%d", url_index)); + controller.LoadURL( + url, Referrer(), 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, Referrer(), PAGE_TRANSITION_RELOAD, false, std::string(), + browser_context()); + entry->SetPageID(0); + entry->SetTitle(ASCIIToUTF16("Title")); + entry->SetPageState(PageState::CreateFromEncodedData("state")); + const base::Time timestamp = base::Time::Now(); + entry->SetTimestamp(timestamp); + entries.push_back(entry); + scoped_ptr<WebContentsImpl> our_contents(static_cast<WebContentsImpl*>( + WebContents::Create(WebContents::CreateParams(browser_context())))); + NavigationControllerImpl& our_controller = our_contents->GetController(); + our_controller.Restore( + 0, + NavigationController::RESTORE_LAST_SESSION_EXITED_CLEANLY, + &entries); + ASSERT_EQ(0u, entries.size()); + + // Before navigating to the restored entry, it should have a restore_type + // and no SiteInstance. + ASSERT_EQ(1, our_controller.GetEntryCount()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_LAST_SESSION_EXITED_CLEANLY, + 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()); + + // Timestamp should remain the same before the navigation finishes. + EXPECT_EQ(timestamp, our_controller.GetEntryAtIndex(0)->GetTimestamp()); + + // Say we navigated to that entry. + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url; + params.transition = PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url); + 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()-> + GetSiteURL()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); + + // Timestamp should have been updated. + EXPECT_GE(our_controller.GetEntryAtIndex(0)->GetTimestamp(), timestamp); +} + +// 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, Referrer(), PAGE_TRANSITION_RELOAD, false, std::string(), + browser_context()); + entry->SetPageID(0); + entry->SetTitle(ASCIIToUTF16("Title")); + entry->SetPageState(PageState::CreateFromEncodedData("state")); + entries.push_back(entry); + scoped_ptr<WebContentsImpl> our_contents(static_cast<WebContentsImpl*>( + WebContents::Create(WebContents::CreateParams(browser_context())))); + NavigationControllerImpl& our_controller = our_contents->GetController(); + our_controller.Restore( + 0, NavigationController::RESTORE_LAST_SESSION_EXITED_CLEANLY, &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_EXITED_CLEANLY, + 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 = PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url); + 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()-> + GetSiteURL()); + 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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + + // Now navigate somewhere with an interstitial. + const GURL url2("http://bar"); + controller.LoadURL( + url1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + set_page_type(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(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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + controller.LoadURL( + url2, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url2); + controller.LoadURL( + url3, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(2, url3); + controller.LoadURL( + url4, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(3, url4); + controller.LoadURL( + url5, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(4, url5); + + // Try to remove the last entry. Will fail because it is the current entry. + EXPECT_FALSE(controller.RemoveEntryAtIndex(controller.GetEntryCount() - 1)); + EXPECT_EQ(5, controller.GetEntryCount()); + EXPECT_EQ(4, controller.GetLastCommittedEntryIndex()); + + // Go back, but don't commit yet. Check that we can't delete the current + // and pending entries. + controller.GoBack(); + EXPECT_FALSE(controller.RemoveEntryAtIndex(controller.GetEntryCount() - 1)); + EXPECT_FALSE(controller.RemoveEntryAtIndex(controller.GetEntryCount() - 2)); + + // Now commit and delete the last entry. + test_rvh()->SendNavigate(3, url4); + EXPECT_TRUE(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. + EXPECT_TRUE(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 url3_ref("http://foo/3#bar"); + const GURL url4("http://foo/4"); + const GURL transient_url("http://foo/transient"); + + controller.LoadURL( + url0, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url0); + controller.LoadURL( + url1, Referrer(), 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.SetTransientEntry(transient_entry); + + // We should not have received any notifications. + EXPECT_EQ(0U, notifications.size()); + + // Check our state. + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->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, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(2, url2); + + // We should have navigated, transient entry should be gone. + EXPECT_EQ(url2, controller.GetVisibleEntry()->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.SetTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(3, url3); + // Transient entry should be gone. + EXPECT_EQ(url3, controller.GetVisibleEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 4); + + // Initiate a navigation, add a transient then commit navigation. + controller.LoadURL( + url4, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.SetTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(4, url4); + EXPECT_EQ(url4, controller.GetVisibleEntry()->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.SetTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->GetURL()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + controller.GoBack(); + // Transient entry should be gone. + EXPECT_EQ(url4, controller.GetVisibleEntry()->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.SetTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->GetURL()); + controller.GoToIndex(1); + // The navigation should have been initiated, transient entry should be gone. + EXPECT_FALSE(controller.GetTransientEntry()); + EXPECT_EQ(url1, controller.GetPendingEntry()->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.SetTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->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.GetPendingEntry()->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.SetTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->GetURL()); + EXPECT_TRUE(controller.CanGoForward()); + controller.GoForward(); + // We should have navigated, transient entry should be gone. + EXPECT_FALSE(controller.GetTransientEntry()); + EXPECT_EQ(url3, controller.GetPendingEntry()->GetURL()); + EXPECT_EQ(url2, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(3, url3); + EXPECT_EQ(url3, controller.GetVisibleEntry()->GetURL()); + + // Add a transient and do an in-page navigation, replacing the current entry. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.SetTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(3, url3_ref); + // Transient entry should be gone. + EXPECT_FALSE(controller.GetTransientEntry()); + EXPECT_EQ(url3_ref, 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_ref); + EXPECT_EQ(controller.GetEntryAtIndex(4)->GetURL(), url4); +} + +// Test that Reload initiates a new navigation to a transient entry's URL. +TEST_F(NavigationControllerTest, ReloadTransient) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url0("http://foo/0"); + const GURL url1("http://foo/1"); + const GURL transient_url("http://foo/transient"); + + // Load |url0|, and start a pending navigation to |url1|. + controller.LoadURL( + url0, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url0); + controller.LoadURL( + url1, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + + // A transient entry is added, interrupting the navigation. + NavigationEntryImpl* transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.SetTransientEntry(transient_entry); + EXPECT_TRUE(controller.GetTransientEntry()); + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->GetURL()); + + // The page is reloaded, which should remove the pending entry for |url1| and + // the transient entry for |transient_url|, and start a navigation to + // |transient_url|. + controller.Reload(true); + EXPECT_FALSE(controller.GetTransientEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(transient_url, controller.GetVisibleEntry()->GetURL()); + ASSERT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url0); + + // Load of |transient_url| completes. + test_rvh()->SendNavigate(1, transient_url); + ASSERT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url0); + EXPECT_EQ(controller.GetEntryAtIndex(1)->GetURL(), transient_url); +} + +// Ensure that renderer initiated pending entries get replaced, so that we +// don't show a stale virtual URL when a navigation commits. +// See http://crbug.com/266922. +TEST_F(NavigationControllerTest, RendererInitiatedPendingEntries) { + NavigationControllerImpl& controller = controller_impl(); + + const GURL url1("nonexistent:12121"); + const GURL url1_fixed("http://nonexistent:12121/"); + const GURL url2("http://foo"); + + // We create pending entries for renderer-initiated navigations so that we + // can show them in new tabs when it is safe. + contents()->DidStartProvisionalLoadForFrame( + test_rvh(), 1, -1, true, url1); + + // Simulate what happens if a BrowserURLHandler rewrites the URL, causing + // the virtual URL to differ from the URL. + controller.GetPendingEntry()->SetURL(url1_fixed); + controller.GetPendingEntry()->SetVirtualURL(url1); + + EXPECT_EQ(url1_fixed, controller.GetPendingEntry()->GetURL()); + EXPECT_EQ(url1, controller.GetPendingEntry()->GetVirtualURL()); + EXPECT_TRUE( + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + is_renderer_initiated()); + + // If the user clicks another link, we should replace the pending entry. + contents()->DidStartProvisionalLoadForFrame( + test_rvh(), 1, -1, true, url2); + EXPECT_EQ(url2, controller.GetPendingEntry()->GetURL()); + EXPECT_EQ(url2, controller.GetPendingEntry()->GetVirtualURL()); + + // Once it commits, the URL and virtual URL should reflect the actual page. + test_rvh()->SendNavigate(0, url2); + EXPECT_EQ(url2, controller.GetLastCommittedEntry()->GetURL()); + EXPECT_EQ(url2, controller.GetLastCommittedEntry()->GetVirtualURL()); + + // We should not replace the pending entry for an error URL. + contents()->DidStartProvisionalLoadForFrame( + test_rvh(), 1, -1, true, url1); + EXPECT_EQ(url1, controller.GetPendingEntry()->GetURL()); + contents()->DidStartProvisionalLoadForFrame( + test_rvh(), 1, -1, true, GURL(kUnreachableWebDataURL)); + EXPECT_EQ(url1, controller.GetPendingEntry()->GetURL()); + + // We should remember if the pending entry will replace the current one. + // http://crbug.com/308444. + contents()->DidStartProvisionalLoadForFrame( + test_rvh(), 1, -1, true, url1); + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + set_should_replace_entry(true); + contents()->DidStartProvisionalLoadForFrame( + test_rvh(), 1, -1, true, url2); + EXPECT_TRUE( + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + should_replace_entry()); + test_rvh()->SendNavigate(0, url2); + EXPECT_EQ(url2, controller.GetLastCommittedEntry()->GetURL()); +} + +// 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 pending and visible entries + // should update before commit. + controller.LoadURL(url0, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(url0, controller.GetPendingEntry()->GetURL()); + EXPECT_EQ(url0, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(0, url0); + + // For link clicks (renderer-initiated navigations), the pending entry should + // update before commit but the visible should not. + NavigationController::LoadURLParams load_url_params(url1); + load_url_params.is_renderer_initiated = true; + controller.LoadURLWithParams(load_url_params); + EXPECT_EQ(url0, controller.GetVisibleEntry()->GetURL()); + EXPECT_EQ(url1, controller.GetPendingEntry()->GetURL()); + EXPECT_TRUE( + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + is_renderer_initiated()); + + // After commit, both visible should be updated, there should be no pending + // entry, and we should no longer treat the entry as renderer-initiated. + test_rvh()->SendNavigate(1, url1); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE( + NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->is_renderer_initiated()); + + notifications.Reset(); +} + +// Tests that the URLs for renderer-initiated navigations in new tabs are +// displayed to the user before commit, as long as the initial about:blank +// page has not been modified. If so, we must revert to showing about:blank. +// See http://crbug.com/9682. +TEST_F(NavigationControllerTest, ShowRendererURLInNewTabUntilModified) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url("http://foo"); + + // For renderer-initiated navigations in new tabs (with no committed entries), + // we show the pending entry's URL as long as the about:blank page is not + // modified. + NavigationController::LoadURLParams load_url_params(url); + load_url_params.transition_type = PAGE_TRANSITION_LINK; + load_url_params.is_renderer_initiated = true; + controller.LoadURLWithParams(load_url_params); + EXPECT_EQ(url, controller.GetVisibleEntry()->GetURL()); + EXPECT_EQ(url, controller.GetPendingEntry()->GetURL()); + EXPECT_TRUE( + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + is_renderer_initiated()); + EXPECT_TRUE(controller.IsInitialNavigation()); + EXPECT_FALSE(test_rvh()->has_accessed_initial_document()); + + // There should be no title yet. + EXPECT_TRUE(contents()->GetTitle().empty()); + + // If something else modifies the contents of the about:blank page, then + // we must revert to showing about:blank to avoid a URL spoof. + test_rvh()->OnMessageReceived( + ViewHostMsg_DidAccessInitialDocument(0)); + EXPECT_TRUE(test_rvh()->has_accessed_initial_document()); + EXPECT_FALSE(controller.GetVisibleEntry()); + EXPECT_EQ(url, controller.GetPendingEntry()->GetURL()); + + notifications.Reset(); +} + +TEST_F(NavigationControllerTest, DontShowRendererURLInNewTabAfterCommit) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo/eh"); + const GURL url2("http://foo/bee"); + + // For renderer-initiated navigations in new tabs (with no committed entries), + // we show the pending entry's URL as long as the about:blank page is not + // modified. + NavigationController::LoadURLParams load_url_params(url1); + load_url_params.transition_type = PAGE_TRANSITION_LINK; + load_url_params.is_renderer_initiated = true; + controller.LoadURLWithParams(load_url_params); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + EXPECT_TRUE( + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + is_renderer_initiated()); + EXPECT_TRUE(controller.IsInitialNavigation()); + EXPECT_FALSE(test_rvh()->has_accessed_initial_document()); + + // Simulate a commit and then starting a new pending navigation. + test_rvh()->SendNavigate(0, url1); + NavigationController::LoadURLParams load_url2_params(url2); + load_url2_params.transition_type = PAGE_TRANSITION_LINK; + load_url2_params.is_renderer_initiated = true; + controller.LoadURLWithParams(load_url2_params); + + // We should not consider this an initial navigation, and thus should + // not show the pending URL. + EXPECT_FALSE(test_rvh()->has_accessed_initial_document()); + EXPECT_FALSE(controller.IsInitialNavigation()); + EXPECT_TRUE(controller.GetVisibleEntry()); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + + 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)); + + // Going to the same url again will be considered in-page + // if the renderer says it is even if the navigation type isn't IN_PAGE. + EXPECT_TRUE(controller.IsURLInPageNavigation(url_with_ref, true, + NAVIGATION_TYPE_UNKNOWN)); + + // Going back to the non ref url will be considered in-page if the navigation + // type is IN_PAGE. + EXPECT_TRUE(controller.IsURLInPageNavigation(url, true, + NAVIGATION_TYPE_IN_PAGE)); +} + +// 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 = PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.page_state = PageState::CreateFromURL(subframe); + 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 WebContentsImpl 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"); + const string16 title(ASCIIToUTF16("Title")); + + NavigateAndCommit(url1); + controller.GetVisibleEntry()->SetTitle(title); + 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()); + + // Ensure that the pending URL and its title are visible. + EXPECT_EQ(url1, clone->GetController().GetVisibleEntry()->GetURL()); + EXPECT_EQ(title, clone->GetTitle()); +} + +// Make sure that reloading a cloned tab doesn't change its pending entry index. +// See http://crbug.com/234491. +TEST_F(NavigationControllerTest, CloneAndReload) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const string16 title(ASCIIToUTF16("Title")); + + NavigateAndCommit(url1); + controller.GetVisibleEntry()->SetTitle(title); + NavigateAndCommit(url2); + + scoped_ptr<WebContents> clone(controller.GetWebContents()->Clone()); + clone->GetController().LoadIfNecessary(); + + ASSERT_EQ(2, clone->GetController().GetEntryCount()); + EXPECT_EQ(1, clone->GetController().GetPendingEntryIndex()); + + clone->GetController().Reload(true); + EXPECT_EQ(1, clone->GetController().GetPendingEntryIndex()); +} + +// Make sure that cloning a WebContentsImpl 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(PAGE_TYPE_INTERSTITIAL); + controller.SetTransientEntry(interstitial_entry); + + scoped_ptr<WebContents> clone(controller.GetWebContents()->Clone()); + + ASSERT_EQ(2, clone->GetController().GetEntryCount()); +} + +// Test requesting and triggering a lazy reload. +TEST_F(NavigationControllerTest, LazyReload) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url("http://foo"); + NavigateAndCommit(url); + ASSERT_FALSE(controller.NeedsReload()); + + // Request a reload to happen when the controller becomes active (e.g. after + // the renderer gets killed in background on Android). + controller.SetNeedsReload(); + ASSERT_TRUE(controller.NeedsReload()); + + // Set the controller as active, triggering the requested reload. + controller.SetActive(true); + ASSERT_FALSE(controller.NeedsReload()); +} + +// 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, Referrer(), 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 = PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.page_state = PageState::CreateFromURL(url1_sub); + 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.GetVisibleEntry()->GetURL()); +} + +// Test CopyStateFrom with 2 urls, the first selected and nothing in the target. +TEST_F(NavigationControllerTest, CopyStateFrom) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + controller.GoBack(); + contents()->CommitPendingNavigation(); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = other_contents->GetController(); + other_controller.CopyStateFrom(controller); + + // other_controller should now contain 2 urls. + ASSERT_EQ(2, other_controller.GetEntryCount()); + // We should be looking at the first one. + ASSERT_EQ(0, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(url2, other_controller.GetEntryAtIndex(1)->GetURL()); + // This is a different site than url1, so the IDs start again at 0. + EXPECT_EQ(0, other_controller.GetEntryAtIndex(1)->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)); + + // Ensure the SessionStorageNamespaceMaps are the same size and have + // the same partitons loaded. + // + // TODO(ajwong): We should load a url from a different partition earlier + // to make sure this map has more than one entry. + const SessionStorageNamespaceMap& session_storage_namespace_map = + controller.GetSessionStorageNamespaceMap(); + const SessionStorageNamespaceMap& other_session_storage_namespace_map = + other_controller.GetSessionStorageNamespaceMap(); + EXPECT_EQ(session_storage_namespace_map.size(), + other_session_storage_namespace_map.size()); + for (SessionStorageNamespaceMap::const_iterator it = + session_storage_namespace_map.begin(); + it != session_storage_namespace_map.end(); + ++it) { + SessionStorageNamespaceMap::const_iterator other = + other_session_storage_namespace_map.find(it->first); + EXPECT_TRUE(other != other_session_storage_namespace_map.end()); + } +} + +// 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->GetController(); + 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 1 entry 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(); + contents()->CommitPendingNavigation(); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = other_contents->GetController(); + other_contents->NavigateAndCommit(url3); + other_contents->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)), 1, + other_controller.GetEntryAtIndex(0)->GetPageID()); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain: url1, url3 + + ASSERT_EQ(2, other_controller.GetEntryCount()); + ASSERT_EQ(1, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(url3, other_controller.GetEntryAtIndex(1)->GetURL()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(1)->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(1)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance1)); +} + +// Test CopyStateFromAndPrune with 2 urls, the last selected and 2 entries 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"); + const GURL url4("http://foo4"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = other_contents->GetController(); + other_contents->NavigateAndCommit(url3); + other_contents->NavigateAndCommit(url4); + other_contents->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(1)), 2, + other_controller.GetEntryAtIndex(0)->GetPageID()); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain: url1, url2, url4 + + 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(url4, other_controller.GetEntryAtIndex(2)->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(2)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance1)); +} + +// Test CopyStateFromAndPrune with 2 urls, 2 entries in the target, with +// not the last entry selected in the target. +TEST_F(NavigationControllerTest, CopyStateFromAndPruneNotLast) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const GURL url3("http://foo3"); + const GURL url4("http://foo4"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = other_contents->GetController(); + other_contents->NavigateAndCommit(url3); + other_contents->NavigateAndCommit(url4); + other_controller.GoBack(); + other_contents->CommitPendingNavigation(); + other_contents->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)), 2, + other_controller.GetEntryAtIndex(0)->GetPageID()); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain: url1, url2, 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()); + + // 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(2)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance1)); +} + +// Test CopyStateFromAndPrune with 2 urls, the first selected and 1 entry plus +// a pending entry in the target. +TEST_F(NavigationControllerTest, CopyStateFromAndPruneTargetPending) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const GURL url3("http://foo3"); + const GURL url4("http://foo4"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + controller.GoBack(); + contents()->CommitPendingNavigation(); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = other_contents->GetController(); + other_contents->NavigateAndCommit(url3); + other_controller.LoadURL( + url4, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + other_contents->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)), 1, + other_controller.GetEntryAtIndex(0)->GetPageID()); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain url1, url3, and a pending entry + // for url4. + + ASSERT_EQ(2, other_controller.GetEntryCount()); + EXPECT_EQ(1, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(url3, other_controller.GetEntryAtIndex(1)->GetURL()); + + // And there should be a pending entry for url4. + ASSERT_TRUE(other_controller.GetPendingEntry()); + EXPECT_EQ(url4, 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)); +} + +// Test CopyStateFromAndPrune with 1 url in the source, 1 entry and a pending +// client redirect entry (with the same page ID) in the target. This used to +// crash because the last committed entry would be pruned but max_page_id +// remembered the page ID (http://crbug.com/234809). +TEST_F(NavigationControllerTest, CopyStateFromAndPruneTargetPending2) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2a("http://foo2/a"); + const GURL url2b("http://foo2/b"); + + NavigateAndCommit(url1); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = other_contents->GetController(); + other_contents->NavigateAndCommit(url2a); + // Simulate a client redirect, which has the same page ID as entry 2a. + other_controller.LoadURL( + url2b, Referrer(), PAGE_TRANSITION_LINK, std::string()); + other_controller.GetPendingEntry()->SetPageID( + other_controller.GetLastCommittedEntry()->GetPageID()); + + other_contents->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)), 1, + other_controller.GetEntryAtIndex(0)->GetPageID()); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain url1, url2a, and a pending entry + // for url2b. + + ASSERT_EQ(2, other_controller.GetEntryCount()); + EXPECT_EQ(1, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(url2a, other_controller.GetEntryAtIndex(1)->GetURL()); + + // And there should be a pending entry for url4. + ASSERT_TRUE(other_controller.GetPendingEntry()); + EXPECT_EQ(url2b, other_controller.GetPendingEntry()->GetURL()); + + // Let the pending entry commit. + other_contents->CommitPendingNavigation(); + + // 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(1)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance1)); +} + +// Test CopyStateFromAndPrune with 2 urls, a back navigation pending in the +// source, and 1 entry in the target. The back pending entry should be ignored. +TEST_F(NavigationControllerTest, CopyStateFromAndPruneSourcePending) { + 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->GetController(); + 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: url1, url2, 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(2)->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(2)); + 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->GetController(); + 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 PruneAllButVisible for the only entry. +TEST_F(NavigationControllerTest, PruneAllButVisibleForSingle) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + NavigateAndCommit(url1); + + contents()->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(controller.GetEntryAtIndex(0)), 0, + controller.GetEntryAtIndex(0)->GetPageID()); + + controller.PruneAllButVisible(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url1); +} + +// Test call to PruneAllButVisible for first entry. +TEST_F(NavigationControllerTest, PruneAllButVisibleForFirst) { + 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(); + + contents()->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(controller.GetEntryAtIndex(0)), 0, + controller.GetEntryAtIndex(0)->GetPageID()); + + controller.PruneAllButVisible(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url1); +} + +// Test call to PruneAllButVisible for intermediate entry. +TEST_F(NavigationControllerTest, PruneAllButVisibleForIntermediate) { + 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(); + + contents()->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(controller.GetEntryAtIndex(1)), 0, + controller.GetEntryAtIndex(1)->GetPageID()); + + controller.PruneAllButVisible(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url2); +} + +// Test call to PruneAllButVisible for a pending entry that is not yet in the +// list of entries. +TEST_F(NavigationControllerTest, PruneAllButVisibleForPendingNotInList) { + 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); + + // Create a pending entry that is not in the entry list. + controller.LoadURL( + url3, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(2, controller.GetEntryCount()); + + contents()->ExpectSetHistoryLengthAndPrune( + NULL, 0, controller.GetPendingEntry()->GetPageID()); + controller.PruneAllButVisible(); + + // We should only have the last committed and pending entries at this point, + // and the pending entry should still not be in the entry list. + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(1, controller.GetEntryCount()); + + // Try to commit the pending entry. + test_rvh()->SendNavigate(2, url3); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(2, controller.GetEntryCount()); + EXPECT_EQ(url3, controller.GetEntryAtIndex(1)->GetURL()); +} + +// 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()); +} + +TEST_F(NavigationControllerTest, IsInitialNavigation) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Initial state. + EXPECT_TRUE(controller.IsInitialNavigation()); + + // After commit, it stays false. + const GURL url1("http://foo1"); + test_rvh()->SendNavigate(0, url1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + EXPECT_FALSE(controller.IsInitialNavigation()); + + // After starting a new navigation, it stays false. + const GURL url2("http://foo2"); + controller.LoadURL( + url2, Referrer(), PAGE_TRANSITION_TYPED, std::string()); +} + +// Check that the favicon is not reused across a client redirect. +// (crbug.com/28515) +TEST_F(NavigationControllerTest, ClearFaviconOnRedirect) { + const GURL kPageWithFavicon("http://withfavicon.html"); + const GURL kPageWithoutFavicon("http://withoutfavicon.html"); + const GURL kIconURL("http://withfavicon.ico"); + const gfx::Image kDefaultFavicon = FaviconStatus().image; + + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + test_rvh()->SendNavigate(0, kPageWithFavicon); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + NavigationEntry* entry = controller.GetLastCommittedEntry(); + EXPECT_TRUE(entry); + EXPECT_EQ(kPageWithFavicon, entry->GetURL()); + + // Simulate Chromium having set the favicon for |kPageWithFavicon|. + content::FaviconStatus& favicon_status = entry->GetFavicon(); + favicon_status.image = CreateImage(SK_ColorWHITE); + favicon_status.url = kIconURL; + favicon_status.valid = true; + EXPECT_FALSE(DoImagesMatch(kDefaultFavicon, entry->GetFavicon().image)); + + test_rvh()->SendNavigateWithTransition( + 0, // same page ID. + kPageWithoutFavicon, + PAGE_TRANSITION_CLIENT_REDIRECT); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + entry = controller.GetLastCommittedEntry(); + EXPECT_TRUE(entry); + EXPECT_EQ(kPageWithoutFavicon, entry->GetURL()); + + EXPECT_TRUE(DoImagesMatch(kDefaultFavicon, entry->GetFavicon().image)); +} + +// Check that the favicon is not cleared for NavigationEntries which were +// previously navigated to. +TEST_F(NavigationControllerTest, BackNavigationDoesNotClearFavicon) { + const GURL kUrl1("http://www.a.com/1"); + const GURL kUrl2("http://www.a.com/2"); + const GURL kIconURL("http://www.a.com/1/favicon.ico"); + + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + test_rvh()->SendNavigate(0, kUrl1); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Simulate Chromium having set the favicon for |kUrl1|. + gfx::Image favicon_image = CreateImage(SK_ColorWHITE); + content::NavigationEntry* entry = controller.GetLastCommittedEntry(); + EXPECT_TRUE(entry); + content::FaviconStatus& favicon_status = entry->GetFavicon(); + favicon_status.image = favicon_image; + favicon_status.url = kIconURL; + favicon_status.valid = true; + + // Navigate to another page and go back to the original page. + test_rvh()->SendNavigate(1, kUrl2); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + test_rvh()->SendNavigateWithTransition( + 0, + kUrl1, + PAGE_TRANSITION_FORWARD_BACK); + EXPECT_EQ(1U, navigation_entry_committed_counter_); + navigation_entry_committed_counter_ = 0; + + // Verify that the favicon for the page at |kUrl1| was not cleared. + entry = controller.GetEntryAtIndex(0); + EXPECT_TRUE(entry); + EXPECT_EQ(kUrl1, entry->GetURL()); + EXPECT_TRUE(DoImagesMatch(favicon_image, entry->GetFavicon().image)); +} + +// The test crashes on android: http://crbug.com/170449 +#if defined(OS_ANDROID) +#define MAYBE_PurgeScreenshot DISABLED_PurgeScreenshot +#else +#define MAYBE_PurgeScreenshot PurgeScreenshot +#endif +// Tests that screenshot are purged correctly. +TEST_F(NavigationControllerTest, MAYBE_PurgeScreenshot) { + NavigationControllerImpl& controller = controller_impl(); + + NavigationEntryImpl* entry; + + // Navigate enough times to make sure that some screenshots are purged. + for (int i = 0; i < 12; ++i) { + const GURL url(base::StringPrintf("http://foo%d/", i)); + NavigateAndCommit(url); + EXPECT_EQ(i, controller.GetCurrentEntryIndex()); + } + + MockScreenshotManager* screenshot_manager = + new MockScreenshotManager(&controller); + controller.SetScreenshotManager(screenshot_manager); + for (int i = 0; i < controller.GetEntryCount(); ++i) { + entry = NavigationEntryImpl::FromNavigationEntry( + controller.GetEntryAtIndex(i)); + screenshot_manager->TakeScreenshotFor(entry); + EXPECT_TRUE(entry->screenshot().get()); + } + + NavigateAndCommit(GURL("https://foo/")); + EXPECT_EQ(13, controller.GetEntryCount()); + entry = NavigationEntryImpl::FromNavigationEntry( + controller.GetEntryAtIndex(11)); + screenshot_manager->TakeScreenshotFor(entry); + + for (int i = 0; i < 2; ++i) { + entry = NavigationEntryImpl::FromNavigationEntry( + controller.GetEntryAtIndex(i)); + EXPECT_FALSE(entry->screenshot().get()) << "Screenshot " << i + << " not purged"; + } + + for (int i = 2; i < controller.GetEntryCount() - 1; ++i) { + entry = NavigationEntryImpl::FromNavigationEntry( + controller.GetEntryAtIndex(i)); + EXPECT_TRUE(entry->screenshot().get()) << "Screenshot not found for " << i; + } + + // Navigate to index 5 and then try to assign screenshot to all entries. + controller.GoToIndex(5); + contents()->CommitPendingNavigation(); + EXPECT_EQ(5, controller.GetCurrentEntryIndex()); + for (int i = 0; i < controller.GetEntryCount() - 1; ++i) { + entry = NavigationEntryImpl::FromNavigationEntry( + controller.GetEntryAtIndex(i)); + screenshot_manager->TakeScreenshotFor(entry); + } + + for (int i = 10; i <= 12; ++i) { + entry = NavigationEntryImpl::FromNavigationEntry( + controller.GetEntryAtIndex(i)); + EXPECT_FALSE(entry->screenshot().get()) << "Screenshot " << i + << " not purged"; + screenshot_manager->TakeScreenshotFor(entry); + } + + // Navigate to index 7 and assign screenshot to all entries. + controller.GoToIndex(7); + contents()->CommitPendingNavigation(); + EXPECT_EQ(7, controller.GetCurrentEntryIndex()); + for (int i = 0; i < controller.GetEntryCount() - 1; ++i) { + entry = NavigationEntryImpl::FromNavigationEntry( + controller.GetEntryAtIndex(i)); + screenshot_manager->TakeScreenshotFor(entry); + } + + for (int i = 0; i < 2; ++i) { + entry = NavigationEntryImpl::FromNavigationEntry( + controller.GetEntryAtIndex(i)); + EXPECT_FALSE(entry->screenshot().get()) << "Screenshot " << i + << " not purged"; + } + + // Clear all screenshots. + EXPECT_EQ(13, controller.GetEntryCount()); + EXPECT_EQ(10, screenshot_manager->GetScreenshotCount()); + controller.ClearAllScreenshots(); + EXPECT_EQ(0, screenshot_manager->GetScreenshotCount()); + for (int i = 0; i < controller.GetEntryCount(); ++i) { + entry = NavigationEntryImpl::FromNavigationEntry( + controller.GetEntryAtIndex(i)); + EXPECT_FALSE(entry->screenshot().get()) << "Screenshot " << i + << " not cleared"; + } +} + +// Test that the navigation controller clears its session history when a +// navigation commits with the clear history list flag set. +TEST_F(NavigationControllerTest, ClearHistoryList) { + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const GURL url3("http://foo3"); + const GURL url4("http://foo4"); + + NavigationControllerImpl& controller = controller_impl(); + + // Create a session history with three entries, second entry is active. + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + controller.GoBack(); + contents()->CommitPendingNavigation(); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(1, controller.GetCurrentEntryIndex()); + + // Create a new pending navigation, and indicate that the session history + // should be cleared. + NavigationController::LoadURLParams params(url4); + params.should_clear_history_list = true; + controller.LoadURLWithParams(params); + + // Verify that the pending entry correctly indicates that the session history + // should be cleared. + NavigationEntryImpl* entry = + NavigationEntryImpl::FromNavigationEntry( + controller.GetPendingEntry()); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->should_clear_history_list()); + + // Assume that the RV correctly cleared its history and commit the navigation. + static_cast<TestRenderViewHost*>(contents()->GetPendingRenderViewHost())-> + set_simulate_history_list_was_cleared(true); + contents()->CommitPendingNavigation(); + + // Verify that the NavigationController's session history was correctly + // cleared. + EXPECT_EQ(1, controller.GetEntryCount()); + EXPECT_EQ(0, controller.GetCurrentEntryIndex()); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(url4, controller.GetVisibleEntry()->GetURL()); +} + +} // namespace content diff --git a/content/browser/frame_host/navigation_entry_impl.cc b/content/browser/frame_host/navigation_entry_impl.cc new file mode 100644 index 0000000..58dbaf4 --- /dev/null +++ b/content/browser/frame_host/navigation_entry_impl.cc @@ -0,0 +1,342 @@ +// Copyright 2013 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/frame_host/navigation_entry_impl.h" + +#include "base/metrics/histogram.h" +#include "base/strings/string_util.h" +#include "base/strings/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/gfx/text_elider.h" + +// 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 { + +int NavigationEntryImpl::kInvalidBindings = -1; + +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), + bindings_(kInvalidBindings), + 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_overriding_user_agent_(false), + http_status_code_(0), + is_renderer_initiated_(false), + should_replace_entry_(false), + should_clear_history_list_(false), + can_load_local_resources_(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), + bindings_(kInvalidBindings), + 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_overriding_user_agent_(false), + http_status_code_(0), + is_renderer_initiated_(is_renderer_initiated), + should_replace_entry_(false), + should_clear_history_list_(false), + can_load_local_resources_(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::SetBaseURLForDataURL(const GURL& url) { + base_url_for_data_url_ = url; +} + +const GURL& NavigationEntryImpl::GetBaseURLForDataURL() const { + return base_url_for_data_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::SetPageState(const PageState& state) { + page_state_ = state; +} + +const PageState& NavigationEntryImpl::GetPageState() const { + return page_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; +} + +void NavigationEntryImpl::SetBindings(int bindings) { + // Ensure this is set to a valid value, and that it stays the same once set. + CHECK_NE(bindings, kInvalidBindings); + CHECK(bindings_ == kInvalidBindings || bindings_ == bindings); + bindings_ = bindings; +} + +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); + } + + gfx::ElideString(title, kMaxTitleChars, &cached_display_title_); + return cached_display_title_; +} + +bool NavigationEntryImpl::IsViewSourceMode() const { + return virtual_url_.SchemeIs(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_; +} + +void NavigationEntryImpl::SetBrowserInitiatedPostData( + const base::RefCountedMemory* data) { + browser_initiated_post_data_ = data; +} + +const base::RefCountedMemory* +NavigationEntryImpl::GetBrowserInitiatedPostData() const { + return browser_initiated_post_data_.get(); +} + + +const FaviconStatus& NavigationEntryImpl::GetFavicon() const { + return favicon_; +} + +FaviconStatus& NavigationEntryImpl::GetFavicon() { + return favicon_; +} + +const SSLStatus& NavigationEntryImpl::GetSSL() const { + return ssl_; +} + +SSLStatus& NavigationEntryImpl::GetSSL() { + return ssl_; +} + +void NavigationEntryImpl::SetOriginalRequestURL(const GURL& original_url) { + original_request_url_ = original_url; +} + +const GURL& NavigationEntryImpl::GetOriginalRequestURL() const { + return original_request_url_; +} + +void NavigationEntryImpl::SetIsOverridingUserAgent(bool override) { + is_overriding_user_agent_ = override; +} + +bool NavigationEntryImpl::GetIsOverridingUserAgent() const { + return is_overriding_user_agent_; +} + +void NavigationEntryImpl::SetTimestamp(base::Time timestamp) { + timestamp_ = timestamp; +} + +base::Time NavigationEntryImpl::GetTimestamp() const { + return timestamp_; +} + +void NavigationEntryImpl::SetHttpStatusCode(int http_status_code) { + http_status_code_ = http_status_code; +} + +int NavigationEntryImpl::GetHttpStatusCode() const { + return http_status_code_; +} + +void NavigationEntryImpl::SetCanLoadLocalResources(bool allow) { + can_load_local_resources_ = allow; +} + +bool NavigationEntryImpl::GetCanLoadLocalResources() const { + return can_load_local_resources_; +} + +void NavigationEntryImpl::SetFrameToNavigate(const std::string& frame_name) { + frame_to_navigate_ = frame_name; +} + +const std::string& NavigationEntryImpl::GetFrameToNavigate() const { + return frame_to_navigate_; +} + +void NavigationEntryImpl::SetExtraData(const std::string& key, + const string16& data) { + extra_data_[key] = data; +} + +bool NavigationEntryImpl::GetExtraData(const std::string& key, + string16* data) const { + std::map<std::string, string16>::const_iterator iter = extra_data_.find(key); + if (iter == extra_data_.end()) + return false; + *data = iter->second; + return true; +} + +void NavigationEntryImpl::ClearExtraData(const std::string& key) { + extra_data_.erase(key); +} + +void NavigationEntryImpl::ResetForCommit() { + // Any state that only matters when a navigation entry is pending should be + // cleared here. + SetBrowserInitiatedPostData(NULL); + set_is_renderer_initiated(false); + set_transferred_global_request_id(GlobalRequestID()); + set_should_replace_entry(false); + redirect_chain_.clear(); + set_should_clear_history_list(false); +} + +void NavigationEntryImpl::SetScreenshotPNGData( + scoped_refptr<base::RefCountedBytes> png_data) { + screenshot_ = png_data; + if (screenshot_.get()) + UMA_HISTOGRAM_MEMORY_KB("Overscroll.ScreenshotSize", screenshot_->size()); +} + +} // namespace content diff --git a/content/browser/frame_host/navigation_entry_impl.h b/content/browser/frame_host/navigation_entry_impl.h new file mode 100644 index 0000000..1149ae6 --- /dev/null +++ b/content/browser/frame_host/navigation_entry_impl.h @@ -0,0 +1,336 @@ +// Copyright 2013 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_FRAME_HOST_NAVIGATION_ENTRY_IMPL_H_ +#define CONTENT_BROWSER_FRAME_HOST_NAVIGATION_ENTRY_IMPL_H_ + +#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/page_state.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); + + // The value of bindings() before it is set during commit. + static int kInvalidBindings; + + 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 SetBaseURLForDataURL(const GURL& url) OVERRIDE; + virtual const GURL& GetBaseURLForDataURL() 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 SetPageState(const PageState& state) OVERRIDE; + virtual const PageState& GetPageState() 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 void SetBrowserInitiatedPostData( + const base::RefCountedMemory* data) OVERRIDE; + virtual const base::RefCountedMemory* + GetBrowserInitiatedPostData() const OVERRIDE; + virtual const FaviconStatus& GetFavicon() const OVERRIDE; + virtual FaviconStatus& GetFavicon() OVERRIDE; + virtual const SSLStatus& GetSSL() const OVERRIDE; + virtual SSLStatus& GetSSL() OVERRIDE; + virtual void SetOriginalRequestURL(const GURL& original_url) OVERRIDE; + virtual const GURL& GetOriginalRequestURL() const OVERRIDE; + virtual void SetIsOverridingUserAgent(bool override) OVERRIDE; + virtual bool GetIsOverridingUserAgent() const OVERRIDE; + virtual void SetTimestamp(base::Time timestamp) OVERRIDE; + virtual base::Time GetTimestamp() const OVERRIDE; + virtual void SetCanLoadLocalResources(bool allow) OVERRIDE; + virtual bool GetCanLoadLocalResources() const OVERRIDE; + virtual void SetFrameToNavigate(const std::string& frame_name) OVERRIDE; + virtual const std::string& GetFrameToNavigate() const OVERRIDE; + virtual void SetExtraData(const std::string& key, + const string16& data) OVERRIDE; + virtual bool GetExtraData(const std::string& key, + string16* data) const OVERRIDE; + virtual void ClearExtraData(const std::string& key) OVERRIDE; + virtual void SetHttpStatusCode(int http_status_code) OVERRIDE; + virtual int GetHttpStatusCode() const OVERRIDE; + + // Once a navigation entry is committed, we should no longer track several + // pieces of non-persisted state, as documented on the members below. + void ResetForCommit(); + + void set_unique_id(int unique_id) { + unique_id_ = unique_id; + } + + // The SiteInstance tells us how to share sub-processes. 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(); + } + + // Remember the set of bindings granted to this NavigationEntry at the time + // of commit, to ensure that we do not grant it additional bindings if we + // navigate back to it in the future. This can only be changed once. + void SetBindings(int bindings); + int bindings() const { + return bindings_; + } + + 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 { + // Restore from the previous session. + RESTORE_LAST_SESSION_EXITED_CLEANLY, + RESTORE_LAST_SESSION_CRASHED, + + // 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 needs to replace current entry. + // Resets to false after commit. + bool should_replace_entry() const { + return should_replace_entry_; + } + + void set_should_replace_entry(bool should_replace_entry) { + should_replace_entry_ = should_replace_entry; + } + + // Any redirects present in a pending entry when it is transferred from one + // process to another. Not valid after commit. + const std::vector<GURL>& redirect_chain() const { + return redirect_chain_; + } + + void set_redirect_chain(const std::vector<GURL>& redirect_chain) { + redirect_chain_ = redirect_chain; + } + + void SetScreenshotPNGData(scoped_refptr<base::RefCountedBytes> png_data); + const scoped_refptr<base::RefCountedBytes> screenshot() const { + return screenshot_; + } + + // Whether this (pending) navigation should clear the session history. Resets + // to false after commit. + bool should_clear_history_list() const { + return should_clear_history_list_; + } + void set_should_clear_history_list(bool should_clear_history_list) { + should_clear_history_list_ = should_clear_history_list; + } + + 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 and Android WebView + // state_serializer.cc appropriately. + // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING + + // See the accessors above for descriptions. + int unique_id_; + scoped_refptr<SiteInstanceImpl> site_instance_; + // TODO(creis): Persist bindings_. http://crbug.com/173672. + int bindings_; + PageType page_type_; + GURL url_; + Referrer referrer_; + GURL virtual_url_; + bool update_virtual_url_with_url_; + string16 title_; + FaviconStatus favicon_; + PageState page_state_; + int32 page_id_; + SSLStatus ssl_; + PageTransition transition_type_; + GURL user_typed_url_; + bool has_post_data_; + int64 post_id_; + RestoreType restore_type_; + GURL original_request_url_; + bool is_overriding_user_agent_; + base::Time timestamp_; + int http_status_code_; + + // This member is not persisted with session restore because it is transient. + // If the post request succeeds, this field is cleared since the same + // information is stored in |content_state_| above. It is also only shallow + // copied with compiler provided copy constructor. + // Cleared in |ResetForCommit|. + scoped_refptr<const base::RefCountedMemory> browser_initiated_post_data_; + + // This is also a transient member (i.e. is not persisted with session + // restore). The screenshot of a page is taken when navigating away from the + // page. This screenshot is displayed during an overscroll-navigation + // gesture. |screenshot_| will be NULL when the screenshot is not available + // (e.g. after a session restore, or if taking the screenshot of a page + // failed). The UI is responsible for dealing with missing screenshots + // appropriately (e.g. display a placeholder image instead). + scoped_refptr<base::RefCountedBytes> screenshot_; + + // This member is not persisted with session restore. + std::string extra_headers_; + + // Used for specifying base URL for pages loaded via data URLs. Only used and + // persisted by Android WebView. + GURL base_url_for_data_url_; + + // 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 in |ResetForCommit| 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. + // Cleared in |ResetForCommit|. + 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 cleared in |ResetForCommit|. + // + // We also use this flag for cross-process redirect navigations, so that the + // browser will replace the current navigation entry (which is the page + // doing the redirect). + bool should_replace_entry_; + + // This is used when transferring a pending entry from one process to another. + // It is cleared in |ResetForCommit| and should not be persisted. + std::vector<GURL> redirect_chain_; + + // This is set to true when this entry's navigation should clear the session + // history both on the renderer and browser side. The browser side history + // won't be cleared until the renderer has committed this navigation. This + // entry is not persisted by the session restore system, as it is always + // cleared in |ResetForCommit|. + bool should_clear_history_list_; + + // Set when this entry should be able to access local file:// resources. This + // value is not needed after the entry commits and is not persisted. + bool can_load_local_resources_; + + // If not empty, the name of the frame to navigate. This field is not + // persisted, because it is currently only used in tests. + std::string frame_to_navigate_; + + // Used to store extra data to support browser features. This member is not + // persisted, unless specific data is taken out/put back in at save/restore + // time (see TabNavigation for an example of this). + std::map<std::string, string16> extra_data_; + + // Copy and assignment is explicitly allowed for this class. +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_NAVIGATION_ENTRY_IMPL_H_ diff --git a/content/browser/frame_host/navigation_entry_impl_unittest.cc b/content/browser/frame_host/navigation_entry_impl_unittest.cc new file mode 100644 index 0000000..6486fde --- /dev/null +++ b/content/browser/frame_host/navigation_entry_impl_unittest.cc @@ -0,0 +1,241 @@ +// Copyright 2013 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/strings/string16.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "content/browser/frame_host/navigation_entry_impl.h" +#include "content/browser/site_instance_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); + +#if !defined(OS_IOS) + instance_ = static_cast<SiteInstanceImpl*>(SiteInstance::Create(NULL)); +#endif + 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_->GetUniqueID(), entry2_->GetUniqueID()); + + // Can set an entry to have the same ID as another + entry2_->set_unique_id(entry1_->GetUniqueID()); + EXPECT_EQ(entry1_->GetUniqueID(), entry2_->GetUniqueID()); +} + +// Test URL accessors +TEST_F(NavigationEntryTest, NavigationEntryURLs) { + // Start with no virtual_url (even if a url is set) + EXPECT_FALSE(entry1_->has_virtual_url()); + EXPECT_FALSE(entry2_->has_virtual_url()); + + EXPECT_EQ(GURL(), entry1_->GetURL()); + EXPECT_EQ(GURL(), entry1_->GetVirtualURL()); + EXPECT_TRUE(entry1_->GetTitleForDisplay(std::string()).empty()); + + // Setting URL affects virtual_url and GetTitleForDisplay + entry1_->SetURL(GURL("http://www.google.com")); + EXPECT_EQ(GURL("http://www.google.com"), entry1_->GetURL()); + EXPECT_EQ(GURL("http://www.google.com"), entry1_->GetVirtualURL()); + EXPECT_EQ(ASCIIToUTF16("www.google.com"), + entry1_->GetTitleForDisplay(std::string())); + + // file:/// URLs should only show the filename. + entry1_->SetURL(GURL("file:///foo/bar baz.txt")); + EXPECT_EQ(ASCIIToUTF16("bar baz.txt"), + entry1_->GetTitleForDisplay(std::string())); + + // Title affects GetTitleForDisplay + entry1_->SetTitle(ASCIIToUTF16("Google")); + EXPECT_EQ(ASCIIToUTF16("Google"), entry1_->GetTitleForDisplay(std::string())); + + // Setting virtual_url doesn't affect URL + entry2_->SetVirtualURL(GURL("display:url")); + EXPECT_TRUE(entry2_->has_virtual_url()); + EXPECT_EQ(GURL("test:url"), entry2_->GetURL()); + EXPECT_EQ(GURL("display:url"), entry2_->GetVirtualURL()); + + // Having a title set in constructor overrides virtual URL + EXPECT_EQ(ASCIIToUTF16("title"), entry2_->GetTitleForDisplay(std::string())); + + // User typed URL is independent of the others + EXPECT_EQ(GURL(), entry1_->GetUserTypedURL()); + EXPECT_EQ(GURL(), entry2_->GetUserTypedURL()); + entry2_->set_user_typed_url(GURL("typedurl")); + EXPECT_EQ(GURL("typedurl"), entry2_->GetUserTypedURL()); +} + +// Test Favicon inner class construction. +TEST_F(NavigationEntryTest, NavigationEntryFavicons) { + EXPECT_EQ(GURL(), entry1_->GetFavicon().url); + EXPECT_FALSE(entry1_->GetFavicon().valid); +} + +// Test SSLStatus inner class +TEST_F(NavigationEntryTest, NavigationEntrySSLStatus) { + // Default (unknown) + EXPECT_EQ(SECURITY_STYLE_UNKNOWN, entry1_->GetSSL().security_style); + EXPECT_EQ(SECURITY_STYLE_UNKNOWN, entry2_->GetSSL().security_style); + EXPECT_EQ(0, entry1_->GetSSL().cert_id); + EXPECT_EQ(0U, entry1_->GetSSL().cert_status); + EXPECT_EQ(-1, entry1_->GetSSL().security_bits); + int content_status = entry1_->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_->site_instance() == NULL); + EXPECT_EQ(instance_, entry2_->site_instance()); + entry1_->set_site_instance(instance_); + EXPECT_EQ(instance_, entry1_->site_instance()); + + // Page type + EXPECT_EQ(PAGE_TYPE_NORMAL, entry1_->GetPageType()); + EXPECT_EQ(PAGE_TYPE_NORMAL, entry2_->GetPageType()); + entry2_->set_page_type(PAGE_TYPE_INTERSTITIAL); + EXPECT_EQ(PAGE_TYPE_INTERSTITIAL, entry2_->GetPageType()); + + // Referrer + EXPECT_EQ(GURL(), entry1_->GetReferrer().url); + EXPECT_EQ(GURL("from"), entry2_->GetReferrer().url); + entry2_->SetReferrer( + Referrer(GURL("from2"), WebKit::WebReferrerPolicyDefault)); + EXPECT_EQ(GURL("from2"), entry2_->GetReferrer().url); + + // Title + EXPECT_EQ(string16(), entry1_->GetTitle()); + EXPECT_EQ(ASCIIToUTF16("title"), entry2_->GetTitle()); + entry2_->SetTitle(ASCIIToUTF16("title2")); + EXPECT_EQ(ASCIIToUTF16("title2"), entry2_->GetTitle()); + + // State + EXPECT_FALSE(entry1_->GetPageState().IsValid()); + EXPECT_FALSE(entry2_->GetPageState().IsValid()); + entry2_->SetPageState(PageState::CreateFromEncodedData("state")); + EXPECT_EQ("state", entry2_->GetPageState().ToEncodedData()); + + // Page ID + EXPECT_EQ(-1, entry1_->GetPageID()); + EXPECT_EQ(3, entry2_->GetPageID()); + entry2_->SetPageID(2); + EXPECT_EQ(2, entry2_->GetPageID()); + + // Transition type + EXPECT_EQ(PAGE_TRANSITION_LINK, entry1_->GetTransitionType()); + EXPECT_EQ(PAGE_TRANSITION_TYPED, entry2_->GetTransitionType()); + entry2_->SetTransitionType(PAGE_TRANSITION_RELOAD); + EXPECT_EQ(PAGE_TRANSITION_RELOAD, entry2_->GetTransitionType()); + + // Is renderer initiated + EXPECT_FALSE(entry1_->is_renderer_initiated()); + EXPECT_FALSE(entry2_->is_renderer_initiated()); + entry2_->set_is_renderer_initiated(true); + EXPECT_TRUE(entry2_->is_renderer_initiated()); + + // Post Data + EXPECT_FALSE(entry1_->GetHasPostData()); + EXPECT_FALSE(entry2_->GetHasPostData()); + entry2_->SetHasPostData(true); + EXPECT_TRUE(entry2_->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_EXITED_CLEANLY); + EXPECT_EQ(NavigationEntryImpl::RESTORE_LAST_SESSION_EXITED_CLEANLY, + entry2_->restore_type()); + + // Original URL + EXPECT_EQ(GURL(), entry1_->GetOriginalRequestURL()); + EXPECT_EQ(GURL(), entry2_->GetOriginalRequestURL()); + entry2_->SetOriginalRequestURL(GURL("original_url")); + EXPECT_EQ(GURL("original_url"), entry2_->GetOriginalRequestURL()); + + // User agent override + EXPECT_FALSE(entry1_->GetIsOverridingUserAgent()); + EXPECT_FALSE(entry2_->GetIsOverridingUserAgent()); + entry2_->SetIsOverridingUserAgent(true); + EXPECT_TRUE(entry2_->GetIsOverridingUserAgent()); + + // Browser initiated post data + EXPECT_EQ(NULL, entry1_->GetBrowserInitiatedPostData()); + EXPECT_EQ(NULL, entry2_->GetBrowserInitiatedPostData()); + const int length = 11; + const unsigned char* raw_data = + reinterpret_cast<const unsigned char*>("post\n\n\0data"); + std::vector<unsigned char> post_data_vector(raw_data, raw_data+length); + scoped_refptr<base::RefCountedBytes> post_data = + base::RefCountedBytes::TakeVector(&post_data_vector); + entry2_->SetBrowserInitiatedPostData(post_data.get()); + EXPECT_EQ(post_data->front(), + entry2_->GetBrowserInitiatedPostData()->front()); + + // Frame to navigate. + EXPECT_TRUE(entry1_->GetFrameToNavigate().empty()); + EXPECT_TRUE(entry2_->GetFrameToNavigate().empty()); +} + +// Test timestamps. +TEST_F(NavigationEntryTest, NavigationEntryTimestamps) { + EXPECT_EQ(base::Time(), entry1_->GetTimestamp()); + const base::Time now = base::Time::Now(); + entry1_->SetTimestamp(now); + EXPECT_EQ(now, entry1_->GetTimestamp()); +} + +// Test extra data stored in the navigation entry. +TEST_F(NavigationEntryTest, NavigationEntryExtraData) { + string16 test_data = ASCIIToUTF16("my search terms"); + string16 output; + entry1_->SetExtraData("search_terms", test_data); + + EXPECT_FALSE(entry1_->GetExtraData("non_existent_key", &output)); + EXPECT_EQ(ASCIIToUTF16(""), output); + EXPECT_TRUE(entry1_->GetExtraData("search_terms", &output)); + EXPECT_EQ(test_data, output); + // Data is cleared. + entry1_->ClearExtraData("search_terms"); + // Content in |output| is not modified if data is not present at the key. + EXPECT_FALSE(entry1_->GetExtraData("search_terms", &output)); + EXPECT_EQ(test_data, output); + // Using an empty string shows that the data is not present in the map. + string16 output2; + EXPECT_FALSE(entry1_->GetExtraData("search_terms", &output2)); + EXPECT_EQ(ASCIIToUTF16(""), output2); +} + +} // namespace content diff --git a/content/browser/frame_host/render_frame_host_impl.cc b/content/browser/frame_host/render_frame_host_impl.cc new file mode 100644 index 0000000..c1fbc30 --- /dev/null +++ b/content/browser/frame_host/render_frame_host_impl.cc @@ -0,0 +1,103 @@ +// Copyright 2013 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/frame_host/render_frame_host_impl.h" + +#include "base/containers/hash_tables.h" +#include "base/lazy_instance.h" +#include "content/browser/frame_host/frame_tree.h" +#include "content/browser/renderer_host/render_view_host_impl.h" +#include "content/common/frame_messages.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_process_host.h" +#include "url/gurl.h" + +namespace content { + +// The (process id, routing id) pair that identifies one RenderFrame. +typedef std::pair<int32, int32> RenderFrameHostID; +typedef base::hash_map<RenderFrameHostID, RenderFrameHostImpl*> + RoutingIDFrameMap; +static base::LazyInstance<RoutingIDFrameMap> g_routing_id_frame_map = + LAZY_INSTANCE_INITIALIZER; + +// static +RenderFrameHostImpl* RenderFrameHostImpl::FromID( + int process_id, int routing_id) { + RoutingIDFrameMap* frames = g_routing_id_frame_map.Pointer(); + RoutingIDFrameMap::iterator it = frames->find( + RenderFrameHostID(process_id, routing_id)); + return it == frames->end() ? NULL : it->second; +} + +RenderFrameHostImpl::RenderFrameHostImpl( + RenderViewHostImpl* render_view_host, + FrameTree* frame_tree, + int routing_id, + bool is_swapped_out) + : render_view_host_(render_view_host), + frame_tree_(frame_tree), + routing_id_(routing_id), + is_swapped_out_(is_swapped_out) { + GetProcess()->AddRoute(routing_id_, this); + g_routing_id_frame_map.Get().insert(std::make_pair( + RenderFrameHostID(GetProcess()->GetID(), routing_id_), + this)); +} + +RenderFrameHostImpl::~RenderFrameHostImpl() { + GetProcess()->RemoveRoute(routing_id_); + g_routing_id_frame_map.Get().erase( + RenderFrameHostID(GetProcess()->GetID(), routing_id_)); + +} + +bool RenderFrameHostImpl::Send(IPC::Message* message) { + return GetProcess()->Send(message); +} + +bool RenderFrameHostImpl::OnMessageReceived(const IPC::Message &msg) { + bool handled = true; + bool msg_is_ok = true; + IPC_BEGIN_MESSAGE_MAP_EX(RenderFrameHostImpl, msg, msg_is_ok) + IPC_MESSAGE_HANDLER(FrameHostMsg_Detach, OnDetach) + IPC_MESSAGE_HANDLER(FrameHostMsg_DidStartProvisionalLoadForFrame, + OnDidStartProvisionalLoadForFrame) + IPC_END_MESSAGE_MAP_EX() + + return handled; +} + +void RenderFrameHostImpl::Init() { + GetProcess()->ResumeRequestsForView(routing_id()); +} + +RenderProcessHost* RenderFrameHostImpl::GetProcess() const { + // TODO(nasko): This should return its own process, once we have working + // cross-process navigation for subframes. + return render_view_host_->GetProcess(); +} + +void RenderFrameHostImpl::OnCreateChildFrame(int new_frame_routing_id, + int64 parent_frame_id, + int64 frame_id, + const std::string& frame_name) { + frame_tree_->AddFrame(new_frame_routing_id, parent_frame_id, frame_id, + frame_name); +} + +void RenderFrameHostImpl::OnDetach(int64 parent_frame_id, int64 frame_id) { + frame_tree_->RemoveFrame(parent_frame_id, frame_id); +} + +void RenderFrameHostImpl::OnDidStartProvisionalLoadForFrame( + int64 frame_id, + int64 parent_frame_id, + bool is_main_frame, + const GURL& url) { + render_view_host_->OnDidStartProvisionalLoadForFrame( + frame_id, parent_frame_id, is_main_frame, url); +} + +} // namespace content diff --git a/content/browser/frame_host/render_frame_host_impl.h b/content/browser/frame_host/render_frame_host_impl.h new file mode 100644 index 0000000..dbe8972 --- /dev/null +++ b/content/browser/frame_host/render_frame_host_impl.h @@ -0,0 +1,77 @@ +// Copyright 2013 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_FRAME_HOST_RENDER_FRAME_HOST_IMPL_H_ +#define CONTENT_BROWSER_FRAME_HOST_RENDER_FRAME_HOST_IMPL_H_ + +#include <string> + +#include "base/compiler_specific.h" +#include "content/public/browser/render_frame_host.h" + +class GURL; + +namespace content { + +class FrameTree; +class RenderProcessHost; +class RenderViewHostImpl; + +class CONTENT_EXPORT RenderFrameHostImpl : public RenderFrameHost { + public: + static RenderFrameHostImpl* FromID(int process_id, int routing_id); + + // TODO(nasko): Remove dependency on RenderViewHost here. RenderProcessHost + // should be the abstraction needed here, but we need RenderViewHost to pass + // into WebContentsObserver::FrameDetached for now. + RenderFrameHostImpl(RenderViewHostImpl* render_view_host, + FrameTree* frame_tree, + int routing_id, + bool is_swapped_out); + virtual ~RenderFrameHostImpl(); + + // IPC::Sender + virtual bool Send(IPC::Message* msg) OVERRIDE; + + // IPC::Listener + virtual bool OnMessageReceived(const IPC::Message& msg) OVERRIDE; + + void Init(); + RenderProcessHost* GetProcess() const; + int routing_id() const { return routing_id_; } + void OnCreateChildFrame(int new_frame_routing_id, + int64 parent_frame_id, + int64 frame_id, + const std::string& frame_name); + + RenderViewHostImpl* render_view_host() { + return render_view_host_; + } + + private: + // IPC Message handlers. + void OnDetach(int64 parent_frame_id, int64 frame_id); + void OnDidStartProvisionalLoadForFrame(int64 frame_id, + int64 parent_frame_id, + bool main_frame, + const GURL& url); + + bool is_swapped_out() { return is_swapped_out_; } + + // TODO(nasko): This should be removed and replaced by RenderProcessHost. + RenderViewHostImpl* render_view_host_; // Not owned. + + // Reference to the whole frame tree that this RenderFrameHost belongs too. + // Allows this RenderFrameHost to add and remove nodes in response to + // messages from the renderer requesting DOM manipulation. + FrameTree* frame_tree_; + int routing_id_; + bool is_swapped_out_; + + DISALLOW_COPY_AND_ASSIGN(RenderFrameHostImpl); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_RENDER_FRAME_HOST_IMPL_H_ diff --git a/content/browser/frame_host/render_frame_message_filter.cc b/content/browser/frame_host/render_frame_message_filter.cc new file mode 100644 index 0000000..c187680 --- /dev/null +++ b/content/browser/frame_host/render_frame_message_filter.cc @@ -0,0 +1,71 @@ +// Copyright 2013 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/frame_host/render_frame_message_filter.h" + +#include "content/browser/frame_host/render_frame_host_impl.h" +#include "content/browser/renderer_host/render_widget_helper.h" +#include "content/common/frame_messages.h" +#include "content/public/browser/browser_thread.h" + +namespace content { + +namespace { + +void CreateChildFrameOnUI(int process_id, + int parent_render_frame_id, + int64 parent_frame_id, + int64 frame_id, + const std::string& frame_name, + int new_render_frame_id) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + RenderFrameHostImpl* render_frame_host = + RenderFrameHostImpl::FromID(process_id, parent_render_frame_id); + // Handles the RenderFrameHost being deleted on the UI thread while + // processing a subframe creation message. + if (render_frame_host) { + render_frame_host->OnCreateChildFrame(new_render_frame_id, + parent_frame_id, frame_id, + frame_name); + } +} + +} // namespace + +RenderFrameMessageFilter::RenderFrameMessageFilter( + int render_process_id, + RenderWidgetHelper* render_widget_helper) + : render_process_id_(render_process_id), + render_widget_helper_(render_widget_helper) { +} + +RenderFrameMessageFilter::~RenderFrameMessageFilter() { +} + +bool RenderFrameMessageFilter::OnMessageReceived(const IPC::Message& message, + bool* message_was_ok) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP_EX(RenderFrameMessageFilter, message, *message_was_ok) + IPC_MESSAGE_HANDLER(FrameHostMsg_CreateChildFrame, OnCreateChildFrame) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP_EX() + + return handled; +} + +void RenderFrameMessageFilter::OnCreateChildFrame( + int parent_render_frame_id, + int64 parent_frame_id, + int64 frame_id, + const std::string& frame_name, + int* new_render_frame_id) { + *new_render_frame_id = render_widget_helper_->GetNextRoutingID(); + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, + base::Bind(&CreateChildFrameOnUI, render_process_id_, + parent_render_frame_id, parent_frame_id, frame_id, frame_name, + *new_render_frame_id)); +} + +} // namespace content diff --git a/content/browser/frame_host/render_frame_message_filter.h b/content/browser/frame_host/render_frame_message_filter.h new file mode 100644 index 0000000..82a1b94 --- /dev/null +++ b/content/browser/frame_host/render_frame_message_filter.h @@ -0,0 +1,44 @@ +// Copyright 2013 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_FRAME_HOST_RENDER_FRAME_MESSAGE_FILTER_H_ +#define CONTENT_BROWSER_FRAME_HOST_RENDER_FRAME_MESSAGE_FILTER_H_ + +#include "content/public/browser/browser_message_filter.h" + +namespace content { +class RenderWidgetHelper; + +// RenderFrameMessageFilter intercepts FrameHost messages on the IO thread +// that require low-latency processing. The canonical example of this is +// child-frame creation which is a sync IPC that provides the renderer +// with the routing id for a newly created RenderFrame. +// +// This object is created on the UI thread and used on the IO thread. +class RenderFrameMessageFilter : public BrowserMessageFilter { + public: + RenderFrameMessageFilter(int render_process_id, + RenderWidgetHelper* render_widget_helper); + + virtual bool OnMessageReceived(const IPC::Message& message, + bool* message_was_ok) OVERRIDE; + + private: + virtual ~RenderFrameMessageFilter(); + + void OnCreateChildFrame(int parent_render_frame_id, + int64 parent_frame_id, + int64 frame_id, + const std::string& frame_name, + int* new_render_frame_id); + + const int render_process_id_; + + // Needed for issuing routing ids and surface ids. + scoped_refptr<RenderWidgetHelper> render_widget_helper_; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_RENDER_FRAME_MESSAGE_FILTER_H_ diff --git a/content/browser/frame_host/render_view_host_manager.cc b/content/browser/frame_host/render_view_host_manager.cc new file mode 100644 index 0000000..34e2a00 --- /dev/null +++ b/content/browser/frame_host/render_view_host_manager.cc @@ -0,0 +1,1119 @@ +// Copyright 2013 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/frame_host/render_view_host_manager.h" + +#include <utility> + +#include "base/command_line.h" +#include "base/debug/trace_event.h" +#include "base/logging.h" +#include "content/browser/devtools/render_view_devtools_agent_host.h" +#include "content/browser/frame_host/interstitial_page_impl.h" +#include "content/browser/frame_host/navigation_controller_impl.h" +#include "content/browser/frame_host/navigation_entry_impl.h" +#include "content/browser/renderer_host/render_process_host_impl.h" +#include "content/browser/renderer_host/render_view_host_factory.h" +#include "content/browser/renderer_host/render_view_host_impl.h" +#include "content/browser/site_instance_impl.h" +#include "content/browser/webui/web_ui_controller_factory_registry.h" +#include "content/browser/webui/web_ui_impl.h" +#include "content/common/view_messages.h" +#include "content/port/browser/render_widget_host_view_port.h" +#include "content/public/browser/content_browser_client.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_widget_host_iterator.h" +#include "content/public/browser/user_metrics.h" +#include "content/public/browser/web_ui_controller.h" +#include "content/public/common/content_switches.h" +#include "content/public/common/url_constants.h" + +namespace content { + +RenderViewHostManager::PendingNavigationParams::PendingNavigationParams() + : is_transfer(false), frame_id(-1) { +} + +RenderViewHostManager::PendingNavigationParams::PendingNavigationParams( + const GlobalRequestID& global_request_id, + bool is_transfer, + const std::vector<GURL>& transfer_url_chain, + Referrer referrer, + PageTransition page_transition, + int64 frame_id) + : global_request_id(global_request_id), + is_transfer(is_transfer), + transfer_url_chain(transfer_url_chain), + referrer(referrer), + page_transition(page_transition), + frame_id(frame_id) { +} + +RenderViewHostManager::PendingNavigationParams::~PendingNavigationParams() {} + +RenderViewHostManager::RenderViewHostManager( + RenderViewHostDelegate* render_view_delegate, + RenderWidgetHostDelegate* render_widget_delegate, + Delegate* delegate) + : delegate_(delegate), + cross_navigation_pending_(false), + render_view_delegate_(render_view_delegate), + render_widget_delegate_(render_widget_delegate), + render_view_host_(NULL), + pending_render_view_host_(NULL), + interstitial_page_(NULL) { +} + +RenderViewHostManager::~RenderViewHostManager() { + if (pending_render_view_host_) + CancelPending(); + + // We should always have a main RenderViewHost except in some tests. + RenderViewHostImpl* render_view_host = render_view_host_; + render_view_host_ = NULL; + if (render_view_host) + render_view_host->Shutdown(); + + // Shut down any swapped out RenderViewHosts. + for (RenderViewHostMap::iterator iter = swapped_out_hosts_.begin(); + iter != swapped_out_hosts_.end(); + ++iter) { + iter->second->Shutdown(); + } +} + +void RenderViewHostManager::Init(BrowserContext* browser_context, + SiteInstance* site_instance, + int routing_id, + int main_frame_routing_id) { + // Create a RenderViewHost, once we have an instance. It is important to + // immediately give this SiteInstance to a RenderViewHost so that it is + // ref counted. + if (!site_instance) + site_instance = SiteInstance::Create(browser_context); + render_view_host_ = static_cast<RenderViewHostImpl*>( + RenderViewHostFactory::Create( + site_instance, render_view_delegate_, render_widget_delegate_, + routing_id, main_frame_routing_id, false, + delegate_->IsHidden())); + render_view_host_->AttachToFrameTree(); + + // Keep track of renderer processes as they start to shut down or are + // crashed/killed. + registrar_.Add(this, NOTIFICATION_RENDERER_PROCESS_CLOSED, + NotificationService::AllSources()); + registrar_.Add(this, NOTIFICATION_RENDERER_PROCESS_CLOSING, + NotificationService::AllSources()); +} + +RenderViewHostImpl* RenderViewHostManager::current_host() const { + return render_view_host_; +} + +RenderViewHostImpl* RenderViewHostManager::pending_render_view_host() const { + return pending_render_view_host_; +} + +RenderWidgetHostView* RenderViewHostManager::GetRenderWidgetHostView() const { + if (interstitial_page_) + return interstitial_page_->GetView(); + if (!render_view_host_) + return NULL; + return render_view_host_->GetView(); +} + +void RenderViewHostManager::SetPendingWebUI(const NavigationEntryImpl& entry) { + pending_web_ui_.reset( + delegate_->CreateWebUIForRenderManager(entry.GetURL())); + pending_and_current_web_ui_.reset(); + + // If we have assigned (zero or more) bindings to this NavigationEntry in the + // past, make sure we're not granting it different bindings than it had + // before. If so, note it and don't give it any bindings, to avoid a + // potential privilege escalation. + if (pending_web_ui_.get() && + entry.bindings() != NavigationEntryImpl::kInvalidBindings && + pending_web_ui_->GetBindings() != entry.bindings()) { + RecordAction(UserMetricsAction("ProcessSwapBindingsMismatch_RVHM")); + pending_web_ui_.reset(); + } +} + +RenderViewHostImpl* RenderViewHostManager::Navigate( + const NavigationEntryImpl& entry) { + TRACE_EVENT0("browser", "RenderViewHostManager:Navigate"); + // Create a pending RenderViewHost. It will give us the one we should use + RenderViewHostImpl* dest_render_view_host = + static_cast<RenderViewHostImpl*>(UpdateRendererStateForNavigate(entry)); + if (!dest_render_view_host) + return NULL; // We weren't able to create a pending render view host. + + // If the current render_view_host_ isn't live, we should create it so + // that we don't show a sad tab while the dest_render_view_host fetches + // its first page. (Bug 1145340) + if (dest_render_view_host != render_view_host_ && + !render_view_host_->IsRenderViewLive()) { + // Note: we don't call InitRenderView here because we are navigating away + // soon anyway, and we don't have the NavigationEntry for this host. + delegate_->CreateRenderViewForRenderManager(render_view_host_, + MSG_ROUTING_NONE); + } + + // If the renderer crashed, then try to create a new one to satisfy this + // navigation request. + if (!dest_render_view_host->IsRenderViewLive()) { + // Recreate the opener chain. + int opener_route_id = delegate_->CreateOpenerRenderViewsForRenderManager( + dest_render_view_host->GetSiteInstance()); + if (!InitRenderView(dest_render_view_host, opener_route_id)) + return NULL; + + // Now that we've created a new renderer, be sure to hide it if it isn't + // our primary one. Otherwise, we might crash if we try to call Show() + // on it later. + if (dest_render_view_host != render_view_host_ && + dest_render_view_host->GetView()) { + dest_render_view_host->GetView()->Hide(); + } else { + // This is our primary renderer, notify here as we won't be calling + // CommitPending (which does the notify). + delegate_->NotifySwappedFromRenderManager(NULL, render_view_host_); + } + } + + return dest_render_view_host; +} + +void RenderViewHostManager::Stop() { + render_view_host_->Stop(); + + // If we are cross-navigating, we should stop the pending renderers. This + // will lead to a DidFailProvisionalLoad, which will properly destroy them. + if (cross_navigation_pending_) { + pending_render_view_host_->Send( + new ViewMsg_Stop(pending_render_view_host_->GetRoutingID())); + } +} + +void RenderViewHostManager::SetIsLoading(bool is_loading) { + render_view_host_->SetIsLoading(is_loading); + if (pending_render_view_host_) + pending_render_view_host_->SetIsLoading(is_loading); +} + +bool RenderViewHostManager::ShouldCloseTabOnUnresponsiveRenderer() { + if (!cross_navigation_pending_) + return true; + + // We should always have a pending RVH when there's a cross-process navigation + // in progress. Sanity check this for http://crbug.com/276333. + CHECK(pending_render_view_host_); + + // If the tab becomes unresponsive during {before}unload while doing a + // cross-site navigation, proceed with the navigation. (This assumes that + // the pending RenderViewHost is still responsive.) + if (render_view_host_->is_waiting_for_unload_ack()) { + // The request has been started and paused while we're waiting for the + // unload handler to finish. We'll pretend that it did. The pending + // renderer will then be swapped in as part of the usual DidNavigate logic. + // (If the unload handler later finishes, this call will be ignored because + // the pending_nav_params_ state will already be cleaned up.) + current_host()->OnSwappedOut(true); + } else if (render_view_host_->is_waiting_for_beforeunload_ack()) { + // Haven't gotten around to starting the request, because we're still + // waiting for the beforeunload handler to finish. We'll pretend that it + // did finish, to let the navigation proceed. Note that there's a danger + // that the beforeunload handler will later finish and possibly return + // false (meaning the navigation should not proceed), but we'll ignore it + // in this case because it took too long. + if (pending_render_view_host_->are_navigations_suspended()) + pending_render_view_host_->SetNavigationsSuspended( + false, base::TimeTicks::Now()); + } + return false; +} + +void RenderViewHostManager::SwappedOut(RenderViewHost* render_view_host) { + // Make sure this is from our current RVH, and that we have a pending + // navigation from OnCrossSiteResponse. (There may be no pending navigation + // for data URLs that don't make network requests, for example.) If not, + // just return early and ignore. + if (render_view_host != render_view_host_ || !pending_nav_params_.get()) { + pending_nav_params_.reset(); + return; + } + + // Now that the unload handler has run, we need to either initiate the + // pending transfer (if there is one) or resume the paused response (if not). + // TODO(creis): The blank swapped out page is visible during this time, but + // we can shorten this by delivering the response directly, rather than + // forcing an identical request to be made. + if (pending_nav_params_->is_transfer) { + // Treat the last URL in the chain as the destination and the remainder as + // the redirect chain. + CHECK(pending_nav_params_->transfer_url_chain.size()); + GURL transfer_url = pending_nav_params_->transfer_url_chain.back(); + pending_nav_params_->transfer_url_chain.pop_back(); + + // We don't know whether the original request had |user_action| set to true. + // However, since we force the navigation to be in the current tab, it + // doesn't matter. + render_view_host->GetDelegate()->RequestTransferURL( + transfer_url, + pending_nav_params_->transfer_url_chain, + pending_nav_params_->referrer, + pending_nav_params_->page_transition, + CURRENT_TAB, + pending_nav_params_->frame_id, + pending_nav_params_->global_request_id, + false, + true); + } else if (pending_render_view_host_) { + RenderProcessHostImpl* pending_process = + static_cast<RenderProcessHostImpl*>( + pending_render_view_host_->GetProcess()); + pending_process->ResumeDeferredNavigation( + pending_nav_params_->global_request_id); + } + pending_nav_params_.reset(); +} + +void RenderViewHostManager::DidNavigateMainFrame( + RenderViewHost* render_view_host) { + if (!cross_navigation_pending_) { + DCHECK(!pending_render_view_host_); + + // We should only hear this from our current renderer. + DCHECK(render_view_host == render_view_host_); + + // Even when there is no pending RVH, there may be a pending Web UI. + if (pending_web_ui()) + CommitPending(); + return; + } + + if (render_view_host == pending_render_view_host_) { + // The pending cross-site navigation completed, so show the renderer. + // If it committed without sending network requests (e.g., data URLs), + // then we still need to swap out the old RVH first and run its unload + // handler. OK for that to happen in the background. + if (pending_render_view_host_->HasPendingCrossSiteRequest()) + SwapOutOldPage(); + + CommitPending(); + cross_navigation_pending_ = false; + } else if (render_view_host == render_view_host_) { + // A navigation in the original page has taken place. Cancel the pending + // one. + CancelPending(); + cross_navigation_pending_ = false; + } else { + // No one else should be sending us DidNavigate in this state. + DCHECK(false); + } +} + +void RenderViewHostManager::DidDisownOpener(RenderViewHost* render_view_host) { + // Notify all swapped out hosts, including the pending RVH. + for (RenderViewHostMap::iterator iter = swapped_out_hosts_.begin(); + iter != swapped_out_hosts_.end(); + ++iter) { + DCHECK_NE(iter->second->GetSiteInstance(), + current_host()->GetSiteInstance()); + iter->second->DisownOpener(); + } +} + +void RenderViewHostManager::RendererAbortedProvisionalLoad( + RenderViewHost* render_view_host) { + // We used to cancel the pending renderer here for cross-site downloads. + // However, it's not safe to do that because the download logic repeatedly + // looks for this WebContents based on a render view ID. Instead, we just + // leave the pending renderer around until the next navigation event + // (Navigate, DidNavigate, etc), which will clean it up properly. + // TODO(creis): All of this will go away when we move the cross-site logic + // to ResourceDispatcherHost, so that we intercept responses rather than + // navigation events. (That's necessary to support onunload anyway.) Once + // we've made that change, we won't create a pending renderer until we know + // the response is not a download. +} + +void RenderViewHostManager::RendererProcessClosing( + RenderProcessHost* render_process_host) { + // Remove any swapped out RVHs from this process, so that we don't try to + // swap them back in while the process is exiting. Start by finding them, + // since there could be more than one. + std::list<int> ids_to_remove; + for (RenderViewHostMap::iterator iter = swapped_out_hosts_.begin(); + iter != swapped_out_hosts_.end(); + ++iter) { + if (iter->second->GetProcess() == render_process_host) + ids_to_remove.push_back(iter->first); + } + + // Now delete them. + while (!ids_to_remove.empty()) { + swapped_out_hosts_[ids_to_remove.back()]->Shutdown(); + swapped_out_hosts_.erase(ids_to_remove.back()); + ids_to_remove.pop_back(); + } +} + +void RenderViewHostManager::ShouldClosePage( + bool for_cross_site_transition, + bool proceed, + const base::TimeTicks& proceed_time) { + if (for_cross_site_transition) { + // Ignore if we're not in a cross-site navigation. + if (!cross_navigation_pending_) + return; + + if (proceed) { + // Ok to unload the current page, so proceed with the cross-site + // navigation. Note that if navigations are not currently suspended, it + // might be because the renderer was deemed unresponsive and this call was + // already made by ShouldCloseTabOnUnresponsiveRenderer. In that case, it + // is ok to do nothing here. + if (pending_render_view_host_ && + pending_render_view_host_->are_navigations_suspended()) { + pending_render_view_host_->SetNavigationsSuspended(false, proceed_time); + } + } else { + // Current page says to cancel. + CancelPending(); + cross_navigation_pending_ = false; + } + } else { + // Non-cross site transition means closing the entire tab. + bool proceed_to_fire_unload; + delegate_->BeforeUnloadFiredFromRenderManager(proceed, proceed_time, + &proceed_to_fire_unload); + + if (proceed_to_fire_unload) { + // If we're about to close the tab and there's a pending RVH, cancel it. + // Otherwise, if the navigation in the pending RVH completes before the + // close in the current RVH, we'll lose the tab close. + if (pending_render_view_host_) { + CancelPending(); + cross_navigation_pending_ = false; + } + + // This is not a cross-site navigation, the tab is being closed. + render_view_host_->ClosePage(); + } + } +} + +void RenderViewHostManager::OnCrossSiteResponse( + RenderViewHost* pending_render_view_host, + const GlobalRequestID& global_request_id, + bool is_transfer, + const std::vector<GURL>& transfer_url_chain, + const Referrer& referrer, + PageTransition page_transition, + int64 frame_id) { + // This should be called either when the pending RVH is ready to commit or + // when we realize that the current RVH's request requires a transfer. + DCHECK( + pending_render_view_host == pending_render_view_host_ || + pending_render_view_host == render_view_host_); + + // TODO(creis): Eventually we will want to check all navigation responses + // here, but currently we pass information for a transfer if + // ShouldSwapProcessesForRedirect returned true in the network stack. + // In that case, we should set up a transfer after the unload handler runs. + // If is_transfer is false, we will just run the unload handler and resume. + pending_nav_params_.reset(new PendingNavigationParams( + global_request_id, is_transfer, transfer_url_chain, referrer, + page_transition, frame_id)); + + // Run the unload handler of the current page. + SwapOutOldPage(); +} + +void RenderViewHostManager::SwapOutOldPage() { + // Should only see this while we have a pending renderer or transfer. + CHECK(cross_navigation_pending_ || pending_nav_params_.get()); + + // First close any modal dialogs that would prevent us from swapping out. + // TODO(creis): This is not a guarantee. The renderer could immediately + // create another dialog in a loop, potentially causing a renderer crash when + // we tell it to swap out with a nested message loop and PageGroupLoadDeferrer + // on the stack. We should prevent the renderer from showing more dialogs + // until the SwapOut. See http://crbug.com/312490. + delegate_->CancelModalDialogsForRenderManager(); + + // Tell the old renderer it is being swapped out. This will fire the unload + // handler (without firing the beforeunload handler a second time). When the + // unload handler finishes and the navigation completes, we will send a + // message to the ResourceDispatcherHost, allowing the pending RVH's response + // to resume. + render_view_host_->SwapOut(); + + // ResourceDispatcherHost has told us to run the onunload handler, which + // means it is not a download or unsafe page, and we are going to perform the + // navigation. Thus, we no longer need to remember that the RenderViewHost + // is part of a pending cross-site request. + if (pending_render_view_host_) + pending_render_view_host_->SetHasPendingCrossSiteRequest(false); +} + +void RenderViewHostManager::Observe( + int type, + const NotificationSource& source, + const NotificationDetails& details) { + switch (type) { + case NOTIFICATION_RENDERER_PROCESS_CLOSED: + case NOTIFICATION_RENDERER_PROCESS_CLOSING: + RendererProcessClosing( + Source<RenderProcessHost>(source).ptr()); + break; + + default: + NOTREACHED(); + } +} + +bool RenderViewHostManager::ShouldTransitionCrossSite() { + // False in the single-process mode, as it makes RVHs to accumulate + // in swapped_out_hosts_. + // True if we are using process-per-site-instance (default) or + // process-per-site (kProcessPerSite). + return + !CommandLine::ForCurrentProcess()->HasSwitch(switches::kSingleProcess) && + !CommandLine::ForCurrentProcess()->HasSwitch(switches::kProcessPerTab); +} + +bool RenderViewHostManager::ShouldSwapProcessesForNavigation( + const NavigationEntry* curr_entry, + const NavigationEntryImpl* new_entry) const { + DCHECK(new_entry); + + // Check for reasons to swap processes even if we are in a process model that + // doesn't usually swap (e.g., process-per-tab). + + // For security, we should transition between processes when one is a Web UI + // page and one isn't. If there's no curr_entry, check the current RVH's + // site, which might already be committed to a Web UI URL (such as the NTP). + const GURL& current_url = (curr_entry) ? curr_entry->GetURL() : + render_view_host_->GetSiteInstance()->GetSiteURL(); + BrowserContext* browser_context = + delegate_->GetControllerForRenderManager().GetBrowserContext(); + if (WebUIControllerFactoryRegistry::GetInstance()->UseWebUIForURL( + browser_context, current_url)) { + // Force swap if it's not an acceptable URL for Web UI. + // Here, data URLs are never allowed. + if (!WebUIControllerFactoryRegistry::GetInstance()->IsURLAcceptableForWebUI( + browser_context, new_entry->GetURL(), false)) { + return true; + } + } else { + // Force swap if it's a Web UI URL. + if (WebUIControllerFactoryRegistry::GetInstance()->UseWebUIForURL( + browser_context, new_entry->GetURL())) { + return true; + } + } + + if (GetContentClient()->browser()->ShouldSwapProcessesForNavigation( + render_view_host_->GetSiteInstance(), + curr_entry ? curr_entry->GetURL() : GURL(), + new_entry->GetURL())) { + return true; + } + + if (!curr_entry) + return false; + + // We can't switch a RenderView between view source and non-view source mode + // without screwing up the session history sometimes (when navigating between + // "view-source:http://foo.com/" and "http://foo.com/", WebKit doesn't treat + // it as a new navigation). So require a view switch. + if (curr_entry->IsViewSourceMode() != new_entry->IsViewSourceMode()) + return true; + + return false; +} + +bool RenderViewHostManager::ShouldReuseWebUI( + const NavigationEntry* curr_entry, + const NavigationEntryImpl* new_entry) const { + NavigationControllerImpl& controller = + delegate_->GetControllerForRenderManager(); + return curr_entry && web_ui_.get() && + (WebUIControllerFactoryRegistry::GetInstance()->GetWebUIType( + controller.GetBrowserContext(), curr_entry->GetURL()) == + WebUIControllerFactoryRegistry::GetInstance()->GetWebUIType( + controller.GetBrowserContext(), new_entry->GetURL())); +} + +SiteInstance* RenderViewHostManager::GetSiteInstanceForEntry( + const NavigationEntryImpl& entry, + SiteInstance* curr_instance) { + // NOTE: This is only called when ShouldTransitionCrossSite is true. + + const GURL& dest_url = entry.GetURL(); + NavigationControllerImpl& controller = + delegate_->GetControllerForRenderManager(); + BrowserContext* browser_context = controller.GetBrowserContext(); + + // If the entry has an instance already we should use it. + if (entry.site_instance()) + return entry.site_instance(); + + // (UGLY) HEURISTIC, process-per-site only: + // + // If this navigation is generated, then it probably corresponds to a search + // query. Given that search results typically lead to users navigating to + // other sites, we don't really want to use the search engine hostname to + // determine the site instance for this navigation. + // + // NOTE: This can be removed once we have a way to transition between + // RenderViews in response to a link click. + // + if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kProcessPerSite) && + PageTransitionCoreTypeIs(entry.GetTransitionType(), + PAGE_TRANSITION_GENERATED)) { + return curr_instance; + } + + SiteInstanceImpl* curr_site_instance = + static_cast<SiteInstanceImpl*>(curr_instance); + + // If we haven't used our SiteInstance (and thus RVH) yet, then we can use it + // for this entry. We won't commit the SiteInstance to this site until the + // navigation commits (in DidNavigate), unless the navigation entry was + // restored or it's a Web UI as described below. + if (!curr_site_instance->HasSite()) { + // If we've already created a SiteInstance for our destination, we don't + // want to use this unused SiteInstance; use the existing one. (We don't + // do this check if the curr_instance has a site, because for now, we want + // to compare against the current URL and not the SiteInstance's site. In + // this case, there is no current URL, so comparing against the site is ok. + // See additional comments below.) + // + // Also, if the URL should use process-per-site mode and there is an + // existing process for the site, we should use it. We can call + // GetRelatedSiteInstance() for this, which will eagerly set the site and + // thus use the correct process. + bool use_process_per_site = + RenderProcessHost::ShouldUseProcessPerSite(browser_context, dest_url) && + RenderProcessHostImpl::GetProcessHostForSite(browser_context, dest_url); + if (curr_site_instance->HasRelatedSiteInstance(dest_url) || + use_process_per_site) { + return curr_site_instance->GetRelatedSiteInstance(dest_url); + } + + // For extensions, Web UI URLs (such as the new tab page), and apps we do + // not want to use the curr_instance if it has no site, since it will have a + // RenderProcessHost of PRIV_NORMAL. Create a new SiteInstance for this + // URL instead (with the correct process type). + if (curr_site_instance->HasWrongProcessForURL(dest_url)) + return curr_site_instance->GetRelatedSiteInstance(dest_url); + + // View-source URLs must use a new SiteInstance and BrowsingInstance. + // TODO(nasko): This is the same condition as later in the function. This + // should be taken into account when refactoring this method as part of + // http://crbug.com/123007. + if (entry.IsViewSourceMode()) + return SiteInstance::CreateForURL(browser_context, dest_url); + + // If we are navigating from a blank SiteInstance to a WebUI, make sure we + // create a new SiteInstance. + if (WebUIControllerFactoryRegistry::GetInstance()->UseWebUIForURL( + browser_context, dest_url)) { + return SiteInstance::CreateForURL(browser_context, dest_url); + } + + // Normally the "site" on the SiteInstance is set lazily when the load + // actually commits. This is to support better process sharing in case + // the site redirects to some other site: we want to use the destination + // site in the site instance. + // + // In the case of session restore, as it loads all the pages immediately + // we need to set the site first, otherwise after a restore none of the + // pages would share renderers in process-per-site. + if (entry.restore_type() != NavigationEntryImpl::RESTORE_NONE) + curr_site_instance->SetSite(dest_url); + + return curr_site_instance; + } + + // Otherwise, only create a new SiteInstance for cross-site navigation. + + // TODO(creis): Once we intercept links and script-based navigations, we + // will be able to enforce that all entries in a SiteInstance actually have + // the same site, and it will be safe to compare the URL against the + // SiteInstance's site, as follows: + // const GURL& current_url = curr_instance->site(); + // For now, though, we're in a hybrid model where you only switch + // SiteInstances if you type in a cross-site URL. This means we have to + // compare the entry's URL to the last committed entry's URL. + NavigationEntry* curr_entry = controller.GetLastCommittedEntry(); + if (interstitial_page_) { + // The interstitial is currently the last committed entry, but we want to + // compare against the last non-interstitial entry. + curr_entry = controller.GetEntryAtOffset(-1); + } + // If there is no last non-interstitial entry (and curr_instance already + // has a site), then we must have been opened from another tab. We want + // to compare against the URL of the page that opened us, but we can't + // get to it directly. The best we can do is check against the site of + // the SiteInstance. This will be correct when we intercept links and + // script-based navigations, but for now, it could place some pages in a + // new process unnecessarily. We should only hit this case if a page tries + // to open a new tab to an interstitial-inducing URL, and then navigates + // the page to a different same-site URL. (This seems very unlikely in + // practice.) + const GURL& current_url = (curr_entry) ? curr_entry->GetURL() : + curr_instance->GetSiteURL(); + + // View-source URLs must use a new SiteInstance and BrowsingInstance. + // TODO(creis): Refactor this method so this duplicated code isn't needed. + // See http://crbug.com/123007. + if (curr_entry && + curr_entry->IsViewSourceMode() != entry.IsViewSourceMode()) { + return SiteInstance::CreateForURL(browser_context, dest_url); + } + + // Use the current SiteInstance for same site navigations, as long as the + // process type is correct. (The URL may have been installed as an app since + // the last time we visited it.) + if (SiteInstance::IsSameWebSite(browser_context, current_url, dest_url) && + !static_cast<SiteInstanceImpl*>(curr_instance)->HasWrongProcessForURL( + dest_url)) { + return curr_instance; + } else if (ShouldSwapProcessesForNavigation(curr_entry, &entry)) { + // When we're swapping, we need to force the site instance AND browsing + // instance to be different ones. This addresses special cases where we use + // a single BrowsingInstance for all pages of a certain type (e.g., New Tab + // Pages), keeping them in the same process. When you navigate away from + // that page, we want to explicity ignore that BrowsingInstance and group + // this page into the appropriate SiteInstance for its URL. + return SiteInstance::CreateForURL(browser_context, dest_url); + } else { + // Start the new renderer in a new SiteInstance, but in the current + // BrowsingInstance. It is important to immediately give this new + // SiteInstance to a RenderViewHost (if it is different than our current + // SiteInstance), so that it is ref counted. This will happen in + // CreateRenderView. + return curr_instance->GetRelatedSiteInstance(dest_url); + } +} + +int RenderViewHostManager::CreateRenderView( + SiteInstance* instance, + int opener_route_id, + bool swapped_out, + bool hidden) { + CHECK(instance); + DCHECK(!swapped_out || hidden); // Swapped out views should always be hidden. + + // Check if we've already created an RVH for this SiteInstance. If so, try + // to re-use the existing one, which has already been initialized. We'll + // remove it from the list of swapped out hosts if it commits. + RenderViewHostImpl* new_render_view_host = static_cast<RenderViewHostImpl*>( + GetSwappedOutRenderViewHost(instance)); + if (new_render_view_host) { + // Prevent the process from exiting while we're trying to use it. + if (!swapped_out) + new_render_view_host->GetProcess()->AddPendingView(); + } else { + // Create a new RenderViewHost if we don't find an existing one. + new_render_view_host = static_cast<RenderViewHostImpl*>( + RenderViewHostFactory::Create(instance, + render_view_delegate_, + render_widget_delegate_, + MSG_ROUTING_NONE, + MSG_ROUTING_NONE, + swapped_out, + hidden)); + + // If the new RVH is swapped out already, store it. Otherwise prevent the + // process from exiting while we're trying to navigate in it. + if (swapped_out) { + swapped_out_hosts_[instance->GetId()] = new_render_view_host; + } else { + new_render_view_host->GetProcess()->AddPendingView(); + } + + bool success = InitRenderView(new_render_view_host, opener_route_id); + if (success) { + // Don't show the view until we get a DidNavigate from it. + new_render_view_host->GetView()->Hide(); + } else if (!swapped_out) { + CancelPending(); + } + } + + // Use this as our new pending RVH if it isn't swapped out. + if (!swapped_out) + pending_render_view_host_ = new_render_view_host; + + return new_render_view_host->GetRoutingID(); +} + +bool RenderViewHostManager::InitRenderView(RenderViewHost* render_view_host, + int opener_route_id) { + // If the pending navigation is to a WebUI and the RenderView is not in a + // guest process, tell the RenderView about any bindings it will need enabled. + if (pending_web_ui() && !render_view_host->GetProcess()->IsGuest()) + render_view_host->AllowBindings(pending_web_ui()->GetBindings()); + + return delegate_->CreateRenderViewForRenderManager(render_view_host, + opener_route_id); +} + +void RenderViewHostManager::CommitPending() { + // First check whether we're going to want to focus the location bar after + // this commit. We do this now because the navigation hasn't formally + // committed yet, so if we've already cleared |pending_web_ui_| the call chain + // this triggers won't be able to figure out what's going on. + bool will_focus_location_bar = delegate_->FocusLocationBarByDefault(); + + // We currently can't guarantee that the renderer isn't showing a new modal + // dialog, even though we canceled them in SwapOutOldPage. (It may have + // created another in the meantime.) Make sure we run and reset the callback + // now before we delete its RVH below. + // TODO(creis): Remove this if we can guarantee that no new dialogs will be + // shown after SwapOutOldPage. See http://crbug.com/312490. + delegate_->CancelModalDialogsForRenderManager(); + + // Next commit the Web UI, if any. Either replace |web_ui_| with + // |pending_web_ui_|, or clear |web_ui_| if there is no pending WebUI, or + // leave |web_ui_| as is if reusing it. + DCHECK(!(pending_web_ui_.get() && pending_and_current_web_ui_.get())); + if (pending_web_ui_) + web_ui_.reset(pending_web_ui_.release()); + else if (!pending_and_current_web_ui_.get()) + web_ui_.reset(); + + // It's possible for the pending_render_view_host_ to be NULL when we aren't + // crossing process boundaries. If so, we just needed to handle the Web UI + // committing above and we're done. + if (!pending_render_view_host_) { + if (will_focus_location_bar) + delegate_->SetFocusToLocationBar(false); + return; + } + + // Remember if the page was focused so we can focus the new renderer in + // that case. + bool focus_render_view = !will_focus_location_bar && + render_view_host_->GetView() && render_view_host_->GetView()->HasFocus(); + + // Swap in the pending view and make it active. Also ensure the FrameTree + // stays in sync. + RenderViewHostImpl* old_render_view_host = render_view_host_; + render_view_host_ = pending_render_view_host_; + pending_render_view_host_ = NULL; + render_view_host_->AttachToFrameTree(); + + // The process will no longer try to exit, so we can decrement the count. + render_view_host_->GetProcess()->RemovePendingView(); + + // If the view is gone, then this RenderViewHost died while it was hidden. + // We ignored the RenderProcessGone call at the time, so we should send it now + // to make sure the sad tab shows up, etc. + if (!render_view_host_->GetView()) + delegate_->RenderProcessGoneFromRenderManager(render_view_host_); + else if (!delegate_->IsHidden()) + render_view_host_->GetView()->Show(); + + // Hide the old view now that the new one is visible. + if (old_render_view_host->GetView()) { + old_render_view_host->GetView()->Hide(); + old_render_view_host->WasSwappedOut(); + } + + // Make sure the size is up to date. (Fix for bug 1079768.) + delegate_->UpdateRenderViewSizeForRenderManager(); + + if (will_focus_location_bar) + delegate_->SetFocusToLocationBar(false); + else if (focus_render_view && render_view_host_->GetView()) + RenderWidgetHostViewPort::FromRWHV(render_view_host_->GetView())->Focus(); + + // Notify that we've swapped RenderViewHosts. We do this + // before shutting down the RVH so that we can clean up + // RendererResources related to the RVH first. + delegate_->NotifySwappedFromRenderManager(old_render_view_host, + render_view_host_); + + // If the pending view was on the swapped out list, we can remove it. + swapped_out_hosts_.erase(render_view_host_->GetSiteInstance()->GetId()); + + // If there are no active RVHs in this SiteInstance, it means that + // this RVH was the last active one in the SiteInstance. Now that we + // know that all RVHs are swapped out, we can delete all the RVHs in + // this SiteInstance. + if (!static_cast<SiteInstanceImpl*>(old_render_view_host->GetSiteInstance())-> + active_view_count()) { + ShutdownRenderViewHostsInSiteInstance( + old_render_view_host->GetSiteInstance()->GetId()); + // This is deleted while cleaning up the SitaInstance's views. + old_render_view_host = NULL; + } else if (old_render_view_host->IsRenderViewLive()) { + // If the old RVH is live, we are swapping it out and should keep track of + // it in case we navigate back to it. + DCHECK(old_render_view_host->is_swapped_out()); + // Temp fix for http://crbug.com/90867 until we do a better cleanup to make + // sure we don't get different rvh instances for the same site instance + // in the same rvhmgr. + // TODO(creis): Clean this up. + int32 old_site_instance_id = + old_render_view_host->GetSiteInstance()->GetId(); + RenderViewHostMap::iterator iter = + swapped_out_hosts_.find(old_site_instance_id); + if (iter != swapped_out_hosts_.end() && + iter->second != old_render_view_host) { + // Shutdown the RVH that will be replaced in the map to avoid a leak. + iter->second->Shutdown(); + } + swapped_out_hosts_[old_site_instance_id] = old_render_view_host; + } else { + old_render_view_host->Shutdown(); + old_render_view_host = NULL; // Shutdown() deletes it. + } +} + +void RenderViewHostManager::ShutdownRenderViewHostsInSiteInstance( + int32 site_instance_id) { + // First remove any swapped out RVH for this SiteInstance from our + // list. + swapped_out_hosts_.erase(site_instance_id); + + scoped_ptr<RenderWidgetHostIterator> widgets( + RenderWidgetHostImpl::GetAllRenderWidgetHosts()); + while (RenderWidgetHost* widget = widgets->GetNextHost()) { + if (!widget->IsRenderView()) + continue; + RenderViewHostImpl* rvh = + static_cast<RenderViewHostImpl*>(RenderViewHost::From(widget)); + if (site_instance_id == rvh->GetSiteInstance()->GetId()) + rvh->Shutdown(); + } +} + +RenderViewHostImpl* RenderViewHostManager::UpdateRendererStateForNavigate( + const NavigationEntryImpl& entry) { + // If we are cross-navigating, then we want to get back to normal and navigate + // as usual. + if (cross_navigation_pending_) { + if (pending_render_view_host_) + CancelPending(); + cross_navigation_pending_ = false; + } + + // render_view_host_ will not be deleted before the end of this method, so we + // don't have to worry about this SiteInstance's ref count dropping to zero. + SiteInstance* curr_instance = render_view_host_->GetSiteInstance(); + + // Determine if we need a new SiteInstance for this entry. + // Again, new_instance won't be deleted before the end of this method, so it + // is safe to use a normal pointer here. + SiteInstance* new_instance = curr_instance; + const NavigationEntry* curr_entry = + delegate_->GetLastCommittedNavigationEntryForRenderManager(); + bool is_guest_scheme = curr_instance->GetSiteURL().SchemeIs(kGuestScheme); + bool force_swap = ShouldSwapProcessesForNavigation(curr_entry, &entry); + if (!is_guest_scheme && (ShouldTransitionCrossSite() || force_swap)) + new_instance = GetSiteInstanceForEntry(entry, curr_instance); + + if (!is_guest_scheme && (new_instance != curr_instance || force_swap)) { + // New SiteInstance. + DCHECK(!cross_navigation_pending_); + + // This will possibly create (set to NULL) a Web UI object for the pending + // page. We'll use this later to give the page special access. This must + // happen before the new renderer is created below so it will get bindings. + // It must also happen after the above conditional call to CancelPending(), + // otherwise CancelPending may clear the pending_web_ui_ and the page will + // not have its bindings set appropriately. + SetPendingWebUI(entry); + + // Ensure that we have created RVHs for the new RVH's opener chain if + // we are staying in the same BrowsingInstance. This allows the pending RVH + // to send cross-process script calls to its opener(s). + int opener_route_id = MSG_ROUTING_NONE; + if (new_instance->IsRelatedSiteInstance(curr_instance)) { + opener_route_id = + delegate_->CreateOpenerRenderViewsForRenderManager(new_instance); + } + + // Create a non-swapped-out pending RVH with the given opener and navigate + // it. + int route_id = CreateRenderView(new_instance, opener_route_id, false, + delegate_->IsHidden()); + if (route_id == MSG_ROUTING_NONE) + return NULL; + + // Check if our current RVH is live before we set up a transition. + if (!render_view_host_->IsRenderViewLive()) { + if (!cross_navigation_pending_) { + // The current RVH is not live. There's no reason to sit around with a + // sad tab or a newly created RVH while we wait for the pending RVH to + // navigate. Just switch to the pending RVH now and go back to non + // cross-navigating (Note that we don't care about on{before}unload + // handlers if the current RVH isn't live.) + CommitPending(); + return render_view_host_; + } else { + NOTREACHED(); + return render_view_host_; + } + } + // Otherwise, it's safe to treat this as a pending cross-site transition. + + // We need to wait until the beforeunload handler has run, unless we are + // transferring an existing request (in which case it has already run). + // Suspend the new render view (i.e., don't let it send the cross-site + // Navigate message) until we hear back from the old renderer's + // beforeunload handler. If the handler returns false, we'll have to + // cancel the request. + DCHECK(!pending_render_view_host_->are_navigations_suspended()); + bool is_transfer = + entry.transferred_global_request_id() != GlobalRequestID(); + if (is_transfer) { + // We don't need to stop the old renderer or run beforeunload/unload + // handlers, because those have already been done. + DCHECK(pending_nav_params_->global_request_id == + entry.transferred_global_request_id()); + } else { + // Also make sure the old render view stops, in case a load is in + // progress. (We don't want to do this for transfers, since it will + // interrupt the transfer with an unexpected DidStopLoading.) + render_view_host_->Send( + new ViewMsg_Stop(render_view_host_->GetRoutingID())); + + pending_render_view_host_->SetNavigationsSuspended(true, + base::TimeTicks()); + + // Tell the CrossSiteRequestManager that this RVH has a pending cross-site + // request, so that ResourceDispatcherHost will know to tell us to run the + // old page's unload handler before it sends the response. + pending_render_view_host_->SetHasPendingCrossSiteRequest(true); + } + + // We now have a pending RVH. + DCHECK(!cross_navigation_pending_); + cross_navigation_pending_ = true; + + // Unless we are transferring an existing request, we should now + // tell the old render view to run its beforeunload handler, since it + // doesn't otherwise know that the cross-site request is happening. This + // will trigger a call to ShouldClosePage with the reply. + if (!is_transfer) + render_view_host_->FirePageBeforeUnload(true); + + return pending_render_view_host_; + } else { + if (ShouldReuseWebUI(curr_entry, &entry)) { + pending_web_ui_.reset(); + pending_and_current_web_ui_ = web_ui_->AsWeakPtr(); + } else { + SetPendingWebUI(entry); + + // Make sure the new RenderViewHost has the right bindings. + if (pending_web_ui() && !render_view_host_->GetProcess()->IsGuest()) + render_view_host_->AllowBindings(pending_web_ui()->GetBindings()); + } + + if (pending_web_ui() && render_view_host_->IsRenderViewLive()) + pending_web_ui()->GetController()->RenderViewReused(render_view_host_); + + // The renderer can exit view source mode when any error or cancellation + // happen. We must overwrite to recover the mode. + if (entry.IsViewSourceMode()) { + render_view_host_->Send( + new ViewMsg_EnableViewSourceMode(render_view_host_->GetRoutingID())); + } + } + + // Same SiteInstance can be used. Navigate render_view_host_ if we are not + // cross navigating. + DCHECK(!cross_navigation_pending_); + return render_view_host_; +} + +void RenderViewHostManager::CancelPending() { + RenderViewHostImpl* pending_render_view_host = pending_render_view_host_; + pending_render_view_host_ = NULL; + + RenderViewDevToolsAgentHost::OnCancelPendingNavigation( + pending_render_view_host, + render_view_host_); + + // We no longer need to prevent the process from exiting. + pending_render_view_host->GetProcess()->RemovePendingView(); + + // The pending RVH may already be on the swapped out list if we started to + // swap it back in and then canceled. If so, make sure it gets swapped out + // again. If it's not on the swapped out list (e.g., aborting a pending + // load), then it's safe to shut down. + if (IsOnSwappedOutList(pending_render_view_host)) { + // Any currently suspended navigations are no longer needed. + pending_render_view_host->CancelSuspendedNavigations(); + + pending_render_view_host->SwapOut(); + } else { + // We won't be coming back, so shut this one down. + pending_render_view_host->Shutdown(); + } + + pending_web_ui_.reset(); + pending_and_current_web_ui_.reset(); +} + +void RenderViewHostManager::RenderViewDeleted(RenderViewHost* rvh) { + // We are doing this in order to work around and to track a crasher + // (http://crbug.com/23411) where it seems that pending_render_view_host_ is + // deleted (not sure from where) but not NULLed. + if (rvh == pending_render_view_host_) { + // If you hit this NOTREACHED, please report it in the following bug + // http://crbug.com/23411 Make sure to include what you were doing when it + // happened (navigating to a new page, closing a tab...) and if you can + // reproduce. + NOTREACHED(); + pending_render_view_host_ = NULL; + } + + // Make sure deleted RVHs are not kept in the swapped out list while we are + // still alive. (If render_view_host_ is null, we're already being deleted.) + if (!render_view_host_) + return; + // We can't look it up by SiteInstance ID, which may no longer be valid. + for (RenderViewHostMap::iterator iter = swapped_out_hosts_.begin(); + iter != swapped_out_hosts_.end(); + ++iter) { + if (iter->second == rvh) { + swapped_out_hosts_.erase(iter); + break; + } + } +} + +bool RenderViewHostManager::IsOnSwappedOutList(RenderViewHost* rvh) const { + if (!rvh->GetSiteInstance()) + return false; + + RenderViewHostMap::const_iterator iter = swapped_out_hosts_.find( + rvh->GetSiteInstance()->GetId()); + if (iter == swapped_out_hosts_.end()) + return false; + + return iter->second == rvh; +} + +RenderViewHostImpl* RenderViewHostManager::GetSwappedOutRenderViewHost( + SiteInstance* instance) { + RenderViewHostMap::iterator iter = swapped_out_hosts_.find(instance->GetId()); + if (iter != swapped_out_hosts_.end()) + return iter->second; + + return NULL; +} + +} // namespace content diff --git a/content/browser/frame_host/render_view_host_manager.h b/content/browser/frame_host/render_view_host_manager.h new file mode 100644 index 0000000..377758f --- /dev/null +++ b/content/browser/frame_host/render_view_host_manager.h @@ -0,0 +1,397 @@ +// Copyright 2013 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_FRAME_HOST_RENDER_VIEW_HOST_MANAGER_H_ +#define CONTENT_BROWSER_FRAME_HOST_RENDER_VIEW_HOST_MANAGER_H_ + +#include "base/basictypes.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "content/browser/renderer_host/render_view_host_delegate.h" +#include "content/browser/site_instance_impl.h" +#include "content/common/content_export.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "content/public/common/referrer.h" + + +namespace content { +class BrowserContext; +class InterstitialPageImpl; +class NavigationControllerImpl; +class NavigationEntry; +class NavigationEntryImpl; +class RenderViewHost; +class RenderViewHostImpl; +class RenderViewHostManagerTest; +class RenderWidgetHostDelegate; +class RenderWidgetHostView; +class TestWebContents; +class WebUIImpl; + +// Manages RenderViewHosts for a WebContentsImpl. Normally there is only one and +// it is easy to do. But we can also have transitions of processes (and hence +// RenderViewHosts) that can get complex. +class CONTENT_EXPORT RenderViewHostManager + : public RenderViewHostDelegate::RendererManagement, + public NotificationObserver { + public: + // Functions implemented by our owner that we need. + // + // TODO(brettw) Clean this up! These are all the functions in WebContentsImpl + // that are required to run this class. The design should probably be better + // such that these are more clear. + // + // There is additional complexity that some of the functions we need in + // WebContentsImpl are inherited and non-virtual. These are named with + // "RenderManager" so that the duplicate implementation of them will be clear. + class CONTENT_EXPORT Delegate { + public: + // Initializes the given renderer if necessary and creates the view ID + // corresponding to this view host. If this method is not called and the + // process is not shared, then the WebContentsImpl will act as though the + // renderer is not running (i.e., it will render "sad tab"). This method is + // automatically called from LoadURL. + // + // If you are attaching to an already-existing RenderView, you should call + // InitWithExistingID. + virtual bool CreateRenderViewForRenderManager( + RenderViewHost* render_view_host, int opener_route_id) = 0; + virtual void BeforeUnloadFiredFromRenderManager( + bool proceed, const base::TimeTicks& proceed_time, + bool* proceed_to_fire_unload) = 0; + virtual void RenderProcessGoneFromRenderManager( + RenderViewHost* render_view_host) = 0; + virtual void UpdateRenderViewSizeForRenderManager() = 0; + virtual void CancelModalDialogsForRenderManager() = 0; + virtual void NotifySwappedFromRenderManager( + RenderViewHost* old_host, RenderViewHost* new_host) = 0; + virtual NavigationControllerImpl& + GetControllerForRenderManager() = 0; + + // Create swapped out RenderViews in the given SiteInstance for each tab in + // the opener chain of this tab, if any. This allows the current tab to + // make cross-process script calls to its opener(s). Returns the route ID + // of the immediate opener, if one exists (otherwise MSG_ROUTING_NONE). + virtual int CreateOpenerRenderViewsForRenderManager( + SiteInstance* instance) = 0; + + // Creates a WebUI object for the given URL if one applies. Ownership of the + // returned pointer will be passed to the caller. If no WebUI applies, + // returns NULL. + virtual WebUIImpl* CreateWebUIForRenderManager(const GURL& url) = 0; + + // Returns the navigation entry of the current navigation, or NULL if there + // is none. + virtual NavigationEntry* + GetLastCommittedNavigationEntryForRenderManager() = 0; + + // Returns true if the location bar should be focused by default rather than + // the page contents. The view calls this function when the tab is focused + // to see what it should do. + virtual bool FocusLocationBarByDefault() = 0; + + // Focuses the location bar. + virtual void SetFocusToLocationBar(bool select_all) = 0; + + // Creates a view and sets the size for the specified RVH. + virtual void CreateViewAndSetSizeForRVH(RenderViewHost* rvh) = 0; + + // Returns true if views created for this delegate should be created in a + // hidden state. + virtual bool IsHidden() = 0; + + protected: + virtual ~Delegate() {} + }; + + // All three delegate pointers must be non-NULL and are not owned by this + // class. They must outlive this class. The RenderViewHostDelegate and + // RenderWidgetHostDelegate are what will be installed into all + // RenderViewHosts that are created. + // + // You must call Init() before using this class. + RenderViewHostManager( + RenderViewHostDelegate* render_view_delegate, + RenderWidgetHostDelegate* render_widget_delegate, + Delegate* delegate); + virtual ~RenderViewHostManager(); + + // For arguments, see WebContentsImpl constructor. + void Init(BrowserContext* browser_context, + SiteInstance* site_instance, + int routing_id, + int main_frame_routing_id); + + // Returns the currently active RenderViewHost. + // + // This will be non-NULL between Init() and Shutdown(). You may want to NULL + // check it in many cases, however. Windows can send us messages during the + // destruction process after it has been shut down. + RenderViewHostImpl* current_host() const; + + // Returns the view associated with the current RenderViewHost, or NULL if + // there is no current one. + RenderWidgetHostView* GetRenderWidgetHostView() const; + + // Returns the pending render view host, or NULL if there is no pending one. + RenderViewHostImpl* pending_render_view_host() const; + + // Returns the current committed Web UI or NULL if none applies. + WebUIImpl* web_ui() const { return web_ui_.get(); } + + // Returns the Web UI for the pending navigation, or NULL of none applies. + WebUIImpl* pending_web_ui() const { + return pending_web_ui_.get() ? pending_web_ui_.get() : + pending_and_current_web_ui_.get(); + } + + // Sets the pending Web UI for the pending navigation, ensuring that the + // bindings are appropriate for the given NavigationEntry. + void SetPendingWebUI(const NavigationEntryImpl& entry); + + // Called when we want to instruct the renderer to navigate to the given + // navigation entry. It may create a new RenderViewHost or re-use an existing + // one. The RenderViewHost to navigate will be returned. Returns NULL if one + // could not be created. + RenderViewHostImpl* Navigate(const NavigationEntryImpl& entry); + + // Instructs the various live views to stop. Called when the user directed the + // page to stop loading. + void Stop(); + + // Notifies the regular and pending RenderViewHosts that a load is or is not + // happening. Even though the message is only for one of them, we don't know + // which one so we tell both. + void SetIsLoading(bool is_loading); + + // Whether to close the tab or not when there is a hang during an unload + // handler. If we are mid-crosssite navigation, then we should proceed + // with the navigation instead of closing the tab. + bool ShouldCloseTabOnUnresponsiveRenderer(); + + // The RenderViewHost has been swapped out, so we should resume the pending + // network response and allow the pending RenderViewHost to commit. + void SwappedOut(RenderViewHost* render_view_host); + + // Called when a renderer's main frame navigates. + void DidNavigateMainFrame(RenderViewHost* render_view_host); + + // Called when a renderer sets its opener to null. + void DidDisownOpener(RenderViewHost* render_view_host); + + // Helper method to create a RenderViewHost. If |swapped_out| is true, it + // will be initially placed on the swapped out hosts list. Otherwise, it + // will be used for a pending cross-site navigation. + int CreateRenderView(SiteInstance* instance, + int opener_route_id, + bool swapped_out, + bool hidden); + + // Called when a provisional load on the given renderer is aborted. + void RendererAbortedProvisionalLoad(RenderViewHost* render_view_host); + + // Sets the passed passed interstitial as the currently showing interstitial. + // |interstitial_page| should be non NULL (use the remove_interstitial_page + // method to unset the interstitial) and no interstitial page should be set + // when there is already a non NULL interstitial page set. + void set_interstitial_page(InterstitialPageImpl* interstitial_page) { + DCHECK(!interstitial_page_ && interstitial_page); + interstitial_page_ = interstitial_page; + } + + // Unsets the currently showing interstitial. + void remove_interstitial_page() { + DCHECK(interstitial_page_); + interstitial_page_ = NULL; + } + + // Returns the currently showing interstitial, NULL if no interstitial is + // showing. + InterstitialPageImpl* interstitial_page() const { return interstitial_page_; } + + // RenderViewHostDelegate::RendererManagement implementation. + virtual void ShouldClosePage( + bool for_cross_site_transition, + bool proceed, + const base::TimeTicks& proceed_time) OVERRIDE; + virtual void OnCrossSiteResponse( + RenderViewHost* pending_render_view_host, + const GlobalRequestID& global_request_id, + bool is_transfer, + const std::vector<GURL>& transfer_url_chain, + const Referrer& referrer, + PageTransition page_transition, + int64 frame_id) OVERRIDE; + + // NotificationObserver implementation. + virtual void Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) OVERRIDE; + + // Called when a RenderViewHost is about to be deleted. + void RenderViewDeleted(RenderViewHost* rvh); + + // Returns whether the given RenderViewHost is on the list of swapped out + // RenderViewHosts. + bool IsOnSwappedOutList(RenderViewHost* rvh) const; + + // Returns the swapped out RenderViewHost for the given SiteInstance, if any. + RenderViewHostImpl* GetSwappedOutRenderViewHost(SiteInstance* instance); + + // Runs the unload handler in the current page, when we know that a pending + // cross-process navigation is going to commit. We may initiate a transfer + // to a new process after this completes or times out. + void SwapOutOldPage(); + + private: + friend class RenderViewHostManagerTest; + friend class TestWebContents; + + // Tracks information about a navigation while a cross-process transition is + // in progress, in case we need to transfer it to a new RenderViewHost. + struct PendingNavigationParams { + PendingNavigationParams(); + PendingNavigationParams(const GlobalRequestID& global_request_id, + bool is_transfer, + const std::vector<GURL>& transfer_url, + Referrer referrer, + PageTransition page_transition, + int64 frame_id); + ~PendingNavigationParams(); + + // The child ID and request ID for the pending navigation. Present whether + // |is_transfer| is true or false. + GlobalRequestID global_request_id; + + // Whether this pending navigation needs to be transferred to another + // process than the one it was going to commit in. If so, the + // |transfer_url|, |referrer|, and |frame_id| parameters will be set. + bool is_transfer; + + // If |is_transfer|, this is the URL chain of the request. The first entry + // is the original request URL, and the last entry is the destination URL to + // request in the new process. + std::vector<GURL> transfer_url_chain; + + // If |is_transfer|, this is the referrer to use for the request in the new + // process. + Referrer referrer; + + // If |is_transfer|, this is the transition type for the original + // navigation. + PageTransition page_transition; + + // If |is_transfer|, this is the frame ID to use in RequestTransferURL. + int64 frame_id; + }; + + // Returns whether this tab should transition to a new renderer for + // cross-site URLs. Enabled unless we see the --process-per-tab command line + // switch. Can be overridden in unit tests. + bool ShouldTransitionCrossSite(); + + // Returns true if the two navigation entries are incompatible in some way + // other than site instances. Cases where this can happen include Web UI + // to regular web pages. It will cause us to swap RenderViewHosts (and hence + // RenderProcessHosts) even if the site instance would otherwise be the same. + // As part of this, we'll also force new SiteInstances and BrowsingInstances. + // Either of the entries may be NULL. + bool ShouldSwapProcessesForNavigation( + const NavigationEntry* curr_entry, + const NavigationEntryImpl* new_entry) const; + + bool ShouldReuseWebUI( + const NavigationEntry* curr_entry, + const NavigationEntryImpl* new_entry) const; + + // Returns an appropriate SiteInstance object for the given NavigationEntry, + // possibly reusing the current SiteInstance. + // Never called if --process-per-tab is used. + SiteInstance* GetSiteInstanceForEntry( + const NavigationEntryImpl& entry, + SiteInstance* curr_instance); + + // Sets up the necessary state for a new RenderViewHost with the given opener. + bool InitRenderView(RenderViewHost* render_view_host, int opener_route_id); + + // Sets the pending RenderViewHost/WebUI to be the active one. Note that this + // doesn't require the pending render_view_host_ pointer to be non-NULL, since + // there could be Web UI switching as well. Call this for every commit. + void CommitPending(); + + // Shutdown all RenderViewHosts in a SiteInstance. This is called + // to shutdown views when all the views in a SiteInstance are + // confirmed to be swapped out. + void ShutdownRenderViewHostsInSiteInstance(int32 site_instance_id); + + // Helper method to terminate the pending RenderViewHost. + void CancelPending(); + + RenderViewHostImpl* UpdateRendererStateForNavigate( + const NavigationEntryImpl& entry); + + // Called when a renderer process is starting to close. We should not + // schedule new navigations in its swapped out RenderViewHosts after this. + void RendererProcessClosing(RenderProcessHost* render_process_host); + + // Our delegate, not owned by us. Guaranteed non-NULL. + Delegate* delegate_; + + // Whether a navigation requiring different RenderView's is pending. This is + // either cross-site request is (in the new process model), or when required + // for the view type (like view source versus not). + bool cross_navigation_pending_; + + // Implemented by the owner of this class, these delegates are installed into + // all the RenderViewHosts that we create. + RenderViewHostDelegate* render_view_delegate_; + RenderWidgetHostDelegate* render_widget_delegate_; + + // Our RenderView host and its associated Web UI (if any, will be NULL for + // non-DOM-UI pages). This object is responsible for all communication with + // a child RenderView instance. + RenderViewHostImpl* render_view_host_; + scoped_ptr<WebUIImpl> web_ui_; + + // A RenderViewHost used to load a cross-site page. This remains hidden + // while a cross-site request is pending until it calls DidNavigate. It may + // have an associated Web UI, in which case the Web UI pointer will be non- + // NULL. + // + // The |pending_web_ui_| may be non-NULL even when the + // |pending_render_view_host_| is NULL. This will happen when we're + // transitioning between two Web UI pages: the RVH won't be swapped, so the + // pending pointer will be unused, but there will be a pending Web UI + // associated with the navigation. + RenderViewHostImpl* pending_render_view_host_; + + // Tracks information about any current pending cross-process navigation. + scoped_ptr<PendingNavigationParams> pending_nav_params_; + + // If either of these is non-NULL, the pending navigation is to a chrome: + // page. The scoped_ptr is used if pending_web_ui_ != web_ui_, the WeakPtr is + // used for when they reference the same object. If either is non-NULL, the + // other should be NULL. + scoped_ptr<WebUIImpl> pending_web_ui_; + base::WeakPtr<WebUIImpl> pending_and_current_web_ui_; + + // A map of site instance ID to swapped out RenderViewHosts. This may include + // pending_render_view_host_ for navigations to existing entries. + typedef base::hash_map<int32, RenderViewHostImpl*> RenderViewHostMap; + RenderViewHostMap swapped_out_hosts_; + + // The intersitial page currently shown if any, not own by this class + // (the InterstitialPage is self-owned, it deletes itself when hidden). + InterstitialPageImpl* interstitial_page_; + + NotificationRegistrar registrar_; + + DISALLOW_COPY_AND_ASSIGN(RenderViewHostManager); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_RENDER_VIEW_HOST_MANAGER_H_ diff --git a/content/browser/frame_host/render_view_host_manager_unittest.cc b/content/browser/frame_host/render_view_host_manager_unittest.cc new file mode 100644 index 0000000..f55de8a --- /dev/null +++ b/content/browser/frame_host/render_view_host_manager_unittest.cc @@ -0,0 +1,1312 @@ +// Copyright 2013 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/strings/utf_string_conversions.h" +#include "content/browser/frame_host/navigation_controller_impl.h" +#include "content/browser/frame_host/navigation_entry_impl.h" +#include "content/browser/frame_host/render_view_host_manager.h" +#include "content/browser/renderer_host/test_render_view_host.h" +#include "content/browser/site_instance_impl.h" +#include "content/browser/webui/web_ui_controller_factory_registry.h" +#include "content/common/view_messages.h" +#include "content/public/browser/notification_details.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_source.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/render_widget_host_iterator.h" +#include "content/public/browser/web_contents_delegate.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_ui_controller.h" +#include "content/public/common/bindings_policy.h" +#include "content/public/common/javascript_message_type.h" +#include "content/public/common/page_transition_types.h" +#include "content/public/common/url_constants.h" +#include "content/public/common/url_utils.h" +#include "content/public/test/mock_render_process_host.h" +#include "content/public/test/test_notification_tracker.h" +#include "content/test/test_content_browser_client.h" +#include "content/test/test_content_client.h" +#include "content/test/test_web_contents.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { +namespace { + +class RenderViewHostManagerTestWebUIControllerFactory + : public WebUIControllerFactory { + public: + RenderViewHostManagerTestWebUIControllerFactory() + : should_create_webui_(false) { + } + virtual ~RenderViewHostManagerTestWebUIControllerFactory() {} + + void set_should_create_webui(bool should_create_webui) { + should_create_webui_ = should_create_webui; + } + + // WebUIFactory implementation. + virtual WebUIController* CreateWebUIControllerForURL( + WebUI* web_ui, const GURL& url) const OVERRIDE { + if (!(should_create_webui_ && HasWebUIScheme(url))) + return NULL; + return new WebUIController(web_ui); + } + + virtual WebUI::TypeID GetWebUIType(BrowserContext* browser_context, + const GURL& url) const OVERRIDE { + return WebUI::kNoWebUI; + } + + virtual bool UseWebUIForURL(BrowserContext* browser_context, + const GURL& url) const OVERRIDE { + return HasWebUIScheme(url); + } + + virtual bool UseWebUIBindingsForURL(BrowserContext* browser_context, + const GURL& url) const OVERRIDE { + return HasWebUIScheme(url); + } + + private: + bool should_create_webui_; + + DISALLOW_COPY_AND_ASSIGN(RenderViewHostManagerTestWebUIControllerFactory); +}; + +class BeforeUnloadFiredWebContentsDelegate : public WebContentsDelegate { + public: + BeforeUnloadFiredWebContentsDelegate() {} + virtual ~BeforeUnloadFiredWebContentsDelegate() {} + + virtual void BeforeUnloadFired(WebContents* web_contents, + bool proceed, + bool* proceed_to_fire_unload) OVERRIDE { + *proceed_to_fire_unload = proceed; + } + + private: + DISALLOW_COPY_AND_ASSIGN(BeforeUnloadFiredWebContentsDelegate); +}; + +} // namespace + +class RenderViewHostManagerTest + : public RenderViewHostImplTestHarness { + public: + virtual void SetUp() OVERRIDE { + RenderViewHostImplTestHarness::SetUp(); + WebUIControllerFactory::RegisterFactory(&factory_); + } + + virtual void TearDown() OVERRIDE { + RenderViewHostImplTestHarness::TearDown(); + WebUIControllerFactory::UnregisterFactoryForTesting(&factory_); + } + + void set_should_create_webui(bool should_create_webui) { + factory_.set_should_create_webui(should_create_webui); + } + + void NavigateActiveAndCommit(const GURL& url) { + // Note: we navigate the active RenderViewHost because previous navigations + // won't have committed yet, so NavigateAndCommit does the wrong thing + // for us. + controller().LoadURL(url, Referrer(), PAGE_TRANSITION_LINK, std::string()); + TestRenderViewHost* old_rvh = test_rvh(); + + // Simulate the ShouldClose_ACK that is received from the current renderer + // for a cross-site navigation. + if (old_rvh != active_rvh()) + old_rvh->SendShouldCloseACK(true); + + // Commit the navigation with a new page ID. + int32 max_page_id = contents()->GetMaxPageIDForSiteInstance( + active_rvh()->GetSiteInstance()); + + // Simulate the SwapOut_ACK that fires if you commit a cross-site + // navigation. + if (old_rvh != active_rvh()) + old_rvh->OnSwappedOut(false); + + active_test_rvh()->SendNavigate(max_page_id + 1, url); + } + + bool ShouldSwapProcesses(RenderViewHostManager* manager, + const NavigationEntryImpl* cur_entry, + const NavigationEntryImpl* new_entry) const { + return manager->ShouldSwapProcessesForNavigation(cur_entry, new_entry); + } + + // Creates a test RenderViewHost that's swapped out. + TestRenderViewHost* CreateSwappedOutRenderViewHost() { + const GURL kChromeURL("chrome://foo"); + const GURL kDestUrl("http://www.google.com/"); + + // Navigate our first tab to a chrome url and then to the destination. + NavigateActiveAndCommit(kChromeURL); + TestRenderViewHost* ntp_rvh = static_cast<TestRenderViewHost*>( + contents()->GetRenderManagerForTesting()->current_host()); + + // Navigate to a cross-site URL. + contents()->GetController().LoadURL( + kDestUrl, Referrer(), PAGE_TRANSITION_LINK, std::string()); + EXPECT_TRUE(contents()->cross_navigation_pending()); + + // Manually increase the number of active views in the + // SiteInstance that ntp_rvh belongs to, to prevent it from being + // destroyed when it gets swapped out. + static_cast<SiteInstanceImpl*>(ntp_rvh->GetSiteInstance())-> + increment_active_view_count(); + + TestRenderViewHost* dest_rvh = static_cast<TestRenderViewHost*>( + contents()->GetRenderManagerForTesting()->pending_render_view_host()); + CHECK(dest_rvh); + EXPECT_NE(ntp_rvh, dest_rvh); + + // BeforeUnload finishes. + ntp_rvh->SendShouldCloseACK(true); + + // Assume SwapOutACK times out, so the dest_rvh proceeds and commits. + dest_rvh->SendNavigate(101, kDestUrl); + + EXPECT_TRUE(ntp_rvh->is_swapped_out()); + return ntp_rvh; + } + + private: + RenderViewHostManagerTestWebUIControllerFactory factory_; +}; + +// Tests that when you navigate from a chrome:// url to another page, and +// then do that same thing in another tab, that the two resulting pages have +// different SiteInstances, BrowsingInstances, and RenderProcessHosts. This is +// a regression test for bug 9364. +TEST_F(RenderViewHostManagerTest, NewTabPageProcesses) { + set_should_create_webui(true); + const GURL kChromeUrl("chrome://foo"); + const GURL kDestUrl("http://www.google.com/"); + + // Navigate our first tab to the chrome url and then to the destination, + // ensuring we grant bindings to the chrome URL. + NavigateActiveAndCommit(kChromeUrl); + EXPECT_TRUE(active_rvh()->GetEnabledBindings() & BINDINGS_POLICY_WEB_UI); + NavigateActiveAndCommit(kDestUrl); + + // Make a second tab. + scoped_ptr<TestWebContents> contents2( + TestWebContents::Create(browser_context(), NULL)); + + // Load the two URLs in the second tab. Note that the first navigation creates + // a RVH that's not pending (since there is no cross-site transition), so + // we use the committed one. + contents2->GetController().LoadURL( + kChromeUrl, Referrer(), PAGE_TRANSITION_LINK, std::string()); + TestRenderViewHost* ntp_rvh2 = static_cast<TestRenderViewHost*>( + contents2->GetRenderManagerForTesting()->current_host()); + EXPECT_FALSE(contents2->cross_navigation_pending()); + ntp_rvh2->SendNavigate(100, kChromeUrl); + + // The second one is the opposite, creating a cross-site transition and + // requiring a beforeunload ack. + contents2->GetController().LoadURL( + kDestUrl, Referrer(), PAGE_TRANSITION_LINK, std::string()); + EXPECT_TRUE(contents2->cross_navigation_pending()); + TestRenderViewHost* dest_rvh2 = static_cast<TestRenderViewHost*>( + contents2->GetRenderManagerForTesting()->pending_render_view_host()); + ASSERT_TRUE(dest_rvh2); + + ntp_rvh2->SendShouldCloseACK(true); + ntp_rvh2->OnSwappedOut(false); + dest_rvh2->SendNavigate(101, kDestUrl); + + // The two RVH's should be different in every way. + EXPECT_NE(active_rvh()->GetProcess(), dest_rvh2->GetProcess()); + EXPECT_NE(active_rvh()->GetSiteInstance(), dest_rvh2->GetSiteInstance()); + EXPECT_FALSE(active_rvh()->GetSiteInstance()->IsRelatedSiteInstance( + dest_rvh2->GetSiteInstance())); + + // Navigate both to the new tab page, and verify that they share a + // RenderProcessHost (not a SiteInstance). + NavigateActiveAndCommit(kChromeUrl); + + contents2->GetController().LoadURL( + kChromeUrl, Referrer(), PAGE_TRANSITION_LINK, std::string()); + dest_rvh2->SendShouldCloseACK(true); + dest_rvh2->OnSwappedOut(false); + static_cast<TestRenderViewHost*>(contents2->GetRenderManagerForTesting()-> + pending_render_view_host())->SendNavigate(102, kChromeUrl); + + EXPECT_NE(active_rvh()->GetSiteInstance(), + contents2->GetRenderViewHost()->GetSiteInstance()); + EXPECT_EQ(active_rvh()->GetSiteInstance()->GetProcess(), + contents2->GetRenderViewHost()->GetSiteInstance()->GetProcess()); +} + +// Ensure that the browser ignores most IPC messages that arrive from a +// RenderViewHost that has been swapped out. We do not want to take +// action on requests from a non-active renderer. The main exception is +// for synchronous messages, which cannot be ignored without leaving the +// renderer in a stuck state. See http://crbug.com/93427. +TEST_F(RenderViewHostManagerTest, FilterMessagesWhileSwappedOut) { + const GURL kChromeURL("chrome://foo"); + const GURL kDestUrl("http://www.google.com/"); + + // Navigate our first tab to a chrome url and then to the destination. + NavigateActiveAndCommit(kChromeURL); + TestRenderViewHost* ntp_rvh = static_cast<TestRenderViewHost*>( + contents()->GetRenderManagerForTesting()->current_host()); + + // Send an update title message and make sure it works. + const string16 ntp_title = ASCIIToUTF16("NTP Title"); + WebKit::WebTextDirection direction = WebKit::WebTextDirectionLeftToRight; + EXPECT_TRUE(ntp_rvh->OnMessageReceived( + ViewHostMsg_UpdateTitle(rvh()->GetRoutingID(), 0, ntp_title, direction))); + EXPECT_EQ(ntp_title, contents()->GetTitle()); + + // Navigate to a cross-site URL. + contents()->GetController().LoadURL( + kDestUrl, Referrer(), PAGE_TRANSITION_LINK, std::string()); + EXPECT_TRUE(contents()->cross_navigation_pending()); + TestRenderViewHost* dest_rvh = static_cast<TestRenderViewHost*>( + contents()->GetRenderManagerForTesting()->pending_render_view_host()); + ASSERT_TRUE(dest_rvh); + EXPECT_NE(ntp_rvh, dest_rvh); + + // Create one more view in the same SiteInstance where dest_rvh2 + // exists so that it doesn't get deleted on navigation to another + // site. + static_cast<SiteInstanceImpl*>(ntp_rvh->GetSiteInstance())-> + increment_active_view_count(); + + // BeforeUnload finishes. + ntp_rvh->SendShouldCloseACK(true); + + // Assume SwapOutACK times out, so the dest_rvh proceeds and commits. + dest_rvh->SendNavigate(101, kDestUrl); + + // The new RVH should be able to update its title. + const string16 dest_title = ASCIIToUTF16("Google"); + EXPECT_TRUE(dest_rvh->OnMessageReceived( + ViewHostMsg_UpdateTitle(rvh()->GetRoutingID(), 101, dest_title, + direction))); + EXPECT_EQ(dest_title, contents()->GetTitle()); + + // The old renderer, being slow, now updates the title. It should be filtered + // out and not take effect. + EXPECT_TRUE(ntp_rvh->is_swapped_out()); + EXPECT_TRUE(ntp_rvh->OnMessageReceived( + ViewHostMsg_UpdateTitle(rvh()->GetRoutingID(), 0, ntp_title, direction))); + EXPECT_EQ(dest_title, contents()->GetTitle()); + + // We cannot filter out synchronous IPC messages, because the renderer would + // be left waiting for a reply. We pick RunBeforeUnloadConfirm as an example + // that can run easily within a unit test, and that needs to receive a reply + // without showing an actual dialog. + MockRenderProcessHost* ntp_process_host = + static_cast<MockRenderProcessHost*>(ntp_rvh->GetProcess()); + ntp_process_host->sink().ClearMessages(); + const string16 msg = ASCIIToUTF16("Message"); + bool result = false; + string16 unused; + ViewHostMsg_RunBeforeUnloadConfirm before_unload_msg( + rvh()->GetRoutingID(), kChromeURL, msg, false, &result, &unused); + // Enable pumping for check in BrowserMessageFilter::CheckCanDispatchOnUI. + before_unload_msg.EnableMessagePumping(); + EXPECT_TRUE(ntp_rvh->OnMessageReceived(before_unload_msg)); + EXPECT_TRUE(ntp_process_host->sink().GetUniqueMessageMatching(IPC_REPLY_ID)); + + // Also test RunJavaScriptMessage. + ntp_process_host->sink().ClearMessages(); + ViewHostMsg_RunJavaScriptMessage js_msg( + rvh()->GetRoutingID(), msg, msg, kChromeURL, + JAVASCRIPT_MESSAGE_TYPE_CONFIRM, &result, &unused); + js_msg.EnableMessagePumping(); + EXPECT_TRUE(ntp_rvh->OnMessageReceived(js_msg)); + EXPECT_TRUE(ntp_process_host->sink().GetUniqueMessageMatching(IPC_REPLY_ID)); +} + +TEST_F(RenderViewHostManagerTest, WhiteListSwapCompositorFrame) { + TestRenderViewHost* swapped_out_rvh = CreateSwappedOutRenderViewHost(); + TestRenderWidgetHostView* swapped_out_rwhv = + static_cast<TestRenderWidgetHostView*>(swapped_out_rvh->GetView()); + EXPECT_FALSE(swapped_out_rwhv->did_swap_compositor_frame()); + + MockRenderProcessHost* process_host = + static_cast<MockRenderProcessHost*>(swapped_out_rvh->GetProcess()); + process_host->sink().ClearMessages(); + + cc::CompositorFrame frame; + ViewHostMsg_SwapCompositorFrame msg(rvh()->GetRoutingID(), 0, frame); + + EXPECT_TRUE(swapped_out_rvh->OnMessageReceived(msg)); + EXPECT_TRUE(swapped_out_rwhv->did_swap_compositor_frame()); +} + +TEST_F(RenderViewHostManagerTest, WhiteListDidActivateAcceleratedCompositing) { + TestRenderViewHost* swapped_out_rvh = CreateSwappedOutRenderViewHost(); + + MockRenderProcessHost* process_host = + static_cast<MockRenderProcessHost*>(swapped_out_rvh->GetProcess()); + process_host->sink().ClearMessages(); + ViewHostMsg_DidActivateAcceleratedCompositing msg( + rvh()->GetRoutingID(), true); + EXPECT_TRUE(swapped_out_rvh->OnMessageReceived(msg)); + EXPECT_TRUE(swapped_out_rvh->is_accelerated_compositing_active()); +} + +// Test if RenderViewHost::GetRenderWidgetHosts() only returns active +// widgets. +TEST_F(RenderViewHostManagerTest, GetRenderWidgetHostsReturnsActiveViews) { + TestRenderViewHost* swapped_out_rvh = CreateSwappedOutRenderViewHost(); + EXPECT_TRUE(swapped_out_rvh->is_swapped_out()); + + scoped_ptr<RenderWidgetHostIterator> widgets( + RenderWidgetHost::GetRenderWidgetHosts()); + // We know that there is the only one active widget. Another view is + // now swapped out, so the swapped out view is not included in the + // list. + RenderWidgetHost* widget = widgets->GetNextHost(); + EXPECT_FALSE(widgets->GetNextHost()); + RenderViewHost* rvh = RenderViewHost::From(widget); + EXPECT_FALSE(static_cast<RenderViewHostImpl*>(rvh)->is_swapped_out()); +} + +// Test if RenderViewHost::GetRenderWidgetHosts() returns a subset of +// RenderViewHostImpl::GetAllRenderWidgetHosts(). +// RenderViewHost::GetRenderWidgetHosts() returns only active widgets, but +// RenderViewHostImpl::GetAllRenderWidgetHosts() returns everything +// including swapped out ones. +TEST_F(RenderViewHostManagerTest, + GetRenderWidgetHostsWithinGetAllRenderWidgetHosts) { + TestRenderViewHost* swapped_out_rvh = CreateSwappedOutRenderViewHost(); + EXPECT_TRUE(swapped_out_rvh->is_swapped_out()); + + scoped_ptr<RenderWidgetHostIterator> widgets( + RenderWidgetHost::GetRenderWidgetHosts()); + + while (RenderWidgetHost* w = widgets->GetNextHost()) { + bool found = false; + scoped_ptr<RenderWidgetHostIterator> all_widgets( + RenderWidgetHostImpl::GetAllRenderWidgetHosts()); + while (RenderWidgetHost* widget = all_widgets->GetNextHost()) { + if (w == widget) { + found = true; + break; + } + } + EXPECT_TRUE(found); + } +} + +// Test if SiteInstanceImpl::active_view_count() is correctly updated +// as views in a SiteInstance get swapped out and in. +TEST_F(RenderViewHostManagerTest, ActiveViewCountWhileSwappingInandOut) { + const GURL kUrl1("http://www.google.com/"); + const GURL kUrl2("http://www.chromium.org/"); + + // Navigate to an initial URL. + contents()->NavigateAndCommit(kUrl1); + TestRenderViewHost* rvh1 = test_rvh(); + + SiteInstanceImpl* instance1 = + static_cast<SiteInstanceImpl*>(rvh1->GetSiteInstance()); + EXPECT_EQ(instance1->active_view_count(), 1U); + + // Create 2 new tabs and simulate them being the opener chain for the main + // tab. They should be in the same SiteInstance. + scoped_ptr<TestWebContents> opener1( + TestWebContents::Create(browser_context(), instance1)); + contents()->SetOpener(opener1.get()); + + scoped_ptr<TestWebContents> opener2( + TestWebContents::Create(browser_context(), instance1)); + opener1->SetOpener(opener2.get()); + + EXPECT_EQ(instance1->active_view_count(), 3U); + + // Navigate to a cross-site URL (different SiteInstance but same + // BrowsingInstance). + contents()->NavigateAndCommit(kUrl2); + TestRenderViewHost* rvh2 = test_rvh(); + SiteInstanceImpl* instance2 = + static_cast<SiteInstanceImpl*>(rvh2->GetSiteInstance()); + + // rvh2 is on chromium.org which is different from google.com on + // which other tabs are. + EXPECT_EQ(instance2->active_view_count(), 1U); + + // There are two active views on google.com now. + EXPECT_EQ(instance1->active_view_count(), 2U); + + // Navigate to the original origin (google.com). + contents()->NavigateAndCommit(kUrl1); + + EXPECT_EQ(instance1->active_view_count(), 3U); +} + +// This deletes a WebContents when the given RVH is deleted. This is +// only for testing whether deleting an RVH does not cause any UaF in +// other parts of the system. For now, this class is only used for the +// next test cases to detect the bug mentioned at +// http://crbug.com/259859. +class RenderViewHostDestroyer : public WebContentsObserver { + public: + RenderViewHostDestroyer(RenderViewHost* render_view_host, + WebContents* web_contents) + : WebContentsObserver(WebContents::FromRenderViewHost(render_view_host)), + render_view_host_(render_view_host), + web_contents_(web_contents) {} + + virtual void RenderViewDeleted( + RenderViewHost* render_view_host) OVERRIDE { + if (render_view_host == render_view_host_) + delete web_contents_; + } + + private: + RenderViewHost* render_view_host_; + WebContents* web_contents_; + + DISALLOW_COPY_AND_ASSIGN(RenderViewHostDestroyer); +}; + +// Test if ShutdownRenderViewHostsInSiteInstance() does not touch any +// RenderWidget that has been freed while deleting a RenderViewHost in +// a previous iteration. This is a regression test for +// http://crbug.com/259859. +TEST_F(RenderViewHostManagerTest, + DetectUseAfterFreeInShutdownRenderViewHostsInSiteInstance) { + const GURL kChromeURL("chrome://newtab"); + const GURL kUrl1("http://www.google.com"); + const GURL kUrl2("http://www.chromium.org"); + + // Navigate our first tab to a chrome url and then to the destination. + NavigateActiveAndCommit(kChromeURL); + TestRenderViewHost* ntp_rvh = static_cast<TestRenderViewHost*>( + contents()->GetRenderManagerForTesting()->current_host()); + + // Create one more tab and navigate to kUrl1. web_contents is not + // wrapped as scoped_ptr since it intentionally deleted by destroyer + // below as part of this test. + TestWebContents* web_contents = + TestWebContents::Create(browser_context(), ntp_rvh->GetSiteInstance()); + web_contents->NavigateAndCommit(kUrl1); + RenderViewHostDestroyer destroyer(ntp_rvh, web_contents); + + // This causes the first tab to navigate to kUrl2, which destroys + // the ntp_rvh in ShutdownRenderViewHostsInSiteInstance(). When + // ntp_rvh is destroyed, it also destroys the RVHs in web_contents + // too. This can test whether + // SiteInstanceImpl::ShutdownRenderViewHostsInSiteInstance() can + // touch any object freed in this way or not while iterating through + // all widgets. + contents()->NavigateAndCommit(kUrl2); +} + +// When there is an error with the specified page, renderer exits view-source +// mode. See WebFrameImpl::DidFail(). We check by this test that +// EnableViewSourceMode message is sent on every navigation regardless +// RenderView is being newly created or reused. +TEST_F(RenderViewHostManagerTest, AlwaysSendEnableViewSourceMode) { + const GURL kChromeUrl("chrome://foo"); + const GURL kUrl("view-source:http://foo"); + + // We have to navigate to some page at first since without this, the first + // navigation will reuse the SiteInstance created by Init(), and the second + // one will create a new SiteInstance. Because current_instance and + // new_instance will be different, a new RenderViewHost will be created for + // the second navigation. We have to avoid this in order to exercise the + // target code patch. + NavigateActiveAndCommit(kChromeUrl); + + // Navigate. + controller().LoadURL( + kUrl, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + // Simulate response from RenderView for FirePageBeforeUnload. + base::TimeTicks now = base::TimeTicks::Now(); + test_rvh()->OnMessageReceived(ViewHostMsg_ShouldClose_ACK( + rvh()->GetRoutingID(), true, now, now)); + ASSERT_TRUE(pending_rvh()); // New pending RenderViewHost will be created. + RenderViewHost* last_rvh = pending_rvh(); + int32 new_id = contents()->GetMaxPageIDForSiteInstance( + active_rvh()->GetSiteInstance()) + 1; + pending_test_rvh()->SendNavigate(new_id, kUrl); + EXPECT_EQ(controller().GetLastCommittedEntryIndex(), 1); + ASSERT_TRUE(controller().GetLastCommittedEntry()); + EXPECT_TRUE(kUrl == controller().GetLastCommittedEntry()->GetURL()); + EXPECT_FALSE(controller().GetPendingEntry()); + // Because we're using TestWebContents and TestRenderViewHost in this + // unittest, no one calls WebContentsImpl::RenderViewCreated(). So, we see no + // EnableViewSourceMode message, here. + + // Clear queued messages before load. + process()->sink().ClearMessages(); + // Navigate, again. + controller().LoadURL( + kUrl, Referrer(), PAGE_TRANSITION_TYPED, std::string()); + // The same RenderViewHost should be reused. + EXPECT_FALSE(pending_rvh()); + EXPECT_TRUE(last_rvh == rvh()); + test_rvh()->SendNavigate(new_id, kUrl); // The same page_id returned. + EXPECT_EQ(controller().GetLastCommittedEntryIndex(), 1); + EXPECT_FALSE(controller().GetPendingEntry()); + // New message should be sent out to make sure to enter view-source mode. + EXPECT_TRUE(process()->sink().GetUniqueMessageMatching( + ViewMsg_EnableViewSourceMode::ID)); +} + +// Tests the Init function by checking the initial RenderViewHost. +TEST_F(RenderViewHostManagerTest, Init) { + // Using TestBrowserContext. + SiteInstanceImpl* instance = + static_cast<SiteInstanceImpl*>(SiteInstance::Create(browser_context())); + EXPECT_FALSE(instance->HasSite()); + + scoped_ptr<TestWebContents> web_contents( + TestWebContents::Create(browser_context(), instance)); + RenderViewHostManager manager(web_contents.get(), web_contents.get(), + web_contents.get()); + + manager.Init(browser_context(), instance, MSG_ROUTING_NONE, MSG_ROUTING_NONE); + + RenderViewHost* host = manager.current_host(); + ASSERT_TRUE(host); + EXPECT_EQ(instance, host->GetSiteInstance()); + EXPECT_EQ(web_contents.get(), host->GetDelegate()); + EXPECT_TRUE(manager.GetRenderWidgetHostView()); + EXPECT_FALSE(manager.pending_render_view_host()); +} + +// Tests the Navigate function. We navigate three sites consecutively and check +// how the pending/committed RenderViewHost are modified. +TEST_F(RenderViewHostManagerTest, Navigate) { + TestNotificationTracker notifications; + + SiteInstance* instance = SiteInstance::Create(browser_context()); + + scoped_ptr<TestWebContents> web_contents( + TestWebContents::Create(browser_context(), instance)); + notifications.ListenFor(NOTIFICATION_RENDER_VIEW_HOST_CHANGED, + Source<WebContents>(web_contents.get())); + + // Create. + RenderViewHostManager manager(web_contents.get(), web_contents.get(), + web_contents.get()); + + manager.Init(browser_context(), instance, MSG_ROUTING_NONE, MSG_ROUTING_NONE); + + RenderViewHost* host; + + // 1) The first navigation. -------------------------- + const GURL kUrl1("http://www.google.com/"); + NavigationEntryImpl entry1( + NULL /* instance */, -1 /* page_id */, kUrl1, Referrer(), + string16() /* title */, PAGE_TRANSITION_TYPED, + false /* is_renderer_init */); + host = manager.Navigate(entry1); + + // The RenderViewHost created in Init will be reused. + EXPECT_TRUE(host == manager.current_host()); + EXPECT_FALSE(manager.pending_render_view_host()); + + // Commit. + manager.DidNavigateMainFrame(host); + // Commit to SiteInstance should be delayed until RenderView commit. + EXPECT_TRUE(host == manager.current_host()); + ASSERT_TRUE(host); + EXPECT_FALSE(static_cast<SiteInstanceImpl*>(host->GetSiteInstance())-> + HasSite()); + static_cast<SiteInstanceImpl*>(host->GetSiteInstance())->SetSite(kUrl1); + + // 2) Navigate to next site. ------------------------- + const GURL kUrl2("http://www.google.com/foo"); + NavigationEntryImpl entry2( + NULL /* instance */, -1 /* page_id */, kUrl2, + Referrer(kUrl1, WebKit::WebReferrerPolicyDefault), + string16() /* title */, PAGE_TRANSITION_LINK, + true /* is_renderer_init */); + host = manager.Navigate(entry2); + + // The RenderViewHost created in Init will be reused. + EXPECT_TRUE(host == manager.current_host()); + EXPECT_FALSE(manager.pending_render_view_host()); + + // Commit. + manager.DidNavigateMainFrame(host); + EXPECT_TRUE(host == manager.current_host()); + ASSERT_TRUE(host); + EXPECT_TRUE(static_cast<SiteInstanceImpl*>(host->GetSiteInstance())-> + HasSite()); + + // 3) Cross-site navigate to next site. -------------- + const GURL kUrl3("http://webkit.org/"); + NavigationEntryImpl entry3( + NULL /* instance */, -1 /* page_id */, kUrl3, + Referrer(kUrl2, WebKit::WebReferrerPolicyDefault), + string16() /* title */, PAGE_TRANSITION_LINK, + false /* is_renderer_init */); + host = manager.Navigate(entry3); + + // A new RenderViewHost should be created. + EXPECT_TRUE(manager.pending_render_view_host()); + ASSERT_EQ(host, manager.pending_render_view_host()); + + notifications.Reset(); + + // Commit. + manager.DidNavigateMainFrame(manager.pending_render_view_host()); + EXPECT_TRUE(host == manager.current_host()); + ASSERT_TRUE(host); + EXPECT_TRUE(static_cast<SiteInstanceImpl*>(host->GetSiteInstance())-> + HasSite()); + // Check the pending RenderViewHost has been committed. + EXPECT_FALSE(manager.pending_render_view_host()); + + // We should observe a notification. + EXPECT_TRUE( + notifications.Check1AndReset(NOTIFICATION_RENDER_VIEW_HOST_CHANGED)); +} + +// Tests the Navigate function. In this unit test we verify that the Navigate +// function can handle a new navigation event before the previous navigation +// has been committed. This is also a regression test for +// http://crbug.com/104600. +TEST_F(RenderViewHostManagerTest, NavigateWithEarlyReNavigation) { + TestNotificationTracker notifications; + + SiteInstance* instance = SiteInstance::Create(browser_context()); + + scoped_ptr<TestWebContents> web_contents( + TestWebContents::Create(browser_context(), instance)); + notifications.ListenFor(NOTIFICATION_RENDER_VIEW_HOST_CHANGED, + Source<WebContents>(web_contents.get())); + + // Create. + RenderViewHostManager manager(web_contents.get(), web_contents.get(), + web_contents.get()); + + manager.Init(browser_context(), instance, MSG_ROUTING_NONE, MSG_ROUTING_NONE); + + // 1) The first navigation. -------------------------- + const GURL kUrl1("http://www.google.com/"); + NavigationEntryImpl entry1(NULL /* instance */, -1 /* page_id */, kUrl1, + Referrer(), string16() /* title */, + PAGE_TRANSITION_TYPED, + false /* is_renderer_init */); + RenderViewHost* host = manager.Navigate(entry1); + + // The RenderViewHost created in Init will be reused. + EXPECT_TRUE(host == manager.current_host()); + EXPECT_FALSE(manager.pending_render_view_host()); + + // We should observe a notification. + EXPECT_TRUE( + notifications.Check1AndReset(NOTIFICATION_RENDER_VIEW_HOST_CHANGED)); + notifications.Reset(); + + // Commit. + manager.DidNavigateMainFrame(host); + + // Commit to SiteInstance should be delayed until RenderView commit. + EXPECT_TRUE(host == manager.current_host()); + ASSERT_TRUE(host); + EXPECT_FALSE(static_cast<SiteInstanceImpl*>(host->GetSiteInstance())-> + HasSite()); + static_cast<SiteInstanceImpl*>(host->GetSiteInstance())->SetSite(kUrl1); + + // 2) Cross-site navigate to next site. ------------------------- + const GURL kUrl2("http://www.example.com"); + NavigationEntryImpl entry2( + NULL /* instance */, -1 /* page_id */, kUrl2, Referrer(), + string16() /* title */, PAGE_TRANSITION_TYPED, + false /* is_renderer_init */); + RenderViewHostImpl* host2 = static_cast<RenderViewHostImpl*>( + manager.Navigate(entry2)); + int host2_process_id = host2->GetProcess()->GetID(); + + // A new RenderViewHost should be created. + EXPECT_TRUE(manager.pending_render_view_host()); + ASSERT_EQ(host2, manager.pending_render_view_host()); + EXPECT_NE(host2, host); + + // Check that the navigation is still suspended because the old RVH + // is not swapped out, yet. + EXPECT_TRUE(host2->are_navigations_suspended()); + MockRenderProcessHost* test_process_host2 = + static_cast<MockRenderProcessHost*>(host2->GetProcess()); + test_process_host2->sink().ClearMessages(); + host2->NavigateToURL(kUrl2); + EXPECT_FALSE(test_process_host2->sink().GetUniqueMessageMatching( + ViewMsg_Navigate::ID)); + + // Allow closing the current Render View (precondition for swapping out + // the RVH): Simulate response from RenderView for ViewMsg_ShouldClose sent by + // FirePageBeforeUnload. + TestRenderViewHost* test_host = static_cast<TestRenderViewHost*>(host); + MockRenderProcessHost* test_process_host = + static_cast<MockRenderProcessHost*>(test_host->GetProcess()); + EXPECT_TRUE(test_process_host->sink().GetUniqueMessageMatching( + ViewMsg_ShouldClose::ID)); + test_host->SendShouldCloseACK(true); + + // CrossSiteResourceHandler::StartCrossSiteTransition triggers a + // call of RenderViewHostManager::SwapOutOldPage before + // RenderViewHostManager::DidNavigateMainFrame is called. + // The RVH is not swapped out until the commit. + manager.SwapOutOldPage(); + EXPECT_TRUE(test_process_host->sink().GetUniqueMessageMatching( + ViewMsg_SwapOut::ID)); + test_host->OnSwappedOut(false); + + EXPECT_EQ(host, manager.current_host()); + EXPECT_FALSE(static_cast<RenderViewHostImpl*>( + manager.current_host())->is_swapped_out()); + EXPECT_EQ(host2, manager.pending_render_view_host()); + // There should be still no navigation messages being sent. + EXPECT_FALSE(test_process_host2->sink().GetUniqueMessageMatching( + ViewMsg_Navigate::ID)); + + // 3) Cross-site navigate to next site before 2) has committed. -------------- + const GURL kUrl3("http://webkit.org/"); + NavigationEntryImpl entry3(NULL /* instance */, -1 /* page_id */, kUrl3, + Referrer(), string16() /* title */, + PAGE_TRANSITION_TYPED, + false /* is_renderer_init */); + test_process_host->sink().ClearMessages(); + RenderViewHost* host3 = manager.Navigate(entry3); + + // A new RenderViewHost should be created. host2 is now deleted. + EXPECT_TRUE(manager.pending_render_view_host()); + ASSERT_EQ(host3, manager.pending_render_view_host()); + EXPECT_NE(host3, host); + EXPECT_NE(host3->GetProcess()->GetID(), host2_process_id); + + // Navigations in the new RVH should be suspended, which is ok because the + // old RVH is not yet swapped out and can respond to a second beforeunload + // request. + EXPECT_TRUE(static_cast<RenderViewHostImpl*>( + host3)->are_navigations_suspended()); + EXPECT_EQ(host, manager.current_host()); + EXPECT_FALSE(static_cast<RenderViewHostImpl*>( + manager.current_host())->is_swapped_out()); + + // Simulate a response to the second beforeunload request. + EXPECT_TRUE(test_process_host->sink().GetUniqueMessageMatching( + ViewMsg_ShouldClose::ID)); + test_host->SendShouldCloseACK(true); + + // CrossSiteResourceHandler::StartCrossSiteTransition triggers a + // call of RenderViewHostManager::SwapOutOldPage before + // RenderViewHostManager::DidNavigateMainFrame is called. + // The RVH is not swapped out until the commit. + manager.SwapOutOldPage(); + EXPECT_TRUE(test_process_host->sink().GetUniqueMessageMatching( + ViewMsg_SwapOut::ID)); + test_host->OnSwappedOut(false); + + // Commit. + manager.DidNavigateMainFrame(host3); + EXPECT_TRUE(host3 == manager.current_host()); + ASSERT_TRUE(host3); + EXPECT_TRUE(static_cast<SiteInstanceImpl*>(host3->GetSiteInstance())-> + HasSite()); + // Check the pending RenderViewHost has been committed. + EXPECT_FALSE(manager.pending_render_view_host()); + + // We should observe a notification. + EXPECT_TRUE( + notifications.Check1AndReset(NOTIFICATION_RENDER_VIEW_HOST_CHANGED)); +} + +// Tests WebUI creation. +TEST_F(RenderViewHostManagerTest, WebUI) { + set_should_create_webui(true); + SiteInstance* instance = SiteInstance::Create(browser_context()); + + scoped_ptr<TestWebContents> web_contents( + TestWebContents::Create(browser_context(), instance)); + RenderViewHostManager manager(web_contents.get(), web_contents.get(), + web_contents.get()); + + manager.Init(browser_context(), instance, MSG_ROUTING_NONE, MSG_ROUTING_NONE); + EXPECT_FALSE(manager.current_host()->IsRenderViewLive()); + + const GURL kUrl("chrome://foo"); + NavigationEntryImpl entry(NULL /* instance */, -1 /* page_id */, kUrl, + Referrer(), string16() /* title */, + PAGE_TRANSITION_TYPED, + false /* is_renderer_init */); + RenderViewHost* host = manager.Navigate(entry); + + // We commit the pending RenderViewHost immediately because the previous + // RenderViewHost was not live. We test a case where it is live in + // WebUIInNewTab. + EXPECT_TRUE(host); + EXPECT_EQ(host, manager.current_host()); + EXPECT_FALSE(manager.pending_render_view_host()); + + // It's important that the site instance get set on the Web UI page as soon + // as the navigation starts, rather than lazily after it commits, so we don't + // try to re-use the SiteInstance/process for non Web UI things that may + // get loaded in between. + EXPECT_TRUE(static_cast<SiteInstanceImpl*>(host->GetSiteInstance())-> + HasSite()); + EXPECT_EQ(kUrl, host->GetSiteInstance()->GetSiteURL()); + + // The Web UI is committed immediately because the RenderViewHost has not been + // used yet. UpdateRendererStateForNavigate() took the short cut path. + EXPECT_FALSE(manager.pending_web_ui()); + EXPECT_TRUE(manager.web_ui()); + + // Commit. + manager.DidNavigateMainFrame(host); + EXPECT_TRUE(host->GetEnabledBindings() & BINDINGS_POLICY_WEB_UI); +} + +// Tests that we can open a WebUI link in a new tab from a WebUI page and still +// grant the correct bindings. http://crbug.com/189101. +TEST_F(RenderViewHostManagerTest, WebUIInNewTab) { + set_should_create_webui(true); + SiteInstance* blank_instance = SiteInstance::Create(browser_context()); + + // Create a blank tab. + scoped_ptr<TestWebContents> web_contents1( + TestWebContents::Create(browser_context(), blank_instance)); + RenderViewHostManager manager1(web_contents1.get(), web_contents1.get(), + web_contents1.get()); + manager1.Init( + browser_context(), blank_instance, MSG_ROUTING_NONE, MSG_ROUTING_NONE); + // Test the case that new RVH is considered live. + manager1.current_host()->CreateRenderView(string16(), -1, -1); + + // Navigate to a WebUI page. + const GURL kUrl1("chrome://foo"); + NavigationEntryImpl entry1(NULL /* instance */, -1 /* page_id */, kUrl1, + Referrer(), string16() /* title */, + PAGE_TRANSITION_TYPED, + false /* is_renderer_init */); + RenderViewHost* host1 = manager1.Navigate(entry1); + + // We should have a pending navigation to the WebUI RenderViewHost. + // It should already have bindings. + EXPECT_EQ(host1, manager1.pending_render_view_host()); + EXPECT_NE(host1, manager1.current_host()); + EXPECT_TRUE(host1->GetEnabledBindings() & BINDINGS_POLICY_WEB_UI); + + // Commit and ensure we still have bindings. + manager1.DidNavigateMainFrame(host1); + SiteInstance* webui_instance = host1->GetSiteInstance(); + EXPECT_EQ(host1, manager1.current_host()); + EXPECT_TRUE(host1->GetEnabledBindings() & BINDINGS_POLICY_WEB_UI); + + // Now simulate clicking a link that opens in a new tab. + scoped_ptr<TestWebContents> web_contents2( + TestWebContents::Create(browser_context(), webui_instance)); + RenderViewHostManager manager2(web_contents2.get(), web_contents2.get(), + web_contents2.get()); + manager2.Init( + browser_context(), webui_instance, MSG_ROUTING_NONE, MSG_ROUTING_NONE); + // Make sure the new RVH is considered live. This is usually done in + // RenderWidgetHost::Init when opening a new tab from a link. + manager2.current_host()->CreateRenderView(string16(), -1, -1); + + const GURL kUrl2("chrome://foo/bar"); + NavigationEntryImpl entry2(NULL /* instance */, -1 /* page_id */, kUrl2, + Referrer(), string16() /* title */, + PAGE_TRANSITION_LINK, + true /* is_renderer_init */); + RenderViewHost* host2 = manager2.Navigate(entry2); + + // No cross-process transition happens because we are already in the right + // SiteInstance. We should grant bindings immediately. + EXPECT_EQ(host2, manager2.current_host()); + EXPECT_TRUE(host2->GetEnabledBindings() & BINDINGS_POLICY_WEB_UI); + + manager2.DidNavigateMainFrame(host2); +} + +// Tests that we don't end up in an inconsistent state if a page does a back and +// then reload. http://crbug.com/51680 +TEST_F(RenderViewHostManagerTest, PageDoesBackAndReload) { + const GURL kUrl1("http://www.google.com/"); + const GURL kUrl2("http://www.evil-site.com/"); + + // Navigate to a safe site, then an evil site. + // This will switch RenderViewHosts. We cannot assert that the first and + // second RVHs are different, though, because the first one may be promptly + // deleted. + contents()->NavigateAndCommit(kUrl1); + contents()->NavigateAndCommit(kUrl2); + RenderViewHost* evil_rvh = contents()->GetRenderViewHost(); + + // Now let's simulate the evil page calling history.back(). + contents()->OnGoToEntryAtOffset(-1); + // We should have a new pending RVH. + // Note that in this case, the navigation has not committed, so evil_rvh will + // not be deleted yet. + EXPECT_NE(evil_rvh, contents()->GetRenderManagerForTesting()-> + pending_render_view_host()); + + // Before that RVH has committed, the evil page reloads itself. + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = kUrl2; + params.transition = PAGE_TRANSITION_CLIENT_REDIRECT; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.was_within_same_page = false; + params.is_post = false; + params.page_state = PageState::CreateFromURL(kUrl2); + contents()->DidNavigate(evil_rvh, params); + + // That should have cancelled the pending RVH, and the evil RVH should be the + // current one. + EXPECT_TRUE(contents()->GetRenderManagerForTesting()-> + pending_render_view_host() == NULL); + EXPECT_EQ(evil_rvh, contents()->GetRenderManagerForTesting()->current_host()); + + // Also we should not have a pending navigation entry. + EXPECT_TRUE(contents()->GetController().GetPendingEntry() == NULL); + NavigationEntry* entry = contents()->GetController().GetVisibleEntry(); + ASSERT_TRUE(entry != NULL); + EXPECT_EQ(kUrl2, entry->GetURL()); +} + +// Ensure that we can go back and forward even if a SwapOut ACK isn't received. +// See http://crbug.com/93427. +TEST_F(RenderViewHostManagerTest, NavigateAfterMissingSwapOutACK) { + const GURL kUrl1("http://www.google.com/"); + const GURL kUrl2("http://www.chromium.org/"); + + // Navigate to two pages. + contents()->NavigateAndCommit(kUrl1); + TestRenderViewHost* rvh1 = test_rvh(); + + // Keep active_view_count nonzero so that no swapped out views in + // this SiteInstance get forcefully deleted. + static_cast<SiteInstanceImpl*>(rvh1->GetSiteInstance())-> + increment_active_view_count(); + + contents()->NavigateAndCommit(kUrl2); + TestRenderViewHost* rvh2 = test_rvh(); + static_cast<SiteInstanceImpl*>(rvh2->GetSiteInstance())-> + increment_active_view_count(); + + // Now go back, but suppose the SwapOut_ACK isn't received. This shouldn't + // happen, but we have seen it when going back quickly across many entries + // (http://crbug.com/93427). + contents()->GetController().GoBack(); + EXPECT_TRUE(rvh2->is_waiting_for_beforeunload_ack()); + contents()->ProceedWithCrossSiteNavigation(); + EXPECT_FALSE(rvh2->is_waiting_for_beforeunload_ack()); + rvh2->SwapOut(); + EXPECT_TRUE(rvh2->is_waiting_for_unload_ack()); + + // The back navigation commits. We should proactively clear the + // is_waiting_for_unload_ack state to be safe. + const NavigationEntry* entry1 = contents()->GetController().GetPendingEntry(); + rvh1->SendNavigate(entry1->GetPageID(), entry1->GetURL()); + EXPECT_TRUE(rvh2->is_swapped_out()); + EXPECT_FALSE(rvh2->is_waiting_for_unload_ack()); + + // We should be able to navigate forward. + contents()->GetController().GoForward(); + contents()->ProceedWithCrossSiteNavigation(); + const NavigationEntry* entry2 = contents()->GetController().GetPendingEntry(); + rvh2->SendNavigate(entry2->GetPageID(), entry2->GetURL()); + EXPECT_EQ(rvh2, rvh()); + EXPECT_FALSE(rvh2->is_swapped_out()); + EXPECT_TRUE(rvh1->is_swapped_out()); +} + +// Test that we create swapped out RVHs for the opener chain when navigating an +// opened tab cross-process. This allows us to support certain cross-process +// JavaScript calls (http://crbug.com/99202). +TEST_F(RenderViewHostManagerTest, CreateSwappedOutOpenerRVHs) { + const GURL kUrl1("http://www.google.com/"); + const GURL kUrl2("http://www.chromium.org/"); + const GURL kChromeUrl("chrome://foo"); + + // Navigate to an initial URL. + contents()->NavigateAndCommit(kUrl1); + RenderViewHostManager* manager = contents()->GetRenderManagerForTesting(); + TestRenderViewHost* rvh1 = test_rvh(); + + // Create 2 new tabs and simulate them being the opener chain for the main + // tab. They should be in the same SiteInstance. + scoped_ptr<TestWebContents> opener1( + TestWebContents::Create(browser_context(), rvh1->GetSiteInstance())); + RenderViewHostManager* opener1_manager = + opener1->GetRenderManagerForTesting(); + contents()->SetOpener(opener1.get()); + + scoped_ptr<TestWebContents> opener2( + TestWebContents::Create(browser_context(), rvh1->GetSiteInstance())); + RenderViewHostManager* opener2_manager = + opener2->GetRenderManagerForTesting(); + opener1->SetOpener(opener2.get()); + + // Navigate to a cross-site URL (different SiteInstance but same + // BrowsingInstance). + contents()->NavigateAndCommit(kUrl2); + TestRenderViewHost* rvh2 = test_rvh(); + EXPECT_NE(rvh1->GetSiteInstance(), rvh2->GetSiteInstance()); + EXPECT_TRUE(rvh1->GetSiteInstance()->IsRelatedSiteInstance( + rvh2->GetSiteInstance())); + + // Ensure rvh1 is placed on swapped out list of the current tab. + EXPECT_TRUE(manager->IsOnSwappedOutList(rvh1)); + EXPECT_EQ(rvh1, + manager->GetSwappedOutRenderViewHost(rvh1->GetSiteInstance())); + + // Ensure a swapped out RVH is created in the first opener tab. + TestRenderViewHost* opener1_rvh = static_cast<TestRenderViewHost*>( + opener1_manager->GetSwappedOutRenderViewHost(rvh2->GetSiteInstance())); + EXPECT_TRUE(opener1_manager->IsOnSwappedOutList(opener1_rvh)); + EXPECT_TRUE(opener1_rvh->is_swapped_out()); + + // Ensure a swapped out RVH is created in the second opener tab. + TestRenderViewHost* opener2_rvh = static_cast<TestRenderViewHost*>( + opener2_manager->GetSwappedOutRenderViewHost(rvh2->GetSiteInstance())); + EXPECT_TRUE(opener2_manager->IsOnSwappedOutList(opener2_rvh)); + EXPECT_TRUE(opener2_rvh->is_swapped_out()); + + // Navigate to a cross-BrowsingInstance URL. + contents()->NavigateAndCommit(kChromeUrl); + TestRenderViewHost* rvh3 = test_rvh(); + EXPECT_NE(rvh1->GetSiteInstance(), rvh3->GetSiteInstance()); + EXPECT_FALSE(rvh1->GetSiteInstance()->IsRelatedSiteInstance( + rvh3->GetSiteInstance())); + + // No scripting is allowed across BrowsingInstances, so we should not create + // swapped out RVHs for the opener chain in this case. + EXPECT_FALSE(opener1_manager->GetSwappedOutRenderViewHost( + rvh3->GetSiteInstance())); + EXPECT_FALSE(opener2_manager->GetSwappedOutRenderViewHost( + rvh3->GetSiteInstance())); +} + +// Test that we clean up swapped out RenderViewHosts when a process hosting +// those associated RenderViews crashes. http://crbug.com/258993 +TEST_F(RenderViewHostManagerTest, CleanUpSwappedOutRVHOnProcessCrash) { + const GURL kUrl1("http://www.google.com/"); + + // Navigate to an initial URL. + contents()->NavigateAndCommit(kUrl1); + TestRenderViewHost* rvh1 = test_rvh(); + + // Create a new tab as an opener for the main tab. + scoped_ptr<TestWebContents> opener1( + TestWebContents::Create(browser_context(), rvh1->GetSiteInstance())); + RenderViewHostManager* opener1_manager = + opener1->GetRenderManagerForTesting(); + contents()->SetOpener(opener1.get()); + + EXPECT_FALSE(opener1_manager->GetSwappedOutRenderViewHost( + rvh1->GetSiteInstance())); + opener1->CreateSwappedOutRenderView(rvh1->GetSiteInstance()); + EXPECT_TRUE(opener1_manager->GetSwappedOutRenderViewHost( + rvh1->GetSiteInstance())); + + // Fake a process crash. + RenderProcessHost::RendererClosedDetails details( + rvh1->GetProcess()->GetHandle(), + base::TERMINATION_STATUS_PROCESS_CRASHED, + 0); + NotificationService::current()->Notify( + NOTIFICATION_RENDERER_PROCESS_CLOSED, + Source<RenderProcessHost>(rvh1->GetProcess()), + Details<RenderProcessHost::RendererClosedDetails>(&details)); + rvh1->set_render_view_created(false); + + // Ensure that the swapped out RenderViewHost has been deleted. + EXPECT_FALSE(opener1_manager->GetSwappedOutRenderViewHost( + rvh1->GetSiteInstance())); + + // Reload the initial tab. This should recreate the opener. + contents()->GetController().Reload(true); + + EXPECT_EQ(opener1_manager->current_host()->GetRoutingID(), + test_rvh()->opener_route_id()); +} + +// Test that RenderViewHosts created for WebUI navigations are properly +// granted WebUI bindings even if an unprivileged swapped out RenderViewHost +// is in the same process (http://crbug.com/79918). +TEST_F(RenderViewHostManagerTest, EnableWebUIWithSwappedOutOpener) { + set_should_create_webui(true); + const GURL kSettingsUrl("chrome://chrome/settings"); + const GURL kPluginUrl("chrome://plugins"); + + // Navigate to an initial WebUI URL. + contents()->NavigateAndCommit(kSettingsUrl); + + // Ensure the RVH has WebUI bindings. + TestRenderViewHost* rvh1 = test_rvh(); + EXPECT_TRUE(rvh1->GetEnabledBindings() & BINDINGS_POLICY_WEB_UI); + + // Create a new tab and simulate it being the opener for the main + // tab. It should be in the same SiteInstance. + scoped_ptr<TestWebContents> opener1( + TestWebContents::Create(browser_context(), rvh1->GetSiteInstance())); + RenderViewHostManager* opener1_manager = + opener1->GetRenderManagerForTesting(); + contents()->SetOpener(opener1.get()); + + // Navigate to a different WebUI URL (different SiteInstance, same + // BrowsingInstance). + contents()->NavigateAndCommit(kPluginUrl); + TestRenderViewHost* rvh2 = test_rvh(); + EXPECT_NE(rvh1->GetSiteInstance(), rvh2->GetSiteInstance()); + EXPECT_TRUE(rvh1->GetSiteInstance()->IsRelatedSiteInstance( + rvh2->GetSiteInstance())); + + // Ensure a swapped out RVH is created in the first opener tab. + TestRenderViewHost* opener1_rvh = static_cast<TestRenderViewHost*>( + opener1_manager->GetSwappedOutRenderViewHost(rvh2->GetSiteInstance())); + EXPECT_TRUE(opener1_manager->IsOnSwappedOutList(opener1_rvh)); + EXPECT_TRUE(opener1_rvh->is_swapped_out()); + + // Ensure the new RVH has WebUI bindings. + EXPECT_TRUE(rvh2->GetEnabledBindings() & BINDINGS_POLICY_WEB_UI); +} + +// Test that we reuse the same guest SiteInstance if we navigate across sites. +TEST_F(RenderViewHostManagerTest, NoSwapOnGuestNavigations) { + TestNotificationTracker notifications; + + GURL guest_url(std::string(kGuestScheme).append("://abc123")); + SiteInstance* instance = + SiteInstance::CreateForURL(browser_context(), guest_url); + scoped_ptr<TestWebContents> web_contents( + TestWebContents::Create(browser_context(), instance)); + + // Create. + RenderViewHostManager manager(web_contents.get(), web_contents.get(), + web_contents.get()); + + manager.Init(browser_context(), instance, MSG_ROUTING_NONE, MSG_ROUTING_NONE); + + RenderViewHost* host; + + // 1) The first navigation. -------------------------- + const GURL kUrl1("http://www.google.com/"); + NavigationEntryImpl entry1( + NULL /* instance */, -1 /* page_id */, kUrl1, Referrer(), + string16() /* title */, PAGE_TRANSITION_TYPED, + false /* is_renderer_init */); + host = manager.Navigate(entry1); + + // The RenderViewHost created in Init will be reused. + EXPECT_TRUE(host == manager.current_host()); + EXPECT_FALSE(manager.pending_render_view_host()); + EXPECT_EQ(manager.current_host()->GetSiteInstance(), instance); + + // Commit. + manager.DidNavigateMainFrame(host); + // Commit to SiteInstance should be delayed until RenderView commit. + EXPECT_EQ(host, manager.current_host()); + ASSERT_TRUE(host); + EXPECT_TRUE(static_cast<SiteInstanceImpl*>(host->GetSiteInstance())-> + HasSite()); + + // 2) Navigate to a different domain. ------------------------- + // Guests stay in the same process on navigation. + const GURL kUrl2("http://www.chromium.org"); + NavigationEntryImpl entry2( + NULL /* instance */, -1 /* page_id */, kUrl2, + Referrer(kUrl1, WebKit::WebReferrerPolicyDefault), + string16() /* title */, PAGE_TRANSITION_LINK, + true /* is_renderer_init */); + host = manager.Navigate(entry2); + + // The RenderViewHost created in Init will be reused. + EXPECT_EQ(host, manager.current_host()); + EXPECT_FALSE(manager.pending_render_view_host()); + + // Commit. + manager.DidNavigateMainFrame(host); + EXPECT_EQ(host, manager.current_host()); + ASSERT_TRUE(host); + EXPECT_EQ(static_cast<SiteInstanceImpl*>(host->GetSiteInstance()), + instance); +} + +// Test that we cancel a pending RVH if we close the tab while it's pending. +// http://crbug.com/294697. +TEST_F(RenderViewHostManagerTest, NavigateWithEarlyClose) { + TestNotificationTracker notifications; + + SiteInstance* instance = SiteInstance::Create(browser_context()); + + BeforeUnloadFiredWebContentsDelegate delegate; + scoped_ptr<TestWebContents> web_contents( + TestWebContents::Create(browser_context(), instance)); + web_contents->SetDelegate(&delegate); + notifications.ListenFor(NOTIFICATION_RENDER_VIEW_HOST_CHANGED, + Source<WebContents>(web_contents.get())); + + // Create. + RenderViewHostManager manager(web_contents.get(), web_contents.get(), + web_contents.get()); + + manager.Init(browser_context(), instance, MSG_ROUTING_NONE, MSG_ROUTING_NONE); + + // 1) The first navigation. -------------------------- + const GURL kUrl1("http://www.google.com/"); + NavigationEntryImpl entry1(NULL /* instance */, -1 /* page_id */, kUrl1, + Referrer(), string16() /* title */, + PAGE_TRANSITION_TYPED, + false /* is_renderer_init */); + RenderViewHost* host = manager.Navigate(entry1); + + // The RenderViewHost created in Init will be reused. + EXPECT_EQ(host, manager.current_host()); + EXPECT_FALSE(manager.pending_render_view_host()); + + // We should observe a notification. + EXPECT_TRUE( + notifications.Check1AndReset(NOTIFICATION_RENDER_VIEW_HOST_CHANGED)); + notifications.Reset(); + + // Commit. + manager.DidNavigateMainFrame(host); + + // Commit to SiteInstance should be delayed until RenderView commit. + EXPECT_EQ(host, manager.current_host()); + EXPECT_FALSE(static_cast<SiteInstanceImpl*>(host->GetSiteInstance())-> + HasSite()); + static_cast<SiteInstanceImpl*>(host->GetSiteInstance())->SetSite(kUrl1); + + // 2) Cross-site navigate to next site. ------------------------- + const GURL kUrl2("http://www.example.com"); + NavigationEntryImpl entry2( + NULL /* instance */, -1 /* page_id */, kUrl2, Referrer(), + string16() /* title */, PAGE_TRANSITION_TYPED, + false /* is_renderer_init */); + RenderViewHostImpl* host2 = static_cast<RenderViewHostImpl*>( + manager.Navigate(entry2)); + + // A new RenderViewHost should be created. + ASSERT_EQ(host2, manager.pending_render_view_host()); + EXPECT_NE(host2, host); + + EXPECT_EQ(host, manager.current_host()); + EXPECT_FALSE(static_cast<RenderViewHostImpl*>( + manager.current_host())->is_swapped_out()); + EXPECT_EQ(host2, manager.pending_render_view_host()); + + // 3) Close the tab. ------------------------- + notifications.ListenFor(NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED, + Source<RenderWidgetHost>(host2)); + manager.ShouldClosePage(false, true, base::TimeTicks()); + + EXPECT_TRUE( + notifications.Check1AndReset(NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED)); + EXPECT_FALSE(manager.pending_render_view_host()); + EXPECT_EQ(host, manager.current_host()); +} + +} // namespace content diff --git a/content/browser/frame_host/web_contents_screenshot_manager.cc b/content/browser/frame_host/web_contents_screenshot_manager.cc new file mode 100644 index 0000000..97ac533 --- /dev/null +++ b/content/browser/frame_host/web_contents_screenshot_manager.cc @@ -0,0 +1,273 @@ +// Copyright 2013 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/frame_host/web_contents_screenshot_manager.h" + +#include "base/command_line.h" +#include "base/threading/worker_pool.h" +#include "content/browser/frame_host/navigation_controller_impl.h" +#include "content/browser/frame_host/navigation_entry_impl.h" +#include "content/browser/renderer_host/render_view_host_impl.h" +#include "content/public/browser/render_widget_host.h" +#include "content/public/browser/render_widget_host_view.h" +#include "content/public/common/content_switches.h" +#include "ui/gfx/codec/png_codec.h" + +namespace { + +// Minimum delay between taking screenshots. +const int kMinScreenshotIntervalMS = 1000; + +} + +namespace content { + +// Encodes an SkBitmap to PNG data in a worker thread. +class ScreenshotData : public base::RefCountedThreadSafe<ScreenshotData> { + public: + ScreenshotData() { + } + + void EncodeScreenshot(const SkBitmap& bitmap, base::Closure callback) { + if (!base::WorkerPool::PostTaskAndReply(FROM_HERE, + base::Bind(&ScreenshotData::EncodeOnWorker, + this, + bitmap), + callback, + true)) { + callback.Run(); + } + } + + scoped_refptr<base::RefCountedBytes> data() const { return data_; } + + private: + friend class base::RefCountedThreadSafe<ScreenshotData>; + virtual ~ScreenshotData() { + } + + void EncodeOnWorker(const SkBitmap& bitmap) { + std::vector<unsigned char> data; + if (gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, true, &data)) + data_ = new base::RefCountedBytes(data); + } + + scoped_refptr<base::RefCountedBytes> data_; + + DISALLOW_COPY_AND_ASSIGN(ScreenshotData); +}; + +WebContentsScreenshotManager::WebContentsScreenshotManager( + NavigationControllerImpl* owner) + : owner_(owner), + screenshot_factory_(this), + min_screenshot_interval_ms_(kMinScreenshotIntervalMS) { +} + +WebContentsScreenshotManager::~WebContentsScreenshotManager() { +} + +void WebContentsScreenshotManager::TakeScreenshot() { + static bool overscroll_enabled = CommandLine::ForCurrentProcess()-> + GetSwitchValueASCII(switches::kOverscrollHistoryNavigation) != "0"; + if (!overscroll_enabled) + return; + + NavigationEntryImpl* entry = + NavigationEntryImpl::FromNavigationEntry(owner_->GetLastCommittedEntry()); + if (!entry) + return; + + RenderViewHost* render_view_host = + owner_->delegate()->GetRenderViewHost(); + if (!static_cast<RenderViewHostImpl*> + (render_view_host)->overscroll_controller()) { + return; + } + content::RenderWidgetHostView* view = render_view_host->GetView(); + if (!view) + return; + + // Make sure screenshots aren't taken too frequently. + base::Time now = base::Time::Now(); + if (now - last_screenshot_time_ < + base::TimeDelta::FromMilliseconds(min_screenshot_interval_ms_)) { + return; + } + + last_screenshot_time_ = now; + + TakeScreenshotImpl(render_view_host, entry); +} + +// Implemented here and not in NavigationEntry because this manager keeps track +// of the total number of screen shots across all entries. +void WebContentsScreenshotManager::ClearAllScreenshots() { + int count = owner_->GetEntryCount(); + for (int i = 0; i < count; ++i) { + ClearScreenshot(NavigationEntryImpl::FromNavigationEntry( + owner_->GetEntryAtIndex(i))); + } + DCHECK_EQ(GetScreenshotCount(), 0); +} + +void WebContentsScreenshotManager::TakeScreenshotImpl( + RenderViewHost* host, + NavigationEntryImpl* entry) { + DCHECK(host && host->GetView()); + DCHECK(entry); + host->CopyFromBackingStore(gfx::Rect(), + host->GetView()->GetViewBounds().size(), + base::Bind(&WebContentsScreenshotManager::OnScreenshotTaken, + screenshot_factory_.GetWeakPtr(), + entry->GetUniqueID())); +} + +void WebContentsScreenshotManager::SetMinScreenshotIntervalMS(int interval_ms) { + DCHECK_GE(interval_ms, 0); + min_screenshot_interval_ms_ = interval_ms; +} + +void WebContentsScreenshotManager::OnScreenshotTaken(int unique_id, + bool success, + const SkBitmap& bitmap) { + NavigationEntryImpl* entry = NULL; + int entry_count = owner_->GetEntryCount(); + for (int i = 0; i < entry_count; ++i) { + NavigationEntry* iter = owner_->GetEntryAtIndex(i); + if (iter->GetUniqueID() == unique_id) { + entry = NavigationEntryImpl::FromNavigationEntry(iter); + break; + } + } + + if (!entry) { + LOG(ERROR) << "Invalid entry with unique id: " << unique_id; + return; + } + + if (!success || bitmap.empty() || bitmap.isNull()) { + if (!ClearScreenshot(entry)) + OnScreenshotSet(entry); + return; + } + + scoped_refptr<ScreenshotData> screenshot = new ScreenshotData(); + screenshot->EncodeScreenshot( + bitmap, + base::Bind(&WebContentsScreenshotManager::OnScreenshotEncodeComplete, + screenshot_factory_.GetWeakPtr(), + unique_id, + screenshot)); +} + +int WebContentsScreenshotManager::GetScreenshotCount() const { + int screenshot_count = 0; + int entry_count = owner_->GetEntryCount(); + for (int i = 0; i < entry_count; ++i) { + NavigationEntryImpl* entry = + NavigationEntryImpl::FromNavigationEntry(owner_->GetEntryAtIndex(i)); + if (entry->screenshot().get()) + screenshot_count++; + } + return screenshot_count; +} + +void WebContentsScreenshotManager::OnScreenshotEncodeComplete( + int unique_id, + scoped_refptr<ScreenshotData> screenshot) { + NavigationEntryImpl* entry = NULL; + int entry_count = owner_->GetEntryCount(); + for (int i = 0; i < entry_count; ++i) { + NavigationEntry* iter = owner_->GetEntryAtIndex(i); + if (iter->GetUniqueID() == unique_id) { + entry = NavigationEntryImpl::FromNavigationEntry(iter); + break; + } + } + if (!entry) + return; + entry->SetScreenshotPNGData(screenshot->data()); + OnScreenshotSet(entry); +} + +void WebContentsScreenshotManager::OnScreenshotSet(NavigationEntryImpl* entry) { + if (entry->screenshot().get()) + PurgeScreenshotsIfNecessary(); +} + +bool WebContentsScreenshotManager::ClearScreenshot(NavigationEntryImpl* entry) { + if (!entry->screenshot().get()) + return false; + + entry->SetScreenshotPNGData(NULL); + return true; +} + +void WebContentsScreenshotManager::PurgeScreenshotsIfNecessary() { + // Allow only a certain number of entries to keep screenshots. + const int kMaxScreenshots = 10; + int screenshot_count = GetScreenshotCount(); + if (screenshot_count < kMaxScreenshots) + return; + + const int current = owner_->GetCurrentEntryIndex(); + const int num_entries = owner_->GetEntryCount(); + int available_slots = kMaxScreenshots; + if (NavigationEntryImpl::FromNavigationEntry(owner_->GetEntryAtIndex(current)) + ->screenshot().get()) { + --available_slots; + } + + // Keep screenshots closer to the current navigation entry, and purge the ones + // that are farther away from it. So in each step, look at the entries at + // each offset on both the back and forward history, and start counting them + // to make sure that the correct number of screenshots are kept in memory. + // Note that it is possible for some entries to be missing screenshots (e.g. + // when taking the screenshot failed for some reason). So there may be a state + // where there are a lot of entries in the back history, but none of them has + // any screenshot. In such cases, keep the screenshots for |kMaxScreenshots| + // entries in the forward history list. + int back = current - 1; + int forward = current + 1; + while (available_slots > 0 && (back >= 0 || forward < num_entries)) { + if (back >= 0) { + NavigationEntryImpl* entry = NavigationEntryImpl::FromNavigationEntry( + owner_->GetEntryAtIndex(back)); + if (entry->screenshot().get()) + --available_slots; + --back; + } + + if (available_slots > 0 && forward < num_entries) { + NavigationEntryImpl* entry = NavigationEntryImpl::FromNavigationEntry( + owner_->GetEntryAtIndex(forward)); + if (entry->screenshot().get()) + --available_slots; + ++forward; + } + } + + // Purge any screenshot at |back| or lower indices, and |forward| or higher + // indices. + while (screenshot_count > kMaxScreenshots && back >= 0) { + NavigationEntryImpl* entry = NavigationEntryImpl::FromNavigationEntry( + owner_->GetEntryAtIndex(back)); + if (ClearScreenshot(entry)) + --screenshot_count; + --back; + } + + while (screenshot_count > kMaxScreenshots && forward < num_entries) { + NavigationEntryImpl* entry = NavigationEntryImpl::FromNavigationEntry( + owner_->GetEntryAtIndex(forward)); + if (ClearScreenshot(entry)) + --screenshot_count; + ++forward; + } + CHECK_GE(screenshot_count, 0); + CHECK_LE(screenshot_count, kMaxScreenshots); +} + +} // namespace content diff --git a/content/browser/frame_host/web_contents_screenshot_manager.h b/content/browser/frame_host/web_contents_screenshot_manager.h new file mode 100644 index 0000000..10339a2 --- /dev/null +++ b/content/browser/frame_host/web_contents_screenshot_manager.h @@ -0,0 +1,91 @@ +// Copyright 2013 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_FRAME_HOST_WEB_CONTENTS_SCREENSHOT_MANAGER_H_ +#define CONTENT_BROWSER_FRAME_HOST_WEB_CONTENTS_SCREENSHOT_MANAGER_H_ + +#include "base/compiler_specific.h" +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "content/common/content_export.h" + +class SkBitmap; + +namespace content { + +class NavigationControllerImpl; +class NavigationEntryImpl; +class RenderViewHost; +class ScreenshotData; + +// WebContentsScreenshotManager takes care of taking image-captures for the +// current navigation entry of a NavigationControllerImpl, and managing these +// captured images. These image-captures are used for history navigation using +// overscroll gestures. +// TODO(nasko): Rename this to better reflect that it is used for +// navigation entries and not WebContents. +class CONTENT_EXPORT WebContentsScreenshotManager { + public: + explicit WebContentsScreenshotManager(NavigationControllerImpl* controller); + virtual ~WebContentsScreenshotManager(); + + // Takes a screenshot of the last-committed entry of the controller. + void TakeScreenshot(); + + // Clears screenshots of all navigation entries. + void ClearAllScreenshots(); + + protected: + virtual void TakeScreenshotImpl(RenderViewHost* host, + NavigationEntryImpl* entry); + + // Called after a screenshot has been set on an NavigationEntryImpl. + // Overridden in tests to get notified of when a screenshot is set. + virtual void OnScreenshotSet(NavigationEntryImpl* entry); + + NavigationControllerImpl* owner() { return owner_; } + + void SetMinScreenshotIntervalMS(int interval_ms); + + // The callback invoked when taking the screenshot of the page is complete. + // This sets the screenshot on the navigation entry. + void OnScreenshotTaken(int unique_id, + bool success, + const SkBitmap& bitmap); + + // Returns the number of entries with screenshots. + int GetScreenshotCount() const; + + private: + // This is called when the screenshot data has beene encoded to PNG in a + // worker thread. + void OnScreenshotEncodeComplete(int unique_id, + scoped_refptr<ScreenshotData> data); + + // Removes the screenshot for the entry, returning true if the entry had a + // screenshot. + bool ClearScreenshot(NavigationEntryImpl* entry); + + // The screenshots in the NavigationEntryImpls can accumulate and consume a + // large amount of memory. This function makes sure that the memory + // consumption is within a certain limit. + void PurgeScreenshotsIfNecessary(); + + // The navigation controller that owns this screenshot-manager. + NavigationControllerImpl* owner_; + + // Taking a screenshot and encoding them can be async. So use a weakptr for + // the callback to make sure that the screenshot/encoding completion callback + // does not trigger on a destroyed WebContentsScreenshotManager. + base::WeakPtrFactory<WebContentsScreenshotManager> screenshot_factory_; + + base::Time last_screenshot_time_; + int min_screenshot_interval_ms_; + + DISALLOW_COPY_AND_ASSIGN(WebContentsScreenshotManager); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_FRAME_HOST_WEB_CONTENTS_SCREENSHOT_MANAGER_H_ |