Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2012 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/extension_browsertest.h"
      6 
      7 #include <vector>
      8 
      9 #include "base/command_line.h"
     10 #include "base/file_util.h"
     11 #include "base/files/file_path.h"
     12 #include "base/files/scoped_temp_dir.h"
     13 #include "base/path_service.h"
     14 #include "base/strings/string_number_conversions.h"
     15 #include "base/strings/stringprintf.h"
     16 #include "base/strings/utf_string_conversions.h"
     17 #include "chrome/browser/chrome_notification_types.h"
     18 #include "chrome/browser/extensions/browsertest_util.h"
     19 #include "chrome/browser/extensions/component_loader.h"
     20 #include "chrome/browser/extensions/crx_installer.h"
     21 #include "chrome/browser/extensions/extension_creator.h"
     22 #include "chrome/browser/extensions/extension_error_reporter.h"
     23 #include "chrome/browser/extensions/extension_host.h"
     24 #include "chrome/browser/extensions/extension_install_prompt.h"
     25 #include "chrome/browser/extensions/extension_service.h"
     26 #include "chrome/browser/extensions/extension_system.h"
     27 #include "chrome/browser/extensions/extension_util.h"
     28 #include "chrome/browser/extensions/unpacked_installer.h"
     29 #include "chrome/browser/profiles/profile.h"
     30 #include "chrome/browser/profiles/profile_manager.h"
     31 #include "chrome/browser/ui/browser.h"
     32 #include "chrome/browser/ui/browser_window.h"
     33 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     34 #include "chrome/common/chrome_paths.h"
     35 #include "chrome/common/chrome_switches.h"
     36 #include "chrome/common/chrome_version_info.h"
     37 #include "chrome/common/extensions/extension_set.h"
     38 #include "chrome/test/base/ui_test_utils.h"
     39 #include "content/public/browser/navigation_controller.h"
     40 #include "content/public/browser/navigation_entry.h"
     41 #include "content/public/browser/notification_registrar.h"
     42 #include "content/public/browser/notification_service.h"
     43 #include "content/public/browser/render_view_host.h"
     44 #include "content/public/test/browser_test_utils.h"
     45 #include "extensions/common/constants.h"
     46 #include "sync/api/string_ordinal.h"
     47 
     48 #if defined(OS_CHROMEOS)
     49 #include "chromeos/chromeos_switches.h"
     50 #endif
     51 
     52 using extensions::Extension;
     53 using extensions::ExtensionCreator;
     54 using extensions::FeatureSwitch;
     55 using extensions::Manifest;
     56 
     57 ExtensionBrowserTest::ExtensionBrowserTest()
     58     : loaded_(false),
     59       installed_(false),
     60       current_channel_(chrome::VersionInfo::CHANNEL_DEV),
     61       override_prompt_for_external_extensions_(
     62           FeatureSwitch::prompt_for_external_extensions(),
     63           false),
     64       profile_(NULL) {
     65   EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
     66 }
     67 
     68 ExtensionBrowserTest::~ExtensionBrowserTest() {
     69 }
     70 
     71 Profile* ExtensionBrowserTest::profile() {
     72   if (!profile_) {
     73     if (browser())
     74       profile_ = browser()->profile();
     75     else
     76       profile_ = ProfileManager::GetActiveUserProfile();
     77   }
     78   return profile_;
     79 }
     80 
     81 // static
     82 const Extension* ExtensionBrowserTest::GetExtensionByPath(
     83     const ExtensionSet* extensions, const base::FilePath& path) {
     84   base::FilePath extension_path = base::MakeAbsoluteFilePath(path);
     85   EXPECT_TRUE(!extension_path.empty());
     86   for (ExtensionSet::const_iterator iter = extensions->begin();
     87        iter != extensions->end(); ++iter) {
     88     if ((*iter)->path() == extension_path) {
     89       return iter->get();
     90     }
     91   }
     92   return NULL;
     93 }
     94 
     95 void ExtensionBrowserTest::SetUpCommandLine(CommandLine* command_line) {
     96   PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir_);
     97   test_data_dir_ = test_data_dir_.AppendASCII("extensions");
     98   observer_.reset(new ExtensionTestNotificationObserver(browser()));
     99 
    100 #if defined(OS_CHROMEOS)
    101   // This makes sure that we create the Default profile first, with no
    102   // ExtensionService and then the real profile with one, as we do when
    103   // running on chromeos.
    104   command_line->AppendSwitchASCII(chromeos::switches::kLoginUser,
    105                                   "TestUser (at) gmail.com");
    106   command_line->AppendSwitchASCII(chromeos::switches::kLoginProfile, "user");
    107 #endif
    108 }
    109 
    110 void ExtensionBrowserTest::SetUpOnMainThread() {
    111   InProcessBrowserTest::SetUpOnMainThread();
    112   observer_.reset(new ExtensionTestNotificationObserver(browser()));
    113 }
    114 
    115 const Extension* ExtensionBrowserTest::LoadExtensionWithFlags(
    116     const base::FilePath& path, int flags) {
    117   ExtensionService* service = extensions::ExtensionSystem::Get(
    118       profile())->extension_service();
    119   {
    120     observer_->Watch(chrome::NOTIFICATION_EXTENSION_LOADED,
    121                      content::NotificationService::AllSources());
    122 
    123     scoped_refptr<extensions::UnpackedInstaller> installer(
    124         extensions::UnpackedInstaller::Create(service));
    125     installer->set_prompt_for_plugins(false);
    126     installer->set_require_modern_manifest_version(
    127         (flags & kFlagAllowOldManifestVersions) == 0);
    128     installer->Load(path);
    129 
    130     observer_->Wait();
    131   }
    132 
    133   // Find the loaded extension by its path. See crbug.com/59531 for why
    134   // we cannot just use last_loaded_extension_id().
    135   const Extension* extension = GetExtensionByPath(service->extensions(), path);
    136   if (!extension)
    137     return NULL;
    138 
    139   if (!(flags & kFlagIgnoreManifestWarnings)) {
    140     const std::vector<extensions::InstallWarning>& install_warnings =
    141         extension->install_warnings();
    142     if (!install_warnings.empty()) {
    143       std::string install_warnings_message = base::StringPrintf(
    144           "Unexpected warnings when loading test extension %s:\n",
    145           path.AsUTF8Unsafe().c_str());
    146 
    147       for (std::vector<extensions::InstallWarning>::const_iterator it =
    148           install_warnings.begin(); it != install_warnings.end(); ++it) {
    149         install_warnings_message += "  " + it->message + "\n";
    150       }
    151 
    152       EXPECT_TRUE(extension->install_warnings().empty()) <<
    153           install_warnings_message;
    154       return NULL;
    155     }
    156   }
    157 
    158   const std::string extension_id = extension->id();
    159 
    160   // The call to OnExtensionInstalled ensures the other extension prefs
    161   // are set up with the defaults.
    162   service->extension_prefs()->OnExtensionInstalled(
    163       extension,
    164       Extension::ENABLED,
    165       false,
    166       syncer::StringOrdinal::CreateInitialOrdinal());
    167 
    168   // Toggling incognito or file access will reload the extension, so wait for
    169   // the reload and grab the new extension instance. The default state is
    170   // incognito disabled and file access enabled, so we don't wait in those
    171   // cases.
    172   {
    173     content::WindowedNotificationObserver load_signal(
    174         chrome::NOTIFICATION_EXTENSION_LOADED,
    175         content::Source<Profile>(profile()));
    176     CHECK(!extension_util::IsIncognitoEnabled(extension_id, service) ||
    177           extension->force_incognito_enabled());
    178 
    179     if (flags & kFlagEnableIncognito) {
    180       extension_util::SetIsIncognitoEnabled(extension_id, service, true);
    181       load_signal.Wait();
    182       extension = service->GetExtensionById(extension_id, false);
    183       CHECK(extension) << extension_id << " not found after reloading.";
    184     }
    185   }
    186 
    187   {
    188     content::WindowedNotificationObserver load_signal(
    189         chrome::NOTIFICATION_EXTENSION_LOADED,
    190         content::Source<Profile>(profile()));
    191     CHECK(extension_util::AllowFileAccess(extension, service));
    192     if (!(flags & kFlagEnableFileAccess)) {
    193       extension_util::SetAllowFileAccess(extension, service, false);
    194       load_signal.Wait();
    195       extension = service->GetExtensionById(extension_id, false);
    196       CHECK(extension) << extension_id << " not found after reloading.";
    197     }
    198   }
    199 
    200   if (!observer_->WaitForExtensionViewsToLoad())
    201     return NULL;
    202 
    203   return extension;
    204 }
    205 
    206 const Extension* ExtensionBrowserTest::LoadExtension(
    207     const base::FilePath& path) {
    208   return LoadExtensionWithFlags(path, kFlagEnableFileAccess);
    209 }
    210 
    211 const Extension* ExtensionBrowserTest::LoadExtensionIncognito(
    212     const base::FilePath& path) {
    213   return LoadExtensionWithFlags(path,
    214                                 kFlagEnableFileAccess | kFlagEnableIncognito);
    215 }
    216 
    217 const Extension* ExtensionBrowserTest::LoadExtensionAsComponentWithManifest(
    218     const base::FilePath& path,
    219     const base::FilePath::CharType* manifest_relative_path) {
    220   ExtensionService* service = extensions::ExtensionSystem::Get(
    221       profile())->extension_service();
    222 
    223   std::string manifest;
    224   if (!base::ReadFileToString(path.Append(manifest_relative_path), &manifest)) {
    225     return NULL;
    226   }
    227 
    228   std::string extension_id = service->component_loader()->Add(manifest, path);
    229   const Extension* extension = service->extensions()->GetByID(extension_id);
    230   if (!extension)
    231     return NULL;
    232   observer_->set_last_loaded_extension_id(extension->id());
    233   return extension;
    234 }
    235 
    236 const Extension* ExtensionBrowserTest::LoadExtensionAsComponent(
    237     const base::FilePath& path) {
    238   return LoadExtensionAsComponentWithManifest(path,
    239                                               extensions::kManifestFilename);
    240 }
    241 
    242 base::FilePath ExtensionBrowserTest::PackExtension(
    243     const base::FilePath& dir_path) {
    244   base::FilePath crx_path = temp_dir_.path().AppendASCII("temp.crx");
    245   if (!base::DeleteFile(crx_path, false)) {
    246     ADD_FAILURE() << "Failed to delete crx: " << crx_path.value();
    247     return base::FilePath();
    248   }
    249 
    250   // Look for PEM files with the same name as the directory.
    251   base::FilePath pem_path =
    252       dir_path.ReplaceExtension(FILE_PATH_LITERAL(".pem"));
    253   base::FilePath pem_path_out;
    254 
    255   if (!base::PathExists(pem_path)) {
    256     pem_path = base::FilePath();
    257     pem_path_out = crx_path.DirName().AppendASCII("temp.pem");
    258     if (!base::DeleteFile(pem_path_out, false)) {
    259       ADD_FAILURE() << "Failed to delete pem: " << pem_path_out.value();
    260       return base::FilePath();
    261     }
    262   }
    263 
    264   return PackExtensionWithOptions(dir_path, crx_path, pem_path, pem_path_out);
    265 }
    266 
    267 base::FilePath ExtensionBrowserTest::PackExtensionWithOptions(
    268     const base::FilePath& dir_path,
    269     const base::FilePath& crx_path,
    270     const base::FilePath& pem_path,
    271     const base::FilePath& pem_out_path) {
    272   if (!base::PathExists(dir_path)) {
    273     ADD_FAILURE() << "Extension dir not found: " << dir_path.value();
    274     return base::FilePath();
    275   }
    276 
    277   if (!base::PathExists(pem_path) && pem_out_path.empty()) {
    278     ADD_FAILURE() << "Must specify a PEM file or PEM output path";
    279     return base::FilePath();
    280   }
    281 
    282   scoped_ptr<ExtensionCreator> creator(new ExtensionCreator());
    283   if (!creator->Run(dir_path,
    284                     crx_path,
    285                     pem_path,
    286                     pem_out_path,
    287                     ExtensionCreator::kOverwriteCRX)) {
    288     ADD_FAILURE() << "ExtensionCreator::Run() failed: "
    289                   << creator->error_message();
    290     return base::FilePath();
    291   }
    292 
    293   if (!base::PathExists(crx_path)) {
    294     ADD_FAILURE() << crx_path.value() << " was not created.";
    295     return base::FilePath();
    296   }
    297   return crx_path;
    298 }
    299 
    300 // This class is used to simulate an installation abort by the user.
    301 class MockAbortExtensionInstallPrompt : public ExtensionInstallPrompt {
    302  public:
    303   MockAbortExtensionInstallPrompt() : ExtensionInstallPrompt(NULL) {
    304   }
    305 
    306   // Simulate a user abort on an extension installation.
    307   virtual void ConfirmInstall(
    308       Delegate* delegate,
    309       const Extension* extension,
    310       const ShowDialogCallback& show_dialog_callback) OVERRIDE {
    311     delegate->InstallUIAbort(true);
    312     base::MessageLoopForUI::current()->Quit();
    313   }
    314 
    315   virtual void OnInstallSuccess(const Extension* extension,
    316                                 SkBitmap* icon) OVERRIDE {}
    317 
    318   virtual void OnInstallFailure(
    319       const extensions::CrxInstallerError& error) OVERRIDE {}
    320 };
    321 
    322 class MockAutoConfirmExtensionInstallPrompt : public ExtensionInstallPrompt {
    323  public:
    324   explicit MockAutoConfirmExtensionInstallPrompt(
    325       content::WebContents* web_contents)
    326     : ExtensionInstallPrompt(web_contents) {}
    327 
    328   // Proceed without confirmation prompt.
    329   virtual void ConfirmInstall(
    330       Delegate* delegate,
    331       const Extension* extension,
    332       const ShowDialogCallback& show_dialog_callback) OVERRIDE {
    333     delegate->InstallUIProceed();
    334   }
    335 };
    336 
    337 const Extension* ExtensionBrowserTest::UpdateExtensionWaitForIdle(
    338     const std::string& id,
    339     const base::FilePath& path,
    340     int expected_change) {
    341   return InstallOrUpdateExtension(id,
    342                                   path,
    343                                   INSTALL_UI_TYPE_NONE,
    344                                   expected_change,
    345                                   Manifest::INTERNAL,
    346                                   browser(),
    347                                   Extension::NO_FLAGS,
    348                                   true);
    349 }
    350 
    351 const Extension* ExtensionBrowserTest::InstallExtensionFromWebstore(
    352     const base::FilePath& path,
    353     int expected_change) {
    354   return InstallOrUpdateExtension(std::string(),
    355                                   path,
    356                                   INSTALL_UI_TYPE_NONE,
    357                                   expected_change,
    358                                   Manifest::INTERNAL,
    359                                   browser(),
    360                                   Extension::FROM_WEBSTORE,
    361                                   false);
    362 }
    363 
    364 const Extension* ExtensionBrowserTest::InstallOrUpdateExtension(
    365     const std::string& id,
    366     const base::FilePath& path,
    367     InstallUIType ui_type,
    368     int expected_change) {
    369   return InstallOrUpdateExtension(id, path, ui_type, expected_change,
    370       Manifest::INTERNAL, browser(), Extension::NO_FLAGS, false);
    371 }
    372 
    373 const Extension* ExtensionBrowserTest::InstallOrUpdateExtension(
    374     const std::string& id,
    375     const base::FilePath& path,
    376     InstallUIType ui_type,
    377     int expected_change,
    378     Browser* browser,
    379     Extension::InitFromValueFlags creation_flags) {
    380   return InstallOrUpdateExtension(id, path, ui_type, expected_change,
    381                                   Manifest::INTERNAL, browser, creation_flags,
    382                                   false);
    383 }
    384 
    385 const Extension* ExtensionBrowserTest::InstallOrUpdateExtension(
    386     const std::string& id,
    387     const base::FilePath& path,
    388     InstallUIType ui_type,
    389     int expected_change,
    390     Manifest::Location install_source) {
    391   return InstallOrUpdateExtension(id, path, ui_type, expected_change,
    392       install_source, browser(), Extension::NO_FLAGS, false);
    393 }
    394 
    395 const Extension* ExtensionBrowserTest::InstallOrUpdateExtension(
    396     const std::string& id,
    397     const base::FilePath& path,
    398     InstallUIType ui_type,
    399     int expected_change,
    400     Manifest::Location install_source,
    401     Browser* browser,
    402     Extension::InitFromValueFlags creation_flags,
    403     bool wait_for_idle) {
    404   ExtensionService* service = profile()->GetExtensionService();
    405   service->set_show_extensions_prompts(false);
    406   size_t num_before = service->extensions()->size();
    407 
    408   {
    409     scoped_ptr<ExtensionInstallPrompt> install_ui;
    410     if (ui_type == INSTALL_UI_TYPE_CANCEL) {
    411       install_ui.reset(new MockAbortExtensionInstallPrompt());
    412     } else if (ui_type == INSTALL_UI_TYPE_NORMAL) {
    413       install_ui.reset(new ExtensionInstallPrompt(
    414           browser->tab_strip_model()->GetActiveWebContents()));
    415     } else if (ui_type == INSTALL_UI_TYPE_AUTO_CONFIRM) {
    416       install_ui.reset(new MockAutoConfirmExtensionInstallPrompt(
    417           browser->tab_strip_model()->GetActiveWebContents()));
    418     }
    419 
    420     // TODO(tessamac): Update callers to always pass an unpacked extension
    421     //                 and then always pack the extension here.
    422     base::FilePath crx_path = path;
    423     if (crx_path.Extension() != FILE_PATH_LITERAL(".crx")) {
    424       crx_path = PackExtension(path);
    425     }
    426     if (crx_path.empty())
    427       return NULL;
    428 
    429     scoped_refptr<extensions::CrxInstaller> installer(
    430         extensions::CrxInstaller::Create(service, install_ui.Pass()));
    431     installer->set_expected_id(id);
    432     installer->set_creation_flags(creation_flags);
    433     installer->set_install_source(install_source);
    434     installer->set_install_wait_for_idle(wait_for_idle);
    435     if (!installer->is_gallery_install()) {
    436       installer->set_off_store_install_allow_reason(
    437           extensions::CrxInstaller::OffStoreInstallAllowedInTest);
    438     }
    439 
    440     observer_->Watch(
    441         chrome::NOTIFICATION_CRX_INSTALLER_DONE,
    442         content::Source<extensions::CrxInstaller>(installer.get()));
    443 
    444     installer->InstallCrx(crx_path);
    445 
    446     observer_->Wait();
    447   }
    448 
    449   size_t num_after = service->extensions()->size();
    450   EXPECT_EQ(num_before + expected_change, num_after);
    451   if (num_before + expected_change != num_after) {
    452     VLOG(1) << "Num extensions before: " << base::IntToString(num_before)
    453             << " num after: " << base::IntToString(num_after)
    454             << " Installed extensions follow:";
    455 
    456     for (ExtensionSet::const_iterator it = service->extensions()->begin();
    457          it != service->extensions()->end(); ++it)
    458       VLOG(1) << "  " << (*it)->id();
    459 
    460     VLOG(1) << "Errors follow:";
    461     const std::vector<base::string16>* errors =
    462         ExtensionErrorReporter::GetInstance()->GetErrors();
    463     for (std::vector<base::string16>::const_iterator iter = errors->begin();
    464          iter != errors->end(); ++iter)
    465       VLOG(1) << *iter;
    466 
    467     return NULL;
    468   }
    469 
    470   if (!observer_->WaitForExtensionViewsToLoad())
    471     return NULL;
    472   return service->GetExtensionById(last_loaded_extension_id(), false);
    473 }
    474 
    475 void ExtensionBrowserTest::ReloadExtension(const std::string extension_id) {
    476   observer_->Watch(chrome::NOTIFICATION_EXTENSION_LOADED,
    477                 content::NotificationService::AllSources());
    478 
    479   ExtensionService* service =
    480       extensions::ExtensionSystem::Get(profile())->extension_service();
    481   service->ReloadExtension(extension_id);
    482 
    483   observer_->Wait();
    484   observer_->WaitForExtensionViewsToLoad();
    485 }
    486 
    487 void ExtensionBrowserTest::UnloadExtension(const std::string& extension_id) {
    488   ExtensionService* service = extensions::ExtensionSystem::Get(
    489       profile())->extension_service();
    490   service->UnloadExtension(extension_id,
    491                            extensions::UnloadedExtensionInfo::REASON_DISABLE);
    492 }
    493 
    494 void ExtensionBrowserTest::UninstallExtension(const std::string& extension_id) {
    495   ExtensionService* service = extensions::ExtensionSystem::Get(
    496       profile())->extension_service();
    497   service->UninstallExtension(extension_id, false, NULL);
    498 }
    499 
    500 void ExtensionBrowserTest::DisableExtension(const std::string& extension_id) {
    501   ExtensionService* service = extensions::ExtensionSystem::Get(
    502       profile())->extension_service();
    503   service->DisableExtension(extension_id, Extension::DISABLE_USER_ACTION);
    504 }
    505 
    506 void ExtensionBrowserTest::EnableExtension(const std::string& extension_id) {
    507   ExtensionService* service = extensions::ExtensionSystem::Get(
    508       profile())->extension_service();
    509   service->EnableExtension(extension_id);
    510 }
    511 
    512 void ExtensionBrowserTest::OpenWindow(content::WebContents* contents,
    513                                       const GURL& url,
    514                                       bool newtab_process_should_equal_opener,
    515                                       content::WebContents** newtab_result) {
    516   content::WindowedNotificationObserver windowed_observer(
    517       content::NOTIFICATION_LOAD_STOP,
    518       content::NotificationService::AllSources());
    519   ASSERT_TRUE(content::ExecuteScript(contents,
    520                                      "window.open('" + url.spec() + "');"));
    521 
    522   // The above window.open call is not user-initiated, so it will create
    523   // a popup window instead of a new tab in current window.
    524   // The stop notification will come from the new tab.
    525   windowed_observer.Wait();
    526   content::NavigationController* controller =
    527       content::Source<content::NavigationController>(
    528           windowed_observer.source()).ptr();
    529   content::WebContents* newtab = controller->GetWebContents();
    530   ASSERT_TRUE(newtab);
    531   EXPECT_EQ(url, controller->GetLastCommittedEntry()->GetURL());
    532   if (newtab_process_should_equal_opener)
    533     EXPECT_EQ(contents->GetRenderProcessHost(), newtab->GetRenderProcessHost());
    534   else
    535     EXPECT_NE(contents->GetRenderProcessHost(), newtab->GetRenderProcessHost());
    536 
    537   if (newtab_result)
    538     *newtab_result = newtab;
    539 }
    540 
    541 void ExtensionBrowserTest::NavigateInRenderer(content::WebContents* contents,
    542                                               const GURL& url) {
    543   bool result = false;
    544   content::WindowedNotificationObserver windowed_observer(
    545       content::NOTIFICATION_LOAD_STOP,
    546       content::NotificationService::AllSources());
    547   ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
    548       contents,
    549       "window.addEventListener('unload', function() {"
    550       "    window.domAutomationController.send(true);"
    551       "}, false);"
    552       "window.location = '" + url.spec() + "';",
    553       &result));
    554   ASSERT_TRUE(result);
    555   windowed_observer.Wait();
    556   EXPECT_EQ(url, contents->GetController().GetLastCommittedEntry()->GetURL());
    557 }
    558 
    559 extensions::ExtensionHost* ExtensionBrowserTest::FindHostWithPath(
    560     extensions::ProcessManager* manager,
    561     const std::string& path,
    562     int expected_hosts) {
    563   extensions::ExtensionHost* host = NULL;
    564   int num_hosts = 0;
    565   extensions::ProcessManager::ExtensionHostSet background_hosts =
    566       manager->background_hosts();
    567   for (extensions::ProcessManager::const_iterator iter =
    568            background_hosts.begin();
    569        iter != background_hosts.end();
    570        ++iter) {
    571     if ((*iter)->GetURL().path() == path) {
    572       EXPECT_FALSE(host);
    573       host = *iter;
    574     }
    575     num_hosts++;
    576   }
    577   EXPECT_EQ(expected_hosts, num_hosts);
    578   return host;
    579 }
    580 
    581 std::string ExtensionBrowserTest::ExecuteScriptInBackgroundPage(
    582     const std::string& extension_id,
    583     const std::string& script) {
    584   return extensions::browsertest_util::ExecuteScriptInBackgroundPage(
    585       profile(), extension_id, script);
    586 }
    587