|  | // Copyright (c) 2012 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/shell_integration_win.h" | 
|  |  | 
|  | #include <stddef.h> | 
|  |  | 
|  | #include <string> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/files/file_util.h" | 
|  | #include "base/files/scoped_temp_dir.h" | 
|  | #include "base/macros.h" | 
|  | #include "base/strings/string_number_conversions.h" | 
|  | #include "base/strings/string_util.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/test/test_shortcut_win.h" | 
|  | #include "base/win/scoped_com_initializer.h" | 
|  | #include "chrome/browser/shell_integration.h" | 
|  | #include "chrome/browser/web_applications/components/web_app_helpers.h" | 
|  | #include "chrome/browser/web_applications/components/web_app_shortcut_win.h" | 
|  | #include "chrome/common/chrome_constants.h" | 
|  | #include "chrome/common/chrome_paths_internal.h" | 
|  | #include "chrome/install_static/install_util.h" | 
|  | #include "chrome/installer/util/install_util.h" | 
|  | #include "chrome/installer/util/shell_util.h" | 
|  | #include "chrome/installer/util/util_constants.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | namespace shell_integration { | 
|  | namespace win { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | struct ShortcutTestObject { | 
|  | base::FilePath path; | 
|  | base::win::ShortcutProperties properties; | 
|  | }; | 
|  |  | 
|  | class ShellIntegrationWinMigrateShortcutTest : public testing::Test { | 
|  | protected: | 
|  | ShellIntegrationWinMigrateShortcutTest() {} | 
|  |  | 
|  | void SetUp() override { | 
|  | ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); | 
|  | ASSERT_TRUE( | 
|  | temp_dir_sub_dir_.CreateUniqueTempDirUnderPath(temp_dir_.GetPath())); | 
|  | // A path to a random target. | 
|  | base::CreateTemporaryFileInDir(temp_dir_.GetPath(), &other_target_); | 
|  |  | 
|  | // This doesn't need to actually have a base name of "chrome.exe". | 
|  | base::CreateTemporaryFileInDir(temp_dir_.GetPath(), &chrome_exe_); | 
|  |  | 
|  | chrome_app_id_ = ShellUtil::GetBrowserModelId(true); | 
|  |  | 
|  | base::FilePath default_user_data_dir; | 
|  | chrome::GetDefaultUserDataDirectory(&default_user_data_dir); | 
|  | base::FilePath default_profile_path = | 
|  | default_user_data_dir.AppendASCII(chrome::kInitialProfile); | 
|  | non_default_user_data_dir_ = base::FilePath(FILE_PATH_LITERAL("root")) | 
|  | .Append(FILE_PATH_LITERAL("Non Default Data Dir")); | 
|  | non_default_profile_ = L"NonDefault"; | 
|  | non_default_profile_chrome_app_id_ = GetAppUserModelIdForBrowser( | 
|  | default_user_data_dir.Append(non_default_profile_)); | 
|  | non_default_user_data_dir_chrome_app_id_ = GetAppUserModelIdForBrowser( | 
|  | non_default_user_data_dir_.AppendASCII(chrome::kInitialProfile)); | 
|  | non_default_user_data_dir_and_profile_chrome_app_id_ = | 
|  | GetAppUserModelIdForBrowser( | 
|  | non_default_user_data_dir_.Append(non_default_profile_)); | 
|  |  | 
|  | extension_id_ = L"chromiumexampleappidforunittests"; | 
|  | std::wstring app_name = | 
|  | base::UTF8ToWide(web_app::GenerateApplicationNameFromAppId( | 
|  | base::WideToUTF8(extension_id_))); | 
|  | extension_app_id_ = GetAppUserModelIdForApp(app_name, default_profile_path); | 
|  | non_default_profile_extension_app_id_ = GetAppUserModelIdForApp( | 
|  | app_name, default_user_data_dir.Append(non_default_profile_)); | 
|  | } | 
|  |  | 
|  | // Creates a test shortcut corresponding to |shortcut_properties| and resets | 
|  | // |shortcut_properties| after copying it to an internal structure for later | 
|  | // verification. | 
|  | void AddTestShortcutAndResetProperties( | 
|  | const base::FilePath& shortcut_dir, | 
|  | base::win::ShortcutProperties* shortcut_properties) { | 
|  | ShortcutTestObject shortcut_test_object; | 
|  | base::FilePath shortcut_path = shortcut_dir.Append( | 
|  | L"Shortcut " + base::NumberToWString(shortcuts_.size()) + | 
|  | installer::kLnkExt); | 
|  | shortcut_test_object.path = shortcut_path; | 
|  | shortcut_test_object.properties = *shortcut_properties; | 
|  | shortcuts_.push_back(shortcut_test_object); | 
|  | ASSERT_TRUE(base::win::CreateOrUpdateShortcutLink( | 
|  | shortcut_path, *shortcut_properties, | 
|  | base::win::SHORTCUT_CREATE_ALWAYS)); | 
|  | shortcut_properties->options = 0U; | 
|  | } | 
|  |  | 
|  | void CreateShortcuts() { | 
|  | // A temporary object to pass properties to | 
|  | // AddTestShortcutAndResetProperties(). | 
|  | base::win::ShortcutProperties temp_properties; | 
|  |  | 
|  | // Shortcut 0 doesn't point to chrome.exe and thus should never be migrated. | 
|  | temp_properties.set_target(other_target_); | 
|  | temp_properties.set_app_id(L"Dumbo"); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 1 points to chrome.exe and thus should be migrated. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(L"Dumbo"); | 
|  | temp_properties.set_dual_mode(false); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 2 points to chrome.exe, but already has the right appid and thus | 
|  | // should only be migrated if dual_mode is desired. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(chrome_app_id_); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 3 is like shortcut 1, but it's appid is a prefix of the expected | 
|  | // appid instead of being totally different. | 
|  | std::wstring chrome_app_id_is_prefix(chrome_app_id_); | 
|  | chrome_app_id_is_prefix.push_back(L'1'); | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(chrome_app_id_is_prefix); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 4 is like shortcut 1, but it's appid is of the same size as the | 
|  | // expected appid. | 
|  | std::wstring same_size_as_chrome_app_id(chrome_app_id_.size(), L'1'); | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(same_size_as_chrome_app_id); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 5 doesn't have an app_id, nor is dual_mode even set; they should | 
|  | // be set as expected upon migration. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 6 has a non-default profile directory and so should get a non- | 
|  | // default app id. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(L"Dumbo"); | 
|  | temp_properties.set_arguments( | 
|  | L"--profile-directory=" + non_default_profile_); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 7 has a non-default user data directory and so should get a non- | 
|  | // default app id. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(L"Dumbo"); | 
|  | temp_properties.set_arguments( | 
|  | L"--user-data-dir=\"" + non_default_user_data_dir_.value() + L"\""); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 8 has a non-default user data directory as well as a non-default | 
|  | // profile directory and so should get a non-default app id. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(L"Dumbo"); | 
|  | temp_properties.set_arguments( | 
|  | L"--user-data-dir=\"" + non_default_user_data_dir_.value() + L"\" " + | 
|  | L"--profile-directory=" + non_default_profile_); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 9 is a shortcut to an app and should get an app id for that app | 
|  | // rather than the chrome app id. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(L"Dumbo"); | 
|  | temp_properties.set_arguments( | 
|  | L"--app-id=" + extension_id_); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 10 is a shortcut to an app with a non-default profile and should | 
|  | // get an app id for that app with a non-default app id rather than the | 
|  | // chrome app id. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(L"Dumbo"); | 
|  | temp_properties.set_arguments( | 
|  | L"--app-id=" + extension_id_ + | 
|  | L" --profile-directory=" + non_default_profile_); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 11 points to chrome.exe, already has the right appid, and has | 
|  | // dual_mode set and thus should only be migrated if dual_mode is being | 
|  | // cleared. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(chrome_app_id_); | 
|  | temp_properties.set_dual_mode(true); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 12 is similar to 11 but with dual_mode explicitly set to false. | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(chrome_app_id_); | 
|  | temp_properties.set_dual_mode(false); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Shortcut 13 is like shortcut 1, but it's appid explicitly includes the | 
|  | // default profile. | 
|  | std::wstring chrome_app_id_with_default_profile = | 
|  | chrome_app_id_ + L".Default"; | 
|  | temp_properties.set_target(chrome_exe_); | 
|  | temp_properties.set_app_id(chrome_app_id_with_default_profile); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_.GetPath(), &temp_properties)); | 
|  | } | 
|  |  | 
|  | base::win::ScopedCOMInitializer com_initializer_; | 
|  |  | 
|  | base::ScopedTempDir temp_dir_; | 
|  |  | 
|  | // Used to test migration of shortcuts in ImplicitApps sub-directories. | 
|  | base::ScopedTempDir temp_dir_sub_dir_; | 
|  |  | 
|  | // Test shortcuts. | 
|  | std::vector<ShortcutTestObject> shortcuts_; | 
|  |  | 
|  | // The path to a fake chrome.exe. | 
|  | base::FilePath chrome_exe_; | 
|  |  | 
|  | // The path to a random target. | 
|  | base::FilePath other_target_; | 
|  |  | 
|  | // Chrome's AppUserModelId. | 
|  | std::wstring chrome_app_id_; | 
|  |  | 
|  | // A profile that isn't the Default profile. | 
|  | std::wstring non_default_profile_; | 
|  |  | 
|  | // A user data dir that isn't the default. | 
|  | base::FilePath non_default_user_data_dir_; | 
|  |  | 
|  | // Chrome's AppUserModelId for the non-default profile. | 
|  | std::wstring non_default_profile_chrome_app_id_; | 
|  |  | 
|  | // Chrome's AppUserModelId for the non-default user data dir. | 
|  | std::wstring non_default_user_data_dir_chrome_app_id_; | 
|  |  | 
|  | // Chrome's AppUserModelId for the non-default user data dir and non-default | 
|  | // profile. | 
|  | std::wstring non_default_user_data_dir_and_profile_chrome_app_id_; | 
|  |  | 
|  | // An example extension id of an example app. | 
|  | std::wstring extension_id_; | 
|  |  | 
|  | // The app id of the example app for the default profile and user data dir. | 
|  | std::wstring extension_app_id_; | 
|  |  | 
|  | // The app id of the example app for the non-default profile. | 
|  | std::wstring non_default_profile_extension_app_id_; | 
|  |  | 
|  | private: | 
|  | DISALLOW_COPY_AND_ASSIGN(ShellIntegrationWinMigrateShortcutTest); | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | TEST_F(ShellIntegrationWinMigrateShortcutTest, ClearDualModeAndAdjustAppIds) { | 
|  | CreateShortcuts(); | 
|  | // 10 shortcuts should have their app id updated below and shortcut 11 should | 
|  | // be migrated away from dual_mode for a total of 11 shortcuts migrated. | 
|  | EXPECT_EQ(11, | 
|  | MigrateShortcutsInPathInternal(chrome_exe_, temp_dir_.GetPath())); | 
|  |  | 
|  | // Shortcut 1, 3, 4, 5, 6, 7, 8, 9, 10, and 13 should have had their app_id | 
|  | //  fixed. | 
|  | shortcuts_[1].properties.set_app_id(chrome_app_id_); | 
|  | shortcuts_[3].properties.set_app_id(chrome_app_id_); | 
|  | shortcuts_[4].properties.set_app_id(chrome_app_id_); | 
|  | shortcuts_[5].properties.set_app_id(chrome_app_id_); | 
|  | shortcuts_[6].properties.set_app_id(non_default_profile_chrome_app_id_); | 
|  | shortcuts_[7].properties.set_app_id(non_default_user_data_dir_chrome_app_id_); | 
|  | shortcuts_[8].properties.set_app_id( | 
|  | non_default_user_data_dir_and_profile_chrome_app_id_); | 
|  | shortcuts_[9].properties.set_app_id(extension_app_id_); | 
|  | shortcuts_[10].properties.set_app_id(non_default_profile_extension_app_id_); | 
|  | shortcuts_[13].properties.set_app_id(chrome_app_id_); | 
|  |  | 
|  | // No shortcut should still have the dual_mode property. | 
|  | for (size_t i = 0; i < shortcuts_.size(); ++i) | 
|  | shortcuts_[i].properties.set_dual_mode(false); | 
|  |  | 
|  | for (size_t i = 0; i < shortcuts_.size(); ++i) { | 
|  | SCOPED_TRACE(i); | 
|  | base::win::ValidateShortcut(shortcuts_[i].path, shortcuts_[i].properties); | 
|  | } | 
|  |  | 
|  | // Make sure shortcuts are not re-migrated. | 
|  | EXPECT_EQ(0, | 
|  | MigrateShortcutsInPathInternal(chrome_exe_, temp_dir_.GetPath())); | 
|  | } | 
|  |  | 
|  | // Test that chrome_proxy.exe shortcuts (PWA) have their app_id migrated | 
|  | // to not include the default profile name. This tests both shortcuts in the | 
|  | // DIR_TASKBAR_PINS and sub-directories of DIR_IMPLICIT_APP_SHORTCUTS. | 
|  | TEST_F(ShellIntegrationWinMigrateShortcutTest, MigrateChromeProxyTest) { | 
|  | // Create shortcut to chrome_proxy_exe in executable directory, | 
|  | // using the default profile, with the AppModelId not containing the | 
|  | // profile name. | 
|  | base::win::ShortcutProperties temp_properties; | 
|  | temp_properties.set_target(web_app::GetChromeProxyPath()); | 
|  | temp_properties.set_app_id(L"Dumbo.Default"); | 
|  | ASSERT_NO_FATAL_FAILURE( | 
|  | AddTestShortcutAndResetProperties(temp_dir_.GetPath(), &temp_properties)); | 
|  | temp_properties.set_target(web_app::GetChromeProxyPath()); | 
|  | temp_properties.set_app_id(L"Dumbo2.Default"); | 
|  | ASSERT_NO_FATAL_FAILURE(AddTestShortcutAndResetProperties( | 
|  | temp_dir_sub_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Check that a chrome proxy shortcut whose app_id is just the extension app | 
|  | // id has its AUMI migrated to start with the browser's app_id. | 
|  | // It technically doesn't matter what ShortcutProperties's app_id is, | 
|  | // since the migration is based on ShortcutProperties.arguments. | 
|  | temp_properties.set_target(web_app::GetChromeProxyPath()); | 
|  | temp_properties.set_app_id(L"Dumbo3.Default"); | 
|  | base::CommandLine cmd_line = shell_integration::CommandLineArgsForLauncher( | 
|  | GURL(), base::WideToUTF8(extension_id_), base::FilePath(), ""); | 
|  | ASSERT_EQ(cmd_line.GetCommandLineString(), L" --app-id=" + extension_id_); | 
|  | temp_properties.set_arguments(cmd_line.GetCommandLineString()); | 
|  | ASSERT_NO_FATAL_FAILURE( | 
|  | AddTestShortcutAndResetProperties(temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | // Check that a chrome proxy shortcut with a kApp url in its command line | 
|  | // has its AUMI migrated to start with the browser's app_id. | 
|  | temp_properties.set_target(web_app::GetChromeProxyPath()); | 
|  | temp_properties.set_app_id(L"Dumbo4.Default"); | 
|  | GURL url("http://www.example.com"); | 
|  | cmd_line = shell_integration::CommandLineArgsForLauncher( | 
|  | url, std::string(), base::FilePath(), ""); | 
|  | ASSERT_EQ(cmd_line.GetCommandLineString(), L" --app=http://www.example.com/"); | 
|  | temp_properties.set_arguments(cmd_line.GetCommandLineString()); | 
|  | ASSERT_NO_FATAL_FAILURE( | 
|  | AddTestShortcutAndResetProperties(temp_dir_.GetPath(), &temp_properties)); | 
|  |  | 
|  | MigrateTaskbarPinsCallback(temp_dir_.GetPath(), temp_dir_.GetPath()); | 
|  | // Verify that the migrated shortcut in temp_dir_ does not contain the default | 
|  | // profile name. | 
|  | shortcuts_[0].properties.set_app_id(chrome_app_id_); | 
|  | base::win::ValidateShortcut(shortcuts_[0].path, shortcuts_[0].properties); | 
|  | // Verify that the migrated shortcut in temp_dir_sub does not contain the | 
|  | // default profile name. | 
|  | shortcuts_[1].properties.set_app_id(chrome_app_id_); | 
|  | base::win::ValidateShortcut(shortcuts_[1].path, shortcuts_[1].properties); | 
|  |  | 
|  | shortcuts_[2].properties.set_app_id(extension_app_id_); | 
|  | base::win::ValidateShortcut(shortcuts_[2].path, shortcuts_[2].properties); | 
|  |  | 
|  | shortcuts_[3].properties.set_app_id(GetAppUserModelIdForApp( | 
|  | base::UTF8ToWide(web_app::GenerateApplicationNameFromURL(url)), | 
|  | base::FilePath())); | 
|  | base::win::ValidateShortcut(shortcuts_[3].path, shortcuts_[3].properties); | 
|  | } | 
|  |  | 
|  | // This test verifies that MigrateTaskbarPins does a case-insensitive | 
|  | // comparison when comparing the shortcut target with the chrome exe path. | 
|  | TEST_F(ShellIntegrationWinMigrateShortcutTest, MigrateMixedCaseDirTest) { | 
|  | base::win::ShortcutProperties temp_properties; | 
|  | base::FilePath chrome_proxy_path(web_app::GetChromeProxyPath()); | 
|  | ASSERT_EQ(chrome_proxy_path.Extension(), FILE_PATH_LITERAL(".exe")); | 
|  | temp_properties.set_target( | 
|  | chrome_proxy_path.ReplaceExtension(FILE_PATH_LITERAL("EXE"))); | 
|  | temp_properties.set_app_id(L"Dumbo.Default"); | 
|  | ASSERT_NO_FATAL_FAILURE( | 
|  | AddTestShortcutAndResetProperties(temp_dir_.GetPath(), &temp_properties)); | 
|  | MigrateTaskbarPinsCallback(temp_dir_.GetPath(), temp_dir_.GetPath()); | 
|  | // Verify that the shortcut was migrated, i.e., its app_id does not contain | 
|  | // the default profile name. | 
|  | shortcuts_[0].properties.set_app_id(chrome_app_id_); | 
|  | base::win::ValidateShortcut(shortcuts_[0].path, shortcuts_[0].properties); | 
|  | } | 
|  |  | 
|  | TEST(ShellIntegrationWinTest, GetAppModelIdForProfileTest) { | 
|  | const std::wstring base_app_id(install_static::GetBaseAppId()); | 
|  |  | 
|  | // Empty profile path should get chrome::kBrowserAppID | 
|  | std::wstring app_name = L"app"; | 
|  | std::wstring expected_model_id_without_profile = | 
|  | base_app_id + L"." + app_name; | 
|  | base::FilePath empty_path; | 
|  | EXPECT_EQ(expected_model_id_without_profile, | 
|  | GetAppUserModelIdForApp(app_name, empty_path)); | 
|  |  | 
|  | // Default profile path should get chrome::kBrowserAppID | 
|  | base::FilePath default_user_data_dir; | 
|  | chrome::GetDefaultUserDataDirectory(&default_user_data_dir); | 
|  | base::FilePath default_profile_path = | 
|  | default_user_data_dir.AppendASCII(chrome::kInitialProfile); | 
|  | EXPECT_EQ(expected_model_id_without_profile, | 
|  | GetAppUserModelIdForApp(app_name, default_profile_path)); | 
|  |  | 
|  | // Non-default profile path should get chrome::kBrowserAppID joined with | 
|  | // profile info. | 
|  | base::FilePath profile_path(FILE_PATH_LITERAL("root")); | 
|  | profile_path = profile_path.Append(FILE_PATH_LITERAL("udd")); | 
|  | profile_path = profile_path.Append(FILE_PATH_LITERAL("User Data - Test")); | 
|  | EXPECT_EQ(expected_model_id_without_profile + L".udd.UserDataTest", | 
|  | GetAppUserModelIdForApp(app_name, profile_path)); | 
|  | } | 
|  |  | 
|  | }  // namespace win | 
|  | }  // namespace shell_integration |