From 86c008e8a7da9c00c5a676eb201ba5d0c976748e Mon Sep 17 00:00:00 2001 From: "erikkay@chromium.org" Date: Fri, 28 Aug 2009 20:26:05 +0000 Subject: override chrome:// URLs via extensions. Overrides are declared in an extension's manifest. The last one installed wins. However, we keep a list of those installed per page so that priority is preserved and so that uninstall will revert to a previous state. Review URL: http://codereview.chromium.org/174277 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@24791 0039d316-1c4b-4281-b951-d872f2087c98 --- base/values.cc | 15 +- base/values.h | 9 +- chrome/browser/browser_about_handler.cc | 2 +- chrome/browser/browser_about_handler.h | 3 +- chrome/browser/browser_prefs.cc | 2 + chrome/browser/browser_url_handler.cc | 11 +- chrome/browser/browser_url_handler.h | 5 +- chrome/browser/extensions/extension_apitest.cc | 55 +++-- chrome/browser/extensions/extension_apitest.h | 8 +- chrome/browser/extensions/extension_browsertest.cc | 23 ++- chrome/browser/extensions/extension_dom_ui.cc | 222 ++++++++++++++++++++- chrome/browser/extensions/extension_dom_ui.h | 34 ++++ chrome/browser/extensions/extension_host.cc | 22 +- .../extensions/extension_override_apitest.cc | 13 ++ chrome/browser/extensions/extensions_service.cc | 13 ++ .../browser/tab_contents/navigation_controller.cc | 2 +- chrome/chrome.gyp | 2 + chrome/common/extensions/extension.cc | 31 +++ chrome/common/extensions/extension.h | 9 + chrome/common/extensions/extension_constants.cc | 3 + chrome/common/extensions/extension_constants.h | 2 + chrome/common/notification_type.h | 4 + .../data/extensions/api_test/override1/api_test.js | 107 ++++++++++ .../extensions/api_test/override1/background.html | 2 + .../extensions/api_test/override1/downloads.html | 6 + .../extensions/api_test/override1/history.html | 4 + .../extensions/api_test/override1/manifest.json | 12 ++ .../data/extensions/api_test/override1/newtab.html | 4 + .../extensions/api_test/override1/nonexistant.html | 4 + .../data/extensions/api_test/override1/test.js | 11 + 30 files changed, 582 insertions(+), 58 deletions(-) create mode 100644 chrome/browser/extensions/extension_override_apitest.cc create mode 100755 chrome/test/data/extensions/api_test/override1/api_test.js create mode 100644 chrome/test/data/extensions/api_test/override1/background.html create mode 100644 chrome/test/data/extensions/api_test/override1/downloads.html create mode 100644 chrome/test/data/extensions/api_test/override1/history.html create mode 100644 chrome/test/data/extensions/api_test/override1/manifest.json create mode 100644 chrome/test/data/extensions/api_test/override1/newtab.html create mode 100644 chrome/test/data/extensions/api_test/override1/nonexistant.html create mode 100644 chrome/test/data/extensions/api_test/override1/test.js diff --git a/base/values.cc b/base/values.cc index 3b5bd38..716fdf3 100644 --- a/base/values.cc +++ b/base/values.cc @@ -610,13 +610,15 @@ bool ListValue::Remove(size_t index, Value** out_value) { return true; } -void ListValue::Remove(const Value& value) { +int ListValue::Remove(const Value& value) { for (ValueVector::iterator i(list_.begin()); i != list_.end(); ++i) { if ((*i)->Equals(&value)) { + size_t index = i - list_.begin(); list_.erase(i); - break; + return index; } } + return -1; } void ListValue::Append(Value* in_value) { @@ -624,6 +626,15 @@ void ListValue::Append(Value* in_value) { list_.push_back(in_value); } +bool ListValue::Insert(size_t index, Value* in_value) { + DCHECK(in_value); + if (index < 0 || index > list_.size()) + return false; + + list_.insert(list_.begin() + index, in_value); + return true; +} + Value* ListValue::DeepCopy() const { ListValue* result = new ListValue; diff --git a/base/values.h b/base/values.h index cd68b10..482ffa0 100644 --- a/base/values.h +++ b/base/values.h @@ -334,12 +334,17 @@ class ListValue : public Value { // it will return false and the ListValue object will be unchanged. bool Remove(size_t index, Value** out_value); - // Removes the first instance of |value| found in the list, if any. - void Remove(const Value& value); + // Removes the first instance of |value| found in the list, if any, returning + // the index that it was located at (-1 for not present). + int Remove(const Value& value); // Appends a Value to the end of the list. void Append(Value* in_value); + // Insert a Value at index. + // Returns true if successful, or false if the index was out of range. + bool Insert(size_t index, Value* in_value); + // Iteration typedef ValueVector::iterator iterator; typedef ValueVector::const_iterator const_iterator; diff --git a/chrome/browser/browser_about_handler.cc b/chrome/browser/browser_about_handler.cc index 9db4260..6cc4491 100644 --- a/chrome/browser/browser_about_handler.cc +++ b/chrome/browser/browser_about_handler.cc @@ -698,7 +698,7 @@ void AboutMemoryHandler::OnDetailsAvailable() { // ----------------------------------------------------------------------------- -bool WillHandleBrowserAboutURL(GURL* url) { +bool WillHandleBrowserAboutURL(GURL* url, Profile* profile) { // We only handle about: schemes. if (!url->SchemeIs(chrome::kAboutScheme)) return false; diff --git a/chrome/browser/browser_about_handler.h b/chrome/browser/browser_about_handler.h index dedf929..87e5e75 100644 --- a/chrome/browser/browser_about_handler.h +++ b/chrome/browser/browser_about_handler.h @@ -8,6 +8,7 @@ #define CHROME_BROWSER_BROWSER_ABOUT_HANDLER_H_ class GURL; +class Profile; // Decides whether the given URL will be handled by the browser about handler // and returns true if so. On true, it may also modify the given URL to be the @@ -15,7 +16,7 @@ class GURL; // handles all "about:" URLs as "about:blank. // // This is used by BrowserURLHandler. -bool WillHandleBrowserAboutURL(GURL* url); +bool WillHandleBrowserAboutURL(GURL* url, Profile* profile); // We have a few magic commands that don't cause navigations, but rather pop up // dialogs. This function handles those cases, and returns true if so. In this diff --git a/chrome/browser/browser_prefs.cc b/chrome/browser/browser_prefs.cc index 74e9b65..cb1a8cc 100644 --- a/chrome/browser/browser_prefs.cc +++ b/chrome/browser/browser_prefs.cc @@ -12,6 +12,7 @@ #include "chrome/browser/debugger/devtools_manager.h" #include "chrome/browser/dom_ui/new_tab_ui.h" #include "chrome/browser/download/download_manager.h" +#include "chrome/browser/extensions/extension_dom_ui.h" #include "chrome/browser/external_protocol_handler.h" #include "chrome/browser/google_url_tracker.h" #include "chrome/browser/metrics/metrics_service.h" @@ -75,6 +76,7 @@ void RegisterAllPrefs(PrefService* user_prefs, PrefService* local_state) { AutofillManager::RegisterUserPrefs(user_prefs); TabContents::RegisterUserPrefs(user_prefs); TemplateURLPrepopulateData::RegisterUserPrefs(user_prefs); + ExtensionDOMUI::RegisterUserPrefs(user_prefs); NewTabUI::RegisterUserPrefs(user_prefs); BlockedPopupContainer::RegisterUserPrefs(user_prefs); DevToolsManager::RegisterUserPrefs(user_prefs); diff --git a/chrome/browser/browser_url_handler.cc b/chrome/browser/browser_url_handler.cc index e42e34e..506ccfd 100644 --- a/chrome/browser/browser_url_handler.cc +++ b/chrome/browser/browser_url_handler.cc @@ -7,11 +7,13 @@ #include "base/string_util.h" #include "chrome/browser/browser_about_handler.h" #include "chrome/browser/dom_ui/dom_ui_factory.h" +#include "chrome/browser/extensions/extension_dom_ui.h" +#include "chrome/browser/profile.h" #include "chrome/common/url_constants.h" #include "googleurl/src/gurl.h" // Handles rewriting view-source URLs for what we'll actually load. -static bool HandleViewSource(GURL* url) { +static bool HandleViewSource(GURL* url, Profile* profile) { if (url->SchemeIs(chrome::kViewSourceScheme)) { // Load the inner URL instead. *url = GURL(url->path()); @@ -21,7 +23,7 @@ static bool HandleViewSource(GURL* url) { } // Handles URLs for DOM UI. These URLs need no rewriting. -static bool HandleDOMUI(GURL* url) { +static bool HandleDOMUI(GURL* url, Profile* profile) { if (!DOMUIFactory::UseDOMUIForURL(*url)) return false; return true; @@ -35,17 +37,18 @@ void BrowserURLHandler::InitURLHandlers() { return; // Add the default URL handlers. + url_handlers_.push_back(&ExtensionDOMUI::HandleChromeURLOverride); url_handlers_.push_back(&WillHandleBrowserAboutURL); // about: url_handlers_.push_back(&HandleDOMUI); // chrome: & friends. url_handlers_.push_back(&HandleViewSource); // view-source: } // static -void BrowserURLHandler::RewriteURLIfNecessary(GURL* url) { +void BrowserURLHandler::RewriteURLIfNecessary(GURL* url, Profile* profile) { if (url_handlers_.empty()) InitURLHandlers(); for (size_t i = 0; i < url_handlers_.size(); ++i) { - if ((*url_handlers_[i])(url)) + if ((*url_handlers_[i])(url, profile)) return; } } diff --git a/chrome/browser/browser_url_handler.h b/chrome/browser/browser_url_handler.h index 8200c8a..593408e 100644 --- a/chrome/browser/browser_url_handler.h +++ b/chrome/browser/browser_url_handler.h @@ -15,6 +15,7 @@ #include class GURL; +class Profile; // BrowserURLHandler manages the list of all special URLs and manages // dispatching the URL handling to registered handlers. @@ -26,11 +27,11 @@ class BrowserURLHandler { // - optionally set |dispatcher| to the necessary DOMMessageDispatcher // - return true. // If the URL is not handled by a handler, it should return false. - typedef bool (*URLHandler)(GURL* url); + typedef bool (*URLHandler)(GURL* url, Profile* profile); // HandleBrowserURL gives all registered URLHandlers a shot at processing // the given URL, and modifies it in place. - static void RewriteURLIfNecessary(GURL* url); + static void RewriteURLIfNecessary(GURL* url, Profile* profile); // We initialize the list of url_handlers_ lazily the first time MaybeHandle // is called. diff --git a/chrome/browser/extensions/extension_apitest.cc b/chrome/browser/extensions/extension_apitest.cc index 3c45524..415d1c9 100644 --- a/chrome/browser/extensions/extension_apitest.cc +++ b/chrome/browser/extensions/extension_apitest.cc @@ -14,34 +14,48 @@ static const int kTimeoutMs = 60 * 1000; // 1 minute // Load an extension and wait for it to notify of PASSED or FAILED. bool ExtensionApiTest::RunExtensionTest(const char* extension_name) { - bool result; - completed_ = false; { NotificationRegistrar registrar; registrar.Add(this, NotificationType::EXTENSION_TEST_PASSED, NotificationService::AllSources()); registrar.Add(this, NotificationType::EXTENSION_TEST_FAILED, NotificationService::AllSources()); - result = LoadExtension(test_data_dir_.AppendASCII(extension_name)); - // If the test runs quickly, we may get the notification while waiting - // for the Load to finish. - if (completed_) { - result = passed_; - } else { - result = WaitForPassFail(); + if (!LoadExtension(test_data_dir_.AppendASCII(extension_name))) { + message_ = "Failed to load extension."; + return false; } } - return result; + + // TODO(erikkay) perhaps we shouldn't do this implicitly. + return WaitForPassFail(); } bool ExtensionApiTest::WaitForPassFail() { - completed_ = false; - passed_ = false; - MessageLoop::current()->PostDelayedTask( - FROM_HERE, new MessageLoop::QuitTask, kTimeoutMs); - ui_test_utils::RunMessageLoop(); - return passed_; + NotificationRegistrar registrar; + registrar.Add(this, NotificationType::EXTENSION_TEST_PASSED, + NotificationService::AllSources()); + registrar.Add(this, NotificationType::EXTENSION_TEST_FAILED, + NotificationService::AllSources()); + + // Depending on the tests, multiple results can come in from a single call + // to RunMessageLoop(), so we maintain a queue of results and just pull them + // off as the test calls this, going to the run loop only when the queue is + // empty. + if (!results_.size()) { + MessageLoop::current()->PostDelayedTask( + FROM_HERE, new MessageLoop::QuitTask, kTimeoutMs); + ui_test_utils::RunMessageLoop(); + } + if (results_.size()) { + bool ret = results_.front(); + results_.pop_front(); + message_ = messages_.front(); + messages_.pop_front(); + return ret; + } + message_ = "No response from message loop."; + return false; } void ExtensionApiTest::SetUpCommandLine(CommandLine* command_line) { @@ -55,16 +69,15 @@ void ExtensionApiTest::Observe(NotificationType type, switch (type.value) { case NotificationType::EXTENSION_TEST_PASSED: std::cout << "Got EXTENSION_TEST_PASSED notification.\n"; - completed_ = true; - passed_ = true; + results_.push_back(true); + messages_.push_back(""); MessageLoopForUI::current()->Quit(); break; case NotificationType::EXTENSION_TEST_FAILED: std::cout << "Got EXTENSION_TEST_FAILED notification.\n"; - completed_ = true; - passed_ = false; - message_ = *(Details(details).ptr()); + results_.push_back(false); + messages_.push_back(*(Details(details).ptr())); MessageLoopForUI::current()->Quit(); break; diff --git a/chrome/browser/extensions/extension_apitest.h b/chrome/browser/extensions/extension_apitest.h index af2dd28..466735d 100644 --- a/chrome/browser/extensions/extension_apitest.h +++ b/chrome/browser/extensions/extension_apitest.h @@ -33,13 +33,11 @@ class ExtensionApiTest : public ExtensionBrowserTest { void Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details); - // Did the extension side of the unit test complete? - bool completed_; - - // Did the extension side of the unit test pass? - bool passed_; + // A sequential list of pass/fail notifications from the test extension(s). + std::deque results_; // If it failed, what was the error message? + std::deque messages_; std::string message_; }; diff --git a/chrome/browser/extensions/extension_browsertest.cc b/chrome/browser/extensions/extension_browsertest.cc index 27e6311..cf7a513 100644 --- a/chrome/browser/extensions/extension_browsertest.cc +++ b/chrome/browser/extensions/extension_browsertest.cc @@ -149,22 +149,20 @@ bool ExtensionBrowserTest::WaitForExtensionHostsToLoad() { // Wait for all the extension hosts that exist to finish loading. // NOTE: This assumes that the extension host list is not changing while // this method is running. + + NotificationRegistrar registrar; + registrar.Add(this, NotificationType::EXTENSION_HOST_DID_STOP_LOADING, + NotificationService::AllSources()); + ExtensionProcessManager* manager = browser()->profile()->GetExtensionProcessManager(); base::Time start_time = base::Time::Now(); + MessageLoop::current()->PostDelayedTask(FROM_HERE, + new MessageLoop::QuitTask, kTimeoutMs); for (ExtensionProcessManager::const_iterator iter = manager->begin(); iter != manager->end(); ++iter) { - while (!(*iter)->did_stop_loading()) { - if ((base::Time::Now() - start_time).InMilliseconds() > kTimeoutMs) { - std::cout << "Extension host did not load for URL: " - << (*iter)->GetURL().spec(); - return false; - } - - MessageLoop::current()->PostDelayedTask(FROM_HERE, - new MessageLoop::QuitTask, 200); + if (!(*iter)->did_stop_loading()) ui_test_utils::RunMessageLoop(); - } } return true; @@ -184,6 +182,11 @@ void ExtensionBrowserTest::Observe(NotificationType type, MessageLoopForUI::current()->Quit(); break; + case NotificationType::EXTENSION_HOST_DID_STOP_LOADING: + std::cout << "Got EXTENSION_HOST_DID_STOP_LOADING notification.\n"; + MessageLoopForUI::current()->Quit(); + break; + default: NOTREACHED(); break; diff --git a/chrome/browser/extensions/extension_dom_ui.cc b/chrome/browser/extensions/extension_dom_ui.cc index ed5abb9..044a28a 100644 --- a/chrome/browser/extensions/extension_dom_ui.cc +++ b/chrome/browser/extensions/extension_dom_ui.cc @@ -5,8 +5,16 @@ #include "chrome/browser/extensions/extension_dom_ui.h" #include "chrome/browser/browser.h" +#include "chrome/browser/browser_list.h" +#include "chrome/browser/profile.h" #include "chrome/browser/tab_contents/tab_contents.h" #include "chrome/common/bindings_policy.h" +#include "chrome/common/pref_service.h" +#include "chrome/common/url_constants.h" + +namespace { +const wchar_t kExtensionURLOverrides[] = L"extensions.chrome_url_overrides"; +} ExtensionDOMUI::ExtensionDOMUI(TabContents* tab_contents) : DOMUI(tab_contents) { @@ -14,18 +22,35 @@ ExtensionDOMUI::ExtensionDOMUI(TabContents* tab_contents) hide_favicon_ = true; should_hide_url_ = true; bindings_ = BindingsPolicy::EXTENSION; + + // For chrome:// overrides, some of the defaults are a little different. + GURL url = tab_contents->GetURL(); + if (url.SchemeIs(chrome::kChromeUIScheme)) { + if (url.host() == chrome::kChromeUINewTabHost) { + focus_location_bar_by_default_ = true; + } else { + // Current behavior of other chrome:// pages is to display the URL. + should_hide_url_ = false; + } + } } -void ExtensionDOMUI::RenderViewCreated(RenderViewHost* render_view_host) { +void ExtensionDOMUI::ResetExtensionFunctionDispatcher( + RenderViewHost* render_view_host) { + // Use the NavigationController to get the URL rather than the TabContents + // since this is the real underlying URL (see HandleChromeURLOverride). + NavigationController& controller = tab_contents()->controller(); + const GURL& url = controller.pending_entry()->url(); extension_function_dispatcher_.reset( - new ExtensionFunctionDispatcher(render_view_host, this, - tab_contents()->GetURL())); + new ExtensionFunctionDispatcher(render_view_host, this, url)); +} + +void ExtensionDOMUI::RenderViewCreated(RenderViewHost* render_view_host) { + ResetExtensionFunctionDispatcher(render_view_host); } void ExtensionDOMUI::RenderViewReused(RenderViewHost* render_view_host) { - extension_function_dispatcher_.reset( - new ExtensionFunctionDispatcher(render_view_host, this, - tab_contents()->GetURL())); + ResetExtensionFunctionDispatcher(render_view_host); } void ExtensionDOMUI::ProcessDOMUIMessage(const std::string& message, @@ -39,3 +64,188 @@ void ExtensionDOMUI::ProcessDOMUIMessage(const std::string& message, Browser* ExtensionDOMUI::GetBrowser() { return static_cast(tab_contents()->delegate()); } + +//////////////////////////////////////////////////////////////////////////////// +// chrome:// URL overrides + +// static +void ExtensionDOMUI::RegisterUserPrefs(PrefService* prefs) { + prefs->RegisterDictionaryPref(kExtensionURLOverrides); +} + +// static +bool ExtensionDOMUI::HandleChromeURLOverride(GURL* url, Profile* profile) { + if (!url->SchemeIs(chrome::kChromeUIScheme)) + return false; + + // Even when the extensions service is enabled by default, it's still + // disabled in incognito mode. + ExtensionsService* service = profile->GetExtensionsService(); + if (!service) + return false; + + const DictionaryValue* overrides = + profile->GetPrefs()->GetDictionary(kExtensionURLOverrides); + std::string page = url->host(); + ListValue* url_list; + if (!overrides || !overrides->GetList(UTF8ToWide(page), &url_list)) + return false; + + if (!service->is_ready()) { + // TODO(erikkay) So far, it looks like extensions load before the new tab + // page. I don't know if we have anything that enforces this, so add this + // check for safety. + NOTREACHED() << "Chrome URL override requested before extensions loaded"; + return false; + } + + while (url_list->GetSize()) { + Value* val; + url_list->Get(0, &val); + + // Verify that the override value is good. If not, unregister it and find + // the next one. + std::string override; + if (!val->GetAsString(&override)) { + NOTREACHED(); + UnregisterChromeURLOverride(page, profile, val); + continue; + } + GURL extension_url(override); + if (!extension_url.is_valid()) { + NOTREACHED(); + UnregisterChromeURLOverride(page, profile, val); + continue; + } + + // Verify that the extension that's being referred to actually exists. + Extension* extension = service->GetExtensionByURL(extension_url); + if (!extension) { + // This can currently happen if you use --load-extension one run, and + // then don't use it the next. It could also happen if an extension + // were deleted directly from the filesystem, etc. + LOG(WARNING) << "chrome URL override present for non-existant extension"; + UnregisterChromeURLOverride(page, profile, val); + continue; + } + + *url = extension_url; + return true; + } + return false; +} + +// static +void ExtensionDOMUI::RegisterChromeURLOverrides( + Profile* profile, const DictionaryValue* new_overrides) { + if (!new_overrides) + return; + + PrefService* prefs = profile->GetPrefs(); + DictionaryValue* all_overrides = + prefs->GetMutableDictionary(kExtensionURLOverrides); + + // For each override provided by the extension, add it to the front of + // the override list if it's not already in the list. + DictionaryValue::key_iterator iter = new_overrides->begin_keys(); + for (;iter != new_overrides->end_keys(); ++iter) { + Value* val; + new_overrides->Get(*iter, &val); + std::string string_val; + if (!val->GetAsString(&string_val)) { + NOTREACHED(); + continue; + } + ListValue* page_overrides; + if (!all_overrides->GetList(*iter, &page_overrides)) { + page_overrides = new ListValue(); + all_overrides->Set(*iter, page_overrides); + } else { + // Verify that the override isn't already in the list. + ListValue::iterator i = page_overrides->begin(); + for (; i != page_overrides->end(); ++i) { + std::string override_val; + if (!(*i)->GetAsString(&override_val)) { + NOTREACHED(); + continue; + } + if (override_val == string_val) + break; + } + // This value is already in the list, leave it alone. + if (i != page_overrides->end()) + continue; + } + // Insert the override at the front of the list. Last registered override + // wins. + page_overrides->Insert(0, val->DeepCopy()); + } +} + +// static +void ExtensionDOMUI::UnregisterAndReplaceOverride(const std::string& page, + Profile* profile, ListValue* list, Value* override) { + int index = list->Remove(*override); + if (index == 0) { + // This is the active override, so we need to find all existing + // tabs for this override and get them to reload the original URL. + for (TabContentsIterator iterator; !iterator.done(); iterator++) { + TabContents* tab = *iterator; + if (tab->profile() != profile) + continue; + + GURL url = tab->GetURL(); + if (!url.SchemeIs(chrome::kChromeUIScheme) || url.host() != page) + continue; + + // Don't use Reload() since |url| isn't the same as the internal URL + // that NavigationController has. + tab->controller().LoadURL(url, url, PageTransition::RELOAD); + } + } +} + +// static +void ExtensionDOMUI::UnregisterChromeURLOverride(const std::string& page, + Profile* profile, Value* override) { + if (!override) + return; + PrefService* prefs = profile->GetPrefs(); + DictionaryValue* all_overrides = + prefs->GetMutableDictionary(kExtensionURLOverrides); + ListValue* page_overrides; + if (!all_overrides->GetList(UTF8ToWide(page), &page_overrides)) { + // If it's being unregistered, it should already be in the list. + NOTREACHED(); + return; + } else { + UnregisterAndReplaceOverride(page, profile, page_overrides, override); + } +} + +// static +void ExtensionDOMUI::UnregisterChromeURLOverrides( + Profile* profile, const DictionaryValue* new_overrides) { + if (!new_overrides) + return; + PrefService* prefs = profile->GetPrefs(); + DictionaryValue* all_overrides = + prefs->GetMutableDictionary(kExtensionURLOverrides); + DictionaryValue::key_iterator iter = new_overrides->begin_keys(); + for (; iter != new_overrides->end_keys(); ++iter) { + Value* val; + if (!new_overrides->Get(*iter, &val)) { + NOTREACHED(); + return; + } + ListValue* page_overrides; + if (!all_overrides->GetList(*iter, &page_overrides)) { + // If it's being unregistered, it should already be in the list. + NOTREACHED(); + continue; + } else { + UnregisterAndReplaceOverride(WideToUTF8(*iter), profile, page_overrides, + val); + } + } +} diff --git a/chrome/browser/extensions/extension_dom_ui.h b/chrome/browser/extensions/extension_dom_ui.h index ff64a9d..b8ef8dc 100644 --- a/chrome/browser/extensions/extension_dom_ui.h +++ b/chrome/browser/extensions/extension_dom_ui.h @@ -9,6 +9,10 @@ #include "chrome/browser/dom_ui/dom_ui.h" #include "chrome/browser/extensions/extension_function_dispatcher.h" +class ListValue; +class PrefService; +class TabContents; + // This class implements DOMUI for extensions and allows extensions to put UI in // the main tab contents area. class ExtensionDOMUI @@ -16,6 +20,7 @@ class ExtensionDOMUI public ExtensionFunctionDispatcher::Delegate { public: explicit ExtensionDOMUI(TabContents* tab_contents); + ExtensionFunctionDispatcher* extension_function_dispatcher() const { return extension_function_dispatcher_.get(); } @@ -31,7 +36,36 @@ class ExtensionDOMUI // ExtensionFunctionDispatcher::Delegate virtual Browser* GetBrowser(); + // BrowserURLHandler + static bool HandleChromeURLOverride(GURL* url, Profile* profile); + + // Register and unregister a dictionary of one or more overrides. + // Page names are the keys, and chrome-extension: URLs are the values. + // (e.g. { "newtab": "chrome-extension:///my_new_tab.html" } + static void RegisterChromeURLOverrides(Profile* profile, + const DictionaryValue* overrides); + static void UnregisterChromeURLOverrides(Profile* profile, + const DictionaryValue* overrides); + static void UnregisterChromeURLOverride(const std::string& page, + Profile* profile, + Value* override); + + // Called from BrowserPrefs + static void RegisterUserPrefs(PrefService* prefs); + private: + // Unregister the specified override, and if it's the currently active one, + // ensure that something takes its place. + static void UnregisterAndReplaceOverride(const std::string& page, + Profile* profile, + ListValue* list, + Value* override); + + // When the RenderViewHost changes (RenderViewCreated and RenderViewReused), + // we need to reset the ExtensionFunctionDispatcher so it's talking to the + // right one, as well as being linked to the correct URL. + void ResetExtensionFunctionDispatcher(RenderViewHost* render_view_host); + scoped_ptr extension_function_dispatcher_; }; diff --git a/chrome/browser/extensions/extension_host.cc b/chrome/browser/extensions/extension_host.cc index bc03f8f..60aa0d3 100644 --- a/chrome/browser/extensions/extension_host.cc +++ b/chrome/browser/extensions/extension_host.cc @@ -174,7 +174,13 @@ void ExtensionHost::DidStopLoading(RenderViewHost* render_view_host) { } #endif - did_stop_loading_ = true; + if (!did_stop_loading_) { + NotificationService::current()->Notify( + NotificationType::EXTENSION_HOST_DID_STOP_LOADING, + Source(profile_), + Details(this)); + did_stop_loading_ = true; + } } void ExtensionHost::DocumentAvailableInMainFrame(RenderViewHost* rvh) { @@ -303,8 +309,18 @@ Browser* ExtensionHost::GetBrowser() { if (view_.get()) return view_->browser(); #endif - Browser* browser = BrowserList::GetLastActiveWithProfile( - render_view_host()->process()->profile()); + Profile* profile = render_view_host()->process()->profile(); + Browser* browser = BrowserList::GetLastActiveWithProfile(profile); + + // It's possible for a browser to exist, but to have never been active. + // This can happen if you launch the browser on a machine without an active + // desktop (a headless buildbot) or if you quickly give another app focus + // at launch time. This is easy to do with browser_tests. + if (!browser) + browser = BrowserList::FindBrowserWithProfile(profile); + + // TODO(erikkay): can this still return NULL? Is Rafael's comment still + // valid here? // NOTE(rafaelw): This can return NULL in some circumstances. In particular, // a toolstrip or background_page onload chrome.tabs api call can make it // into here before the browser is sufficiently initialized to return here. diff --git a/chrome/browser/extensions/extension_override_apitest.cc b/chrome/browser/extensions/extension_override_apitest.cc new file mode 100644 index 0000000..7b10e33 --- /dev/null +++ b/chrome/browser/extensions/extension_override_apitest.cc @@ -0,0 +1,13 @@ +// Copyright (c) 2009 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 "chrome/browser/extensions/extension_apitest.h" + +IN_PROC_BROWSER_TEST_F(ExtensionApiTest, Overrides) { + ASSERT_TRUE(RunExtensionTest("override1")) << message_; // new tab + EXPECT_EQ(results_.size(), 0U); + + // TODO(erikkay) load a second override and verify behavior, then unload + // the first and verify behavior, etc. +} diff --git a/chrome/browser/extensions/extensions_service.cc b/chrome/browser/extensions/extensions_service.cc index acb74cd..b53f243 100644 --- a/chrome/browser/extensions/extensions_service.cc +++ b/chrome/browser/extensions/extensions_service.cc @@ -11,6 +11,7 @@ #include "chrome/browser/chrome_thread.h" #include "chrome/browser/extensions/crx_installer.h" #include "chrome/browser/extensions/extension_browser_event_router.h" +#include "chrome/browser/extensions/extension_dom_ui.h" #include "chrome/browser/extensions/extension_file_util.h" #include "chrome/browser/extensions/extension_updater.h" #include "chrome/browser/extensions/external_extension_provider.h" @@ -195,6 +196,9 @@ void ExtensionsService::UninstallExtension(const std::string& extension_id, install_directory_)); } + ExtensionDOMUI::UnregisterChromeURLOverrides(profile_, + extension->GetChromeURLOverrides()); + UnloadExtension(extension_id); } @@ -213,6 +217,9 @@ void ExtensionsService::EnableExtension(const std::string& extension_id) { extension); disabled_extensions_.erase(iter); + ExtensionDOMUI::RegisterChromeURLOverrides(profile_, + extension->GetChromeURLOverrides()); + NotificationService::current()->Notify( NotificationType::EXTENSION_LOADED, Source(this), @@ -314,6 +321,9 @@ void ExtensionsService::UnloadExtension(const std::string& extension_id) { // Callers should not send us nonexistant extensions. CHECK(extension.get()); + ExtensionDOMUI::UnregisterChromeURLOverrides(profile_, + extension->GetChromeURLOverrides()); + ExtensionList::iterator iter = std::find(disabled_extensions_.begin(), disabled_extensions_.end(), extension.get()); @@ -426,6 +436,9 @@ void ExtensionsService::OnExtensionLoaded(Extension* extension, NotificationType::THEME_INSTALLED, Source(this), Details(extension)); + } else { + ExtensionDOMUI::RegisterChromeURLOverrides(profile_, + extension->GetChromeURLOverrides()); } break; case Extension::DISABLED: diff --git a/chrome/browser/tab_contents/navigation_controller.cc b/chrome/browser/tab_contents/navigation_controller.cc index 244f251..3704169 100644 --- a/chrome/browser/tab_contents/navigation_controller.cc +++ b/chrome/browser/tab_contents/navigation_controller.cc @@ -358,7 +358,7 @@ NavigationEntry* NavigationController::CreateNavigationEntry( // will actually be loaded. This real URL won't be shown to the user, just // used internally. GURL loaded_url(url); - BrowserURLHandler::RewriteURLIfNecessary(&loaded_url); + BrowserURLHandler::RewriteURLIfNecessary(&loaded_url, profile_); NavigationEntry* entry = new NavigationEntry(NULL, -1, loaded_url, referrer, string16(), transition); diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index c1d3a25..8c768ad 100644 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -47,6 +47,7 @@ 'browser/extensions/extension_browsertest.cc', 'browser/extensions/extension_browsertest.h', 'browser/extensions/extension_browsertests_misc.cc', + 'browser/extensions/extension_override_apitest.cc', 'browser/extensions/extension_startup_unittest.cc', ], 'browser_tests_sources_win_specific': [ @@ -71,6 +72,7 @@ 'browser/extensions/extension_browsertest.cc', 'browser/extensions/extension_browsertest.h', 'browser/extensions/extension_browsertests_misc.cc', + 'browser/extensions/extension_override_apitest.cc', 'browser/extensions/extension_startup_unittest.cc', ], # TODO(jcampan): move these vars to views.gyp. diff --git a/chrome/common/extensions/extension.cc b/chrome/common/extensions/extension.cc index c826c07..3f3c8539 100644 --- a/chrome/common/extensions/extension.cc +++ b/chrome/common/extensions/extension.cc @@ -998,6 +998,37 @@ bool Extension::InitFromValue(const DictionaryValue& source, bool require_id, set_default_locale(default_locale); } + // Chrome URL overrides (optional) + if (source.HasKey(keys::kChromeURLOverrides)) { + DictionaryValue* overrides; + if (!source.GetDictionary(keys::kChromeURLOverrides, &overrides)) { + *error = errors::kInvalidChromeURLOverrides; + return false; + } + // Validate that the overrides are all strings + DictionaryValue::key_iterator iter = overrides->begin_keys(); + while (iter != overrides->end_keys()) { + // For now, only allow the new tab page. Others will work when we remove + // this check, but let's keep it simple for now. + // TODO(erikkay) enable other pages as well + if (WideToUTF8(*iter) != chrome::kChromeUINewTabHost) { + *error = errors::kInvalidChromeURLOverrides; + return false; + } + std::string val; + if (!overrides->GetString(*iter, &val)) { + *error = errors::kInvalidChromeURLOverrides; + return false; + } + // Replace the entry with a fully qualified chrome-extension:// URL. + GURL url = GetResourceURL(val); + overrides->SetString(*iter, url.spec()); + ++iter; + } + chrome_url_overrides_.reset( + static_cast(overrides->DeepCopy())); + } + return true; } diff --git a/chrome/common/extensions/extension.h b/chrome/common/extensions/extension.h index 6e7215f..e8cdd10 100644 --- a/chrome/common/extensions/extension.h +++ b/chrome/common/extensions/extension.h @@ -262,6 +262,11 @@ class Extension { default_locale_ = default_locale; } + // Chrome URL overrides (see ExtensionOverrideUI). + DictionaryValue* GetChromeURLOverrides() const { + return chrome_url_overrides_.get(); + } + // Runtime data: // Put dynamic data about the state of a running extension below. @@ -382,6 +387,10 @@ class Extension { // Default locale, used for fallback. std::string default_locale_; + // A map of chrome:// hostnames (newtab, downloads, etc.) to Extension URLs + // which override the handling of those URLs. + scoped_ptr chrome_url_overrides_; + // Runtime data: // True if the background page is ready. diff --git a/chrome/common/extensions/extension_constants.cc b/chrome/common/extensions/extension_constants.cc index d887678..d72fbcf 100644 --- a/chrome/common/extensions/extension_constants.cc +++ b/chrome/common/extensions/extension_constants.cc @@ -7,6 +7,7 @@ namespace extension_manifest_keys { const wchar_t* kBackground = L"background_page"; +const wchar_t* kChromeURLOverrides = L"chrome_url_overrides"; const wchar_t* kContentScripts = L"content_scripts"; const wchar_t* kCss = L"css"; const wchar_t* kDefaultLocale = L"default_locale"; @@ -52,6 +53,8 @@ const char* kPageActionTypePermanent = "permanent"; // printf because we want to unit test them and scanf is hard to make // cross-platform. namespace extension_manifest_errors { +const char* kInvalidChromeURLOverrides = + "Invalid value for 'chrome_url_overrides'."; const char* kInvalidContentScript = "Invalid value for 'content_scripts[*]'."; const char* kInvalidContentScriptsList = diff --git a/chrome/common/extensions/extension_constants.h b/chrome/common/extensions/extension_constants.h index b808665..09429a2 100644 --- a/chrome/common/extensions/extension_constants.h +++ b/chrome/common/extensions/extension_constants.h @@ -8,6 +8,7 @@ // Keys used in JSON representation of extensions. namespace extension_manifest_keys { extern const wchar_t* kBackground; + extern const wchar_t* kChromeURLOverrides; extern const wchar_t* kContentScripts; extern const wchar_t* kCss; extern const wchar_t* kDefaultLocale; @@ -51,6 +52,7 @@ namespace extension_manifest_values { // Error messages returned from Extension::InitFromValue(). namespace extension_manifest_errors { + extern const char* kInvalidChromeURLOverrides; extern const char* kInvalidContentScript; extern const char* kInvalidContentScriptsList; extern const char* kInvalidCss; diff --git a/chrome/common/notification_type.h b/chrome/common/notification_type.h index ba74687..7c7140f 100644 --- a/chrome/common/notification_type.h +++ b/chrome/common/notification_type.h @@ -631,6 +631,10 @@ class NotificationType { // an ExtensionHost* and the source is a Profile*. EXTENSION_HOST_DESTROYED, + // Send by an ExtensionHost when it finished its initial page load. + // The details are an ExtensionHost* and the source is a Profile*. + EXTENSION_HOST_DID_STOP_LOADING, + // Sent after an extension render process is created and fully functional. // The details are an ExtensionHost*. EXTENSION_PROCESS_CREATED, diff --git a/chrome/test/data/extensions/api_test/override1/api_test.js b/chrome/test/data/extensions/api_test/override1/api_test.js new file mode 100755 index 0000000..84d0bb1 --- /dev/null +++ b/chrome/test/data/extensions/api_test/override1/api_test.js @@ -0,0 +1,107 @@ +// api_test.js +// mini-framework for ExtensionApiTest browser tests +// TODO(erikkay) - figure out a way to share this code across extensions + +var completed = false; +var tests; +var currentTest; + +function complete() { + completed = true; + + // a bit of a hack just to try to get the script to stop running at this point + throw "completed"; +} + +function fail(message) { + if (completed) throw "completed"; + + var stack; + try { + crash.me += 0; // intentional exception to get the stack trace + } catch (e) { + stack = e.stack.split("\n"); + stack = stack.slice(2); // remove title and fail() + stack = stack.join("\n"); + } + + if (!message) { + message = "FAIL (no message)"; + } + message += "\n" + stack; + console.log("[FAIL] " + currentTest.name + ": " + message); + chrome.test.fail(message); + complete(); +} + +function allTestsSucceeded() { + console.log("All tests succeeded"); + if (completed) throw "completed"; + + chrome.test.pass(); + complete(); +} + +function runNextTest() { + currentTest = tests.shift(); + if (!currentTest) { + allTestsSucceeded(); + return; + } + currentTest.call(); +} + +function succeed() { + console.log("[SUCCESS] " + currentTest.name); + runNextTest(); +} + +window.onerror = function(message, url, code) { + if (completed) return; + + fail(message); +}; + +function assertTrue(test, message) { + if (test !== true) { + if (typeof(test) == "string") { + if (message) { + message = test + "\n" + message; + } else { + message = test; + } + } + fail(message); + } +} + +function assertNoLastError() { + if (chrome.extension.lastError != undefined) { + fail("lastError.message == " + chrome.extension.lastError.message); + } +} + +// Wrapper for generating test functions, that takes care of calling +// assertNoLastError() and succeed() for you. +function testFunction(func) { + return function() { + assertNoLastError(); + try { + func.apply(null, arguments); + } catch (e) { + var stack = null; + if (typeof(e.stack) != "undefined") { + stack = e.stack.toString() + } + var msg = "Exception during execution of testFunction in " + + currentTest.name; + if (stack) { + msg += "\n" + stack; + } else { + msg += "\n(no stack available)"; + } + fail(msg); + } + succeed(); + }; +} diff --git a/chrome/test/data/extensions/api_test/override1/background.html b/chrome/test/data/extensions/api_test/override1/background.html new file mode 100644 index 0000000..0f02a821 --- /dev/null +++ b/chrome/test/data/extensions/api_test/override1/background.html @@ -0,0 +1,2 @@ + + diff --git a/chrome/test/data/extensions/api_test/override1/downloads.html b/chrome/test/data/extensions/api_test/override1/downloads.html new file mode 100644 index 0000000..565343c --- /dev/null +++ b/chrome/test/data/extensions/api_test/override1/downloads.html @@ -0,0 +1,6 @@ +download1 + +Downloads Override diff --git a/chrome/test/data/extensions/api_test/override1/history.html b/chrome/test/data/extensions/api_test/override1/history.html new file mode 100644 index 0000000..ba1e65b --- /dev/null +++ b/chrome/test/data/extensions/api_test/override1/history.html @@ -0,0 +1,4 @@ + +History Override diff --git a/chrome/test/data/extensions/api_test/override1/manifest.json b/chrome/test/data/extensions/api_test/override1/manifest.json new file mode 100644 index 0000000..1fd8311 --- /dev/null +++ b/chrome/test/data/extensions/api_test/override1/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "Override Test 1", + "version": "0.1", + "description": "Test chrome:// overrides", + "background_page": "background.html", + "permissions": [ + "tabs" + ], + "chrome_url_overrides": { + "newtab": "newtab.html" + } +} diff --git a/chrome/test/data/extensions/api_test/override1/newtab.html b/chrome/test/data/extensions/api_test/override1/newtab.html new file mode 100644 index 0000000..a1028a2 --- /dev/null +++ b/chrome/test/data/extensions/api_test/override1/newtab.html @@ -0,0 +1,4 @@ + +New Tab Override diff --git a/chrome/test/data/extensions/api_test/override1/nonexistant.html b/chrome/test/data/extensions/api_test/override1/nonexistant.html new file mode 100644 index 0000000..66cfbd0 --- /dev/null +++ b/chrome/test/data/extensions/api_test/override1/nonexistant.html @@ -0,0 +1,4 @@ + +Nonexistant Override diff --git a/chrome/test/data/extensions/api_test/override1/test.js b/chrome/test/data/extensions/api_test/override1/test.js new file mode 100644 index 0000000..1684f2f --- /dev/null +++ b/chrome/test/data/extensions/api_test/override1/test.js @@ -0,0 +1,11 @@ +var tests = [ + function newtab() { + chrome.tabs.create({"url": "chrome://newtab/"}, + testFunction(function(response) { + console.log("AFTER"); + })); + } +]; + +runNextTest(); + -- cgit v1.1