| // 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 "chrome/browser/extensions/error_console/error_console.h" |
| |
| #include <stddef.h> |
| |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/macros.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/extensions/extension_action_runner.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/url_constants.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/prefs/pref_service.h" |
| #include "extensions/browser/extension_error.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/error_utils.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/extension_urls.h" |
| #include "extensions/common/feature_switch.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| using base::string16; |
| using base::UTF8ToUTF16; |
| |
| namespace extensions { |
| |
| namespace { |
| |
| const char kTestingPage[] = "/extensions/test_file.html"; |
| const char kAnonymousFunction[] = "(anonymous function)"; |
| const char* const kBackgroundPageName = |
| extensions::kGeneratedBackgroundPageFilename; |
| const int kNoFlags = 0; |
| |
| const StackTrace& GetStackTraceFromError(const ExtensionError* error) { |
| CHECK(error->type() == ExtensionError::RUNTIME_ERROR); |
| return (static_cast<const RuntimeError*>(error))->stack_trace(); |
| } |
| |
| // Verify that a given |frame| has the proper source and function name. |
| void CheckStackFrame(const StackFrame& frame, |
| const std::string& source, |
| const std::string& function) { |
| EXPECT_EQ(base::UTF8ToUTF16(source), frame.source); |
| EXPECT_EQ(base::UTF8ToUTF16(function), frame.function); |
| } |
| |
| // Verify that all properties of a given |frame| are correct. Overloaded because |
| // we commonly do not check line/column numbers, as they are too likely |
| // to change. |
| void CheckStackFrame(const StackFrame& frame, |
| const std::string& source, |
| const std::string& function, |
| size_t line_number, |
| size_t column_number) { |
| CheckStackFrame(frame, source, function); |
| EXPECT_EQ(line_number, frame.line_number); |
| EXPECT_EQ(column_number, frame.column_number); |
| } |
| |
| // Verify that all properties of a given |error| are correct. |
| void CheckError(const ExtensionError* error, |
| ExtensionError::Type type, |
| const std::string& id, |
| const std::string& source, |
| bool from_incognito, |
| const std::string& message) { |
| ASSERT_TRUE(error); |
| EXPECT_EQ(type, error->type()); |
| EXPECT_EQ(id, error->extension_id()); |
| EXPECT_EQ(base::UTF8ToUTF16(source), error->source()); |
| EXPECT_EQ(from_incognito, error->from_incognito()); |
| EXPECT_EQ(base::UTF8ToUTF16(message), error->message()); |
| } |
| |
| // Verify that all properties of a JS runtime error are correct. |
| void CheckRuntimeError(const ExtensionError* error, |
| const std::string& id, |
| const std::string& source, |
| bool from_incognito, |
| const std::string& message, |
| logging::LogSeverity level, |
| const GURL& context, |
| size_t expected_stack_size) { |
| CheckError(error, |
| ExtensionError::RUNTIME_ERROR, |
| id, |
| source, |
| from_incognito, |
| message); |
| |
| const RuntimeError* runtime_error = static_cast<const RuntimeError*>(error); |
| EXPECT_EQ(level, runtime_error->level()); |
| EXPECT_EQ(context, runtime_error->context_url()); |
| EXPECT_EQ(expected_stack_size, runtime_error->stack_trace().size()); |
| } |
| |
| void CheckManifestError(const ExtensionError* error, |
| const std::string& id, |
| const std::string& message, |
| const std::string& manifest_key, |
| const std::string& manifest_specific) { |
| CheckError(error, |
| ExtensionError::MANIFEST_ERROR, |
| id, |
| // source is always the manifest for ManifestErrors. |
| base::FilePath(kManifestFilename).AsUTF8Unsafe(), |
| false, // manifest errors are never from incognito. |
| message); |
| |
| const ManifestError* manifest_error = |
| static_cast<const ManifestError*>(error); |
| EXPECT_EQ(base::UTF8ToUTF16(manifest_key), manifest_error->manifest_key()); |
| EXPECT_EQ(base::UTF8ToUTF16(manifest_specific), |
| manifest_error->manifest_specific()); |
| } |
| |
| } // namespace |
| |
| class ErrorConsoleBrowserTest : public ExtensionBrowserTest { |
| public: |
| ErrorConsoleBrowserTest() : error_console_(NULL) { } |
| ~ErrorConsoleBrowserTest() override {} |
| |
| protected: |
| // A helper class in order to wait for the proper number of errors to be |
| // caught by the ErrorConsole. This will run the MessageLoop until a given |
| // number of errors are observed. |
| // Usage: |
| // ... |
| // ErrorObserver observer(3, error_console); |
| // <Cause three errors...> |
| // observer.WaitForErrors(); |
| // <Perform any additional checks...> |
| class ErrorObserver : public ErrorConsole::Observer { |
| public: |
| ErrorObserver(size_t errors_expected, ErrorConsole* error_console) |
| : errors_observed_(0), |
| errors_expected_(errors_expected), |
| waiting_(false), |
| error_console_(error_console) { |
| error_console_->AddObserver(this); |
| } |
| virtual ~ErrorObserver() { |
| if (error_console_) |
| error_console_->RemoveObserver(this); |
| } |
| |
| // ErrorConsole::Observer implementation. |
| void OnErrorAdded(const ExtensionError* error) override { |
| ++errors_observed_; |
| if (errors_observed_ >= errors_expected_) { |
| if (waiting_) |
| base::RunLoop::QuitCurrentWhenIdleDeprecated(); |
| } |
| } |
| |
| void OnErrorConsoleDestroyed() override { error_console_ = NULL; } |
| |
| // Spin until the appropriate number of errors have been observed. |
| void WaitForErrors() { |
| if (errors_observed_ < errors_expected_) { |
| waiting_ = true; |
| content::RunMessageLoop(); |
| waiting_ = false; |
| } |
| } |
| |
| private: |
| size_t errors_observed_; |
| size_t errors_expected_; |
| bool waiting_; |
| |
| ErrorConsole* error_console_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ErrorObserver); |
| }; |
| |
| // The type of action which we take after we load an extension in order to |
| // cause any errors. |
| enum Action { |
| // Navigate to a (non-chrome) page to allow a content script to run. |
| ACTION_NAVIGATE, |
| // Simulate a browser action click. |
| ACTION_BROWSER_ACTION, |
| // Navigate to the new tab page. |
| ACTION_NEW_TAB, |
| // Do nothing (errors will be caused by a background script, |
| // or by a manifest/loading warning). |
| ACTION_NONE |
| }; |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| ExtensionBrowserTest::SetUpInProcessBrowserTestFixture(); |
| |
| // We need to enable the ErrorConsole FeatureSwitch in order to collect |
| // errors. This should be enabled on any channel <= Dev, but let's make |
| // sure (in case a test is running on, e.g., a beta channel). |
| FeatureSwitch::error_console()->SetOverrideValue( |
| FeatureSwitch::OVERRIDE_ENABLED); |
| } |
| |
| void SetUpOnMainThread() override { |
| ExtensionBrowserTest::SetUpOnMainThread(); |
| |
| // Errors are only kept if we have Developer Mode enabled. |
| profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true); |
| |
| error_console_ = ErrorConsole::Get(profile()); |
| CHECK(error_console_); |
| |
| test_data_dir_ = test_data_dir_.AppendASCII("error_console"); |
| } |
| |
| const GURL& GetTestURL() { |
| if (test_url_.is_empty()) { |
| CHECK(embedded_test_server()->Start()); |
| test_url_ = embedded_test_server()->GetURL(kTestingPage); |
| } |
| return test_url_; |
| } |
| |
| // Load the extension at |path|, take the specified |action|, and wait for |
| // |expected_errors| errors. Populate |extension| with a pointer to the loaded |
| // extension. |
| void LoadExtensionAndCheckErrors( |
| const std::string& path, |
| int flags, |
| size_t errors_expected, |
| Action action, |
| const Extension** extension) { |
| ErrorObserver observer(errors_expected, error_console_); |
| *extension = |
| LoadExtensionWithFlags(test_data_dir_.AppendASCII(path), flags); |
| ASSERT_TRUE(*extension); |
| |
| switch (action) { |
| case ACTION_NAVIGATE: { |
| ui_test_utils::NavigateToURL(browser(), GetTestURL()); |
| break; |
| } |
| case ACTION_BROWSER_ACTION: { |
| ExtensionActionRunner::GetForWebContents( |
| browser()->tab_strip_model()->GetActiveWebContents()) |
| ->RunAction(*extension, true); |
| break; |
| } |
| case ACTION_NEW_TAB: { |
| ui_test_utils::NavigateToURL(browser(), |
| GURL(chrome::kChromeUINewTabURL)); |
| break; |
| } |
| case ACTION_NONE: |
| break; |
| default: |
| NOTREACHED(); |
| } |
| |
| observer.WaitForErrors(); |
| |
| // We should only have errors for a single extension, or should have no |
| // entries, if no errors were expected. |
| ASSERT_EQ(errors_expected > 0 ? 1u : 0u, |
| error_console()->get_num_entries_for_test()); |
| ASSERT_EQ( |
| errors_expected, |
| error_console()->GetErrorsForExtension((*extension)->id()).size()); |
| } |
| |
| ErrorConsole* error_console() { return error_console_; } |
| |
| private: |
| // The URL used in testing for simple page navigations. |
| GURL test_url_; |
| |
| // Weak reference to the ErrorConsole. |
| ErrorConsole* error_console_; |
| }; |
| |
| // Test to ensure that we are successfully reporting manifest errors as an |
| // extension is installed. |
| IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, ReportManifestErrors) { |
| const Extension* extension = NULL; |
| // We expect two errors - one for an invalid permission, and a second for |
| // an unknown key. |
| LoadExtensionAndCheckErrors("manifest_warnings", |
| ExtensionBrowserTest::kFlagIgnoreManifestWarnings, |
| 2, |
| ACTION_NONE, |
| &extension); |
| |
| const ErrorList& errors = |
| error_console()->GetErrorsForExtension(extension->id()); |
| |
| // Unfortunately, there's not always a hard guarantee of order in parsing the |
| // manifest, so there's not a definitive order in which these errors may |
| // occur. As such, we need to determine which error corresponds to which |
| // expected error. |
| const ExtensionError* permissions_error = NULL; |
| const ExtensionError* unknown_key_error = NULL; |
| const char kFakeKey[] = "not_a_real_key"; |
| for (const auto& error : errors) { |
| ASSERT_EQ(ExtensionError::MANIFEST_ERROR, error->type()); |
| std::string utf8_key = base::UTF16ToUTF8( |
| (static_cast<const ManifestError*>(error.get()))->manifest_key()); |
| if (utf8_key == manifest_keys::kPermissions) |
| permissions_error = error.get(); |
| else if (utf8_key == kFakeKey) |
| unknown_key_error = error.get(); |
| } |
| ASSERT_TRUE(permissions_error); |
| ASSERT_TRUE(unknown_key_error); |
| |
| const char kFakePermission[] = "not_a_real_permission"; |
| CheckManifestError(permissions_error, |
| extension->id(), |
| ErrorUtils::FormatErrorMessage( |
| manifest_errors::kPermissionUnknownOrMalformed, |
| kFakePermission), |
| manifest_keys::kPermissions, |
| kFakePermission); |
| |
| CheckManifestError(unknown_key_error, |
| extension->id(), |
| ErrorUtils::FormatErrorMessage( |
| manifest_errors::kUnrecognizedManifestKey, |
| kFakeKey), |
| kFakeKey, |
| std::string()); |
| } |
| |
| // Test that we do not store any errors unless the Developer Mode switch is |
| // toggled on the profile. |
| IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, |
| DontStoreErrorsWithoutDeveloperMode) { |
| profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, false); |
| |
| const Extension* extension = NULL; |
| // Same test as ReportManifestErrors, except we don't expect any errors since |
| // we disable Developer Mode. |
| LoadExtensionAndCheckErrors("manifest_warnings", |
| ExtensionBrowserTest::kFlagIgnoreManifestWarnings, |
| 0, |
| ACTION_NONE, |
| &extension); |
| |
| // Now if we enable developer mode, the errors should be reported... |
| profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true); |
| EXPECT_EQ(2u, error_console()->GetErrorsForExtension(extension->id()).size()); |
| |
| // ... and if we disable it again, all errors which we were holding should be |
| // removed. |
| profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, false); |
| EXPECT_EQ(0u, error_console()->GetErrorsForExtension(extension->id()).size()); |
| } |
| |
| // Load an extension which, upon visiting any page, first sends out a console |
| // log, and then crashes with a JS TypeError. |
| IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, |
| ContentScriptLogAndRuntimeError) { |
| const Extension* extension = NULL; |
| LoadExtensionAndCheckErrors( |
| "content_script_log_and_runtime_error", |
| kNoFlags, |
| 2u, // Two errors: A log message and a JS type error. |
| ACTION_NAVIGATE, |
| &extension); |
| |
| std::string script_url = |
| extension->GetResourceURL("content_script.js").spec(); |
| |
| const ErrorList& errors = |
| error_console()->GetErrorsForExtension(extension->id()); |
| |
| // The extension logs a message with console.log(), then another with |
| // console.warn(), and then triggers a TypeError. |
| // There should be exactly two errors (the warning and the TypeError). The |
| // error console ignores logs - this would tend to be too noisy, and doesn't |
| // jive with the big `ERRORS` button in the UI. |
| // See https://crbug.com/837401. |
| ASSERT_EQ(2u, errors.size()); |
| |
| // The first error should be a console log. |
| CheckRuntimeError(errors[0].get(), extension->id(), |
| script_url, // The source should be the content script url. |
| false, // Not from incognito. |
| "warned message", // The error message is the log. |
| logging::LOG_WARNING, |
| GetTestURL(), // Content scripts run in the web page. |
| 2u); |
| |
| const StackTrace& stack_trace1 = GetStackTraceFromError(errors[0].get()); |
| CheckStackFrame(stack_trace1[0], script_url, |
| "warnMessage", // function name |
| 10u, // line number |
| 11u /* column number */); |
| |
| CheckStackFrame(stack_trace1[1], script_url, kAnonymousFunction, 14u, 1u); |
| |
| // The second error should be a runtime error. |
| CheckRuntimeError(errors[1].get(), extension->id(), script_url, |
| false, // not from incognito |
| "Uncaught TypeError: " |
| "Cannot set property 'foo' of undefined", |
| logging::LOG_ERROR, // JS errors are always ERROR level. |
| GetTestURL(), 1u); |
| |
| const StackTrace& stack_trace2 = GetStackTraceFromError(errors[1].get()); |
| CheckStackFrame(stack_trace2[0], script_url, kAnonymousFunction, 17u, 1u); |
| } |
| |
| // Catch an error from a BrowserAction; this is more complex than a content |
| // script error, since browser actions are routed through our own code. |
| IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, BrowserActionRuntimeError) { |
| const Extension* extension = NULL; |
| LoadExtensionAndCheckErrors( |
| "browser_action_runtime_error", |
| kNoFlags, |
| 1u, // One error: A reference error from within the browser action. |
| ACTION_BROWSER_ACTION, |
| &extension); |
| |
| std::string script_url = |
| extension->GetResourceURL("browser_action.js").spec(); |
| |
| const ErrorList& errors = |
| error_console()->GetErrorsForExtension(extension->id()); |
| |
| std::string message; |
| bool use_native_bindings = |
| base::FeatureList::IsEnabled(extensions_features::kNativeCrxBindings); |
| if (use_native_bindings) { |
| // TODO(devlin): The specific event name (here, 'browserAction.onClicked') |
| // may or may not be worth preserving. In most cases, it's unnecessary with |
| // the line number, but it could be useful in some cases. |
| message = |
| "Error in event handler: ReferenceError: " |
| "baz is not defined"; |
| } else { |
| message = |
| "Error in event handler for browserAction.onClicked: " |
| "ReferenceError: baz is not defined"; |
| } |
| |
| CheckRuntimeError(errors[0].get(), extension->id(), script_url, |
| false, // not incognito |
| message, logging::LOG_ERROR, |
| extension->GetResourceURL(kBackgroundPageName), 1u); |
| |
| const StackTrace& stack_trace = GetStackTraceFromError(errors[0].get()); |
| // Note: This test used to have a stack trace of length 6 that contains stack |
| // frames in the extension code, but since crbug.com/404406 was fixed only |
| // stack frames within user-defined extension code are printed. |
| |
| CheckStackFrame(stack_trace[0], script_url, kAnonymousFunction); |
| } |
| |
| // Test that we can catch an error for calling an API with improper arguments. |
| IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, BadAPIArgumentsRuntimeError) { |
| const Extension* extension = NULL; |
| LoadExtensionAndCheckErrors( |
| "bad_api_arguments_runtime_error", |
| kNoFlags, |
| 1, // One error: call an API with improper arguments. |
| ACTION_NONE, |
| &extension); |
| |
| const ErrorList& errors = |
| error_console()->GetErrorsForExtension(extension->id()); |
| |
| std::string source; |
| std::string message; |
| bool use_native_bindings = |
| base::FeatureList::IsEnabled(extensions_features::kNativeCrxBindings); |
| if (use_native_bindings) { |
| source = extension->GetResourceURL("background.js").spec(); |
| message = |
| "Uncaught TypeError: Error in invocation of " |
| "tabs.get(integer tabId, function callback): " |
| "No matching signature."; |
| } else { |
| // API calls are checked in schemaUtils.js with JS bindings. |
| source = "extensions::" + std::string(kSchemaUtils); |
| message = |
| "Uncaught Error: Invocation of form " |
| "tabs.get(string, function) doesn't match definition " |
| "tabs.get(integer tabId, function callback)"; |
| } |
| |
| CheckRuntimeError(errors[0].get(), extension->id(), source, |
| false, // not incognito |
| message, logging::LOG_ERROR, |
| extension->GetResourceURL(kBackgroundPageName), 1u); |
| |
| const StackTrace& stack_trace = GetStackTraceFromError(errors[0].get()); |
| ASSERT_EQ(1u, stack_trace.size()); |
| CheckStackFrame(stack_trace[0], source, kAnonymousFunction); |
| } |
| |
| // Test that we catch an error when we try to call an API method without |
| // permission. |
| IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, BadAPIPermissionsRuntimeError) { |
| const Extension* extension = NULL; |
| LoadExtensionAndCheckErrors( |
| "bad_api_permissions_runtime_error", |
| kNoFlags, |
| 1, // One error: we try to call addUrl() on chrome.history without |
| // permission, which results in a TypeError. |
| ACTION_NONE, |
| &extension); |
| |
| std::string script_url = extension->GetResourceURL("background.js").spec(); |
| |
| const ErrorList& errors = |
| error_console()->GetErrorsForExtension(extension->id()); |
| |
| CheckRuntimeError( |
| errors[0].get(), extension->id(), script_url, |
| false, // not incognito |
| "Uncaught TypeError: Cannot read property 'addUrl' of undefined", |
| logging::LOG_ERROR, extension->GetResourceURL(kBackgroundPageName), 1u); |
| |
| const StackTrace& stack_trace = GetStackTraceFromError(errors[0].get()); |
| ASSERT_EQ(1u, stack_trace.size()); |
| CheckStackFrame(stack_trace[0], |
| script_url, |
| kAnonymousFunction, |
| 5u, 1u); |
| } |
| |
| // Test that if there is an error in an HTML page loaded by an extension (most |
| // common with apps), it is caught and reported by the ErrorConsole. |
| IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, BadExtensionPage) { |
| const Extension* extension = NULL; |
| LoadExtensionAndCheckErrors( |
| "bad_extension_page", |
| kNoFlags, |
| 1, // One error: the page will load JS which has a reference error. |
| ACTION_NEW_TAB, |
| &extension); |
| } |
| |
| // Test that extension errors that go to chrome.runtime.lastError are caught |
| // and reported by the ErrorConsole. |
| IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, CatchesLastError) { |
| const Extension* extension = NULL; |
| LoadExtensionAndCheckErrors( |
| "trigger_last_error", |
| kNoFlags, |
| 1, // One error, which is sent through last error when trying to remove |
| // a non-existent permisison. |
| ACTION_NONE, |
| &extension); |
| |
| const ErrorList& errors = |
| error_console()->GetErrorsForExtension(extension->id()); |
| ASSERT_EQ(1u, errors.size()); |
| |
| std::string source; |
| std::string message; |
| size_t line_number = 0; |
| size_t column_number = 0; |
| if (base::FeatureList::IsEnabled(extensions_features::kNativeCrxBindings)) { |
| // TODO(devlin): This is unfortunate. We lose a lot of context by using |
| // RenderFrame::AddMessageToConsole() instead of console.error(). This could |
| // be expanded; blink::SourceLocation knows how to capture an inspector |
| // stack trace. |
| source = extension->GetResourceURL(kGeneratedBackgroundPageFilename).spec(); |
| // Line number '0' comes from errors that are logged to the render frame |
| // directly (e.g. background_age.html (0)). |
| line_number = 0; |
| // Column number remains at the default specified in StackFrame (1). |
| column_number = 1; |
| message = |
| "Unchecked runtime.lastError: " |
| "'foobar' is not a recognized permission."; |
| } else { |
| source = extension->GetResourceURL("background.js").spec(); |
| line_number = 12; |
| column_number = 20; |
| message = |
| "Unchecked runtime.lastError while running permissions.remove: " |
| "'foobar' is not a recognized permission."; |
| } |
| |
| CheckRuntimeError(errors[0].get(), extension->id(), source, |
| false, // not incognito |
| message, logging::LOG_ERROR, |
| extension->GetResourceURL(kBackgroundPageName), 1u); |
| |
| const StackTrace& stack_trace = GetStackTraceFromError(errors[0].get()); |
| ASSERT_EQ(1u, stack_trace.size()); |
| CheckStackFrame(stack_trace[0], source, kAnonymousFunction, line_number, |
| column_number); |
| } |
| |
| } // namespace extensions |