blob: 555cd8c270c3985741e9c9a77f895eb95cb13931 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/extension_api_frame_id_map.h"
#include <tuple>
#include <utility>
#include "base/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/post_task.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/child_process_host.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/common/constants.h"
namespace extensions {
namespace {
// The map is accessed on the IO and UI thread, so construct it once and never
// delete it.
base::LazyInstance<ExtensionApiFrameIdMap>::Leaky g_map_instance =
LAZY_INSTANCE_INITIALIZER;
bool IsFrameRoutingIdValid(int frame_routing_id) {
// frame_routing_id == -2 = MSG_ROUTING_NONE -> not a RenderFrameHost.
// frame_routing_id == -1 -> should be MSG_ROUTING_NONE, but there are
// callers that use "-1" for unknown frames.
return frame_routing_id > -1;
}
} // namespace
const int ExtensionApiFrameIdMap::kInvalidFrameId = -1;
const int ExtensionApiFrameIdMap::kTopFrameId = 0;
ExtensionApiFrameIdMap::FrameData::FrameData()
: frame_id(kInvalidFrameId),
parent_frame_id(kInvalidFrameId),
tab_id(extension_misc::kUnknownTabId),
window_id(extension_misc::kUnknownWindowId) {}
ExtensionApiFrameIdMap::FrameData::FrameData(int frame_id,
int parent_frame_id,
int tab_id,
int window_id,
GURL last_committed_main_frame_url)
: frame_id(frame_id),
parent_frame_id(parent_frame_id),
tab_id(tab_id),
window_id(window_id),
last_committed_main_frame_url(std::move(last_committed_main_frame_url)) {}
ExtensionApiFrameIdMap::FrameData::~FrameData() = default;
ExtensionApiFrameIdMap::FrameData::FrameData(
const ExtensionApiFrameIdMap::FrameData& other) = default;
ExtensionApiFrameIdMap::FrameData& ExtensionApiFrameIdMap::FrameData::operator=(
const ExtensionApiFrameIdMap::FrameData& other) = default;
ExtensionApiFrameIdMap::RenderFrameIdKey::RenderFrameIdKey()
: render_process_id(content::ChildProcessHost::kInvalidUniqueID),
frame_routing_id(MSG_ROUTING_NONE) {}
ExtensionApiFrameIdMap::RenderFrameIdKey::RenderFrameIdKey(
int render_process_id,
int frame_routing_id)
: render_process_id(render_process_id),
frame_routing_id(frame_routing_id) {}
ExtensionApiFrameIdMap::FrameDataCallbacks::FrameDataCallbacks()
: is_iterating(false) {}
ExtensionApiFrameIdMap::FrameDataCallbacks::FrameDataCallbacks(
const FrameDataCallbacks& other) = default;
ExtensionApiFrameIdMap::FrameDataCallbacks::~FrameDataCallbacks() {}
bool ExtensionApiFrameIdMap::RenderFrameIdKey::operator<(
const RenderFrameIdKey& other) const {
return std::tie(render_process_id, frame_routing_id) <
std::tie(other.render_process_id, other.frame_routing_id);
}
bool ExtensionApiFrameIdMap::RenderFrameIdKey::operator==(
const RenderFrameIdKey& other) const {
return render_process_id == other.render_process_id &&
frame_routing_id == other.frame_routing_id;
}
ExtensionApiFrameIdMap::ExtensionApiFrameIdMap() {
// The browser client can be null in unittests.
if (ExtensionsBrowserClient::Get()) {
helper_ =
ExtensionsBrowserClient::Get()->CreateExtensionApiFrameIdMapHelper(
this);
}
}
ExtensionApiFrameIdMap::~ExtensionApiFrameIdMap() {}
// static
ExtensionApiFrameIdMap* ExtensionApiFrameIdMap::Get() {
return g_map_instance.Pointer();
}
// static
int ExtensionApiFrameIdMap::GetFrameId(content::RenderFrameHost* rfh) {
if (!rfh)
return kInvalidFrameId;
if (rfh->GetParent())
return rfh->GetFrameTreeNodeId();
return kTopFrameId;
}
// static
int ExtensionApiFrameIdMap::GetFrameId(
content::NavigationHandle* navigation_handle) {
return navigation_handle->IsInMainFrame()
? kTopFrameId
: navigation_handle->GetFrameTreeNodeId();
}
// static
int ExtensionApiFrameIdMap::GetParentFrameId(content::RenderFrameHost* rfh) {
return rfh ? GetFrameId(rfh->GetParent()) : kInvalidFrameId;
}
// static
int ExtensionApiFrameIdMap::GetParentFrameId(
content::NavigationHandle* navigation_handle) {
if (navigation_handle->IsInMainFrame())
return kInvalidFrameId;
if (navigation_handle->IsParentMainFrame())
return kTopFrameId;
return navigation_handle->GetParentFrame()->GetFrameTreeNodeId();
}
// static
content::RenderFrameHost* ExtensionApiFrameIdMap::GetRenderFrameHostById(
content::WebContents* web_contents,
int frame_id) {
// Although it is technically possible to map |frame_id| to a RenderFrameHost
// without WebContents, we choose to not do that because in the extension API
// frameIds are only guaranteed to be meaningful in combination with a tabId.
if (!web_contents)
return nullptr;
if (frame_id == kInvalidFrameId)
return nullptr;
if (frame_id == kTopFrameId)
return web_contents->GetMainFrame();
DCHECK_GE(frame_id, 1);
// Unfortunately, extension APIs do not know which process to expect for a
// given frame ID, so we must use an unsafe API here that could return a
// different RenderFrameHost than the caller may have expected (e.g., one that
// changed after a cross-process navigation).
return web_contents->UnsafeFindFrameByFrameTreeNodeId(frame_id);
}
ExtensionApiFrameIdMap::FrameData ExtensionApiFrameIdMap::KeyToValue(
const RenderFrameIdKey& key) const {
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(
key.render_process_id, key.frame_routing_id);
if (!rfh || !rfh->IsRenderFrameLive())
return FrameData();
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(rfh);
// The RenderFrameHost may not have an associated WebContents in cases
// such as interstitial pages.
GURL last_committed_main_frame_url =
web_contents ? web_contents->GetLastCommittedURL() : GURL();
int tab_id = extension_misc::kUnknownTabId;
int window_id = extension_misc::kUnknownWindowId;
if (helper_)
helper_->PopulateTabData(rfh, &tab_id, &window_id);
return FrameData(GetFrameId(rfh), GetParentFrameId(rfh), tab_id, window_id,
std::move(last_committed_main_frame_url));
}
ExtensionApiFrameIdMap::FrameData ExtensionApiFrameIdMap::LookupFrameDataOnUI(
const RenderFrameIdKey& key,
bool is_from_io) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
bool lookup_successful = false;
FrameData data;
FrameDataMap::const_iterator frame_id_iter = frame_data_map_.find(key);
if (frame_id_iter != frame_data_map_.end()) {
lookup_successful = true;
data = frame_id_iter->second;
} else {
data = KeyToValue(key);
// Don't save invalid values in the map.
if (data.frame_id != kInvalidFrameId) {
lookup_successful = true;
auto kvpair = FrameDataMap::value_type(key, data);
base::AutoLock lock(frame_data_map_lock_);
frame_data_map_.insert(kvpair);
}
}
// TODO(devlin): Depending on how the data looks, this may be removable after
// a few cycles. Check back in M52 to see if it's still needed.
if (is_from_io) {
UMA_HISTOGRAM_BOOLEAN("Extensions.ExtensionFrameMapLookupSuccessful",
lookup_successful);
}
return data;
}
void ExtensionApiFrameIdMap::ReceivedFrameDataOnIO(
const RenderFrameIdKey& key,
const FrameData& cached_frame_data) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
auto map_iter = callbacks_map_.find(key);
if (map_iter == callbacks_map_.end()) {
// Can happen if ReceivedFrameDataOnIO was called after the frame ID was
// resolved (e.g. via GetFrameDataOnIO), but before PostTaskAndReply
// replied.
return;
}
FrameDataCallbacks& callbacks = map_iter->second;
if (callbacks.is_iterating)
return;
callbacks.is_iterating = true;
// Note: Extra items can be appended to |callbacks| during this loop if a
// callback calls GetFrameDataOnIO().
for (auto it = callbacks.callbacks.begin(); it != callbacks.callbacks.end();
++it) {
it->Run(cached_frame_data);
}
callbacks_map_.erase(key);
}
void ExtensionApiFrameIdMap::GetFrameDataOnIO(
int render_process_id,
int frame_routing_id,
const FrameDataCallback& callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
// TODO(robwu): Enable assertion when all callers have been fixed.
// DCHECK_EQ(MSG_ROUTING_NONE, -1);
if (!IsFrameRoutingIdValid(frame_routing_id)) {
callback.Run(FrameData());
return;
}
FrameData cached_frame_data;
bool did_find_cached_frame_data = GetCachedFrameDataOnIO(
render_process_id, frame_routing_id, &cached_frame_data);
const RenderFrameIdKey key(render_process_id, frame_routing_id);
auto map_iter = callbacks_map_.find(key);
if (did_find_cached_frame_data) {
// Value already cached, thread hopping is not needed.
if (map_iter == callbacks_map_.end()) {
// If the frame ID was cached, then it is likely that there are no pending
// callbacks. So do not unnecessarily copy the callback, but run it.
callback.Run(cached_frame_data);
} else {
map_iter->second.callbacks.push_back(callback);
ReceivedFrameDataOnIO(key, cached_frame_data);
}
return;
}
// The key was seen for the first time (or the frame has been removed).
// Hop to the UI thread to look up the extension API frame ID.
callbacks_map_[key].callbacks.push_back(callback);
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, {content::BrowserThread::UI},
base::Bind(&ExtensionApiFrameIdMap::LookupFrameDataOnUI,
base::Unretained(this), key, true /* is_from_io */),
base::Bind(&ExtensionApiFrameIdMap::ReceivedFrameDataOnIO,
base::Unretained(this), key));
}
bool ExtensionApiFrameIdMap::GetCachedFrameDataOnIO(int render_process_id,
int frame_routing_id,
FrameData* frame_data_out) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
// TODO(robwu): Enable assertion when all callers have been fixed.
// DCHECK_EQ(MSG_ROUTING_NONE, -1);
if (!IsFrameRoutingIdValid(frame_routing_id))
return false;
// A valid routing ID is only meaningful with a valid process ID.
DCHECK_GE(render_process_id, 0);
bool found = false;
{
base::AutoLock lock(frame_data_map_lock_);
FrameDataMap::const_iterator frame_id_iter = frame_data_map_.find(
RenderFrameIdKey(render_process_id, frame_routing_id));
if (frame_id_iter != frame_data_map_.end()) {
// This is very likely to happen because CacheFrameData() is called as
// soon as the frame is created.
*frame_data_out = frame_id_iter->second;
found = true;
}
}
// TODO(devlin): Depending on how the data looks, this may be removable after
// a few cycles. Check back in M52 to see if it's still needed.
UMA_HISTOGRAM_BOOLEAN("Extensions.ExtensionFrameMapCacheHit", found);
return found;
}
ExtensionApiFrameIdMap::FrameData ExtensionApiFrameIdMap::GetFrameData(
content::RenderFrameHost* rfh) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!rfh)
return FrameData();
const RenderFrameIdKey key(rfh->GetProcess()->GetID(), rfh->GetRoutingID());
return LookupFrameDataOnUI(key, false /* is_from_io */);
}
void ExtensionApiFrameIdMap::InitializeRenderFrameData(
content::RenderFrameHost* rfh) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(rfh);
DCHECK(rfh->IsRenderFrameLive());
const RenderFrameIdKey key(rfh->GetProcess()->GetID(), rfh->GetRoutingID());
CacheFrameData(key);
DCHECK(frame_data_map_.find(key) != frame_data_map_.end());
}
void ExtensionApiFrameIdMap::CacheFrameData(const RenderFrameIdKey& key) {
LookupFrameDataOnUI(key, false /* is_from_io */);
}
void ExtensionApiFrameIdMap::OnRenderFrameDeleted(
content::RenderFrameHost* rfh) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(rfh);
const RenderFrameIdKey key(rfh->GetProcess()->GetID(), rfh->GetRoutingID());
RemoveFrameData(key);
}
void ExtensionApiFrameIdMap::UpdateTabAndWindowId(
int tab_id,
int window_id,
content::RenderFrameHost* rfh) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(rfh);
const RenderFrameIdKey key(rfh->GetProcess()->GetID(), rfh->GetRoutingID());
// Only track FrameData for live render frames.
if (!rfh->IsRenderFrameLive()) {
return;
}
base::AutoLock lock(frame_data_map_lock_);
auto iter = frame_data_map_.find(key);
// The FrameData for |rfh| should have already been initialized.
DCHECK(iter != frame_data_map_.end());
iter->second.tab_id = tab_id;
iter->second.window_id = window_id;
}
void ExtensionApiFrameIdMap::OnMainFrameReadyToCommitNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(navigation_handle->IsInMainFrame());
bool did_insert = false;
std::tie(std::ignore, did_insert) =
ready_to_commit_document_navigations_.insert(navigation_handle);
DCHECK(did_insert);
content::RenderFrameHost* main_frame =
navigation_handle->GetRenderFrameHost();
DCHECK(main_frame);
// We only track live frames.
if (!main_frame->IsRenderFrameLive())
return;
const RenderFrameIdKey key(main_frame->GetProcess()->GetID(),
main_frame->GetRoutingID());
base::AutoLock lock(frame_data_map_lock_);
auto iter = frame_data_map_.find(key);
// We must have already cached the FrameData for this in
// InitializeRenderFrameHost.
DCHECK(iter != frame_data_map_.end());
iter->second.pending_main_frame_url = navigation_handle->GetURL();
}
void ExtensionApiFrameIdMap::OnMainFrameDidFinishNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(navigation_handle->IsInMainFrame());
bool did_fire_ready_to_commit_navigation =
!!ready_to_commit_document_navigations_.erase(navigation_handle);
// It's safe to call NavigationHandle::GetRenderFrameHost here iff the
// navigation committed or a ReadyToCommitNavigation event was dispatched for
// this navigation.
// Note a RenderFrameHost might not be associated with the NavigationHandle in
// WebContentsObserver::DidFinishNavigation. This might happen when the
// navigation doesn't commit which might happen for a variety of reasons like
// the network network request to fetch the navigation url failed, the
// navigation was cancelled, by say a NavigationThrottle etc.
// There's nothing to do if the RenderFrameHost can't be fetched for this
// navigation.
bool can_fetch_render_frame_host =
navigation_handle->HasCommitted() || did_fire_ready_to_commit_navigation;
if (!can_fetch_render_frame_host)
return;
content::RenderFrameHost* main_frame =
navigation_handle->GetRenderFrameHost();
DCHECK(main_frame);
// We only track live frames.
if (!main_frame->IsRenderFrameLive())
return;
const RenderFrameIdKey key(main_frame->GetProcess()->GetID(),
main_frame->GetRoutingID());
base::AutoLock lock(frame_data_map_lock_);
auto iter = frame_data_map_.find(key);
// We must have already cached the FrameData for this in
// InitializeRenderFrameHost.
DCHECK(iter != frame_data_map_.end());
iter->second.last_committed_main_frame_url =
main_frame->GetLastCommittedURL();
iter->second.pending_main_frame_url = base::nullopt;
}
bool ExtensionApiFrameIdMap::HasCachedFrameDataForTesting(
content::RenderFrameHost* rfh) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!rfh)
return false;
const RenderFrameIdKey key(rfh->GetProcess()->GetID(), rfh->GetRoutingID());
return frame_data_map_.find(key) != frame_data_map_.end();
}
size_t ExtensionApiFrameIdMap::GetFrameDataCountForTesting() const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return frame_data_map_.size();
}
void ExtensionApiFrameIdMap::RemoveFrameData(const RenderFrameIdKey& key) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::AutoLock lock(frame_data_map_lock_);
frame_data_map_.erase(key);
}
} // namespace extensions