1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/extensions/error_console/error_console.h"
6
7 #include <stddef.h>
8
9 #include "base/files/file_path.h"
10 #include "base/macros.h"
11 #include "base/run_loop.h"
12 #include "base/strings/string16.h"
13 #include "base/strings/stringprintf.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "build/build_config.h"
16 #include "chrome/browser/extensions/extension_action_runner.h"
17 #include "chrome/browser/extensions/extension_browsertest.h"
18 #include "chrome/browser/profiles/profile.h"
19 #include "chrome/browser/ui/tabs/tab_strip_model.h"
20 #include "chrome/common/pref_names.h"
21 #include "chrome/common/url_constants.h"
22 #include "chrome/test/base/ui_test_utils.h"
23 #include "components/prefs/pref_service.h"
24 #include "content/public/test/browser_test.h"
25 #include "extensions/browser/extension_error.h"
26 #include "extensions/common/constants.h"
27 #include "extensions/common/error_utils.h"
28 #include "extensions/common/extension.h"
29 #include "extensions/common/extension_urls.h"
30 #include "extensions/common/manifest_constants.h"
31 #include "net/test/embedded_test_server/embedded_test_server.h"
32 #include "testing/gtest/include/gtest/gtest.h"
33 #include "url/gurl.h"
34
35 using base::string16;
36 using base::UTF8ToUTF16;
37
38 namespace extensions {
39
40 namespace {
41
42 const char kTestingPage[] = "/extensions/test_file.html";
43 const char kAnonymousFunction[] = "(anonymous function)";
44 const char* const kBackgroundPageName =
45 extensions::kGeneratedBackgroundPageFilename;
46 const int kNoFlags = 0;
47
GetStackTraceFromError(const ExtensionError * error)48 const StackTrace& GetStackTraceFromError(const ExtensionError* error) {
49 CHECK(error->type() == ExtensionError::RUNTIME_ERROR);
50 return (static_cast<const RuntimeError*>(error))->stack_trace();
51 }
52
53 // Verify that a given |frame| has the proper source and function name.
CheckStackFrame(const StackFrame & frame,const std::string & source,const std::string & function)54 void CheckStackFrame(const StackFrame& frame,
55 const std::string& source,
56 const std::string& function) {
57 EXPECT_EQ(base::UTF8ToUTF16(source), frame.source);
58 EXPECT_EQ(base::UTF8ToUTF16(function), frame.function);
59 }
60
61 // Verify that all properties of a given |frame| are correct. Overloaded because
62 // we commonly do not check line/column numbers, as they are too likely
63 // to change.
CheckStackFrame(const StackFrame & frame,const std::string & source,const std::string & function,size_t line_number,size_t column_number)64 void CheckStackFrame(const StackFrame& frame,
65 const std::string& source,
66 const std::string& function,
67 size_t line_number,
68 size_t column_number) {
69 CheckStackFrame(frame, source, function);
70 EXPECT_EQ(line_number, frame.line_number);
71 EXPECT_EQ(column_number, frame.column_number);
72 }
73
74 // Verify that all properties of a given |error| are correct.
CheckError(const ExtensionError * error,ExtensionError::Type type,const std::string & id,const std::string & source,bool from_incognito,const std::string & message)75 void CheckError(const ExtensionError* error,
76 ExtensionError::Type type,
77 const std::string& id,
78 const std::string& source,
79 bool from_incognito,
80 const std::string& message) {
81 ASSERT_TRUE(error);
82 EXPECT_EQ(type, error->type());
83 EXPECT_EQ(id, error->extension_id());
84 EXPECT_EQ(base::UTF8ToUTF16(source), error->source());
85 EXPECT_EQ(from_incognito, error->from_incognito());
86 EXPECT_EQ(base::UTF8ToUTF16(message), error->message());
87 }
88
89 // Verify that all properties of a JS runtime error are correct.
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)90 void CheckRuntimeError(const ExtensionError* error,
91 const std::string& id,
92 const std::string& source,
93 bool from_incognito,
94 const std::string& message,
95 logging::LogSeverity level,
96 const GURL& context,
97 size_t expected_stack_size) {
98 CheckError(error,
99 ExtensionError::RUNTIME_ERROR,
100 id,
101 source,
102 from_incognito,
103 message);
104
105 const RuntimeError* runtime_error = static_cast<const RuntimeError*>(error);
106 EXPECT_EQ(level, runtime_error->level());
107 EXPECT_EQ(context, runtime_error->context_url());
108 EXPECT_EQ(expected_stack_size, runtime_error->stack_trace().size());
109 }
110
CheckManifestError(const ExtensionError * error,const std::string & id,const std::string & message,const std::string & manifest_key,const std::string & manifest_specific)111 void CheckManifestError(const ExtensionError* error,
112 const std::string& id,
113 const std::string& message,
114 const std::string& manifest_key,
115 const std::string& manifest_specific) {
116 CheckError(error,
117 ExtensionError::MANIFEST_ERROR,
118 id,
119 // source is always the manifest for ManifestErrors.
120 base::FilePath(kManifestFilename).AsUTF8Unsafe(),
121 false, // manifest errors are never from incognito.
122 message);
123
124 const ManifestError* manifest_error =
125 static_cast<const ManifestError*>(error);
126 EXPECT_EQ(base::UTF8ToUTF16(manifest_key), manifest_error->manifest_key());
127 EXPECT_EQ(base::UTF8ToUTF16(manifest_specific),
128 manifest_error->manifest_specific());
129 }
130
131 } // namespace
132
133 class ErrorConsoleBrowserTest : public ExtensionBrowserTest {
134 public:
ErrorConsoleBrowserTest()135 ErrorConsoleBrowserTest() : error_console_(nullptr) {}
~ErrorConsoleBrowserTest()136 ~ErrorConsoleBrowserTest() override {}
137
138 protected:
139 // A helper class in order to wait for the proper number of errors to be
140 // caught by the ErrorConsole. This will run the MessageLoop until a given
141 // number of errors are observed.
142 // Usage:
143 // ...
144 // ErrorObserver observer(3, error_console);
145 // <Cause three errors...>
146 // observer.WaitForErrors();
147 // <Perform any additional checks...>
148 class ErrorObserver : public ErrorConsole::Observer {
149 public:
ErrorObserver(size_t errors_expected,ErrorConsole * error_console)150 ErrorObserver(size_t errors_expected, ErrorConsole* error_console)
151 : errors_observed_(0),
152 errors_expected_(errors_expected),
153 waiting_(false),
154 error_console_(error_console) {
155 error_console_->AddObserver(this);
156 }
~ErrorObserver()157 virtual ~ErrorObserver() {
158 if (error_console_)
159 error_console_->RemoveObserver(this);
160 }
161
162 // ErrorConsole::Observer implementation.
OnErrorAdded(const ExtensionError * error)163 void OnErrorAdded(const ExtensionError* error) override {
164 ++errors_observed_;
165 if (errors_observed_ >= errors_expected_) {
166 if (waiting_)
167 base::RunLoop::QuitCurrentWhenIdleDeprecated();
168 }
169 }
170
OnErrorConsoleDestroyed()171 void OnErrorConsoleDestroyed() override { error_console_ = nullptr; }
172
173 // Spin until the appropriate number of errors have been observed.
WaitForErrors()174 void WaitForErrors() {
175 if (errors_observed_ < errors_expected_) {
176 waiting_ = true;
177 content::RunMessageLoop();
178 waiting_ = false;
179 }
180 }
181
182 private:
183 size_t errors_observed_;
184 size_t errors_expected_;
185 bool waiting_;
186
187 ErrorConsole* error_console_;
188
189 DISALLOW_COPY_AND_ASSIGN(ErrorObserver);
190 };
191
192 // The type of action which we take after we load an extension in order to
193 // cause any errors.
194 enum Action {
195 // Navigate to a (non-chrome) page to allow a content script to run.
196 ACTION_NAVIGATE,
197 // Simulate a browser action click.
198 ACTION_BROWSER_ACTION,
199 // Navigate to the new tab page.
200 ACTION_NEW_TAB,
201 // Do nothing (errors will be caused by a background script,
202 // or by a manifest/loading warning).
203 ACTION_NONE
204 };
205
SetUpOnMainThread()206 void SetUpOnMainThread() override {
207 ExtensionBrowserTest::SetUpOnMainThread();
208
209 // Errors are only kept if we have Developer Mode enabled.
210 profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true);
211
212 error_console_ = ErrorConsole::Get(profile());
213 CHECK(error_console_);
214
215 test_data_dir_ = test_data_dir_.AppendASCII("error_console");
216 }
217
GetTestURL()218 const GURL& GetTestURL() {
219 if (test_url_.is_empty()) {
220 CHECK(embedded_test_server()->Start());
221 test_url_ = embedded_test_server()->GetURL(kTestingPage);
222 }
223 return test_url_;
224 }
225
226 // Load the extension at |path|, take the specified |action|, and wait for
227 // |expected_errors| errors. Populate |extension| with a pointer to the loaded
228 // extension.
LoadExtensionAndCheckErrors(const std::string & path,int flags,size_t errors_expected,Action action,const Extension ** extension)229 void LoadExtensionAndCheckErrors(
230 const std::string& path,
231 int flags,
232 size_t errors_expected,
233 Action action,
234 const Extension** extension) {
235 ErrorObserver observer(errors_expected, error_console_);
236 *extension =
237 LoadExtensionWithFlags(test_data_dir_.AppendASCII(path), flags);
238 ASSERT_TRUE(*extension);
239
240 switch (action) {
241 case ACTION_NAVIGATE: {
242 ui_test_utils::NavigateToURL(browser(), GetTestURL());
243 break;
244 }
245 case ACTION_BROWSER_ACTION: {
246 ExtensionActionRunner::GetForWebContents(
247 browser()->tab_strip_model()->GetActiveWebContents())
248 ->RunAction(*extension, true);
249 break;
250 }
251 case ACTION_NEW_TAB: {
252 ui_test_utils::NavigateToURL(browser(),
253 GURL(chrome::kChromeUINewTabURL));
254 break;
255 }
256 case ACTION_NONE:
257 break;
258 default:
259 NOTREACHED();
260 }
261
262 observer.WaitForErrors();
263
264 // We should only have errors for a single extension, or should have no
265 // entries, if no errors were expected.
266 ASSERT_EQ(errors_expected > 0 ? 1u : 0u,
267 error_console()->get_num_entries_for_test());
268 ASSERT_EQ(
269 errors_expected,
270 error_console()->GetErrorsForExtension((*extension)->id()).size());
271 }
272
error_console()273 ErrorConsole* error_console() { return error_console_; }
274
275 private:
276 // The URL used in testing for simple page navigations.
277 GURL test_url_;
278
279 // Weak reference to the ErrorConsole.
280 ErrorConsole* error_console_;
281 };
282
283 // Test to ensure that we are successfully reporting manifest errors as an
284 // extension is installed.
IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,ReportManifestErrors)285 IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, ReportManifestErrors) {
286 const Extension* extension = nullptr;
287 // We expect two errors - one for an invalid permission, and a second for
288 // an unknown key.
289 LoadExtensionAndCheckErrors("manifest_warnings", kFlagIgnoreManifestWarnings,
290 2, ACTION_NONE, &extension);
291
292 const ErrorList& errors =
293 error_console()->GetErrorsForExtension(extension->id());
294
295 // Unfortunately, there's not always a hard guarantee of order in parsing the
296 // manifest, so there's not a definitive order in which these errors may
297 // occur. As such, we need to determine which error corresponds to which
298 // expected error.
299 const ExtensionError* permissions_error = nullptr;
300 const ExtensionError* unknown_key_error = nullptr;
301 const char kFakeKey[] = "not_a_real_key";
302 for (const auto& error : errors) {
303 ASSERT_EQ(ExtensionError::MANIFEST_ERROR, error->type());
304 std::string utf8_key = base::UTF16ToUTF8(
305 (static_cast<const ManifestError*>(error.get()))->manifest_key());
306 if (utf8_key == manifest_keys::kPermissions)
307 permissions_error = error.get();
308 else if (utf8_key == kFakeKey)
309 unknown_key_error = error.get();
310 }
311 ASSERT_TRUE(permissions_error);
312 ASSERT_TRUE(unknown_key_error);
313
314 const char kFakePermission[] = "not_a_real_permission";
315 CheckManifestError(permissions_error,
316 extension->id(),
317 ErrorUtils::FormatErrorMessage(
318 manifest_errors::kPermissionUnknownOrMalformed,
319 kFakePermission),
320 manifest_keys::kPermissions,
321 kFakePermission);
322
323 CheckManifestError(unknown_key_error,
324 extension->id(),
325 ErrorUtils::FormatErrorMessage(
326 manifest_errors::kUnrecognizedManifestKey,
327 kFakeKey),
328 kFakeKey,
329 std::string());
330 }
331
332 // Test that we do not store any errors unless the Developer Mode switch is
333 // toggled on the profile.
IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,DontStoreErrorsWithoutDeveloperMode)334 IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,
335 DontStoreErrorsWithoutDeveloperMode) {
336 profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, false);
337
338 const Extension* extension = nullptr;
339 // Same test as ReportManifestErrors, except we don't expect any errors since
340 // we disable Developer Mode.
341 LoadExtensionAndCheckErrors("manifest_warnings", kFlagIgnoreManifestWarnings,
342 0, ACTION_NONE, &extension);
343
344 // Now if we enable developer mode, the errors should be reported...
345 profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true);
346 EXPECT_EQ(2u, error_console()->GetErrorsForExtension(extension->id()).size());
347
348 // ... and if we disable it again, all errors which we were holding should be
349 // removed.
350 profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, false);
351 EXPECT_EQ(0u, error_console()->GetErrorsForExtension(extension->id()).size());
352 }
353
354 // Load an extension which, upon visiting any page, first sends out a console
355 // log, and then crashes with a JS TypeError.
IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,ContentScriptLogAndRuntimeError)356 IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,
357 ContentScriptLogAndRuntimeError) {
358 const Extension* extension = nullptr;
359 LoadExtensionAndCheckErrors(
360 "content_script_log_and_runtime_error",
361 kNoFlags,
362 2u, // Two errors: A log message and a JS type error.
363 ACTION_NAVIGATE,
364 &extension);
365
366 std::string script_url =
367 extension->GetResourceURL("content_script.js").spec();
368
369 const ErrorList& errors =
370 error_console()->GetErrorsForExtension(extension->id());
371
372 // The extension logs a message with console.log(), then another with
373 // console.warn(), and then triggers a TypeError.
374 // There should be exactly two errors (the warning and the TypeError). The
375 // error console ignores logs - this would tend to be too noisy, and doesn't
376 // jive with the big `ERRORS` button in the UI.
377 // See https://crbug.com/837401.
378 ASSERT_EQ(2u, errors.size());
379
380 // The first error should be a console log.
381 CheckRuntimeError(errors[0].get(), extension->id(),
382 script_url, // The source should be the content script url.
383 false, // Not from incognito.
384 "warned message", // The error message is the log.
385 logging::LOG_WARNING,
386 GetTestURL(), // Content scripts run in the web page.
387 2u);
388
389 const StackTrace& stack_trace1 = GetStackTraceFromError(errors[0].get());
390 CheckStackFrame(stack_trace1[0], script_url,
391 "warnMessage", // function name
392 10u, // line number
393 11u /* column number */);
394
395 CheckStackFrame(stack_trace1[1], script_url, kAnonymousFunction, 14u, 1u);
396
397 // The second error should be a runtime error.
398 CheckRuntimeError(errors[1].get(), extension->id(), script_url,
399 false, // not from incognito
400 "Uncaught TypeError: "
401 "Cannot set property 'foo' of undefined",
402 logging::LOG_ERROR, // JS errors are always ERROR level.
403 GetTestURL(), 1u);
404
405 const StackTrace& stack_trace2 = GetStackTraceFromError(errors[1].get());
406 CheckStackFrame(stack_trace2[0], script_url, kAnonymousFunction, 17u, 1u);
407 }
408
409 // Catch an error from a BrowserAction; this is more complex than a content
410 // script error, since browser actions are routed through our own code.
IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,BrowserActionRuntimeError)411 IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, BrowserActionRuntimeError) {
412 const Extension* extension = nullptr;
413 LoadExtensionAndCheckErrors(
414 "browser_action_runtime_error",
415 kNoFlags,
416 1u, // One error: A reference error from within the browser action.
417 ACTION_BROWSER_ACTION,
418 &extension);
419
420 std::string script_url =
421 extension->GetResourceURL("browser_action.js").spec();
422
423 const ErrorList& errors =
424 error_console()->GetErrorsForExtension(extension->id());
425
426 // TODO(devlin): The specific event name (here, 'browserAction.onClicked')
427 // may or may not be worth preserving. In most cases, it's unnecessary with
428 // the line number, but it could be useful in some cases.
429 std::string message =
430 "Error in event handler: ReferenceError: baz is not defined";
431
432 CheckRuntimeError(errors[0].get(), extension->id(), script_url,
433 false, // not incognito
434 message, logging::LOG_ERROR,
435 extension->GetResourceURL(kBackgroundPageName), 1u);
436
437 const StackTrace& stack_trace = GetStackTraceFromError(errors[0].get());
438 // Note: This test used to have a stack trace of length 6 that contains stack
439 // frames in the extension code, but since crbug.com/404406 was fixed only
440 // stack frames within user-defined extension code are printed.
441
442 CheckStackFrame(stack_trace[0], script_url, kAnonymousFunction);
443 }
444
445 // Test that we can catch an error for calling an API with improper arguments.
IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,BadAPIArgumentsRuntimeError)446 IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, BadAPIArgumentsRuntimeError) {
447 const Extension* extension = nullptr;
448 LoadExtensionAndCheckErrors(
449 "bad_api_arguments_runtime_error",
450 kNoFlags,
451 1, // One error: call an API with improper arguments.
452 ACTION_NONE,
453 &extension);
454
455 const ErrorList& errors =
456 error_console()->GetErrorsForExtension(extension->id());
457
458 std::string source = extension->GetResourceURL("background.js").spec();
459 std::string message =
460 "Uncaught TypeError: Error in invocation of tabs.get"
461 "(integer tabId, function callback): No matching signature.";
462
463 CheckRuntimeError(errors[0].get(), extension->id(), source,
464 false, // not incognito
465 message, logging::LOG_ERROR,
466 extension->GetResourceURL(kBackgroundPageName), 1u);
467
468 const StackTrace& stack_trace = GetStackTraceFromError(errors[0].get());
469 ASSERT_EQ(1u, stack_trace.size());
470 CheckStackFrame(stack_trace[0], source, kAnonymousFunction);
471 }
472
473 // Test that we catch an error when we try to call an API method without
474 // permission.
IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,BadAPIPermissionsRuntimeError)475 IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, BadAPIPermissionsRuntimeError) {
476 const Extension* extension = nullptr;
477 LoadExtensionAndCheckErrors(
478 "bad_api_permissions_runtime_error",
479 kNoFlags,
480 1, // One error: we try to call addUrl() on chrome.history without
481 // permission, which results in a TypeError.
482 ACTION_NONE,
483 &extension);
484
485 std::string script_url = extension->GetResourceURL("background.js").spec();
486
487 const ErrorList& errors =
488 error_console()->GetErrorsForExtension(extension->id());
489
490 CheckRuntimeError(
491 errors[0].get(), extension->id(), script_url,
492 false, // not incognito
493 "Uncaught TypeError: Cannot read property 'addUrl' of undefined",
494 logging::LOG_ERROR, extension->GetResourceURL(kBackgroundPageName), 1u);
495
496 const StackTrace& stack_trace = GetStackTraceFromError(errors[0].get());
497 ASSERT_EQ(1u, stack_trace.size());
498 CheckStackFrame(stack_trace[0],
499 script_url,
500 kAnonymousFunction,
501 5u, 1u);
502 }
503
504 // Test that if there is an error in an HTML page loaded by an extension (most
505 // common with apps), it is caught and reported by the ErrorConsole.
IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,BadExtensionPage)506 IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, BadExtensionPage) {
507 const Extension* extension = nullptr;
508 LoadExtensionAndCheckErrors(
509 "bad_extension_page",
510 kNoFlags,
511 1, // One error: the page will load JS which has a reference error.
512 ACTION_NEW_TAB,
513 &extension);
514 }
515
516 // Test that extension errors that go to chrome.runtime.lastError are caught
517 // and reported by the ErrorConsole.
IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest,CatchesLastError)518 IN_PROC_BROWSER_TEST_F(ErrorConsoleBrowserTest, CatchesLastError) {
519 const Extension* extension = nullptr;
520 LoadExtensionAndCheckErrors(
521 "trigger_last_error",
522 kNoFlags,
523 1, // One error, which is sent through last error when trying to remove
524 // a non-existent permisison.
525 ACTION_NONE,
526 &extension);
527
528 const ErrorList& errors =
529 error_console()->GetErrorsForExtension(extension->id());
530 ASSERT_EQ(1u, errors.size());
531
532 // TODO(devlin): This is unfortunate. We lose a lot of context by using
533 // RenderFrame::AddMessageToConsole() instead of console.error(). This could
534 // be expanded; blink::SourceLocation knows how to capture an inspector
535 // stack trace.
536 std::string source =
537 extension->GetResourceURL(kGeneratedBackgroundPageFilename).spec();
538 // Line number '0' comes from errors that are logged to the render frame
539 // directly (e.g. background_age.html (0)).
540 size_t line_number = 0;
541 // Column number remains at the default specified in StackFrame (1).
542 size_t column_number = 1;
543 std::string message =
544 "Unchecked runtime.lastError: 'foobar' is not a recognized permission.";
545
546 CheckRuntimeError(errors[0].get(), extension->id(), source,
547 false, // not incognito
548 message, logging::LOG_ERROR,
549 extension->GetResourceURL(kBackgroundPageName), 1u);
550
551 const StackTrace& stack_trace = GetStackTraceFromError(errors[0].get());
552 ASSERT_EQ(1u, stack_trace.size());
553 CheckStackFrame(stack_trace[0], source, kAnonymousFunction, line_number,
554 column_number);
555 }
556
557 } // namespace extensions
558