diff options
-rw-r--r-- | chrome/browser/chromeos/first_run/drive_first_run_browsertest.cc | 192 | ||||
-rw-r--r-- | chrome/browser/chromeos/first_run/drive_first_run_controller.cc | 82 | ||||
-rw-r--r-- | chrome/browser/chromeos/first_run/drive_first_run_controller.h | 32 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 1 | ||||
-rw-r--r-- | chrome/test/data/drive_first_run/app.crx | bin | 0 -> 693 bytes | |||
-rw-r--r-- | chrome/test/data/drive_first_run/app.pem | 16 | ||||
-rw-r--r-- | chrome/test/data/drive_first_run/app/manifest.json | 22 | ||||
-rw-r--r-- | chrome/test/data/drive_first_run/bad/endpoint.html | 7 | ||||
-rw-r--r-- | chrome/test/data/drive_first_run/good/background.html | 6 | ||||
-rw-r--r-- | chrome/test/data/drive_first_run/good/endpoint.html | 12 |
10 files changed, 354 insertions, 16 deletions
diff --git a/chrome/browser/chromeos/first_run/drive_first_run_browsertest.cc b/chrome/browser/chromeos/first_run/drive_first_run_browsertest.cc new file mode 100644 index 0000000..c40cabb --- /dev/null +++ b/chrome/browser/chromeos/first_run/drive_first_run_browsertest.cc @@ -0,0 +1,192 @@ +// 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 "base/bind.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/path_service.h" +#include "base/strings/string_number_conversions.h" +#include "chrome/browser/chromeos/first_run/drive_first_run_controller.h" +#include "chrome/browser/extensions/crx_installer.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_system.h" +#include "chrome/browser/extensions/extension_test_notification_observer.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "content/public/test/test_utils.h" +#include "net/dns/mock_host_resolver.h" +#include "net/http/http_status_code.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" + +namespace chromeos { + +namespace { + +// Directory containing data files for the tests. +const char kTestDirectory[] = "drive_first_run"; + +// Directory containing correct hosted app page served by the test server. +const char kGoodServerDirectory[] = "good"; + +// Directory containing incorrect hosted app page served by the test server. +const char kBadServerDirectory[] = "bad"; + +// Name of the test hosted app .crx file. +const char kTestAppCrxName[] = "app.crx"; + +// App id of the test hosted app. +const char kTestAppId[] = "kipccbklifbfblhpplnmklieangbjnhb"; + +// The endpoint belonging to the test hosted app. +const char kTestEndpointUrl[] = "http://example.com/endpoint.html"; + +} // namespace + +class DriveFirstRunTest : public InProcessBrowserTest, + public DriveFirstRunController::Observer { + protected: + DriveFirstRunTest(); + + // InProcessBrowserTest overrides: + virtual void SetUpOnMainThread() OVERRIDE; + virtual void CleanUpOnMainThread() OVERRIDE; + + // DriveFirstRunController::Observer overrides: + virtual void OnCompletion(bool success) OVERRIDE; + virtual void OnTimedOut() OVERRIDE; + + void InstallApp(); + + void InitTestServer(const std::string& directory); + + bool WaitForFirstRunResult(); + + void EnableOfflineMode(); + + void SetDelays(int initial_delay_secs, int timeout_secs); + + bool timed_out() const { return timed_out_; } + + private: + // |controller_| is responsible for its own lifetime. + DriveFirstRunController* controller_; + scoped_refptr<content::MessageLoopRunner> runner_; + + bool timed_out_; + bool waiting_for_result_; + bool success_; + base::FilePath test_data_dir_; + std::string endpoint_url_; +}; + +DriveFirstRunTest::DriveFirstRunTest() : + timed_out_(false), + waiting_for_result_(false), + success_(false) {} + +void DriveFirstRunTest::SetUpOnMainThread() { + InProcessBrowserTest::SetUpOnMainThread(); + PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir_); + test_data_dir_ = test_data_dir_.AppendASCII(kTestDirectory); + + host_resolver()->AddRule("example.com", "127.0.0.1"); + + // |controller_| will delete itself when it completes. + controller_ = new DriveFirstRunController(); + controller_->AddObserver(this); + controller_->SetDelaysForTest(0, 10); + controller_->SetAppInfoForTest(kTestAppId, kTestEndpointUrl); +} + +void DriveFirstRunTest::CleanUpOnMainThread() { + InProcessBrowserTest::CleanUpOnMainThread(); + content::RunAllPendingInMessageLoop(); +} + +void DriveFirstRunTest::InitTestServer(const std::string& directory) { + embedded_test_server()->ServeFilesFromDirectory( + test_data_dir_.AppendASCII(directory)); + ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); + + // Configure the endpoint to use the test server's port. + const GURL url(kTestEndpointUrl); + GURL::Replacements replacements; + std::string port(base::IntToString(embedded_test_server()->port())); + replacements.SetPortStr(port); + endpoint_url_ = url.ReplaceComponents(replacements).spec(); + controller_->SetAppInfoForTest(kTestAppId, endpoint_url_); +} + +void DriveFirstRunTest::InstallApp() { + ExtensionService* extension_service = extensions::ExtensionSystem::Get( + browser()->profile())->extension_service(); + scoped_refptr<extensions::CrxInstaller> installer = + extensions::CrxInstaller::CreateSilent(extension_service); + + installer->InstallCrx(test_data_dir_.AppendASCII(kTestAppCrxName)); + ExtensionTestNotificationObserver observer(browser()); + observer.WaitForExtensionLoad(); + + ASSERT_TRUE(extension_service->GetExtensionById(kTestAppId, false)); +} + +void DriveFirstRunTest::EnableOfflineMode() { + controller_->EnableOfflineMode(); +} + +void DriveFirstRunTest::SetDelays(int initial_delay_secs, int timeout_secs) { + controller_->SetDelaysForTest(initial_delay_secs, timeout_secs); +} + +bool DriveFirstRunTest::WaitForFirstRunResult() { + waiting_for_result_ = true; + runner_ = new content::MessageLoopRunner; + runner_->Run(); + EXPECT_FALSE(waiting_for_result_); + return success_; +} + +void DriveFirstRunTest::OnCompletion(bool success) { + EXPECT_TRUE(waiting_for_result_); + waiting_for_result_ = false; + success_ = success; + runner_->Quit(); + + // |controller_| will eventually delete itself upon completion, so invalidate + // the pointer. + controller_ = NULL; +} + +void DriveFirstRunTest::OnTimedOut() { + timed_out_ = true; +} + +IN_PROC_BROWSER_TEST_F(DriveFirstRunTest, OfflineEnabled) { + InstallApp(); + InitTestServer(kGoodServerDirectory); + EnableOfflineMode(); + EXPECT_TRUE(WaitForFirstRunResult()); +} + +IN_PROC_BROWSER_TEST_F(DriveFirstRunTest, AppNotInstalled) { + InitTestServer(kGoodServerDirectory); + EnableOfflineMode(); + EXPECT_FALSE(WaitForFirstRunResult()); + EXPECT_FALSE(timed_out()); +} + +IN_PROC_BROWSER_TEST_F(DriveFirstRunTest, TimedOut) { + // Test that the controller times out instead of hanging forever. + InstallApp(); + InitTestServer(kBadServerDirectory); + SetDelays(0, 0); + EnableOfflineMode(); + EXPECT_FALSE(WaitForFirstRunResult()); + EXPECT_TRUE(timed_out()); +} + +} // namespace chromeos diff --git a/chrome/browser/chromeos/first_run/drive_first_run_controller.cc b/chrome/browser/chromeos/first_run/drive_first_run_controller.cc index ba523b1..9526948 100644 --- a/chrome/browser/chromeos/first_run/drive_first_run_controller.cc +++ b/chrome/browser/chromeos/first_run/drive_first_run_controller.cc @@ -16,6 +16,7 @@ #include "chrome/browser/chromeos/login/user_manager.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_system.h" +#include "chrome/browser/extensions/extension_web_contents_observer.h" #include "chrome/browser/profiles/profile_manager.h" #include "chrome/browser/tab_contents/background_contents.h" #include "content/public/browser/browser_thread.h" @@ -36,10 +37,10 @@ namespace chromeos { namespace { // The initial time to wait in seconds before starting the opt-in. -const int kInitialDelaySeconds = 180; +int kInitialDelaySeconds = 180; // Time to wait for Drive app background page to come up before giving up. -const int kWebContentsTimeoutSeconds = 15; +int kWebContentsTimeoutSeconds = 15; // Google Drive offline opt-in endpoint. const char kDriveOfflineEndpointUrl[] = "https://drive.google.com/#offline"; @@ -63,6 +64,8 @@ class DriveWebContentsManager : public content::WebContentsObserver, typedef base::Callback<void(bool)> CompletionCallback; DriveWebContentsManager(Profile* profile, + const std::string& app_id, + const std::string& endpoint_url, const CompletionCallback& completion_callback); virtual ~DriveWebContentsManager(); @@ -114,6 +117,8 @@ class DriveWebContentsManager : public content::WebContentsObserver, const content::NotificationDetails& details) OVERRIDE; Profile* profile_; + const std::string app_id_; + const std::string endpoint_url_; scoped_ptr<content::WebContents> web_contents_; content::NotificationRegistrar registrar_; bool started_; @@ -125,8 +130,12 @@ class DriveWebContentsManager : public content::WebContentsObserver, DriveWebContentsManager::DriveWebContentsManager( Profile* profile, + const std::string& app_id, + const std::string& endpoint_url, const CompletionCallback& completion_callback) : profile_(profile), + app_id_(app_id), + endpoint_url_(endpoint_url), started_(false), completion_callback_(completion_callback), weak_ptr_factory_(this) { @@ -140,12 +149,14 @@ DriveWebContentsManager::~DriveWebContentsManager() { void DriveWebContentsManager::StartLoad() { started_ = true; - const GURL url(kDriveOfflineEndpointUrl); + const GURL url(endpoint_url_); content::WebContents::CreateParams create_params( profile_, content::SiteInstance::CreateForURL(profile_, url)); web_contents_.reset(content::WebContents::Create(create_params)); web_contents_->SetDelegate(this); + extensions::ExtensionWebContentsObserver::CreateForWebContents( + web_contents_.get()); content::NavigationController::LoadURLParams load_params(url); load_params.transition_type = content::PAGE_TRANSITION_GENERATED; @@ -183,7 +194,10 @@ void DriveWebContentsManager::DidFailProvisionalLoad( int error_code, const string16& error_description, content::RenderViewHost* render_view_host) { - OnOfflineInit(false); + if (is_main_frame) { + LOG(WARNING) << "Failed to load WebContents to enable offline mode."; + OnOfflineInit(false); + } } void DriveWebContentsManager::DidFailLoad( @@ -193,7 +207,10 @@ void DriveWebContentsManager::DidFailLoad( int error_code, const string16& error_description, content::RenderViewHost* render_view_host) { - OnOfflineInit(false); + if (is_main_frame) { + LOG(WARNING) << "Failed to load WebContents to enable offline mode."; + OnOfflineInit(false); + } } bool DriveWebContentsManager::ShouldCreateWebContents( @@ -213,7 +230,7 @@ bool DriveWebContentsManager::ShouldCreateWebContents( extensions::ExtensionSystem::Get(profile_)->extension_service(); const extensions::Extension *extension = service->GetInstalledApp(target_url); - if (!extension || extension->id() != kDriveHostedAppId) + if (!extension || extension->id() != app_id_) return true; // The background contents creation is normally done in Browser, but @@ -223,7 +240,7 @@ bool DriveWebContentsManager::ShouldCreateWebContents( // Prevent redirection if background contents already exists. if (background_contents_service->GetAppBackgroundContents( - UTF8ToUTF16(kDriveHostedAppId))) { + UTF8ToUTF16(app_id_))) { return false; } BackgroundContents* contents = background_contents_service @@ -231,7 +248,7 @@ bool DriveWebContentsManager::ShouldCreateWebContents( route_id, profile_, frame_name, - ASCIIToUTF16(kDriveHostedAppId), + ASCIIToUTF16(app_id_), partition_id, session_storage_namespace); @@ -253,7 +270,7 @@ void DriveWebContentsManager::Observe( const std::string app_id = UTF16ToUTF8( content::Details<BackgroundContentsOpenedDetails>(details) ->application_id); - if (app_id == kDriveHostedAppId) + if (app_id == app_id_) OnOfflineInit(true); } } @@ -263,7 +280,11 @@ void DriveWebContentsManager::Observe( DriveFirstRunController::DriveFirstRunController() : profile_(ProfileManager::GetDefaultProfile()), - started_(false) { + started_(false), + initial_delay_secs_(kInitialDelaySeconds), + web_contents_timeout_secs_(kWebContentsTimeoutSeconds), + drive_offline_endpoint_url_(kDriveOfflineEndpointUrl), + drive_hosted_app_id_(kDriveHostedAppId) { } DriveFirstRunController::~DriveFirstRunController() { @@ -274,7 +295,7 @@ void DriveFirstRunController::EnableOfflineMode() { started_ = true; initial_delay_timer_.Start( FROM_HERE, - base::TimeDelta::FromSeconds(kInitialDelaySeconds), + base::TimeDelta::FromSeconds(initial_delay_secs_), this, &DriveFirstRunController::EnableOfflineMode); return; @@ -289,7 +310,7 @@ void DriveFirstRunController::EnableOfflineMode() { ExtensionService* extension_service = extensions::ExtensionSystem::Get(profile_)->extension_service(); - if (!extension_service->GetExtensionById(kDriveHostedAppId, false)) { + if (!extension_service->GetExtensionById(drive_hosted_app_id_, false)) { LOG(WARNING) << "Drive app is not installed."; OnOfflineInit(false); return; @@ -298,7 +319,7 @@ void DriveFirstRunController::EnableOfflineMode() { BackgroundContentsService* background_contents_service = BackgroundContentsServiceFactory::GetForProfile(profile_); if (background_contents_service->GetAppBackgroundContents( - UTF8ToUTF16(kDriveHostedAppId))) { + UTF8ToUTF16(drive_hosted_app_id_))) { LOG(WARNING) << "Background page for Drive app already exists"; OnOfflineInit(false); return; @@ -306,18 +327,44 @@ void DriveFirstRunController::EnableOfflineMode() { web_contents_manager_.reset(new DriveWebContentsManager( profile_, + drive_hosted_app_id_, + drive_offline_endpoint_url_, base::Bind(&DriveFirstRunController::OnOfflineInit, base::Unretained(this)))); web_contents_manager_->StartLoad(); web_contents_timer_.Start( FROM_HERE, - base::TimeDelta::FromSeconds(kWebContentsTimeoutSeconds), + base::TimeDelta::FromSeconds(web_contents_timeout_secs_), this, &DriveFirstRunController::OnWebContentsTimedOut); } +void DriveFirstRunController::AddObserver(Observer* observer) { + observer_list_.AddObserver(observer); +} + +void DriveFirstRunController::RemoveObserver(Observer* observer) { + observer_list_.RemoveObserver(observer); +} + +void DriveFirstRunController::SetDelaysForTest(int initial_delay_secs, + int timeout_secs) { + DCHECK(!started_); + initial_delay_secs_ = initial_delay_secs; + web_contents_timeout_secs_ = timeout_secs; +} + +void DriveFirstRunController::SetAppInfoForTest( + const std::string& app_id, + const std::string& endpoint_url) { + DCHECK(!started_); + drive_hosted_app_id_ = app_id; + drive_offline_endpoint_url_ = endpoint_url; +} + void DriveFirstRunController::OnWebContentsTimedOut() { LOG(WARNING) << "Timed out waiting for web contents to opt-in"; + FOR_EACH_OBSERVER(Observer, observer_list_, OnTimedOut()); OnOfflineInit(false); } @@ -330,8 +377,11 @@ void DriveFirstRunController::CleanUp() { void DriveFirstRunController::OnOfflineInit(bool success) { DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); - ash::Shell::GetInstance()->system_tray_notifier() - ->NotifyDriveOfflineEnabled(); + if (success) { + ash::Shell::GetInstance()->system_tray_notifier() + ->NotifyDriveOfflineEnabled(); + } + FOR_EACH_OBSERVER(Observer, observer_list_, OnCompletion(success)); CleanUp(); } diff --git a/chrome/browser/chromeos/first_run/drive_first_run_controller.h b/chrome/browser/chromeos/first_run/drive_first_run_controller.h index 65b3a49..47247ee 100644 --- a/chrome/browser/chromeos/first_run/drive_first_run_controller.h +++ b/chrome/browser/chromeos/first_run/drive_first_run_controller.h @@ -5,6 +5,7 @@ #define CHROME_BROWSER_CHROMEOS_FIRST_RUN_DRIVE_FIRST_RUN_CONTROLLER_H_ #include "base/basictypes.h" +#include "base/observer_list.h" #include "base/timer/timer.h" #include "chrome/browser/profiles/profile.h" @@ -18,12 +19,37 @@ class DriveWebContentsManager; // destroy itself when the initialization succeeds or fails. class DriveFirstRunController { public: + class Observer { + public: + // Called when enabling offline mode times out. OnCompletion will be called + // immediately afterwards. + virtual void OnTimedOut() = 0; + + // Called when the first run flow finishes, informing the observer of + // success or failure. + virtual void OnCompletion(bool success) = 0; + + protected: + virtual ~Observer() {} + }; + DriveFirstRunController(); ~DriveFirstRunController(); // Starts the process to enable offline mode for the user's Drive account. void EnableOfflineMode(); + // Manages observers of the first run flow. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Set delay times for testing purposes. + void SetDelaysForTest(int initial_delay_secs, int timeout_secs); + + // Set the app id and endpoint url for testing purposes. + void SetAppInfoForTest(const std::string& app_id, + const std::string& endpoint_url); + private: // Used as a callback to indicate whether the offline initialization // succeeds or fails. @@ -40,6 +66,12 @@ class DriveFirstRunController { base::OneShotTimer<DriveFirstRunController> web_contents_timer_; base::OneShotTimer<DriveFirstRunController> initial_delay_timer_; bool started_; + ObserverList<Observer> observer_list_; + + int initial_delay_secs_; + int web_contents_timeout_secs_; + std::string drive_offline_endpoint_url_; + std::string drive_hosted_app_id_; DISALLOW_COPY_AND_ASSIGN(DriveFirstRunController); }; diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 68e931a..718de52 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -1026,6 +1026,7 @@ 'browser/chromeos/file_manager/external_filesystem_apitest.cc', 'browser/chromeos/file_manager/file_manager_browsertest.cc', 'browser/chromeos/file_manager/file_manager_jstest.cc', + 'browser/chromeos/first_run/drive_first_run_browsertest.cc', 'browser/chromeos/input_method/input_method_engine_ibus_browserttests.cc', 'browser/chromeos/kiosk_mode/mock_kiosk_mode_settings.cc', 'browser/chromeos/kiosk_mode/mock_kiosk_mode_settings.h', diff --git a/chrome/test/data/drive_first_run/app.crx b/chrome/test/data/drive_first_run/app.crx Binary files differnew file mode 100644 index 0000000..4142419 --- /dev/null +++ b/chrome/test/data/drive_first_run/app.crx diff --git a/chrome/test/data/drive_first_run/app.pem b/chrome/test/data/drive_first_run/app.pem new file mode 100644 index 0000000..b6535d6 --- /dev/null +++ b/chrome/test/data/drive_first_run/app.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALZCHvqUjR5KG7A/2 +UIEdTi1EkinTLqK5toBIgQdUjf33QFkIHVS3ABIMHmfl5T54hpp3P5RPmuSkP0F3i +f/BEvue0cSw3YKONlRIoYkAtGWyMh9MMN58FM6mP8Ck7nhSCPLL3QLkEr5TlauoG0 +KESY8NwYYVMeJajI1QfohnuwfAgMBAAECgYBt5Y7Cb6Jr2im567X37bI1AFHHB0Hn +1Wt/lmFJc9iosMdNWG+N7umDLgQ0wftntAkW/jBoFNr7iEPunYQoP8f5b5F8zt3dy +1Aor8vBqP55GvvQjqOGlzH0bRE67PoCjTFc4K9avCbgVZfyG+Bb41Li5VZBynJCkg +kRVoazkaAwKQJBAOvgYLiy89arKYghhavxtaGWaSqL1HmrfZBPaKppK2Hy/ycekS8 +S6MqEfSnmDhKdUBdGHNnlmNkzM5GNoCY7cIUCQQDFzrQn/xPw2p2Wj8QX9HzefBCs +m1nPMVtoceu2jew1x4WvM1Wr7M/0B3dL+cmvOMRBqHUzjPF3mp1Jj0uhbv1TAkANV +4zBBcZLHzVjMNo5xptKf5KFSJGFLFEW55b5BKfii3cpRE5cBkrKocHeq9eh7+oG1v +1sydLifkXtdsBXSUdtAkEAxFWjmaNkDodfLWcrMr+4BTjNcBWOMcoCuYuBc6QwlTy +h40Enwsr9qXCTp3SaC/JjUew70FwP/DAZ+D5jyisZAwJAZ6UeZMnA1matrZusy1VU +LSjDL+fHEYQXnBVgdcMaV5XimArRz1h5zBzJdLLR+ibSBhlgVXAuYu83O2XQuk8Wc +g== +-----END PRIVATE KEY----- diff --git a/chrome/test/data/drive_first_run/app/manifest.json b/chrome/test/data/drive_first_run/app/manifest.json new file mode 100644 index 0000000..f714070 --- /dev/null +++ b/chrome/test/data/drive_first_run/app/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Drive First Trun Test App", + "description": "App similar to Google Drive app for testing enabling offline mode on Chrome OS first run.", + "version": "1.0", + "offline_enabled": true, + "manifest_version": 2, + "permissions": [ + "background" + ], + "background": { + "allow_js_access": false + }, + "app": { + "launch": { + "web_url": "http://example.com/endpoint.html" + }, + "urls": [ + "*://example.com", + "*://example.com:*" + ] + } +} diff --git a/chrome/test/data/drive_first_run/bad/endpoint.html b/chrome/test/data/drive_first_run/bad/endpoint.html new file mode 100644 index 0000000..b105b49 --- /dev/null +++ b/chrome/test/data/drive_first_run/bad/endpoint.html @@ -0,0 +1,7 @@ +<html> +<title>Bad Test Endpoint</title> +<body> +<p> This endpoint is faulty and does not register a background page for the app. </p> +</script> +</body> +</html> diff --git a/chrome/test/data/drive_first_run/good/background.html b/chrome/test/data/drive_first_run/good/background.html new file mode 100644 index 0000000..c03552e --- /dev/null +++ b/chrome/test/data/drive_first_run/good/background.html @@ -0,0 +1,6 @@ +<html> +<title>Test Background Page</title> +<body> +<p> If this background page is opened, the test was successful. </p> +</body> +</html> diff --git a/chrome/test/data/drive_first_run/good/endpoint.html b/chrome/test/data/drive_first_run/good/endpoint.html new file mode 100644 index 0000000..a1045dd --- /dev/null +++ b/chrome/test/data/drive_first_run/good/endpoint.html @@ -0,0 +1,12 @@ +<html> +<title>Test Endpoint</title> +<body> +<p> This endpoint should open and register a background page for the app. </p> + +<script type="text/javascript"> +document.addEventListener("DOMContentLoaded", function(event) { + window.open("background.html#0", "bg", "background"); +}); +</script> +</body> +</html> |