diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 94dffdeb9b..80e8cfd4d4 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -45,7 +45,6 @@ file.reference.jericho-html-3.3.jar=release/modules/ext/jericho-html-3.3.jar file.reference.jgraphx-v3.8.0.jar=release/modules/ext/jgraphx-v3.8.0.jar file.reference.jhighlight-1.0.3.jar=release\\modules\\ext\\jhighlight-1.0.3.jar file.reference.jmatio-1.5.jar=release\\modules\\ext\\jmatio-1.5.jar -file.reference.jna-5.1.0.jar=release\\modules\\ext\\jna-5.1.0.jar file.reference.json-simple-1.1.1.jar=release\\modules\\ext\\json-simple-1.1.1.jar file.reference.jsoup-1.11.3.jar=release\\modules\\ext\\jsoup-1.11.3.jar file.reference.jul-to-slf4j-1.7.25.jar=release\\modules\\ext\\jul-to-slf4j-1.7.25.jar @@ -97,7 +96,6 @@ file.reference.xz-1.8.jar=release\\modules\\ext\\xz-1.8.jar file.reference.zookeeper-3.4.6.jar=release/modules/ext/zookeeper-3.4.6.jar file.reference.SparseBitSet-1.1.jar=release/modules/ext/SparseBitSet-1.1.jar file.reference.commons-validator-1.6.jar=release/modules/ext/commons-validator-1.6.jar -file.reference.jna-3.4.0.jar=release/modules/ext/jna-3.4.0.jar file.reference.api-common-1.7.0.jar=release/modules/ext/api-common-1.7.0.jar file.reference.gax-1.44.0.jar=release/modules/ext/gax-1.44.0.jar file.reference.gax-grpc-1.44.0.jar=release/modules/ext/gax-grpc-1.44.0.jar diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 182b92a661..02d37221a7 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -615,10 +615,6 @@ ext/commons-validator-1.6.jar release/modules/ext/commons-validator-1.6.jar - - ext/jna-5.1.0.jar - release\modules\ext\jna-5.1.0.jar - ext/jbig2-imageio-3.0.2.jar release\modules\ext\jbig2-imageio-3.0.2.jar diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbChoice.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbChoice.java new file mode 100644 index 0000000000..86f80e843a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbChoice.java @@ -0,0 +1,76 @@ +/* + * Central Repository + * + * Copyright 2015-2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +import org.openide.util.NbBundle.Messages; + +/** + * This represents a database choices available for central repo. + */ +@Messages({ + "CentralRepoDbChoice.Disabled.Text=Disabled", + "CentralRepoDbChoice.Sqlite.Text=SQLite", + "CentralRepoDbChoice.PostgreSQL_Multiuser.Text=PostgreSQL using multi-user settings", + "CentralRepoDbChoice.PostgreSQL.Text=Custom PostgreSQL", +}) +public enum CentralRepoDbChoice { + DISABLED("Disabled", Bundle.CentralRepoDbChoice_Disabled_Text(), CentralRepoPlatforms.DISABLED), + SQLITE("Sqlite", Bundle.CentralRepoDbChoice_Sqlite_Text(), CentralRepoPlatforms.SQLITE), + POSTGRESQL_MULTIUSER("PostgreSQL_Multiuser", Bundle.CentralRepoDbChoice_PostgreSQL_Multiuser_Text(), CentralRepoPlatforms.POSTGRESQL), + POSTGRESQL_CUSTOM("PostgreSQL", Bundle.CentralRepoDbChoice_PostgreSQL_Text(), CentralRepoPlatforms.POSTGRESQL); + + public static final CentralRepoDbChoice[] DB_CHOICES = new CentralRepoDbChoice[]{ + SQLITE, POSTGRESQL_MULTIUSER, POSTGRESQL_CUSTOM + }; + + + private final String settingKey; + private final String title; + private final CentralRepoPlatforms platform; + + CentralRepoDbChoice(String key, String title, CentralRepoPlatforms platform) { + this.settingKey = key; + this.title = title; + this.platform = platform; + } + + /** + * This is the value of this setting when saved to central repo properties. + * @return The value associated with this choice. + */ + public String getSettingKey() { + return settingKey; + } + + /** + * This is the human-readable title for this choice. + * @return The human-readable title for this choice. + */ + public String getTitle() { + return title; + } + + /** + * This represents the database type (i.e. Postgres, SQLite) associated with this choice. + * @return The database type associated with this choice. + */ + public CentralRepoPlatforms getDbPlatform() { + return platform; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbConnectivityManager.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbConnectivityManager.java new file mode 100755 index 0000000000..ade4094013 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbConnectivityManager.java @@ -0,0 +1,76 @@ +/* + * Central Repository + * + * Copyright 2015-2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +/** + * This class is a common interface for settings pertaining to the database in central repository. + */ +public interface CentralRepoDbConnectivityManager { + /** + * This method loads the current settings for this connection. + */ + void loadSettings(); + + /** + * This method saves the altered settings to disk. + */ + void saveSettings(); + + /** + * This method will create a central repository database if necessary. + * @return Whether or not the operation was successful. + */ + boolean createDatabase(); + + /** + * This method deletes a central repository database (used for deleting a corrupted database). + * @return Whether or not the operation was successful. + */ + boolean deleteDatabase(); + + /** + * This method uses the current settings and the validation query to test the connection + * to the database. + * + * @return True if successfull connection, else false. + */ + boolean verifyConnection(); + + /** + * This method checks to see if the database exists. + * + * @return True if exists, else false. + */ + boolean verifyDatabaseExists(); + + /** + * This method is uses the current settings and the schema version query to test the + * database schema. + * + * @return True if successful connection, else false. + */ + boolean verifyDatabaseSchema(); + + /** + * This method tests the connectivity status of this connection and returns the testing result. + * @return The result of testing the database connectivity status. + */ + DatabaseTestResult testStatus(); + +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java index d2647965a5..3fab7af47f 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java @@ -18,26 +18,172 @@ */ package org.sleuthkit.autopsy.centralrepository.datamodel; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; import java.io.File; import java.sql.SQLException; import java.util.logging.Level; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; /** - * Contains business logic for saving and validating settings for central repo + * This class contains business logic for saving and validating settings for central repository. */ public class CentralRepoDbManager { private static final Logger logger = Logger.getLogger(CentralRepoDbManager.class.getName()); private static final String CENTRAL_REPO_DB_NAME = "central_repository"; + private static final String CENTRAL_REPOSITORY_SETTINGS_KEY = "CentralRepository"; + private static final String DB_SELECTED_PLATFORM_KEY = "db.selectedPlatform"; + private static final String DISABLED_DUE_TO_FAILURE_KEY = "disabledDueToFailure"; + + private static volatile CentralRepoDbChoice savedChoice = null; + + private static final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(CentralRepoDbManager.class); + + private static final Object dbChoiceLock = new Object(); + private static final Object disabledDueToFailureLock = new Object(); + + + + /** + * This saves the currently selected database choice and clears any disabledDueToFailure flag. + * @param choice The choice to save. + * @return The newly saved choice. + */ + public static CentralRepoDbChoice saveDbChoice(CentralRepoDbChoice choice) { + return saveDbChoice(choice, true); + } + + /** + * This saves the currently selected database choice. + * @param choice The choice to save. + * @param clearDisabledDueToError Whether or not to clear the 'disabledDueToFailure' settings key. + * @return The newly saved choice. + */ + public static CentralRepoDbChoice saveDbChoice(CentralRepoDbChoice choice, boolean clearDisabledDueToError) { + synchronized(dbChoiceLock) { + // clear disabling due to a failure + if (clearDisabledDueToError) + setDisabledDueToFailure(false); + + // change the settings + CentralRepoDbChoice newChoice = (choice == null) ? CentralRepoDbChoice.DISABLED : choice; + CentralRepoDbChoice oldChoice = savedChoice; + savedChoice = newChoice; + ModuleSettings.setConfigSetting(CENTRAL_REPOSITORY_SETTINGS_KEY, DB_SELECTED_PLATFORM_KEY, newChoice.getSettingKey()); + propertyChangeSupport.firePropertyChange("savedChoice", oldChoice, newChoice); + return newChoice; + } + + } + + /** + * This method indicates whether or not 'PostgreSQL using multi-user settings' is a valid option. + * @return True if 'PostgreSQL using multi-user settings' is valid. + */ + public static boolean isPostgresMultiuserAllowed() { + // if multi user mode is not enabled, then this cannot be used + if (!UserPreferences.getIsMultiUserModeEnabled()) + return false; + + // also validate the connection as well + PostgresCentralRepoSettings multiUserSettings = + new PostgresCentralRepoSettings(PostgresSettingsLoader.MULTIUSER_SETTINGS_LOADER); + + return multiUserSettings.testStatus() == DatabaseTestResult.TESTED_OK; + } + + + /** + * This method loads the selectedPlatform boolean from the config file if it is set. + */ + public static CentralRepoDbChoice getSavedDbChoice() { + synchronized(dbChoiceLock) { + if (savedChoice == null) { + String selectedPlatformString = ModuleSettings.getConfigSetting(CENTRAL_REPOSITORY_SETTINGS_KEY, DB_SELECTED_PLATFORM_KEY); // NON-NLS + savedChoice = fromKey(selectedPlatformString); + } + + return savedChoice; + } + } + + /** + * This method disables the central repository and indicates through a flag that this was due to a failure during database setup. + * This is used when re-enabling multi-user as a flag to determine whether or not CR should be re-enabled. + */ + public static void disableDueToFailure() { + CentralRepoDbUtil.setUseCentralRepo(false); + setDisabledDueToFailure(true); + } + + /** + * This method sets whether or not the repository has been disabled due to a database setup issue; + * This is used when re-enabling multi-user as a flag to determine whether or not CR should be re-enabled. + * + * @param disabledDueToFailure Whether or not the repository has been disabled due to a database setup issue. + */ + private static void setDisabledDueToFailure(boolean disabledDueToFailure) { + synchronized(disabledDueToFailureLock) { + boolean oldValue = isDisabledDueToFailure(); + ModuleSettings.setConfigSetting(CENTRAL_REPOSITORY_SETTINGS_KEY, DISABLED_DUE_TO_FAILURE_KEY, Boolean.toString(disabledDueToFailure)); + propertyChangeSupport.firePropertyChange("disabledDueToFailure", oldValue, disabledDueToFailure); + } + } /** - * obtains the database connectivity for central repository + * This method retrieves setting whether or not the repository has been disabled due to a database setup issue; + * this is used when re-enabling multi-user as a flag to determine whether or not CR should be re-enabled. + * + * @return Whether or not the repository has been disabled due to a database setup issue. + */ + public static boolean isDisabledDueToFailure() { + synchronized(disabledDueToFailureLock) { + return Boolean.toString(true).equals(ModuleSettings.getConfigSetting(CENTRAL_REPOSITORY_SETTINGS_KEY, DISABLED_DUE_TO_FAILURE_KEY)); + } + } + + /** + * This method adds a property change listener. + * NOTE: currently only listening for changes in currently saved db choice and disabling due to failure. + * + * @param listener The listener for the event. + */ + public static void addPropertyChangeListener(PropertyChangeListener listener) { + propertyChangeSupport.addPropertyChangeListener(listener); + } + + /** + * This method removes a propert change listener. + * @param listener The listener to remove. + */ + public static void removePropertyChangeListener(PropertyChangeListener listener) { + propertyChangeSupport.removePropertyChangeListener(listener); + } + + + + private static CentralRepoDbChoice fromKey(String keyName) { + for (CentralRepoDbChoice dbChoice : CentralRepoDbChoice.values()) { + if (dbChoice.getSettingKey().equalsIgnoreCase(keyName)) { + return dbChoice; + } + } + + return CentralRepoDbChoice.DISABLED; + } + + + + /** + * This method obtains the database connectivity for central repository. * - * @return the CentralRepository object to connect to + * @return The CentralRepository object that will be used for connection. * @throws CentralRepoException */ private static CentralRepository obtainCentralRepository() throws CentralRepoException { @@ -55,10 +201,10 @@ public class CentralRepoDbManager { } /** - * obtains central repository lock + * This method obtains a central repository lock. * - * @param db the database connection - * @return the lock if acquired + * @param db The database connection. + * @return The lock if acquired. * @throws CentralRepoException */ private static CoordinationService.Lock obtainCentralRepoLock(CentralRepository db) throws CentralRepoException { @@ -79,10 +225,10 @@ public class CentralRepoDbManager { } /** - * updates central repository schema if necessary + * This method updates the central repository schema if necessary. * - * @param db the database connectivity - * @param lock the acquired lock + * @param db The database connectivity object. + * @param lock The acquired lock. * @throws CentralRepoException */ private static void updatedDbSchema(CentralRepository db, CoordinationService.Lock lock) throws CentralRepoException { @@ -111,7 +257,7 @@ public class CentralRepoDbManager { } /** - * Upgrade the current Central Reposity schema to the newest version. If the + * This method upgrades the current Central Reposity schema to the newest version. If the * upgrade fails, the Central Repository will be disabled and the current * settings will be cleared. */ @@ -142,8 +288,7 @@ public class CentralRepoDbManager { } catch (CentralRepoException ex2) { logger.log(Level.SEVERE, "Error shutting down central repo connection pool", ex2); } - CentralRepoPlatforms.setSelectedPlatform(CentralRepoPlatforms.DISABLED.name()); - CentralRepoPlatforms.saveSelectedPlatform(); + saveDbChoice(CentralRepoDbChoice.DISABLED, false); if (innerException == null) { throw new CentralRepoException(message, desc); } else { @@ -151,41 +296,56 @@ public class CentralRepoDbManager { } } + + private DatabaseTestResult testingStatus; - private CentralRepoPlatforms selectedPlatform; + private CentralRepoDbChoice selectedDbChoice; private final PostgresCentralRepoSettings dbSettingsPostgres; + private final PostgresCentralRepoSettings dbSettingsMultiUser; private final SqliteCentralRepoSettings dbSettingsSqlite; private boolean configurationChanged = false; public CentralRepoDbManager() { - dbSettingsPostgres = new PostgresCentralRepoSettings(); + selectedDbChoice = getSavedDbChoice(); + dbSettingsPostgres = new PostgresCentralRepoSettings(PostgresSettingsLoader.CUSTOM_SETTINGS_LOADER); + dbSettingsMultiUser = new PostgresCentralRepoSettings(PostgresSettingsLoader.MULTIUSER_SETTINGS_LOADER); dbSettingsSqlite = new SqliteCentralRepoSettings(); - selectedPlatform = CentralRepoPlatforms.getSelectedPlatform(); - - // set the default selected platform for displaying in the ui of EamDbSettingsDialog - // if selected option is not applicable - if (selectedPlatform == null || selectedPlatform.equals(CentralRepoPlatforms.DISABLED)) { - selectedPlatform = CentralRepoPlatforms.POSTGRESQL; - } } + + /** + * This method retrieves the current multi-user database settings. + * @return The current multi-user database settings. + */ + public PostgresCentralRepoSettings getDbSettingsMultiUser() { + return dbSettingsMultiUser; + } + + /** + * This method retrieves the current custom postgres database settings. + * @return The current custom postgres database settings. + */ public PostgresCentralRepoSettings getDbSettingsPostgres() { return dbSettingsPostgres; } + /** + * This method returns the current SQLite database settings for central repository. + * @return The current SQLite database settings + */ public SqliteCentralRepoSettings getDbSettingsSqlite() { return dbSettingsSqlite; } /** - * setup sqlite db with default settings - * @throws CentralRepoException if unable to successfully set up database + * This method sets up the sqlite database with default settings. + * @throws CentralRepoException if unable to successfully set up database. */ public void setupDefaultSqliteDb() throws CentralRepoException { // change in-memory settings to default sqlite - selectedPlatform = CentralRepoPlatforms.SQLITE; + selectedDbChoice = CentralRepoDbChoice.SQLITE; dbSettingsSqlite.setupDefaultSettings(); // if db is not present, attempt to create it @@ -196,7 +356,7 @@ public class CentralRepoDbManager { } // the only successful setup status is tested ok - if (curStatus != DatabaseTestResult.TESTEDOK) { + if (curStatus != DatabaseTestResult.TESTED_OK) { throw new CentralRepoException("Unable to successfully create sqlite database"); } @@ -206,48 +366,55 @@ public class CentralRepoDbManager { } /** - * Returns if changes to the central repository configuration were - * successfully applied + * This method returns if changes to the central repository configuration were + * successfully applied. * - * @return true if the database configuration was successfully changed false - * if it was not + * @return Returns true if the database configuration was successfully changed false + * if it was not. */ public boolean wasConfigurationChanged() { return configurationChanged; } - private CentralRepoDbSettings getSelectedSettings() throws CentralRepoException { - switch (selectedPlatform) { - case POSTGRESQL: - return dbSettingsPostgres; - case SQLITE: - return dbSettingsSqlite; - case DISABLED: - return null; - default: - throw new CentralRepoException("Unknown database type: " + selectedPlatform); - } + private CentralRepoDbConnectivityManager getSelectedSettings() throws CentralRepoException { + if (selectedDbChoice == CentralRepoDbChoice.POSTGRESQL_MULTIUSER) + return dbSettingsMultiUser; + if (selectedDbChoice == CentralRepoDbChoice.POSTGRESQL_CUSTOM) + return dbSettingsPostgres; + if (selectedDbChoice == CentralRepoDbChoice.SQLITE) + return dbSettingsSqlite; + if (selectedDbChoice == CentralRepoDbChoice.DISABLED) + return null; + + throw new CentralRepoException("Unknown database type: " + selectedDbChoice); } private RdbmsCentralRepoFactory getDbFactory() throws CentralRepoException { - switch (selectedPlatform) { - case POSTGRESQL: - return new RdbmsCentralRepoFactory(selectedPlatform, dbSettingsPostgres); - case SQLITE: - return new RdbmsCentralRepoFactory(selectedPlatform, dbSettingsSqlite); - case DISABLED: - return null; - default: - throw new CentralRepoException("Unknown database type: " + selectedPlatform); - } + if (selectedDbChoice == CentralRepoDbChoice.POSTGRESQL_MULTIUSER) + return new RdbmsCentralRepoFactory(CentralRepoPlatforms.POSTGRESQL, dbSettingsMultiUser); + if (selectedDbChoice == CentralRepoDbChoice.POSTGRESQL_CUSTOM) + return new RdbmsCentralRepoFactory(CentralRepoPlatforms.POSTGRESQL, dbSettingsPostgres); + if (selectedDbChoice == CentralRepoDbChoice.SQLITE) + return new RdbmsCentralRepoFactory(CentralRepoPlatforms.SQLITE, dbSettingsSqlite); + if (selectedDbChoice == CentralRepoDbChoice.DISABLED) + return null; + + throw new CentralRepoException("Unknown database type: " + selectedDbChoice); } + /** + * This method creates a central repo database if it does not already exist. + * @return True if successful; false if unsuccessful. + * @throws CentralRepoException + */ public boolean createDb() throws CentralRepoException { + CentralRepoDbConnectivityManager selectedDbSettings = getSelectedSettings(); + if (selectedDbSettings == null) + throw new CentralRepoException("Unable to derive connectivity manager from settings: " + selectedDbChoice); + boolean result = false; boolean dbCreated = true; - - CentralRepoDbSettings selectedDbSettings = getSelectedSettings(); - + if (!selectedDbSettings.verifyDatabaseExists()) { dbCreated = selectedDbSettings.createDatabase(); } @@ -265,7 +432,6 @@ public class CentralRepoDbManager { if (!result) { // Remove the incomplete database if (dbCreated) { - // RAMAN TBD: migrate deleteDatabase() to RdbmsCentralRepoFactory selectedDbSettings.deleteDatabase(); } @@ -274,12 +440,12 @@ public class CentralRepoDbManager { throw new CentralRepoException(schemaError); } - testingStatus = DatabaseTestResult.TESTEDOK; + testingStatus = DatabaseTestResult.TESTED_OK; return true; } /** - * saves a new central repository based on current settings + * This method saves a new central repository based on current settings. */ @NbBundle.Messages({"CentralRepoDbManager.connectionErrorMsg.text=Failed to connect to central repository database."}) public void saveNewCentralRepo() throws CentralRepoException { @@ -305,18 +471,18 @@ public class CentralRepoDbManager { // Even if we fail to close the existing connections, make sure that we // save the new connection settings, so an Autopsy restart will correctly // start with the new settings. - CentralRepoPlatforms.setSelectedPlatform(selectedPlatform.name()); - CentralRepoPlatforms.saveSelectedPlatform(); + CentralRepoDbUtil.setUseCentralRepo(selectedDbChoice != CentralRepoDbChoice.DISABLED); + saveDbChoice(selectedDbChoice); - CentralRepoDbSettings selectedDbSettings = getSelectedSettings(); + CentralRepoDbConnectivityManager selectedDbSettings = getSelectedSettings(); // save the new settings selectedDbSettings.saveSettings(); // Load those newly saved settings into the postgres db manager instance // in case we are still using the same instance. - if (selectedPlatform == CentralRepoPlatforms.POSTGRESQL || selectedPlatform == CentralRepoPlatforms.SQLITE) { + if (selectedDbChoice != null && selectedDbChoice != CentralRepoDbChoice.DISABLED) { try { - logger.info("Creating central repo db with settings: " + selectedDbSettings); + logger.info("Saving central repo settings for db: " + selectedDbSettings); CentralRepository.getInstance().updateSettings(); configurationChanged = true; } catch (CentralRepoException ex) { @@ -326,82 +492,82 @@ public class CentralRepoDbManager { } } + /** + * This method retrieves the current status. + * Note: this could be a dirty value if testing of the connection has not been performed. + * @return The current status of the database connection. + */ public DatabaseTestResult getStatus() { return testingStatus; } - public CentralRepoPlatforms getSelectedPlatform() { - return selectedPlatform; + /** + * This method retrieves the currently selected database choice. + * NOTE: This choice may not align with the saved setting. + * @return The currently selected database choice. + */ + public CentralRepoDbChoice getSelectedDbChoice() { + return selectedDbChoice; } + /** + * This method clears the current database testing status. + */ public void clearStatus() { testingStatus = DatabaseTestResult.UNTESTED; } - public void setSelectedPlatform(CentralRepoPlatforms newSelected) { - selectedPlatform = newSelected; + /** + * This method sets the currently selected database choice and sets the testing status to untested. + * @param newSelected The new database choice. + */ + public void setSelctedDbChoice(CentralRepoDbChoice newSelected) { + selectedDbChoice = newSelected; testingStatus = DatabaseTestResult.UNTESTED; } /** - * Tests whether or not the database settings are valid. + * This method tests whether or not the settings have been filled in for the UI. + * NOTE: This does not check the connectivity status of these settings. * - * @return True or false. + * @return True if database settings are valid. */ public boolean testDatabaseSettingsAreValid( String tbDbHostname, String tbDbPort, String tbDbUsername, String tfDatabasePath, String jpDbPassword) throws CentralRepoException, NumberFormatException { - switch (selectedPlatform) { - case POSTGRESQL: - dbSettingsPostgres.setHost(tbDbHostname); - dbSettingsPostgres.setPort(Integer.parseInt(tbDbPort)); - dbSettingsPostgres.setDbName(CENTRAL_REPO_DB_NAME); - dbSettingsPostgres.setUserName(tbDbUsername); - dbSettingsPostgres.setPassword(jpDbPassword); - break; - case SQLITE: - File databasePath = new File(tfDatabasePath); - dbSettingsSqlite.setDbName(SqliteCentralRepoSettings.DEFAULT_DBNAME); - dbSettingsSqlite.setDbDirectory(databasePath.getPath()); - break; - default: - throw new IllegalStateException("Central Repo has an unknown selected platform: " + selectedPlatform); + if (selectedDbChoice == CentralRepoDbChoice.POSTGRESQL_CUSTOM) { + dbSettingsPostgres.setHost(tbDbHostname); + dbSettingsPostgres.setPort(Integer.parseInt(tbDbPort)); + dbSettingsPostgres.setDbName(CENTRAL_REPO_DB_NAME); + dbSettingsPostgres.setUserName(tbDbUsername); + dbSettingsPostgres.setPassword(jpDbPassword); + } + else if (selectedDbChoice == CentralRepoDbChoice.SQLITE) { + File databasePath = new File(tfDatabasePath); + dbSettingsSqlite.setDbName(SqliteCentralRepoSettings.DEFAULT_DBNAME); + dbSettingsSqlite.setDbDirectory(databasePath.getPath()); + } + else if (selectedDbChoice != CentralRepoDbChoice.POSTGRESQL_MULTIUSER) { + throw new IllegalStateException("Central Repo has an unknown selected platform: " + selectedDbChoice); } return true; } + /** + * This method tests the current database settings to see if a valid connection can be made. + * @return The result of testing the connection. + */ public DatabaseTestResult testStatus() { - if (selectedPlatform == CentralRepoPlatforms.POSTGRESQL) { - if (dbSettingsPostgres.verifyConnection()) { - if (dbSettingsPostgres.verifyDatabaseExists()) { - if (dbSettingsPostgres.verifyDatabaseSchema()) { - testingStatus = DatabaseTestResult.TESTEDOK; - } else { - testingStatus = DatabaseTestResult.SCHEMA_INVALID; - } - } else { - testingStatus = DatabaseTestResult.DB_DOES_NOT_EXIST; - } - } else { - testingStatus = DatabaseTestResult.CONNECTION_FAILED; - } - } else if (selectedPlatform == CentralRepoPlatforms.SQLITE) { - if (dbSettingsSqlite.dbFileExists()) { - if (dbSettingsSqlite.verifyConnection()) { - if (dbSettingsSqlite.verifyDatabaseSchema()) { - testingStatus = DatabaseTestResult.TESTEDOK; - } else { - testingStatus = DatabaseTestResult.SCHEMA_INVALID; - } - } else { - testingStatus = DatabaseTestResult.SCHEMA_INVALID; - } - } else { - testingStatus = DatabaseTestResult.DB_DOES_NOT_EXIST; - } + try { + CentralRepoDbConnectivityManager manager = getSelectedSettings(); + if (manager != null) + testingStatus = manager.testStatus(); } - + catch (CentralRepoException e) { + logger.log(Level.WARNING, "unable to test status of db connection in central repo", e); + } + return testingStatus; } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbSettings.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbSettings.java deleted file mode 100755 index f9bf520654..0000000000 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbSettings.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Central Repository - * - * Copyright 2015-2020 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.centralrepository.datamodel; - -/** - * common interface for settings pertaining to the database in central repository - */ -public interface CentralRepoDbSettings { - - void saveSettings(); - - boolean createDatabase(); - - boolean deleteDatabase(); - - /** - * Use the current settings and the validation query to test the connection - * to the database. - * - * @return true if successfull connection, else false. - */ - boolean verifyConnection(); - - /** - * Check to see if the database exists. - * - * @return true if exists, else false - */ - boolean verifyDatabaseExists(); - - /** - * Use the current settings and the schema version query to test the - * database schema. - * - * @return true if successful connection, else false. - */ - boolean verifyDatabaseSchema(); - -} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUpgrader.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUpgrader.java new file mode 100644 index 0000000000..ffd0d2936e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUpgrader.java @@ -0,0 +1,41 @@ +/* + * Central Repository + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +import java.sql.Connection; +import java.sql.SQLException; +import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber; + +/** + * Common interface to upgrade central repository database schema. + */ +public interface CentralRepoDbUpgrader { + + /** + * Updates the Central Repository schema using the given open connection. + * + * @param dbSchemaVersion Current schema version. + * @param connection Connection to use for upgrade. + * + * @throws CentralRepoException If there is an error in upgrade. + * @throws SQLException If there is any SQL errors. + */ + void upgradeSchema(CaseDbSchemaVersionNumber dbSchemaVersion, Connection connection) throws CentralRepoException, SQLException; + +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUpgrader13To14.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUpgrader13To14.java new file mode 100644 index 0000000000..0acfd34ea5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUpgrader13To14.java @@ -0,0 +1,114 @@ +/* + * Central Repository + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber; + + +/** + * This class updates CR schema to 1.4 + * + * New correlation types for accounts are added, as well as some accounts related new tables are added in this version. + * + */ +public class CentralRepoDbUpgrader13To14 implements CentralRepoDbUpgrader { + + @Override + public void upgradeSchema(CaseDbSchemaVersionNumber dbSchemaVersion, Connection connection) throws CentralRepoException, SQLException { + + if (dbSchemaVersion.compareTo(new CaseDbSchemaVersionNumber(1, 4)) < 0) { + + try (Statement statement = connection.createStatement();) { + + CentralRepoPlatforms selectedPlatform = CentralRepoDbManager.getSavedDbChoice().getDbPlatform(); + + // Create account_types and accounts tables which are referred by X_instances tables + statement.execute(RdbmsCentralRepoFactory.getCreateAccountTypesTableStatement(selectedPlatform)); + statement.execute(RdbmsCentralRepoFactory.getCreateAccountsTableStatement(selectedPlatform)); + + for (CorrelationAttributeInstance.Type type : CorrelationAttributeInstance.getDefaultCorrelationTypes()) { + String instance_type_dbname = CentralRepoDbUtil.correlationTypeToInstanceTableName(type); + + if (type.getId() >= CorrelationAttributeInstance.ADDITIONAL_TYPES_BASE_ID) { + + // these are new Correlation types - new tables need to be created + statement.execute(String.format(RdbmsCentralRepoFactory.getCreateAccountInstancesTableTemplate(selectedPlatform), instance_type_dbname, instance_type_dbname)); + statement.execute(String.format(RdbmsCentralRepoFactory.getAddCaseIdIndexTemplate(), instance_type_dbname, instance_type_dbname)); + statement.execute(String.format(RdbmsCentralRepoFactory.getAddDataSourceIdIndexTemplate(), instance_type_dbname, instance_type_dbname)); + statement.execute(String.format(RdbmsCentralRepoFactory.getAddValueIndexTemplate(), instance_type_dbname, instance_type_dbname)); + statement.execute(String.format(RdbmsCentralRepoFactory.getAddKnownStatusIndexTemplate(), instance_type_dbname, instance_type_dbname)); + statement.execute(String.format(RdbmsCentralRepoFactory.getAddObjectIdIndexTemplate(), instance_type_dbname, instance_type_dbname)); + + // add new correlation type + CentralRepoDbUtil.insertCorrelationType(connection, type); + + } else if (type.getId() == CorrelationAttributeInstance.EMAIL_TYPE_ID || type.getId() == CorrelationAttributeInstance.PHONE_TYPE_ID) { + // Alter the existing _instance tables for Phone and Email attributes to add account_id column + String sqlStr = String.format(getAlterArtifactInstancesAddAccountIdTemplate(selectedPlatform), instance_type_dbname); + statement.execute(sqlStr); + + // SQLite does NOT allow adding a constraint with Alter Table statement. + // The alternative would be to create new tables, copy all data over, and delete old tables - potentially a time consuming process. + // We decided to not add this constraint for SQLite, since there likely aren't many users using SQLite based Central Repo. + if (selectedPlatform == CentralRepoPlatforms.POSTGRESQL) { + sqlStr = String.format(getAlterArtifactInstancesAddAccountIdConstraintTemplate(), instance_type_dbname); + statement.execute(sqlStr); + } + } + } + + // insert default accounts data + RdbmsCentralRepoFactory.insertDefaultAccountsTablesContent(connection, selectedPlatform); + } + } + + } + + /** + * Returns ALTER TABLE SQL string template to add an account_id column to a + * TYPE_instances table. + * + * @param selectedPlatform + * + * @return SQL string template to alter the table. + */ + static String getAlterArtifactInstancesAddAccountIdTemplate(CentralRepoPlatforms selectedPlatform) { + // Each "%s" will be replaced with the relevant TYPE_instances table name. + return "ALTER TABLE %s" + + " ADD account_id " + RdbmsCentralRepoFactory.getBigIntType(selectedPlatform) + " DEFAULT NULL"; + + } + + /** + * Returns ALTER TABLE SQL string template to add a Foreign Key constraint + * to a TYPE_instances table. + * + * @return SQL string template to alter the table. + */ + static String getAlterArtifactInstancesAddAccountIdConstraintTemplate() { + // Each "%s" will be replaced with the relevant TYPE_instances table name. + return "ALTER TABLE %s" + + " ADD CONSTRAINT account_id_fk foreign key (account_id) references accounts(id)"; + } + + +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUtil.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUtil.java index 60701b44e5..69ea8d4e05 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUtil.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUtil.java @@ -123,6 +123,28 @@ public class CentralRepoDbUtil { return true; } + /** + * Inserts the specified correlation type into the database. + * + * @param conn Open connection to use. + * @param correlationType New correlation type to add. + * + */ + public static void insertCorrelationType(Connection conn, CorrelationAttributeInstance.Type correlationType) throws SQLException { + + String sql = "INSERT INTO correlation_types(id, display_name, db_table_name, supported, enabled) VALUES (?, ?, ?, ?, ?)"; + try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { + + preparedStatement.setInt(1, correlationType.getId()); + preparedStatement.setString(2, correlationType.getDisplayName()); + preparedStatement.setString(3, correlationType.getDbTableName()); + preparedStatement.setInt(4, correlationType.isSupported() ? 1 : 0); + preparedStatement.setInt(5, correlationType.isEnabled() ? 1 : 0); + + preparedStatement.execute(); + } + } + /** * Writes the current schema version into the database. * @@ -303,4 +325,17 @@ public class CentralRepoDbUtil { closeStatement(preparedStatement); } + /** + * Checks if the given correlation attribute type has an account behind it. + * + * @param type Correlation type to check. + * + * @return True If the specified correlation type has an account. + */ + static boolean correlationAttribHasAnAccount(CorrelationAttributeInstance.Type type) { + return (type.getId() >= CorrelationAttributeInstance.ADDITIONAL_TYPES_BASE_ID) + || type.getId() == CorrelationAttributeInstance.PHONE_TYPE_ID + || type.getId() == CorrelationAttributeInstance.EMAIL_TYPE_ID; + } + } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPlatforms.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPlatforms.java index bcdf009ca3..b830e98d16 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPlatforms.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPlatforms.java @@ -18,102 +18,11 @@ */ package org.sleuthkit.autopsy.centralrepository.datamodel; -import org.sleuthkit.autopsy.coreutils.ModuleSettings; - /** - * + * This enum describes the possible database types for central repo. */ public enum CentralRepoPlatforms { - DISABLED("Disabled", true), - SQLITE("SQLite", false), - POSTGRESQL("PostgreSQL", false); - - private final String platformName; - private Boolean selected; - - CentralRepoPlatforms(String name, Boolean selected) { - this.platformName = name; - this.selected = selected; - loadSettings(); - } - - /** - * Load the selectedPlatform boolean from the config file, if it is set. - */ - private void loadSettings() { - String selectedPlatformString = ModuleSettings.getConfigSetting("CentralRepository", "db.selectedPlatform"); // NON-NLS - - if (null != selectedPlatformString) { - selected = this.toString().equalsIgnoreCase(selectedPlatformString); - } else if (this == DISABLED) { - selected = true; - } - } - - @Override - public String toString() { - return platformName; - } - - private void setSelected(Boolean selected) { - this.selected = selected; - } - - public Boolean isSelected() { - return selected; - } - - public static CentralRepoPlatforms fromString(String pName) { - if (null == pName) { - return DISABLED; - } - - for (CentralRepoPlatforms p : CentralRepoPlatforms.values()) { - if (p.toString().equalsIgnoreCase(pName)) { - return p; - } - } - return DISABLED; - } - - /** - * Save the selected platform to the config file. - */ - public static void saveSelectedPlatform() { - CentralRepoPlatforms selectedPlatform = DISABLED; - for (CentralRepoPlatforms p : CentralRepoPlatforms.values()) { - if (p.isSelected()) { - selectedPlatform = p; - } - } - ModuleSettings.setConfigSetting("CentralRepository", "db.selectedPlatform", selectedPlatform.name()); // NON-NLS - } - - /** - * Set the selected db platform. Other platforms will be set as not - * selected. - * - * @param platformString The name of the selected platform. - */ - public static void setSelectedPlatform(String platformString) { - CentralRepoPlatforms pSelected = CentralRepoPlatforms.fromString(platformString); - for (CentralRepoPlatforms p : CentralRepoPlatforms.values()) { - p.setSelected(p == pSelected); - } - } - - /** - * Get the selected platform. - * - * @return The selected platform, or if not platform is selected, default to - * DISABLED. - */ - public static CentralRepoPlatforms getSelectedPlatform() { - for (CentralRepoPlatforms p : CentralRepoPlatforms.values()) { - if (p.isSelected()) { - return p; - } - } - return DISABLED; - } + DISABLED, + SQLITE, + POSTGRESQL } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPostgresSettingsUtil.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPostgresSettingsUtil.java new file mode 100644 index 0000000000..86f929ba0e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPostgresSettingsUtil.java @@ -0,0 +1,185 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2012-2020 Basis Technology Corp. + * + * Copyright 2012 42six Solutions. + * Contact: aebadirad 42six com + * Project Contact/Architect: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; +import org.sleuthkit.autopsy.coreutils.TextConverter; +import org.sleuthkit.autopsy.coreutils.TextConverterException; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; + +/** + * This class handles saving and loading of postgres settings for central repository. + */ +public class CentralRepoPostgresSettingsUtil { + private final static Logger LOGGER = Logger.getLogger(CentralRepoPostgresSettingsUtil.class.getName()); + + private static final String PASSWORD_KEY = "db.postgresql.password"; + private static final String BULK_THRESHOLD_KEY = "db.postgresql.bulkThreshold"; + private static final String PORT_KEY = "db.postgresql.port"; + private static final String USER_KEY = "db.postgresql.user"; + private static final String DBNAME_KEY = "db.postgresql.dbName"; + private static final String HOST_KEY = "db.postgresql.host"; + + private static final String MODULE_KEY = "CentralRepository"; + + private static CentralRepoPostgresSettingsUtil instance = null; + + /** + * This method retrieves a singleton instance of this class. + * @return The singleton instance of this class. + */ + public static synchronized CentralRepoPostgresSettingsUtil getInstance() { + if (instance == null) + instance = new CentralRepoPostgresSettingsUtil(); + + return instance; + } + + private CentralRepoPostgresSettingsUtil() {} + + /** + * Uses setter object to set a value as specified by 'value'. In the event that 'value' + * is null, the setter will not be called. Exceptions that are raised from the setter will + * be logged. + * + * @param setter The setter to call. + * @param value The value to use with the setter. + */ + private void setValOrLog(ValueSetter setter, String value) { + // ignore null values as they indicate a setting that is not set yet + if (value == null || value.isEmpty()) + return; + + try { + setter.set(value); + } + catch (CentralRepoException | NumberFormatException e) { + LOGGER.log(Level.WARNING, "There was an error in converting central repo postgres settings", e); + } + } + + /** + * This interface represents a setter that potentially throws an exception. + */ + private interface ValueSetter { + void set(String value) throws CentralRepoException, NumberFormatException; + } + + /** + * This method loads multi-user settings to be used as a postgres connection to central repository. If + * settings could not be loaded, default values will be returned. + * + * @return The settings loaded from multi-user settings. + */ + public PostgresConnectionSettings loadMultiUserSettings() { + PostgresConnectionSettings settings = new PostgresConnectionSettings(); + + CaseDbConnectionInfo muConn; + try { + muConn = UserPreferences.getDatabaseConnectionInfo(); + } catch (UserPreferencesException ex) { + LOGGER.log(Level.SEVERE, "Failed to import settings from multi-user settings.", ex); + return settings; + } + + setValOrLog((v) -> settings.setHost(v), muConn.getHost()); + setValOrLog((v) -> settings.setUserName(v), muConn.getUserName()); + setValOrLog((v) -> settings.setPassword(v), muConn.getPassword()); + + setValOrLog((v) -> settings.setPort(Integer.parseInt(v)), muConn.getPort()); + + return settings; + } + + + /** + * This method loads the custom postgres settings for central repository. If + * settings could not be loaded, default values will be returned. + * + * @return The settings loaded from custom postgres settings. + */ + public PostgresConnectionSettings loadCustomSettings() { + PostgresConnectionSettings settings = new PostgresConnectionSettings(); + Map keyVals = ModuleSettings.getConfigSettings(MODULE_KEY); + + + setValOrLog((v) -> settings.setHost(v), keyVals.get(HOST_KEY)); + setValOrLog((v) -> settings.setDbName(v), keyVals.get(DBNAME_KEY)); + setValOrLog((v) -> settings.setUserName(v), keyVals.get(USER_KEY)); + + setValOrLog((v) -> settings.setPort(Integer.parseInt(v)), keyVals.get(PORT_KEY)); + setValOrLog((v) -> settings.setBulkThreshold(Integer.parseInt(v)), keyVals.get((BULK_THRESHOLD_KEY))); + + String passwordHex = keyVals.get(PASSWORD_KEY); + if (passwordHex != null) { + String password; + try { + password = TextConverter.convertHexTextToText(passwordHex); + } catch (TextConverterException ex) { + LOGGER.log(Level.WARNING, "Failed to convert password from hex text to text.", ex); + password = null; + } + + final String finalPassword = password; + setValOrLog((v) -> settings.setPassword(v), finalPassword); + } + + return settings; + } + + /** + * This method saves the settings for a custom postgres central repository connection. + * @param settings The settings to save. + */ + public void saveCustomSettings(PostgresConnectionSettings settings) { + Map map = new HashMap(); + map.put(HOST_KEY, settings.getHost()); + map.put(PORT_KEY, Integer.toString(settings.getPort())); + map.put(DBNAME_KEY, settings.getDbName()); + map.put(BULK_THRESHOLD_KEY, Integer.toString(settings.getBulkThreshold())); + map.put(USER_KEY, settings.getUserName()); + try { + map.put(PASSWORD_KEY, TextConverter.convertTextToHexText(settings.getPassword())); // NON-NLS + } catch (TextConverterException ex) { + LOGGER.log(Level.SEVERE, "Failed to convert password from text to hex text.", ex); + } + + ModuleSettings.setConfigSettings(MODULE_KEY, map); + } + + /** + * This method checks if saved settings differ from the in-memory object provided in the 'settings' parameter. + * @param settings The in-memory object. + * @return Whether or not settings parameter differs from saved custom settings. + */ + public boolean areCustomSettingsChanged(PostgresConnectionSettings settings) { + PostgresConnectionSettings saved = loadCustomSettings(); + return saved.equals(settings); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepository.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepository.java index 904712c73c..5b81dc6b3b 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepository.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepository.java @@ -42,7 +42,7 @@ public interface CentralRepository { CentralRepoPlatforms selectedPlatform = CentralRepoPlatforms.DISABLED; if (CentralRepoDbUtil.allowUseOfCentralRepository()) { - selectedPlatform = CentralRepoPlatforms.getSelectedPlatform(); + selectedPlatform = CentralRepoDbManager.getSavedDbChoice().getDbPlatform(); } switch (selectedPlatform) { case POSTGRESQL: @@ -93,7 +93,7 @@ public interface CentralRepository { */ static boolean isEnabled() { return CentralRepoDbUtil.allowUseOfCentralRepository() - && CentralRepoPlatforms.getSelectedPlatform() != CentralRepoPlatforms.DISABLED; + && CentralRepoDbManager.getSavedDbChoice() != CentralRepoDbChoice.DISABLED; } /** diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java index 1ab978bc84..4d5e2857a1 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java @@ -294,8 +294,9 @@ public class CorrelationAttributeInstance implements Serializable { // Create Correlation Types for Accounts. int correlationTypeId = ADDITIONAL_TYPES_BASE_ID; for (Account.Type type : Account.Type.PREDEFINED_ACCOUNT_TYPES) { + // Skip Device account type - we dont want to correlate on those. // Skip Phone and Email accounts as there are already Correlation types defined for those. - if (type != Account.Type.EMAIL && type != Account.Type.PHONE) { + if (type != Account.Type.DEVICE && type != Account.Type.EMAIL && type != Account.Type.PHONE) { defaultCorrelationTypes.add(new CorrelationAttributeInstance.Type(correlationTypeId, type.getDisplayName(), type.getTypeName().toLowerCase() + "_acct", true, true)); //NON-NLS correlationTypeId++; } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java index 04aa6fc16e..ed1f0a0f3d 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java @@ -27,6 +27,7 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoAccount.CentralRepoAccountType; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.BlackboardAttribute; @@ -84,23 +85,12 @@ public class CorrelationAttributeUtil { BlackboardArtifact sourceArtifact = getCorrAttrSourceArtifact(artifact); if (sourceArtifact != null) { int artifactTypeID = sourceArtifact.getArtifactTypeID(); - if (artifactTypeID == ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) { - BlackboardAttribute setNameAttr = sourceArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)); - if (setNameAttr != null && CorrelationAttributeUtil.getEmailAddressAttrDisplayName().equals(setNameAttr.getValueString())) { - makeCorrAttrFromArtifactAttr(correlationAttrs, sourceArtifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD, CorrelationAttributeInstance.EMAIL_TYPE_ID); - } - - } else if (artifactTypeID == ARTIFACT_TYPE.TSK_WEB_BOOKMARK.getTypeID() + if (artifactTypeID == ARTIFACT_TYPE.TSK_WEB_BOOKMARK.getTypeID() || artifactTypeID == ARTIFACT_TYPE.TSK_WEB_COOKIE.getTypeID() || artifactTypeID == ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID() || artifactTypeID == ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID()) { makeCorrAttrFromArtifactAttr(correlationAttrs, sourceArtifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN, CorrelationAttributeInstance.DOMAIN_TYPE_ID); - } else if (artifactTypeID == ARTIFACT_TYPE.TSK_CONTACT.getTypeID() - || artifactTypeID == ARTIFACT_TYPE.TSK_CALLLOG.getTypeID() - || artifactTypeID == ARTIFACT_TYPE.TSK_MESSAGE.getTypeID()) { - makeCorrAttrFromArtifactPhoneAttr(sourceArtifact); - } else if (artifactTypeID == ARTIFACT_TYPE.TSK_DEVICE_ATTACHED.getTypeID()) { makeCorrAttrFromArtifactAttr(correlationAttrs, sourceArtifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_ID, CorrelationAttributeInstance.USBID_TYPE_ID); makeCorrAttrFromArtifactAttr(correlationAttrs, sourceArtifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_MAC_ADDRESS, CorrelationAttributeInstance.MAC_TYPE_ID); @@ -169,58 +159,6 @@ public class CorrelationAttributeUtil { return sourceArtifact; } - /** - * Makes a correlation attribute instance from a phone number attribute of an - * artifact. - * - * @param artifact An artifact with a phone number attribute. - * - * @return The correlation instance artifact or null, if the phone number is - * not a valid correlation attribute. - * - * @throws TskCoreException If there is an error querying the case - * database. - * @throws CentralRepoException If there is an error querying the central - * repository. - */ - private static CorrelationAttributeInstance makeCorrAttrFromArtifactPhoneAttr(BlackboardArtifact artifact) throws TskCoreException, CentralRepoException { - CorrelationAttributeInstance corrAttr = null; - - /* - * Extract the phone number from the artifact attribute. - */ - String value = null; - if (null != artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER))) { - value = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER)).getValueString(); - } else if (null != artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM))) { - value = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM)).getValueString(); - } else if (null != artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO))) { - value = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO)).getValueString(); - } - - /* - * Normalize the phone number. - */ - if (value != null) { - String newValue = value.replaceAll("\\D", ""); - if (value.startsWith("+")) { - newValue = "+" + newValue; - } - value = newValue; - - /* - * Validate the phone number. Three to five digit phone numbers may - * be valid, but they are too short to use as correlation - * attributes. - */ - if (value.length() > 5) { - corrAttr = makeCorrAttr(artifact, CentralRepository.getInstance().getCorrelationTypeById(CorrelationAttributeInstance.PHONE_TYPE_ID), value); - } - } - - return corrAttr; - } - /** * Makes a correlation attribute instance for an account artifact. * @@ -247,25 +185,29 @@ public class CorrelationAttributeUtil { // Get the account type from the artifact BlackboardAttribute accountTypeAttribute = acctArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE)); String accountTypeStr = accountTypeAttribute.getValueString(); + + // do not create any correlation attribute instance for a Device account + if (Account.Type.DEVICE.getTypeName().equalsIgnoreCase(accountTypeStr) == false) { - // Get the corresponding CentralRepoAccountType from the database. - CentralRepoAccountType crAccountType = CentralRepository.getInstance().getAccountTypeByName(accountTypeStr); + // Get the corresponding CentralRepoAccountType from the database. + CentralRepoAccountType crAccountType = CentralRepository.getInstance().getAccountTypeByName(accountTypeStr); - int corrTypeId = crAccountType.getCorrelationTypeId(); - CorrelationAttributeInstance.Type corrType = CentralRepository.getInstance().getCorrelationTypeById(corrTypeId); - - // Get the account identifier - BlackboardAttribute accountIdAttribute = acctArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ID)); - String accountIdStr = accountIdAttribute.getValueString(); - - // add/get the account and get its accountId. - CentralRepoAccount crAccount = CentralRepository.getInstance().getOrCreateAccount(crAccountType, accountIdStr); - - CorrelationAttributeInstance corrAttr = makeCorrAttr(acctArtifact, corrType, accountIdStr); - if (corrAttr != null) { - // set the account_id in correlation attribute - corrAttr.setAccountId(crAccount.getAccountId()); - corrAttrInstances.add(corrAttr); + int corrTypeId = crAccountType.getCorrelationTypeId(); + CorrelationAttributeInstance.Type corrType = CentralRepository.getInstance().getCorrelationTypeById(corrTypeId); + + // Get the account identifier + BlackboardAttribute accountIdAttribute = acctArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ID)); + String accountIdStr = accountIdAttribute.getValueString(); + + // add/get the account and get its accountId. + CentralRepoAccount crAccount = CentralRepository.getInstance().getOrCreateAccount(crAccountType, accountIdStr); + + CorrelationAttributeInstance corrAttr = makeCorrAttr(acctArtifact, corrType, accountIdStr); + if (corrAttr != null) { + // set the account_id in correlation attribute + corrAttr.setAccountId(crAccount.getAccountId()); + corrAttrInstances.add(corrAttr); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/DatabaseTestResult.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/DatabaseTestResult.java index 27a7b16278..ecd72341cf 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/DatabaseTestResult.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/DatabaseTestResult.java @@ -19,13 +19,13 @@ package org.sleuthkit.autopsy.centralrepository.datamodel; /** - * provides the status of the database after attempting to validate central repo settings + * This enum provides the status of the database after attempting to validate central repo settings. */ public enum DatabaseTestResult { UNTESTED, CONNECTION_FAILED, SCHEMA_INVALID, DB_DOES_NOT_EXIST, - TESTEDOK; + TESTED_OK; } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java index 476b67dbc2..165d4ca6e0 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java @@ -24,15 +24,9 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.util.List; import java.util.Properties; import java.util.logging.Level; -import java.util.regex.Pattern; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.ModuleSettings; -import org.sleuthkit.autopsy.coreutils.TextConverter; -import org.sleuthkit.autopsy.coreutils.TextConverterException; -import static org.sleuthkit.autopsy.centralrepository.datamodel.RdbmsCentralRepo.SOFTWARE_CR_DB_SCHEMA_VERSION; /** * Settings for the Postgres implementation of the Central Repository database @@ -40,106 +34,84 @@ import static org.sleuthkit.autopsy.centralrepository.datamodel.RdbmsCentralRepo * NOTE: This is public scope because the options panel calls it directly to * set/get */ -public final class PostgresCentralRepoSettings implements CentralRepoDbSettings { +public final class PostgresCentralRepoSettings implements CentralRepoDbConnectivityManager { private final static Logger LOGGER = Logger.getLogger(PostgresCentralRepoSettings.class.getName()); - private final static String DEFAULT_HOST = ""; // NON-NLS - private final static int DEFAULT_PORT = 5432; - private final static String DEFAULT_DBNAME = "central_repository"; // NON-NLS - private final static String DEFAULT_USERNAME = ""; - private final static String DEFAULT_PASSWORD = ""; private final static String VALIDATION_QUERY = "SELECT version()"; // NON-NLS private final static String JDBC_BASE_URI = "jdbc:postgresql://"; // NON-NLS private final static String JDBC_DRIVER = "org.postgresql.Driver"; // NON-NLS - private final static String DB_NAMES_REGEX = "[a-z][a-z0-9_]*"; // only lower case - private final static String DB_USER_NAMES_REGEX = "[a-zA-Z]\\w*"; - private String host; - private int port; - private String dbName; - private int bulkThreshold; - private String userName; - private String password; - - public PostgresCentralRepoSettings() { + + + private final PostgresSettingsLoader loader; + private PostgresConnectionSettings connSettings; + + private static PostgresSettingsLoader getLoaderFromSaved() throws CentralRepoException { + CentralRepoDbChoice choice = CentralRepoDbManager.getSavedDbChoice(); + if (choice == CentralRepoDbChoice.POSTGRESQL_CUSTOM) + return PostgresSettingsLoader.CUSTOM_SETTINGS_LOADER; + else if (choice == CentralRepoDbChoice.POSTGRESQL_MULTIUSER) + return PostgresSettingsLoader.MULTIUSER_SETTINGS_LOADER; + else + throw new CentralRepoException("cannot load or save postgres settings for selection: " + choice); + } + + /** + * This method loads the settings with a custom {@link PostgresSettingsLoader PostgresSettingsLoader} object. + * @param loader The loader to be used. + */ + public PostgresCentralRepoSettings(PostgresSettingsLoader loader) { + this.loader = loader; loadSettings(); } + + /** + * This is the default constructor that loads settings from selected db choice. + */ + public PostgresCentralRepoSettings() throws CentralRepoException { + this(getLoaderFromSaved()); + } + + @Override + public void loadSettings() { + this.connSettings = loader.loadSettings(); + } + + @Override + public void saveSettings() { + loader.saveSettings(connSettings); + } + + @Override public String toString() { return String.format("PostgresCentralRepoSettings: [db type: postgres, host: %s:%d, db name: %s, username: %s]", getHost(), getPort(), getDbName(), getUserName()); } - public void loadSettings() { - host = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.host"); // NON-NLS - if (host == null || host.isEmpty()) { - host = DEFAULT_HOST; - } - - try { - String portString = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.port"); // NON-NLS - if (portString == null || portString.isEmpty()) { - port = DEFAULT_PORT; - } else { - port = Integer.parseInt(portString); - if (port < 0 || port > 65535) { - port = DEFAULT_PORT; - } - } - } catch (NumberFormatException ex) { - port = DEFAULT_PORT; - } - - dbName = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.dbName"); // NON-NLS - if (dbName == null || dbName.isEmpty()) { - dbName = DEFAULT_DBNAME; - } - - try { - String bulkThresholdString = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.bulkThreshold"); // NON-NLS - if (bulkThresholdString == null || bulkThresholdString.isEmpty()) { - this.bulkThreshold = RdbmsCentralRepo.DEFAULT_BULK_THRESHHOLD; - } else { - this.bulkThreshold = Integer.parseInt(bulkThresholdString); - if (getBulkThreshold() <= 0) { - this.bulkThreshold = RdbmsCentralRepo.DEFAULT_BULK_THRESHHOLD; - } - } - } catch (NumberFormatException ex) { - this.bulkThreshold = RdbmsCentralRepo.DEFAULT_BULK_THRESHHOLD; - } - - userName = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.user"); // NON-NLS - if (userName == null || userName.isEmpty()) { - userName = DEFAULT_USERNAME; - } - - password = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.password"); // NON-NLS - if (password == null || password.isEmpty()) { - password = DEFAULT_PASSWORD; - } else { - try { - password = TextConverter.convertHexTextToText(password); - } catch (TextConverterException ex) { - LOGGER.log(Level.WARNING, "Failed to convert password from hex text to text.", ex); - password = DEFAULT_PASSWORD; - } - } + + /** + * @return the VALIDATION_QUERY + */ + String getValidationQuery() { + return VALIDATION_QUERY; } - public void saveSettings() { - ModuleSettings.setConfigSetting("CentralRepository", "db.postgresql.host", getHost()); // NON-NLS - ModuleSettings.setConfigSetting("CentralRepository", "db.postgresql.port", Integer.toString(port)); // NON-NLS - ModuleSettings.setConfigSetting("CentralRepository", "db.postgresql.dbName", getDbName()); // NON-NLS - ModuleSettings.setConfigSetting("CentralRepository", "db.postgresql.bulkThreshold", Integer.toString(getBulkThreshold())); // NON-NLS - ModuleSettings.setConfigSetting("CentralRepository", "db.postgresql.user", getUserName()); // NON-NLS - try { - ModuleSettings.setConfigSetting("CentralRepository", "db.postgresql.password", TextConverter.convertTextToHexText(getPassword())); // NON-NLS - } catch (TextConverterException ex) { - LOGGER.log(Level.SEVERE, "Failed to convert password from text to hex text.", ex); - } + /** + * @return the POSTGRES_DRIVER + */ + String getDriver() { + return JDBC_DRIVER; } + /** + * @return the JDBC_BASE_URI + */ + String getJDBCBaseURI() { + return JDBC_BASE_URI; + } + + /** * Get the full connection URL as a String * @@ -301,73 +273,33 @@ public final class PostgresCentralRepoSettings implements CentralRepoDbSettings } - - - - - - - - - - - - - - - - - - - - - boolean isChanged() { - String hostString = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.host"); // NON-NLS - String portString = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.port"); // NON-NLS - String dbNameString = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.dbName"); // NON-NLS - String bulkThresholdString = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.bulkThreshold"); // NON-NLS - String userNameString = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.user"); // NON-NLS - String userPasswordString = ModuleSettings.getConfigSetting("CentralRepository", "db.postgresql.password"); // NON-NLS - - return !host.equals(hostString) || !Integer.toString(port).equals(portString) - || !dbName.equals(dbNameString) || !Integer.toString(bulkThreshold).equals(bulkThresholdString) - || !userName.equals(userNameString) || !password.equals(userPasswordString); - } /** * @return the host */ public String getHost() { - return host; + return connSettings.getHost(); } /** * @param host the host to set */ public void setHost(String host) throws CentralRepoException { - if (null != host && !host.isEmpty()) { - this.host = host; - } else { - throw new CentralRepoException("Invalid host name. Cannot be empty."); // NON-NLS - } + connSettings.setHost(host); } /** * @return the port */ public int getPort() { - return port; + return connSettings.getPort(); } /** * @param port the port to set */ public void setPort(int port) throws CentralRepoException { - if (port > 0 && port < 65535) { - this.port = port; - } else { - throw new CentralRepoException("Invalid port. Must be a number greater than 0."); // NON-NLS - } + connSettings.setPort(port); } /** @@ -377,95 +309,72 @@ public final class PostgresCentralRepoSettings implements CentralRepoDbSettings * @return the dbName */ public String getDbName() { - return dbName.toLowerCase(); + return connSettings.getDbName() == null ? null : connSettings.getDbName().toLowerCase(); } /** * @param dbName the dbName to set */ public void setDbName(String dbName) throws CentralRepoException { - if (dbName == null || dbName.isEmpty()) { - throw new CentralRepoException("Invalid database name. Cannot be empty."); // NON-NLS - } else if (!Pattern.matches(DB_NAMES_REGEX, dbName)) { - throw new CentralRepoException("Invalid database name. Name must start with a lowercase letter and can only contain lowercase letters, numbers, and '_'."); // NON-NLS - } - - this.dbName = dbName.toLowerCase(); + connSettings.setDbName(dbName); } /** * @return the bulkThreshold */ int getBulkThreshold() { - return bulkThreshold; + return connSettings.getBulkThreshold(); } /** * @param bulkThreshold the bulkThreshold to set */ public void setBulkThreshold(int bulkThreshold) throws CentralRepoException { - if (bulkThreshold > 0) { - this.bulkThreshold = bulkThreshold; - } else { - throw new CentralRepoException("Invalid bulk threshold."); // NON-NLS - } + connSettings.setBulkThreshold(bulkThreshold); } /** * @return the userName */ public String getUserName() { - return userName; + return connSettings.getUserName(); } /** * @param userName the userName to set */ public void setUserName(String userName) throws CentralRepoException { - if (userName == null || userName.isEmpty()) { - throw new CentralRepoException("Invalid user name. Cannot be empty."); // NON-NLS - } else if (!Pattern.matches(DB_USER_NAMES_REGEX, userName)) { - throw new CentralRepoException("Invalid user name. Name must start with a letter and can only contain letters, numbers, and '_'."); // NON-NLS - } - this.userName = userName; + connSettings.setUserName(userName); } /** * @return the password */ public String getPassword() { - return password; + return connSettings.getPassword(); } /** * @param password the password to set */ public void setPassword(String password) throws CentralRepoException { - if (password == null || password.isEmpty()) { - throw new CentralRepoException("Invalid user password. Cannot be empty."); // NON-NLS + connSettings.setPassword(password); + } + + @Override + public DatabaseTestResult testStatus() { + if (verifyConnection()) { + if (verifyDatabaseExists()) { + if (verifyDatabaseSchema()) { + return DatabaseTestResult.TESTED_OK; + } else { + return DatabaseTestResult.SCHEMA_INVALID; + } + } else { + return DatabaseTestResult.DB_DOES_NOT_EXIST; + } + } else { + return DatabaseTestResult.CONNECTION_FAILED; } - this.password = password; } - - /** - * @return the VALIDATION_QUERY - */ - String getValidationQuery() { - return VALIDATION_QUERY; - } - - /** - * @return the POSTGRES_DRIVER - */ - String getDriver() { - return JDBC_DRIVER; - } - - /** - * @return the JDBC_BASE_URI - */ - String getJDBCBaseURI() { - return JDBC_BASE_URI; - } - } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresConnectionSettings.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresConnectionSettings.java new file mode 100644 index 0000000000..c835d0c8fc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresConnectionSettings.java @@ -0,0 +1,222 @@ +/* + * Central Repository + * + * Copyright 2015-2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * + * This class is a POJO for postgres settings to be used with central repository. + */ +public class PostgresConnectionSettings { + private final static String DB_NAMES_REGEX = "[a-z][a-z0-9_]*"; // only lower case + private final static String DB_USER_NAMES_REGEX = "[a-zA-Z]\\w*"; + + public final static String DEFAULT_HOST = ""; // NON-NLS + public final static int DEFAULT_PORT = 5432; + public final static String DEFAULT_DBNAME = "central_repository"; // NON-NLS + public final static String DEFAULT_USERNAME = ""; + public final static String DEFAULT_PASSWORD = ""; + + private static void validateStr(String s, String errMessage) throws CentralRepoException { + if (null == s || s.isEmpty()) + throw new CentralRepoException(errMessage); + } + + private static void validateRegex(String s, String pattern, String errMessage) throws CentralRepoException { + if (!Pattern.matches(pattern, s)) + throw new CentralRepoException(errMessage); + } + + private static void validateNum(int num, Integer min, Integer max, String errMessage) throws CentralRepoException { + if ((min != null && num < min) || (max != null && num > max)) + throw new CentralRepoException(errMessage); + } + + private String host = DEFAULT_HOST; + private int port = DEFAULT_PORT; + private String dbName = DEFAULT_DBNAME; + private int bulkThreshold = RdbmsCentralRepo.DEFAULT_BULK_THRESHHOLD; + private String userName = DEFAULT_USERNAME; + private String password = DEFAULT_PASSWORD; + + /** + * This method retrieves the postgres host. + * @return The host for these settings. + */ + public String getHost() { + return host; + } + + /** + * This method returns the port number for these settings. + * @return The port number for these settings. + */ + public int getPort() { + return port; + } + + /** + * This method returns the database name for these settings. + * @return The database name for these settings. + */ + public String getDbName() { + return dbName; + } + + /** + * This method returns the bulk threshold. + * @return The bulk threshold. + */ + public int getBulkThreshold() { + return bulkThreshold; + } + + /** + * This method returns the username to use for this connection. + * @return The username to use. + */ + public String getUserName() { + return userName; + } + + /** + * This method returns the password to use for this connection. + * @return The password to use for this connection. + */ + public String getPassword() { + return password; + } + + + /** + * This method sets the host for this connection. + * NOTE: must be non-empty string. + * @param host the host to set + */ + public void setHost(String host) throws CentralRepoException { + validateStr(host, "Invalid host name. Cannot be empty."); + this.host = host; + } + + + /** + * This method sets the port for this connection. + * @param port The port to set (must be [1,65535]). + */ + public void setPort(int port) throws CentralRepoException { + validateNum(port, 1, 65535, "Invalid port. Must be a number greater than 0."); + this.port = port; + } + + + /** + * This methods sets the name of the database. + * NOTE: this name needs to be a valid postgres database name. + * @param dbName The database name. + */ + public void setDbName(String dbName) throws CentralRepoException { + validateStr(dbName, "Invalid database name. Cannot be empty."); // NON-NLS + validateRegex(dbName, DB_NAMES_REGEX, + "Invalid database name. Name must start with a lowercase letter and can only contain lowercase letters, numbers, and '_'."); // NON-NLS + + this.dbName = dbName.toLowerCase(); + } + + + /** + * This method sets the bulk threshold of this connection. + * @param bulkThreshold The bulk threshold to set (must be greater than 0). + */ + public void setBulkThreshold(int bulkThreshold) throws CentralRepoException { + validateNum(bulkThreshold, 1, null, "Invalid bulk threshold."); + this.bulkThreshold = bulkThreshold; + } + + + /** + * This method sets the username for this connection. + * NOTE: must be a valid postgres username. + * @param userName The user name to set. + */ + public void setUserName(String userName) throws CentralRepoException { + validateStr(userName, "Invalid user name. Cannot be empty."); // NON-NLS + validateRegex(userName, DB_USER_NAMES_REGEX, + "Invalid user name. Name must start with a letter and can only contain letters, numbers, and '_'."); + + this.userName = userName; + } + + + /** + * This method sets the password for this connection. + * @param password The password to set. + */ + public void setPassword(String password) throws CentralRepoException { + validateStr(password, "Invalid user password. Cannot be empty."); + this.password = password; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 43 * hash + Objects.hashCode(this.host); + hash = 43 * hash + this.port; + hash = 43 * hash + Objects.hashCode(this.dbName); + hash = 43 * hash + this.bulkThreshold; + hash = 43 * hash + Objects.hashCode(this.userName); + hash = 43 * hash + Objects.hashCode(this.password); + return hash; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final PostgresConnectionSettings other = (PostgresConnectionSettings) obj; + if (this.port != other.port) { + return false; + } + if (this.bulkThreshold != other.bulkThreshold) { + return false; + } + if (!Objects.equals(this.host, other.host)) { + return false; + } + if (!Objects.equals(this.dbName, other.dbName)) { + return false; + } + if (!Objects.equals(this.userName, other.userName)) { + return false; + } + if (!Objects.equals(this.password, other.password)) { + return false; + } + return true; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresSettingsLoader.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresSettingsLoader.java new file mode 100644 index 0000000000..b3514d0ccb --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresSettingsLoader.java @@ -0,0 +1,77 @@ +/* + * Central Repository + * + * Copyright 2015-2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +/** + * This is an interface to load or save postgres settings. + */ +public interface PostgresSettingsLoader { + /** + * This method loads the current settings. + * @return The settings that were loaded. + */ + PostgresConnectionSettings loadSettings(); + + /** + * This method saves the current settings. + * @param settings The settings to save. + */ + void saveSettings(PostgresConnectionSettings settings); + + PostgresSettingsLoader CUSTOM_SETTINGS_LOADER = new Custom(); + PostgresSettingsLoader MULTIUSER_SETTINGS_LOADER = new MultiUser(); + CentralRepoPostgresSettingsUtil SETTINGS_UTIL = CentralRepoPostgresSettingsUtil.getInstance(); + + + /** + * This class loads and saves custom postgres settings. + */ + class Custom implements PostgresSettingsLoader { + @Override + public PostgresConnectionSettings loadSettings() { + return SETTINGS_UTIL.loadCustomSettings(); + } + + @Override + public void saveSettings(PostgresConnectionSettings settings) { + SETTINGS_UTIL.saveCustomSettings(settings); + } + } + + + /** + * This class loads multi-user postgres settings to be used with central repo. + * NOTE: This class does not save settings on save operation as this is merely a proxy. + */ + class MultiUser implements PostgresSettingsLoader { + + @Override + public PostgresConnectionSettings loadSettings() { + return SETTINGS_UTIL.loadMultiUserSettings(); + } + + /** + * NOTE: This action does not do anything. There is no need to save since + * this is just a proxy to multi user settings. + * @param settings The settings to save. + */ + @Override + public void saveSettings(PostgresConnectionSettings settings) {} + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepo.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepo.java index 93f6268979..148513f40c 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepo.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepo.java @@ -65,7 +65,7 @@ abstract class RdbmsCentralRepo implements CentralRepository { static final String SCHEMA_MINOR_VERSION_KEY = "SCHEMA_MINOR_VERSION"; static final String CREATION_SCHEMA_MAJOR_VERSION_KEY = "CREATION_SCHEMA_MAJOR_VERSION"; static final String CREATION_SCHEMA_MINOR_VERSION_KEY = "CREATION_SCHEMA_MINOR_VERSION"; - static final CaseDbSchemaVersionNumber SOFTWARE_CR_DB_SCHEMA_VERSION = new CaseDbSchemaVersionNumber(1, 3); + static final CaseDbSchemaVersionNumber SOFTWARE_CR_DB_SCHEMA_VERSION = new CaseDbSchemaVersionNumber(1, 4); protected final List defaultCorrelationTypes; @@ -1002,18 +1002,26 @@ abstract class RdbmsCentralRepo implements CentralRepository { public void addArtifactInstance(CorrelationAttributeInstance eamArtifact) throws CentralRepoException { checkAddArtifactInstanceNulls(eamArtifact); - - - - // @@@ We should cache the case and data source IDs in memory String tableName = CentralRepoDbUtil.correlationTypeToInstanceTableName(eamArtifact.getCorrelationType()); - String sql - = "INSERT INTO " + boolean artifactHasAnAccount = CentralRepoDbUtil.correlationAttribHasAnAccount(eamArtifact.getCorrelationType()); + + String sql; + // _instance table for accounts have an additional account_id column + if (artifactHasAnAccount) { + sql = "INSERT INTO " + tableName + "(case_id, data_source_id, value, file_path, known_status, comment, file_obj_id, account_id) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?) " + getConflictClause(); + } + else { + sql = "INSERT INTO " + + tableName + + "(case_id, data_source_id, value, file_path, known_status, comment, file_obj_id) " + + "VALUES (?, ?, ?, ?, ?, ?, ?) " + + getConflictClause(); + } try (Connection conn = connect(); PreparedStatement preparedStatement = conn.prepareStatement(sql);) { @@ -1032,10 +1040,13 @@ abstract class RdbmsCentralRepo implements CentralRepository { } preparedStatement.setLong(7, eamArtifact.getFileObjectId()); - if (eamArtifact.getAccountId() >= 0) { - preparedStatement.setLong(8, eamArtifact.getAccountId()); - } else { - preparedStatement.setNull(8, Types.INTEGER); + // set in the accountId only for artifacts that represent accounts + if (artifactHasAnAccount) { + if (eamArtifact.getAccountId() >= 0) { + preparedStatement.setLong(8, eamArtifact.getAccountId()); + } else { + preparedStatement.setNull(8, Types.INTEGER); + } } preparedStatement.executeUpdate(); @@ -3553,7 +3564,7 @@ abstract class RdbmsCentralRepo implements CentralRepository { conn = connect(false); conn.setAutoCommit(false); statement = conn.createStatement(); - selectedPlatform = CentralRepoPlatforms.getSelectedPlatform(); + selectedPlatform = CentralRepoDbManager.getSavedDbChoice().getDbPlatform(); int minorVersion = 0; String minorVersionStr = null; resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='" + RdbmsCentralRepo.SCHEMA_MINOR_VERSION_KEY + "'"); @@ -3808,6 +3819,10 @@ abstract class RdbmsCentralRepo implements CentralRepository { throw new CentralRepoException("Currently selected database platform \"" + selectedPlatform.name() + "\" can not be upgraded.", Bundle.AbstractSqlEamDb_cannotUpgrage_message(selectedPlatform.name())); } } + + // Upgrade to 1.4 + (new CentralRepoDbUpgrader13To14()).upgradeSchema(dbSchemaVersion, conn); + updateSchemaVersion(conn); conn.commit(); logger.log(Level.INFO, String.format("Central Repository schema updated to version %s", SOFTWARE_CR_DB_SCHEMA_VERSION)); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepoFactory.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepoFactory.java index 66227ea366..f76c13cd63 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepoFactory.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepoFactory.java @@ -83,6 +83,7 @@ public class RdbmsCentralRepoFactory { public boolean initializeDatabaseSchema() { String createArtifactInstancesTableTemplate = getCreateArtifactInstancesTableTemplate(selectedPlatform); + String createAccountInstancesTableTemplate = getCreateAccountInstancesTableTemplate(selectedPlatform); String instancesCaseIdIdx = getAddCaseIdIndexTemplate(); String instancesDatasourceIdIdx = getAddDataSourceIdIndexTemplate(); @@ -147,7 +148,13 @@ public class RdbmsCentralRepoFactory { reference_type_dbname = CentralRepoDbUtil.correlationTypeToReferenceTableName(type); instance_type_dbname = CentralRepoDbUtil.correlationTypeToInstanceTableName(type); - stmt.execute(String.format(createArtifactInstancesTableTemplate, instance_type_dbname, instance_type_dbname)); + // use the correct create table template, based on whether the attribute type represents an account or not. + String createTableTemplate = (CentralRepoDbUtil.correlationAttribHasAnAccount(type)) + ? createAccountInstancesTableTemplate + : createArtifactInstancesTableTemplate; + + stmt.execute(String.format(createTableTemplate, instance_type_dbname, instance_type_dbname)); + stmt.execute(String.format(instancesCaseIdIdx, instance_type_dbname, instance_type_dbname)); stmt.execute(String.format(instancesDatasourceIdIdx, instance_type_dbname, instance_type_dbname)); stmt.execute(String.format(instancesValueIdx, instance_type_dbname, instance_type_dbname)); @@ -193,7 +200,7 @@ public class RdbmsCentralRepoFactory { result = CentralRepoDbUtil.insertDefaultCorrelationTypes(conn) && CentralRepoDbUtil.insertDefaultOrganization(conn) && - insertDefaultAccountsTablesContent(conn); + RdbmsCentralRepoFactory.insertDefaultAccountsTablesContent(conn, selectedPlatform ); // @TODO: uncomment when ready to create/populate persona tables // && insertDefaultPersonaTablesContent(conn); @@ -357,7 +364,7 @@ public class RdbmsCentralRepoFactory { + ")"; } /** - * Get the template String for creating a new _instances table in a Sqlite + * Get the template String for creating a new _instances table for non account artifacts in * central repository. %s will exist in the template where the name of the * new table will be added. * @@ -365,6 +372,31 @@ public class RdbmsCentralRepoFactory { */ static String getCreateArtifactInstancesTableTemplate(CentralRepoPlatforms selectedPlatform) { // Each "%s" will be replaced with the relevant TYPE_instances table name. + + return "CREATE TABLE IF NOT EXISTS %s (" + + getNumericPrimaryKeyClause("id", selectedPlatform) + + "case_id integer NOT NULL," + + "data_source_id integer NOT NULL," + + "value text NOT NULL," + + "file_path text NOT NULL," + + "known_status integer NOT NULL," + + "comment text," + + "file_obj_id " + getBigIntType(selectedPlatform) + " ," + + "CONSTRAINT %s_multi_unique UNIQUE(data_source_id, value, file_path)" + getOnConflictIgnoreClause(selectedPlatform) + "," + + "foreign key (case_id) references cases(id) ON UPDATE SET NULL ON DELETE SET NULL," + + "foreign key (data_source_id) references data_sources(id) ON UPDATE SET NULL ON DELETE SET NULL)"; + } + + /** + * Get the template String for creating a new _instances table for Accounts in + * central repository. %s will exist in the template where the name of the + * new table will be added. + * + * @return a String which is a template for creating a _instances table + */ + static String getCreateAccountInstancesTableTemplate(CentralRepoPlatforms selectedPlatform) { + // Each "%s" will be replaced with the relevant TYPE_instances table name. + return "CREATE TABLE IF NOT EXISTS %s (" + getNumericPrimaryKeyClause("id", selectedPlatform) + "case_id integer NOT NULL," @@ -380,7 +412,7 @@ public class RdbmsCentralRepoFactory { + "foreign key (case_id) references cases(id) ON UPDATE SET NULL ON DELETE SET NULL," + "foreign key (data_source_id) references data_sources(id) ON UPDATE SET NULL ON DELETE SET NULL)"; } - + /** * Get the statement String for creating a new data_sources table in a * Sqlite central repository. @@ -532,7 +564,7 @@ public class RdbmsCentralRepoFactory { * * @return SQL clause. */ - private static String getBigIntType(CentralRepoPlatforms selectedPlatform) { + static String getBigIntType(CentralRepoPlatforms selectedPlatform) { switch (selectedPlatform) { case POSTGRESQL: return " BIGINT "; @@ -766,33 +798,6 @@ public class RdbmsCentralRepoFactory { } - /** - * Inserts the default content in accounts related tables. - * - * @param conn Database connection to use. - * - * @return True if success, false otherwise. - */ - private boolean insertDefaultAccountsTablesContent(Connection conn) { - - try (Statement stmt = conn.createStatement()) { - // Populate the account_types table - for (Account.Type type : Account.Type.PREDEFINED_ACCOUNT_TYPES) { - int correlationTypeId = getCorrelationTypeIdForAccountType(conn, type); - if (correlationTypeId > 0) { - String sqlString = String.format("INSERT INTO account_types (type_name, display_name, correlation_type_id) VALUES ('%s', '%s', %d)" + getOnConflictDoNothingClause(selectedPlatform), - type.getTypeName(), type.getDisplayName(), correlationTypeId); - stmt.execute(sqlString); - } - } - } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, String.format("Failed to populate default data in Accounts tables."), ex); - return false; - } - - return true; - } - /** * Inserts the default content in persona related tables. * @@ -800,7 +805,7 @@ public class RdbmsCentralRepoFactory { * * @return True if success, false otherwise. */ - private boolean insertDefaultPersonaTablesContent(Connection conn) { + private static boolean insertDefaultPersonaTablesContent(Connection conn, CentralRepoPlatforms selectedPlatform) { try (Statement stmt = conn.createStatement()) { // populate the confidence table @@ -825,6 +830,37 @@ public class RdbmsCentralRepoFactory { return true; } + /** + * Inserts the default content in accounts related tables. + * + * @param conn Database connection to use. + * + * @return True if success, false otherwise. + */ + static boolean insertDefaultAccountsTablesContent(Connection conn, CentralRepoPlatforms selectedPlatform) { + + try (Statement stmt = conn.createStatement();) { + + // Populate the account_types table + for (Account.Type type : Account.Type.PREDEFINED_ACCOUNT_TYPES) { + if (type != Account.Type.DEVICE) { + int correlationTypeId = getCorrelationTypeIdForAccountType(conn, type); + if (correlationTypeId > 0) { + String sqlString = String.format("INSERT INTO account_types (type_name, display_name, correlation_type_id) VALUES ('%s', '%s', %d)" + getOnConflictDoNothingClause(selectedPlatform), + type.getTypeName(), type.getDisplayName(), correlationTypeId); + stmt.execute(sqlString); + } + } + } + + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, String.format("Failed to populate default data in account_types table."), ex); + return false; + } + + return true; + } + /** * Returns the correlation type id for the given account type, * from the correlation_types table. @@ -834,7 +870,7 @@ public class RdbmsCentralRepoFactory { * ' * @return correlation type id. */ - private int getCorrelationTypeIdForAccountType(Connection conn, Account.Type accountType) { + static int getCorrelationTypeIdForAccountType(Connection conn, Account.Type accountType) { int typeId = -1; if (accountType == Account.Type.EMAIL) { diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepoSettings.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepoSettings.java index 675a164972..a938dd166a 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepoSettings.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepoSettings.java @@ -37,7 +37,7 @@ import org.sleuthkit.autopsy.coreutils.PlatformUtil; * NOTE: This is public scope because the options panel calls it directly to * set/get */ -public final class SqliteCentralRepoSettings implements CentralRepoDbSettings { +public final class SqliteCentralRepoSettings implements CentralRepoDbConnectivityManager { public final static String DEFAULT_DBNAME = "central_repository.db"; // NON-NLS private final static Logger LOGGER = Logger.getLogger(SqliteCentralRepoSettings.class.getName()); @@ -45,7 +45,7 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbSettings { private final static String JDBC_DRIVER = "org.sqlite.JDBC"; // NON-NLS private final static String JDBC_BASE_URI = "jdbc:sqlite:"; // NON-NLS private final static String VALIDATION_QUERY = "SELECT count(*) from sqlite_master"; // NON-NLS - + private final static String DB_NAMES_REGEX = "[a-z][a-z0-9_]*(\\.db)?"; private String dbName; private String dbDirectory; @@ -80,11 +80,11 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbSettings { this.bulkThreshold = RdbmsCentralRepo.DEFAULT_BULK_THRESHHOLD; } } - + public String toString() { return String.format("SqliteCentralRepoSettings: [db type: sqlite, directory: %s, name: %s]", getDbDirectory(), getDbName()); } - + /** * sets database directory and name to defaults */ @@ -115,13 +115,11 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbSettings { return (!dbFile.isDirectory()); } - @Override public boolean verifyDatabaseExists() { return dbDirectoryExists(); } - /** * Verify that the db directory path exists. * @@ -143,6 +141,7 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbSettings { /** * creates database directory for sqlite database if it does not exist + * * @return whether or not operation occurred successfully */ @Override @@ -150,7 +149,6 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbSettings { return createDbDirectory(); } - /** * Create the db directory if it does not exist. * @@ -354,4 +352,21 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbSettings { String getJDBCBaseURI() { return JDBC_BASE_URI; } + + @Override + public DatabaseTestResult testStatus() { + if (dbFileExists()) { + if (verifyConnection()) { + if (verifyDatabaseSchema()) { + return DatabaseTestResult.TESTED_OK; + } else { + return DatabaseTestResult.SCHEMA_INVALID; + } + } else { + return DatabaseTestResult.SCHEMA_INVALID; + } + } else { + return DatabaseTestResult.DB_DOES_NOT_EXIST; + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModule.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModule.java index e561aeeff6..1f7472d330 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModule.java @@ -33,6 +33,7 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationDataSource; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeUtil; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoPlatforms; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbManager; import org.sleuthkit.autopsy.centralrepository.eventlisteners.IngestEventsListener; import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.Logger; @@ -274,7 +275,7 @@ final class CentralRepoIngestModule implements FileIngestModule { // Don't allow sqlite central repo databases to be used for multi user cases if ((autopsyCase.getCaseType() == Case.CaseType.MULTI_USER_CASE) - && (CentralRepoPlatforms.getSelectedPlatform() == CentralRepoPlatforms.SQLITE)) { + && (CentralRepoDbManager.getSavedDbChoice().getDbPlatform() == CentralRepoPlatforms.SQLITE)) { logger.log(Level.SEVERE, "Cannot run correlation engine on a multi-user case with a SQLite central repository."); throw new IngestModuleException("Cannot run on a multi-user case with a SQLite central repository."); // NON-NLS } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED index 7277910e7d..9ea90a452a 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED @@ -19,7 +19,7 @@ EamDbSettingsDialog.okButton.createDbDialog.title=Database Does Not Exist EamDbSettingsDialog.okButton.createDbError.title=Unable to Create Database EamDbSettingsDialog.okButton.createPostgresDbError.message=Unable to create Postgres Database, please ensure address, port, and login credentials are correct for Postgres server and try again. EamDbSettingsDialog.okButton.createSQLiteDbError.message=Unable to create SQLite Database, please ensure location exists and you have write permissions and try again. -EamDbSettingsDialog.okButton.databaseConnectionFailed.message=Unable to connect to database please check your settings and try again. +EamDbSettingsDialog.okButton.databaseConnectionFailed.message=Unable to connect to database. Please check your settings and try again. EamDbSettingsDialog.okButton.databaseConnectionFailed.title=Database Connection Failed EamDbSettingsDialog.okButton.errorMsg.text=Please restart Autopsy to begin using the new database platform. EamDbSettingsDialog.okButton.errorTitle.text=Restart Required. @@ -33,6 +33,12 @@ EamDbSettingsDialog.validation.finished=Click OK to save your database settings EamDbSettingsDialog.validation.incompleteFields=Fill in all values for the selected database. EamOptionsController.moduleErr=Error processing value changes. EamOptionsController.moduleErr.msg=Value change processing failed. +GlobalSettingsPanel.onMultiUserChange.disabledMu.description=The Central Repository will be reconfigured to use a local SQLite database. +GlobalSettingsPanel.onMultiUserChange.disabledMu.description2=Press Configure PostgreSQL to change to a PostgreSQL database. +GlobalSettingsPanel.onMultiUserChange.disabledMu.title=Central Repository Change Necessary +GlobalSettingsPanel.onMultiUserChange.enable.description=Do you want to update the Central Repository to use this PostgreSQL database? +GlobalSettingsPanel.onMultiUserChange.enable.description2=The Central Repository stores hash values and accounts from past cases. +GlobalSettingsPanel.onMultiUserChange.enable.title=Use with Central Repository? GlobalSettingsPanel.updateFailed.title=Central repository disabled GlobalSettingsPanel.validationErrMsg.ingestRunning=You cannot change settings while ingest is running. GlobalSettingsPanel.validationerrMsg.mustConfigure=Configure the database to enable this module. diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.form b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.form index 27eae7629c..5a1e2de77e 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.form @@ -138,16 +138,16 @@ - + - - - + + + @@ -320,7 +320,7 @@ - + @@ -330,7 +330,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java index 25289da2af..4c70d6aa5c 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java @@ -19,17 +19,24 @@ package org.sleuthkit.autopsy.centralrepository.optionspanel; import java.awt.Color; +import java.awt.Component; import java.awt.Cursor; +import java.awt.HeadlessException; import java.io.File; import java.io.IOException; +import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.logging.Level; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JTextField; +import javax.swing.ListCellRenderer; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; @@ -38,6 +45,7 @@ import org.netbeans.spi.options.OptionsPanelController; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbChoice; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbManager; import org.sleuthkit.autopsy.corecomponents.TextPrompt; import org.sleuthkit.autopsy.coreutils.Logger; @@ -45,8 +53,6 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoPlatforms; import org.sleuthkit.autopsy.centralrepository.datamodel.DatabaseTestResult; import org.sleuthkit.autopsy.centralrepository.datamodel.SqliteCentralRepoSettings; -import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; -import org.sleuthkit.autopsy.centralrepository.datamodel.RdbmsCentralRepoFactory; /** * Configuration dialog for Central Repository database settings. @@ -57,11 +63,42 @@ public class EamDbSettingsDialog extends JDialog { private static final Logger logger = Logger.getLogger(EamDbSettingsDialog.class.getName()); private static final long serialVersionUID = 1L; + + /** + * This class handles displaying and rendering drop down menu for database choices in central repo. + */ + private class DbChoiceRenderer extends JLabel implements ListCellRenderer, Serializable { + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent( + JList list, CentralRepoDbChoice value, + int index, boolean isSelected, boolean cellHasFocus) { + + // disable cell if it is the db connection from multi user settings + // and that option is not enabled in multi user settings + setText(value.getTitle()); + setEnabled(isDbChoiceSelectable(value)); + return this; + } + } + + private final Collection textBoxes; private final TextBoxChangedListener textBoxChangedListener; private final CentralRepoDbManager manager = new CentralRepoDbManager(); - - + private final boolean isMultiUserSelectable = CentralRepoDbManager.isPostgresMultiuserAllowed(); + private final DbChoiceRenderer DB_CHOICE_RENDERER = new DbChoiceRenderer(); + + public EamDbSettingsDialog() { + this(null); + } + + private boolean isDbChoiceSelectable(CentralRepoDbChoice item) { + return (item != CentralRepoDbChoice.POSTGRESQL_MULTIUSER || isMultiUserSelectable); + } + + /** * Creates new form EamDbSettingsDialog */ @@ -69,7 +106,7 @@ public class EamDbSettingsDialog extends JDialog { "EamDbSettingsDialog.lbSingleUserSqLite.text=SQLite should only be used by one examiner at a time.", "EamDbSettingsDialog.lbDatabaseType.text=Database Type :", "EamDbSettingsDialog.fcDatabasePath.title=Select location for central_repository.db"}) - public EamDbSettingsDialog() { + public EamDbSettingsDialog(CentralRepoDbChoice initialMenuItem) { super((JFrame) WindowManager.getDefault().getMainWindow(), Bundle.EamDbSettingsDialog_title_text(), true); @@ -95,29 +132,43 @@ public class EamDbSettingsDialog extends JDialog { return "Directories and Central Repository databases"; } }); - cbDatabaseType.setSelectedItem(manager.getSelectedPlatform()); - customizeComponents(); + + setupDbChoice(initialMenuItem); valid(); display(); + } + + private void setupDbChoice(CentralRepoDbChoice initialMenuItem) { + // setup initially selected item + CentralRepoDbChoice toSelect = (initialMenuItem == null) ? + (Arrays.asList(CentralRepoDbChoice.DB_CHOICES).contains(manager.getSelectedDbChoice())) ? + manager.getSelectedDbChoice() : + CentralRepoDbChoice.DB_CHOICES[0] : + initialMenuItem; + + cbDatabaseType.setRenderer(DB_CHOICE_RENDERER); + changeDbSelection(toSelect); } /** - * prompts user based on testing status (i.e. failure to connect, invalid schema, db does not exist, etc.) - * @return whether or not the ultimate status after prompts is okay to continue + * This method prompts user based on testing status (i.e. failure to connect, invalid schema, db does not exist, etc.). + * @param manager The manager to use when setting up the database. + * @param dialog If non-null value, validates settings and updates 'okay' button enabled state. + * @return Whether or not the ultimate status after prompts is okay to continue. */ @NbBundle.Messages({"EamDbSettingsDialog.okButton.corruptDatabaseExists.title=Error Loading Database", "EamDbSettingsDialog.okButton.corruptDatabaseExists.message=Database exists but is not the right format. Manually delete it or choose a different path (if applicable).", "EamDbSettingsDialog.okButton.createDbDialog.title=Database Does Not Exist", "EamDbSettingsDialog.okButton.createDbDialog.message=Database does not exist, would you like to create it?", "EamDbSettingsDialog.okButton.databaseConnectionFailed.title=Database Connection Failed", - "EamDbSettingsDialog.okButton.databaseConnectionFailed.message=Unable to connect to database please check your settings and try again.", + "EamDbSettingsDialog.okButton.databaseConnectionFailed.message=Unable to connect to database. Please check your settings and try again.", "EamDbSettingsDialog.okButton.createSQLiteDbError.message=Unable to create SQLite Database, please ensure location exists and you have write permissions and try again.", "EamDbSettingsDialog.okButton.createPostgresDbError.message=Unable to create Postgres Database, please ensure address, port, and login credentials are correct for Postgres server and try again.", "EamDbSettingsDialog.okButton.createDbError.title=Unable to Create Database"}) - private boolean promptTestStatusWarnings() { + private static boolean promptTestStatusWarnings(CentralRepoDbManager manager, EamDbSettingsDialog dialog) { if (manager.getStatus() == DatabaseTestResult.CONNECTION_FAILED) { JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), Bundle.EamDbSettingsDialog_okButton_databaseConnectionFailed_message(), @@ -135,36 +186,49 @@ public class EamDbSettingsDialog extends JDialog { Bundle.EamDbSettingsDialog_okButton_createDbDialog_message(), Bundle.EamDbSettingsDialog_okButton_createDbDialog_title(), JOptionPane.YES_NO_OPTION)) { - try { - manager.createDb(); - } - catch (CentralRepoException e) { - // in the event that there is a failure to connect, notify user with corresponding message - String errorMessage; - switch (manager.getSelectedPlatform()) { - case POSTGRESQL: - errorMessage = Bundle.EamDbSettingsDialog_okButton_createPostgresDbError_message(); - break; - case SQLITE: - errorMessage = Bundle.EamDbSettingsDialog_okButton_createSQLiteDbError_message(); - break; - default: - errorMessage = ""; - break; - } - - JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), - errorMessage, - Bundle.EamDbSettingsDialog_okButton_createDbError_title(), - JOptionPane.WARNING_MESSAGE); - } - - valid(); + onUserPromptCreateDb(manager, dialog); } } - return (manager.getStatus() == DatabaseTestResult.TESTEDOK); + return (manager.getStatus() == DatabaseTestResult.TESTED_OK); } + + /** + * When a new database needs to be created on user selecting cr, this code will be ran when user selects create cr. + * @param manager The manager handling the database creation. + * @param dialog The dialog that prompted database creation. + */ + private static void onUserPromptCreateDb(CentralRepoDbManager manager, EamDbSettingsDialog dialog) { + try { + manager.createDb(); + } catch (CentralRepoException e) { + onPromptStatusError(manager); + } + if (dialog != null) + dialog.valid(); + } + + + /** + * When an error occurs while going through promptTestStatusWarning, this method is called. + * @param manager1 The manager to use as service class. + * @throws HeadlessException + */ + private static void onPromptStatusError(CentralRepoDbManager manager1) { + // in the event that there is a failure to connect, notify user with corresponding message + String errorMessage = ""; + if (manager1 == null || manager1.getSelectedDbChoice() == null) { + errorMessage = ""; + } else if (manager1.getSelectedDbChoice().getDbPlatform() == CentralRepoPlatforms.POSTGRESQL) { + errorMessage = Bundle.EamDbSettingsDialog_okButton_createPostgresDbError_message(); + } else if (manager1.getSelectedDbChoice().getDbPlatform() == CentralRepoPlatforms.SQLITE) { + errorMessage = Bundle.EamDbSettingsDialog_okButton_createSQLiteDbError_message(); + } + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + errorMessage, + Bundle.EamDbSettingsDialog_okButton_createDbError_title(), + JOptionPane.WARNING_MESSAGE); + } /** @@ -281,7 +345,7 @@ public class EamDbSettingsDialog extends JDialog { jpDbPassword.setPreferredSize(new java.awt.Dimension(509, 20)); - cbDatabaseType.setModel(new javax.swing.DefaultComboBoxModel<>(new CentralRepoPlatforms[]{CentralRepoPlatforms.POSTGRESQL, CentralRepoPlatforms.SQLITE})); + cbDatabaseType.setModel(new javax.swing.DefaultComboBoxModel<>(org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbChoice.DB_CHOICES)); cbDatabaseType.setPreferredSize(new java.awt.Dimension(120, 20)); cbDatabaseType.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { @@ -323,18 +387,18 @@ public class EamDbSettingsDialog extends JDialog { .addComponent(lbUserName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(lbPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) - .addComponent(lbDatabaseDesc, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(lbDatabaseDesc, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 94, Short.MAX_VALUE) .addComponent(lbUserPassword, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) .addGap(10, 10, 10) - .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) .addGroup(pnSQLiteSettingsLayout.createSequentialGroup() .addComponent(tfDatabasePath, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(bnDatabasePathFileOpen)) .addGroup(pnSQLiteSettingsLayout.createSequentialGroup() - .addComponent(cbDatabaseType, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(lbSingleUserSqLite, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(cbDatabaseType, javax.swing.GroupLayout.PREFERRED_SIZE, 210, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(lbSingleUserSqLite, javax.swing.GroupLayout.PREFERRED_SIZE, 1, Short.MAX_VALUE)) .addComponent(jpDbPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(tbDbUsername, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(tbDbPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -414,15 +478,20 @@ public class EamDbSettingsDialog extends JDialog { setTextPrompts(); setTextBoxListeners(); manager.clearStatus(); - if (manager.getSelectedPlatform() == CentralRepoPlatforms.SQLITE) { + if (manager.getSelectedDbChoice() == CentralRepoDbChoice.SQLITE) { updatePostgresFields(false); updateSqliteFields(true); } - else { + else if (manager.getSelectedDbChoice() == CentralRepoDbChoice.POSTGRESQL_CUSTOM) { updatePostgresFields(true); updateSqliteFields(false); } - displayDatabaseSettings(CentralRepoPlatforms.POSTGRESQL.equals(manager.getSelectedPlatform())); + else { + updatePostgresFields(false); + updateSqliteFields(false); + } + + displayDatabaseSettings(manager.getSelectedDbChoice()); } private void display() { @@ -452,14 +521,40 @@ public class EamDbSettingsDialog extends JDialog { "EamDbSettingsDialog.okButton.errorMsg.text=Please restart Autopsy to begin using the new database platform.", "EamDbSettingsDialog.okButton.connectionErrorMsg.text=Failed to connect to central repository database."}) private void bnOkActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnOkActionPerformed - setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + testStatusAndCreate(this, manager, this); + dispose(); + }//GEN-LAST:event_bnOkActionPerformed + + + /** + * This method tests status for central repo db / creation and prompts user accordingly. + * @param parent The parent component (the anchor for displaying dialogs). + * @param manager The central repo db manager with settings to be tested and saved. + * @return Whether or not central repo db was successfully be created or found. + */ + public static boolean testStatusAndCreate(Component parent, CentralRepoDbManager manager) { + return testStatusAndCreate(parent, manager, null); + } + + + /** + * This method tests status for central repo db / creation and prompts user accordingly. + * @param parent The parent component (the anchor for displaying dialogs). + * @param manager The central repo db manager with settings to be tested and saved. + * @param dialog The db settings dialog; if non-null, will validate okay button state. + * @return Whether or not central repo db was successfully be created or found. + */ + private static boolean testStatusAndCreate(Component parent, CentralRepoDbManager manager, EamDbSettingsDialog dialog) { + parent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); manager.testStatus(); - valid(); - boolean testedOk = promptTestStatusWarnings(); + if (dialog != null) + dialog.valid(); + + boolean testedOk = promptTestStatusWarnings(manager, dialog); if (!testedOk) { - setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - return; + parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return false; } try{ @@ -467,25 +562,26 @@ public class EamDbSettingsDialog extends JDialog { } catch (CentralRepoException e) { SwingUtilities.invokeLater(() -> { - JOptionPane.showMessageDialog(this, + JOptionPane.showMessageDialog(parent, Bundle.EamDbSettingsDialog_okButton_errorMsg_text(), Bundle.EamDbSettingsDialog_okButton_errorTitle_text(), JOptionPane.WARNING_MESSAGE); }); + + parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return false; } - - - setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - dispose(); - }//GEN-LAST:event_bnOkActionPerformed + parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return true; + } /** - * Returns if changes to the central repository configuration were - * successfully applied + * This method returns if changes to the central repository configuration were + * successfully applied. * - * @return true if the database configuration was successfully changed false - * if it was not + * @return True if the database configuration was successfully changed; false + * if it was not. */ public boolean wasConfigurationChanged() { return manager.wasConfigurationChanged(); @@ -497,22 +593,38 @@ public class EamDbSettingsDialog extends JDialog { private void cbDatabaseTypeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cbDatabaseTypeActionPerformed - manager.setSelectedPlatform((CentralRepoPlatforms) cbDatabaseType.getSelectedItem()); - customizeComponents(); + CentralRepoDbChoice selectedItem = (CentralRepoDbChoice) cbDatabaseType.getSelectedItem(); + changeDbSelection(selectedItem); }//GEN-LAST:event_cbDatabaseTypeActionPerformed + private void changeDbSelection(CentralRepoDbChoice selectedItem) { + if (isDbChoiceSelectable(selectedItem)) { + manager.setSelctedDbChoice(selectedItem); + cbDatabaseType.setSelectedItem(selectedItem); + } + else { + cbDatabaseType.setSelectedItem(manager.getSelectedDbChoice()); + } + + customizeComponents(); + } + private void updateFullDbPath() { dataBaseFileTextArea.setText(tfDatabasePath.getText() + File.separator + SqliteCentralRepoSettings.DEFAULT_DBNAME); dataBaseFileTextArea.setCaretPosition(dataBaseFileTextArea.getText().length()); } - private void displayDatabaseSettings(boolean isPostgres) { - lbDatabasePath.setVisible(!isPostgres); - tfDatabasePath.setVisible(!isPostgres); - lbDatabaseDesc.setVisible(!isPostgres); - dataBaseFileTextArea.setVisible(!isPostgres); - lbSingleUserSqLite.setVisible(!isPostgres); - bnDatabasePathFileOpen.setVisible(!isPostgres); + private void displayDatabaseSettings(CentralRepoDbChoice choice) { + boolean isSqlite = choice == CentralRepoDbChoice.SQLITE; + boolean isPostgres = choice == CentralRepoDbChoice.POSTGRESQL_CUSTOM; + + lbDatabasePath.setVisible(isSqlite); + tfDatabasePath.setVisible(isSqlite); + lbDatabaseDesc.setVisible(isSqlite); + dataBaseFileTextArea.setVisible(isSqlite); + lbSingleUserSqLite.setVisible(isSqlite); + bnDatabasePathFileOpen.setVisible(isSqlite); + lbHostName.setVisible(isPostgres); tbDbHostname.setVisible(isPostgres); lbPort.setVisible(isPostgres); @@ -610,14 +722,14 @@ public class EamDbSettingsDialog extends JDialog { @Messages({"EamDbSettingsDialog.validation.incompleteFields=Fill in all values for the selected database."}) private boolean databaseFieldsArePopulated() { boolean result = true; - if (manager.getSelectedPlatform() == CentralRepoPlatforms.POSTGRESQL) { + if (manager.getSelectedDbChoice() == CentralRepoDbChoice.POSTGRESQL_CUSTOM) { result = !tbDbHostname.getText().trim().isEmpty() && !tbDbPort.getText().trim().isEmpty() // && !tbDbName.getText().trim().isEmpty() && !tbDbUsername.getText().trim().isEmpty() && 0 < jpDbPassword.getPassword().length; } - else if (manager.getSelectedPlatform() == CentralRepoPlatforms.SQLITE) { + else if (manager.getSelectedDbChoice() == CentralRepoDbChoice.SQLITE) { result = !tfDatabasePath.getText().trim().isEmpty(); } @@ -722,7 +834,7 @@ public class EamDbSettingsDialog extends JDialog { private javax.swing.JButton bnDatabasePathFileOpen; private javax.swing.ButtonGroup bnGrpDatabasePlatforms; private javax.swing.JButton bnOk; - private javax.swing.JComboBox cbDatabaseType; + private javax.swing.JComboBox cbDatabaseType; private javax.swing.JScrollPane dataBaseFileScrollPane; private javax.swing.JTextArea dataBaseFileTextArea; private javax.swing.JFileChooser fcDatabasePath; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.form index 13ba7876cd..99594871b0 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.form @@ -61,7 +61,7 @@ - + @@ -258,7 +258,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.java index ab0ea0401f..e84d2b90f7 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.java @@ -33,33 +33,39 @@ import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbManager; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoPlatforms; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestModuleGlobalSettingsPanel; -import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoPlatforms; -import static org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoPlatforms.DISABLED; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbChoice; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbUtil; import org.sleuthkit.autopsy.centralrepository.datamodel.PostgresCentralRepoSettings; import org.sleuthkit.autopsy.centralrepository.datamodel.SqliteCentralRepoSettings; +import java.awt.Component; +import java.util.logging.Level; /** * Main settings panel for the Central Repository */ @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel implements OptionsPanel { - + private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(GlobalSettingsPanel.class.getName()); private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.STARTED, IngestManager.IngestJobEvent.CANCELLED, IngestManager.IngestJobEvent.COMPLETED); private final IngestJobEventPropertyChangeListener ingestJobEventListener; - + + + /** * Creates new form EamOptionsPanel */ public GlobalSettingsPanel() { ingestJobEventListener = new IngestJobEventPropertyChangeListener(); - + + // listen for change events in currently saved choice + CentralRepoDbManager.addPropertyChangeListener((PropertyChangeEvent evt) -> ingestStateUpdated(Case.isCaseOpen())); initComponents(); customizeComponents(); addIngestJobEventsListener(); @@ -68,7 +74,8 @@ public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel i ingestStateUpdated(evt.getNewValue() != null); }); } - + + private void customizeComponents() { setName(NbBundle.getMessage(GlobalSettingsPanel.class, "GlobalSettingsPanel.pnCorrelationProperties.border.title")); } @@ -78,26 +85,163 @@ public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel i ingestStateUpdated(Case.isCaseOpen()); } - @Messages({"GlobalSettingsPanel.updateFailed.title=Central repository disabled"}) private void updateDatabase() { + updateDatabase(this); + } + + /** + * This method invokes central repository database choice selection as well as input for necessary configuration. + * @param parent The parent component for displaying dialogs. + * @param initialSelection If non-null, the menu item will be set to this choice; if null, + * the currently selected db choice will be selected. + * @return True if there was a change. + */ + private static boolean invokeCrChoice(Component parent, CentralRepoDbChoice initialSelection) { + EamDbSettingsDialog dialog = (initialSelection != null) ? + new EamDbSettingsDialog(initialSelection) : + new EamDbSettingsDialog(); + + if (dialog.wasConfigurationChanged()) { + updateDatabase(parent); + return true; + } + + return false; + } + + + /** + * When multi user settings are updated, this function triggers pertinent updates for central repository. + * NOTE: If multi user settings were previously enabled and multi user settings are currently selected, this function assumes + * there is a change in the postgres connectivity. + * + * @param parent The swing component that serves as a parent for dialogs that may arise. + * @param muPreviouslySelected If multi user settings were previously enabled. + * @param muCurrentlySelected If multi user settings are currently enabled as of most recent change. + */ + @NbBundle.Messages({ + "GlobalSettingsPanel.onMultiUserChange.enable.title=Use with Central Repository?", + "GlobalSettingsPanel.onMultiUserChange.enable.description=Do you want to update the Central Repository to use this PostgreSQL database?", + "GlobalSettingsPanel.onMultiUserChange.enable.description2=The Central Repository stores hash values and accounts from past cases." + }) + public static void onMultiUserChange(Component parent, boolean muPreviouslySelected, boolean muCurrentlySelected) { + boolean crEnabled = CentralRepoDbUtil.allowUseOfCentralRepository(); + boolean crMultiUser = CentralRepoDbManager.getSavedDbChoice() == CentralRepoDbChoice.POSTGRESQL_MULTIUSER; + boolean crDisabledDueToFailure = CentralRepoDbManager.isDisabledDueToFailure(); + + if (!muPreviouslySelected && muCurrentlySelected) { + SwingUtilities.invokeLater(() -> { + if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(parent, + "" + + "
" + + "

" + NbBundle.getMessage(GlobalSettingsPanel.class, "GlobalSettingsPanel.onMultiUserChange.enable.description") + "

" + + "

" + NbBundle.getMessage(GlobalSettingsPanel.class, "GlobalSettingsPanel.onMultiUserChange.enable.description2") + "

" + + "
" + + "", + NbBundle.getMessage(GlobalSettingsPanel.class, "GlobalSettingsPanel.onMultiUserChange.enable.title"), + JOptionPane.YES_NO_OPTION)) { + + // setup database for CR + CentralRepoDbUtil.setUseCentralRepo(true); + CentralRepoDbManager.saveDbChoice(CentralRepoDbChoice.POSTGRESQL_MULTIUSER); + handleDbChange(parent); + } + }); + } + // moving from selected to not selected && 'PostgreSQL using multi-user settings' is selected + else if (muPreviouslySelected && !muCurrentlySelected && crEnabled && crMultiUser) { + SwingUtilities.invokeLater(() -> { + askForCentralRepoDbChoice(parent); + }); + } + // changing multi-user settings connection && 'PostgreSQL using multi-user settings' is selected && + // central repo either enabled or was disabled due to error + else if (muPreviouslySelected && muCurrentlySelected && crMultiUser && (crEnabled || crDisabledDueToFailure)) { + // test databse for CR change + CentralRepoDbUtil.setUseCentralRepo(true); + handleDbChange(parent); + } + } - if (CentralRepoPlatforms.getSelectedPlatform().equals(DISABLED)) { + + /** + * This method is called when a user must select a new database other than using database from multi user settings. + * @param parent The parent component to use for displaying dialogs in reference. + */ + @NbBundle.Messages({ + "GlobalSettingsPanel.onMultiUserChange.disabledMu.title=Central Repository Change Necessary", + "GlobalSettingsPanel.onMultiUserChange.disabledMu.description=The Central Repository will be reconfigured to use a local SQLite database.", + "GlobalSettingsPanel.onMultiUserChange.disabledMu.description2=Press Configure PostgreSQL to change to a PostgreSQL database." + }) + private static void askForCentralRepoDbChoice(Component parent) { + // disable central repository until user makes choice + CentralRepoDbUtil.setUseCentralRepo(false); + CentralRepoDbManager.saveDbChoice(CentralRepoDbChoice.DISABLED, false); + + Object[] options = { + "Use SQLite", + "Configure PostgreSQL", + "Disable Central Repository" + }; + + int result = JOptionPane.showOptionDialog( + parent, + "" + + "
" + + "

" + NbBundle.getMessage(GlobalSettingsPanel.class, "GlobalSettingsPanel.onMultiUserChange.disabledMu.description") + "

" + + "

" + NbBundle.getMessage(GlobalSettingsPanel.class, "GlobalSettingsPanel.onMultiUserChange.disabledMu.description2") + "

" + + "
" + + "", + NbBundle.getMessage(GlobalSettingsPanel.class, "GlobalSettingsPanel.onMultiUserChange.disabledMu.title"), + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + options, + options[0] + ); + + if (JOptionPane.YES_OPTION == result) { + invokeCrChoice(parent, CentralRepoDbChoice.SQLITE); + } + else if (JOptionPane.NO_OPTION == result) { + invokeCrChoice(parent, CentralRepoDbChoice.POSTGRESQL_CUSTOM); + } + } + + + private static void handleDbChange(Component parent) { + SwingUtilities.invokeLater(() -> { + boolean successful = EamDbSettingsDialog.testStatusAndCreate(parent, new CentralRepoDbManager()); + if (successful) { + updateDatabase(parent); + } + else { + // disable central repository due to error + CentralRepoDbManager.disableDueToFailure(); + } + }); + } + + + @Messages({"GlobalSettingsPanel.updateFailed.title=Central repository disabled"}) + private static void updateDatabase(Component parent) { + if (CentralRepoDbChoice.DISABLED.equals(CentralRepoDbManager.getSavedDbChoice())) { return; } - setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + parent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { CentralRepoDbManager.upgradeDatabase(); - setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } catch (CentralRepoException ex) { - setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - JOptionPane.showMessageDialog(this, + parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + JOptionPane.showMessageDialog(parent, ex.getUserMessage(), - NbBundle.getMessage(this.getClass(), + NbBundle.getMessage(GlobalSettingsPanel.class, "GlobalSettingsPanel.updateFailed.title"), JOptionPane.WARNING_MESSAGE); } finally { - setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } @@ -425,9 +569,8 @@ public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel i private void bnDbConfigureActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnDbConfigureActionPerformed store(); - EamDbSettingsDialog dialog = new EamDbSettingsDialog(); - if (dialog.wasConfigurationChanged()) { - updateDatabase(); + boolean changed = invokeCrChoice(this, null); + if (changed) { load(); // reload db settings content and update buttons firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); } @@ -446,6 +589,16 @@ public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel i private void cbUseCentralRepoActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cbUseCentralRepoActionPerformed //if saved setting is disabled checkbox should be disabled already store(); + + // if moving to using CR, multi-user mode is disabled and selection is multiuser settings, set to disabled + if (cbUseCentralRepo.isSelected() && + !CentralRepoDbManager.isPostgresMultiuserAllowed() && + CentralRepoDbManager.getSavedDbChoice() == CentralRepoDbChoice.POSTGRESQL_MULTIUSER) { + + CentralRepoDbManager.saveDbChoice(CentralRepoDbChoice.DISABLED); + } + + updateDatabase(); load(); this.ingestStateUpdated(Case.isCaseOpen()); @@ -457,31 +610,35 @@ public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel i public void load() { tbOops.setText(""); enableButtonSubComponents(false); - CentralRepoPlatforms selectedPlatform = CentralRepoPlatforms.getSelectedPlatform(); + CentralRepoDbChoice selectedChoice = CentralRepoDbManager.getSavedDbChoice(); cbUseCentralRepo.setSelected(CentralRepoDbUtil.allowUseOfCentralRepository()); // NON-NLS - switch (selectedPlatform) { - case POSTGRESQL: - PostgresCentralRepoSettings dbSettingsPg = new PostgresCentralRepoSettings(); - lbDbPlatformValue.setText(CentralRepoPlatforms.POSTGRESQL.toString()); - lbDbNameValue.setText(dbSettingsPg.getDbName()); - lbDbLocationValue.setText(dbSettingsPg.getHost()); - enableButtonSubComponents(cbUseCentralRepo.isSelected()); - break; - case SQLITE: + + lbDbPlatformValue.setText(selectedChoice.getTitle()); + CentralRepoPlatforms selectedDb = selectedChoice.getDbPlatform(); + + if (selectedChoice == null || selectedDb == CentralRepoPlatforms.DISABLED) { + lbDbNameValue.setText(""); + lbDbLocationValue.setText(""); + tbOops.setText(Bundle.GlobalSettingsPanel_validationerrMsg_mustConfigure()); + } + else { + enableButtonSubComponents(cbUseCentralRepo.isSelected()); + if (selectedDb == CentralRepoPlatforms.POSTGRESQL) { + try { + PostgresCentralRepoSettings dbSettingsPg = new PostgresCentralRepoSettings(); + lbDbNameValue.setText(dbSettingsPg.getDbName()); + lbDbLocationValue.setText(dbSettingsPg.getHost()); + } + catch (CentralRepoException e) { + logger.log(Level.WARNING, "Unable to load settings into global panel for postgres settings", e); + } + } + else if (selectedDb == CentralRepoPlatforms.SQLITE) { SqliteCentralRepoSettings dbSettingsSqlite = new SqliteCentralRepoSettings(); - lbDbPlatformValue.setText(CentralRepoPlatforms.SQLITE.toString()); lbDbNameValue.setText(dbSettingsSqlite.getDbName()); lbDbLocationValue.setText(dbSettingsSqlite.getDbDirectory()); - enableButtonSubComponents(cbUseCentralRepo.isSelected()); - break; - default: - lbDbPlatformValue.setText(CentralRepoPlatforms.DISABLED.toString()); - lbDbNameValue.setText(""); - lbDbLocationValue.setText(""); - tbOops.setText(Bundle.GlobalSettingsPanel_validationerrMsg_mustConfigure()); - break; + } } - } @Override @@ -490,12 +647,12 @@ public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel i } /** - * Validates that the dialog/panel is filled out correctly for our usage. + * This method validates that the dialog/panel is filled out correctly for our usage. * - * @return true if it's okay, false otherwise. + * @return True if it is okay, false otherwise. */ public boolean valid() { - return !cbUseCentralRepo.isSelected() || !lbDbPlatformValue.getText().equals(DISABLED.toString()); + return !cbUseCentralRepo.isSelected() || !lbDbPlatformValue.getText().equals(CentralRepoDbChoice.DISABLED.toString()); } @Override @@ -574,9 +731,9 @@ public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel i enableButtonSubComponents(cbUseCentralRepo.isSelected()); } else { load(); - enableDatabaseConfigureButton(cbUseCentralRepo.isSelected() && !caseIsOpen); } + enableDatabaseConfigureButton(cbUseCentralRepo.isSelected() && !caseIsOpen); } /** diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java index 3dfaf53001..f5d9a5c641 100755 --- a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java @@ -343,6 +343,7 @@ public class CommandLineIngestManager { * @param baseCaseName Case name * @param rootOutputDirectory Full path to directory in which case * output folder will be created + * @param caseType Type of case being created * * @throws CaseActionException */ diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/CallLogNode.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/CallLogNode.java index 288b3efa3f..3151d75780 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/CallLogNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/CallLogNode.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.communications.relationships; import java.util.logging.Level; +import org.apache.commons.lang.StringUtils; import org.openide.nodes.Sheet; import org.sleuthkit.autopsy.communications.Utils; import static org.sleuthkit.autopsy.communications.relationships.RelationshipsNodeUtilities.getAttributeDisplayString; @@ -67,14 +68,6 @@ final class CallLogNode extends BlackboardArtifactNode { return sheet; } - String phoneNumber = getAttributeDisplayString(artifact, TSK_PHONE_NUMBER_FROM); - if(phoneNumber == null || phoneNumber.isEmpty()) { - phoneNumber = getAttributeDisplayString(artifact, TSK_PHONE_NUMBER_TO); - } - if(phoneNumber == null || phoneNumber.isEmpty()) { - phoneNumber = getAttributeDisplayString(artifact, TSK_PHONE_NUMBER); - } - long duration = -1; try{ duration = getCallDuration(artifact); @@ -84,7 +77,7 @@ final class CallLogNode extends BlackboardArtifactNode { sheetSet.put(createNode(TSK_DATETIME_START, artifact)); sheetSet.put(createNode(TSK_DIRECTION, artifact)); - sheetSet.put(new NodeProperty<>(TSK_PHONE_NUMBER.getLabel(), TSK_PHONE_NUMBER.getDisplayName(), "", phoneNumber)); + sheetSet.put(new NodeProperty<>(TSK_PHONE_NUMBER.getLabel(), TSK_PHONE_NUMBER.getDisplayName(), "", getPhoneNumber(artifact))); if(duration != -1) { sheetSet.put(new NodeProperty<>("duration", "Duration", "", Long.toString(duration))); } @@ -107,6 +100,59 @@ final class CallLogNode extends BlackboardArtifactNode { return endAttribute.getValueLong() - startAttribute.getValueLong(); } + /** + * Returns the phone number to display in the To/From column. The number is + * picked from one of the 3 possible phone number attributes, based on the + * direction of the call. + * + * @param artifact Call log artifact. + * + * @return Phone number to display. + */ + private String getPhoneNumber(BlackboardArtifact artifact) { + String direction = getAttributeDisplayString(artifact, TSK_DIRECTION); + + String phoneNumberToReturn; + String fromPhoneNumber = getAttributeDisplayString(artifact, TSK_PHONE_NUMBER_FROM); + String toPhoneNumber = getAttributeDisplayString(artifact, TSK_PHONE_NUMBER_TO); + String phoneNumber = getAttributeDisplayString(artifact, TSK_PHONE_NUMBER); + switch (direction.toLowerCase()) { + case "incoming": // NON-NLS + phoneNumberToReturn = getFirstNonBlank(fromPhoneNumber, phoneNumber, toPhoneNumber); + break; + case "outgoing": // NON-NLS + phoneNumberToReturn = getFirstNonBlank(toPhoneNumber, phoneNumber, fromPhoneNumber); + break; + default: + phoneNumberToReturn = getFirstNonBlank(toPhoneNumber, fromPhoneNumber, phoneNumber ); + break; + } + + return phoneNumberToReturn; + } + + /** + * Checks the given string arguments in order and returns the first non blank string. + * Returns a blank string if all the input strings are blank. + * + * @param string1 First string to check + * @param string2 Second string to check + * @param string3 Third string to check + * + * @retunr first non blank string if there is one, blank string otherwise. + * + */ + private String getFirstNonBlank(String string1, String string2, String string3 ) { + + if (!StringUtils.isBlank(string1)) { + return string1; + } else if (!StringUtils.isBlank(string2)) { + return string2; + } else if (!StringUtils.isBlank(string3)) { + return string3; + } + return ""; + } /** * Circumvent DataResultFilterNode's slightly odd delegation to * BlackboardArtifactNode.getSourceName(). diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED index 737b6900d4..c0a6f73aaf 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED @@ -41,6 +41,7 @@ MediaFileViewer.AccessibleContext.accessibleDescription= MediaFileViewer.title=Media MediaFileViewer.toolTip=Displays supported multimedia files (images, videos, audio) MediaPlayerPanel.noSupport=File not supported. +MediaPlayerPanel.playbackDisabled=A problem was encountered with the video and audio playback service. Video and audio playback will be disabled for the remainder of the session. MediaPlayerPanel.timeFormat=%02d:%02d:%02d MediaPlayerPanel.unknownTime=Unknown MediaViewImagePanel.createTagOption=Create @@ -168,7 +169,7 @@ MediaPlayerPanel.playBackSpeedLabel.text=Speed: SQLiteViewer.readTable.errorText=Error getting rows for table: {0} # {0} - tableName SQLiteViewer.selectTable.errorText=Error getting row count for table: {0} -TextTranslatableComponent.setPanelContent.onSetContentError=Unable to display text at this time. -TextTranslatableComponent.setTranslated.onTranslateError=Unable to translate text at this time. TranslatablePanel.comboBoxOption.originalText=Original Text TranslatablePanel.comboBoxOption.translatedText=Translated Text +# {0} - exception message +TranslatablePanel.onSetContentError.text=There was an error displaying the text: {0} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java index 56dd8e9ebe..9decc79175 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java @@ -194,6 +194,6 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { @Override public boolean isSupported(AbstractFile file){ - return true; + return mediaPlayerPanel.isSupported(file) || imagePanel.isSupported(file); } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form index 0b00c43d8a..793e31830b 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form @@ -63,7 +63,7 @@ - +
@@ -129,12 +129,6 @@ - - - - - - diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java index c518abdd45..40122acea7 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -74,7 +74,9 @@ import org.freedesktop.gstreamer.Format; import org.freedesktop.gstreamer.GstException; import org.freedesktop.gstreamer.event.SeekFlags; import org.freedesktop.gstreamer.event.SeekType; -import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.contentviewers.utils.GstLoader; +import org.sleuthkit.autopsy.contentviewers.utils.GstLoader.GstStatus; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; /** * This is a video player that is part of the Media View layered pane. It uses @@ -213,6 +215,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie //and the TrackListener on the slider itself. private final Semaphore sliderLock; + private static volatile boolean IS_GST_ENABLED = true; + /** * Creates a new MediaPlayerPanel */ @@ -222,18 +226,10 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie //True for fairness. In other words, //acquire() calls are processed in order of invocation. sliderLock = new Semaphore(1, true); - - /** - * See JIRA-5888 for details. Initializing gstreamer here is more stable - * on Windows. - */ - if (PlatformUtil.isWindowsOS()) { - Gst.init(); - } } private void customizeComponents() { - progressSlider.setEnabled(false); // disable slider; enable after user plays vid + enableComponents(false); progressSlider.setMinimum(0); progressSlider.setMaximum(PROGRESS_SLIDER_SIZE); progressSlider.setValue(0); @@ -241,7 +237,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie progressSlider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { - if (progressSlider.getValueIsAdjusting()) { + if (progressSlider.getValueIsAdjusting() && gstPlayBin != null) { long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); double relativePosition = progressSlider.getValue() * 1.0 / PROGRESS_SLIDER_SIZE; long newStartTime = (long) (relativePosition * duration); @@ -264,20 +260,23 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie //Manage the video while the user is performing actions on the track. progressSlider.addMouseListener(new MouseListener() { private State previousState = State.NULL; - + @Override public void mousePressed(MouseEvent e) { - previousState = gstPlayBin.getState(); - gstPlayBin.pause(); + if (gstPlayBin != null) { + previousState = gstPlayBin.getState(); + gstPlayBin.pause(); + } } @Override public void mouseReleased(MouseEvent e) { - if(previousState.equals(State.PLAYING)) { + if (previousState.equals(State.PLAYING) && gstPlayBin != null) { gstPlayBin.play(); } previousState = State.NULL; } + @Override public void mouseClicked(MouseEvent e) { } @@ -289,11 +288,11 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override public void mouseExited(MouseEvent e) { } - + }); //Manage the audio level when the user is adjusting the volume slider audioSlider.addChangeListener((ChangeEvent event) -> { - if (audioSlider.getValueIsAdjusting()) { + if (audioSlider.getValueIsAdjusting() && gstPlayBin != null) { double audioPercent = (audioSlider.getValue() * 2.0) / 100.0; gstPlayBin.setVolume(audioPercent); } @@ -327,11 +326,13 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie endOfStreamListener = new Bus.EOS() { @Override public void endOfStream(GstObject go) { - gstPlayBin.seek(ClockTime.ZERO); - /** - * Keep the video from automatically playing - */ - Gst.getExecutor().submit(() -> gstPlayBin.pause()); + if (gstPlayBin != null) { + gstPlayBin.seek(ClockTime.ZERO); + /** + * Keep the video from automatically playing + */ + Gst.getExecutor().submit(() -> gstPlayBin.pause()); + } } }; } @@ -385,13 +386,16 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie } timer.stop(); if (gstPlayBin != null) { - gstPlayBin.stop(); - gstPlayBin.getBus().disconnect(endOfStreamListener); - gstPlayBin.getBus().disconnect(stateChangeListener); - gstPlayBin.getBus().disconnect(errorListener); - gstPlayBin.dispose(); - fxAppSink.clear(); - gstPlayBin = null; + Gst.getExecutor().submit(() -> { + gstPlayBin.stop(); + gstPlayBin.getBus().disconnect(endOfStreamListener); + gstPlayBin.getBus().disconnect(stateChangeListener); + gstPlayBin.getBus().disconnect(errorListener); + gstPlayBin.getBus().dispose(); + gstPlayBin.dispose(); + fxAppSink.clear(); + gstPlayBin = null; + }); } videoPanel.removeAll(); resetComponents(); @@ -420,6 +424,10 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override public boolean isSupported(AbstractFile file) { + if (!IS_GST_ENABLED) { + return false; + } + String extension = file.getNameExtension(); /** * Although it seems too restrictive, requiring both a supported @@ -537,6 +545,11 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie /* * Initialize the playback components if the extraction was successful. */ + @NbBundle.Messages({ + "MediaPlayerPanel.playbackDisabled=A problem was encountered with" + + " the video and audio playback service. Video and audio " + + "playback will be disabled for the remainder of the session." + }) @Override protected void done() { try { @@ -546,41 +559,49 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie return; } - // Initialize Gstreamer. It is safe to call this for every file. - // It was moved here from the constructor because having it happen - // earlier resulted in conflicts on Linux. See JIRA-5888. - if (!PlatformUtil.isWindowsOS()) { - Gst.init(); - } + GstStatus loadStatus = GstLoader.tryLoad(); + if (loadStatus == GstStatus.FAILURE) { + MessageNotifyUtil.Message.error(Bundle.MediaPlayerPanel_playbackDisabled()); - //Video is ready for playback. Create new components - gstPlayBin = new PlayBin("VideoPlayer", tempFile.toURI()); - //Configure event handling - Bus playBinBus = gstPlayBin.getBus(); - playBinBus.connect(endOfStreamListener); - playBinBus.connect(stateChangeListener); - playBinBus.connect(errorListener); - - if (this.isCancelled()) { + // This will disable the panel for future use. + IS_GST_ENABLED = false; return; } - JFXPanel fxPanel = new JFXPanel(); - videoPanel.removeAll(); - videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); - videoPanel.add(fxPanel); - fxAppSink = new JavaFxAppSink("JavaFxAppSink", fxPanel); - gstPlayBin.setVideoSink(fxAppSink); + Gst.getExecutor().submit(() -> { + //Video is ready for playback. Create new components + gstPlayBin = new PlayBin("VideoPlayer", tempFile.toURI()); + //Configure event handling + Bus playBinBus = gstPlayBin.getBus(); + playBinBus.connect(endOfStreamListener); + playBinBus.connect(stateChangeListener); + playBinBus.connect(errorListener); - if (this.isCancelled()) { - return; - } + if (this.isCancelled()) { + return; + } - gstPlayBin.setVolume((audioSlider.getValue() * 2.0) / 100.0); - gstPlayBin.pause(); + JFXPanel fxPanel = new JFXPanel(); + videoPanel.removeAll(); + videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); + videoPanel.add(fxPanel); + fxAppSink = new JavaFxAppSink("JavaFxAppSink", fxPanel); + if (gstPlayBin != null) { + gstPlayBin.setVideoSink(fxAppSink); + } + if (this.isCancelled()) { + return; + } + if (gstPlayBin != null) { + gstPlayBin.setVolume((audioSlider.getValue() * 2.0) / 100.0); + gstPlayBin.pause(); + } - timer.start(); - enableComponents(true); + timer.start(); + SwingUtilities.invokeLater(() -> { + enableComponents(true); + }); + }); } catch (CancellationException ex) { logger.log(Level.INFO, "Media buffering was canceled."); //NON-NLS } catch (InterruptedException ex) { @@ -598,24 +619,30 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override public void actionPerformed(ActionEvent e) { - if (!progressSlider.getValueIsAdjusting()) { - sliderLock.acquireUninterruptibly(); - long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); - /** - * Duration may not be known until there is video data in the - * pipeline. We start this updater when data-flow has just been - * initiated so buffering may still be in progress. - */ - if (duration >= 0 && position >= 0) { - double relativePosition = (double) position / duration; - progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); - } + if (!progressSlider.getValueIsAdjusting() && gstPlayBin != null) { + Gst.getExecutor().submit(() -> { + try { + sliderLock.acquireUninterruptibly(); + long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); + /** + * Duration may not be known until there is video data + * in the pipeline. We start this updater when data-flow + * has just been initiated so buffering may still be in + * progress. + */ + if (duration >= 0 && position >= 0) { + double relativePosition = (double) position / duration; + progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); + } - SwingUtilities.invokeLater(() -> { - updateTimeLabel(position, duration); + SwingUtilities.invokeLater(() -> { + updateTimeLabel(position, duration); + }); + } finally { + sliderLock.release(); + } }); - sliderLock.release(); } } } @@ -641,7 +668,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie public CircularJSliderUI(JSlider slider, Dimension thumbDimension) { super(slider); this.thumbDimension = thumbDimension; - + //Configure track and thumb colors. Color lightBlue = new Color(0, 130, 255); thumbColor = lightBlue; @@ -655,8 +682,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie } /** - * Modifies the View to be an oval rather than the underlying - * rectangle Controller. + * Modifies the View to be an oval rather than the underlying rectangle + * Controller. */ @Override public void paintThumb(Graphics graphic) { @@ -705,12 +732,13 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override protected TrackListener createTrackListener(JSlider slider) { /** - * This track listener will force the thumb to be snapped to the mouse - * location. This makes grabbing and dragging the JSlider much easier. - * Using the default track listener, the user would have to click - * exactly on the slider thumb to drag it. Now the thumb positions - * itself under the mouse so that it can always be dragged. - */ + * This track listener will force the thumb to be snapped to the + * mouse location. This makes grabbing and dragging the JSlider much + * easier. Using the default track listener, the user would have to + * click exactly on the slider thumb to drag it. Now the thumb + * positions itself under the mouse so that it can always be + * dragged. + */ return new TrackListener() { @Override public void mousePressed(MouseEvent e) { @@ -982,88 +1010,104 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie }// //GEN-END:initComponents private void rewindButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rewindButtonActionPerformed - long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - //Skip 30 seconds. - long rewindDelta = TimeUnit.NANOSECONDS.convert(SKIP_IN_SECONDS, TimeUnit.SECONDS); - //Ensure new video position is within bounds - long newTime = Math.max(currentTime - rewindDelta, 0); - double playBackRate = getPlayBackRate(); - gstPlayBin.seek(playBackRate, - Format.TIME, - //FLUSH - flushes the pipeline - //ACCURATE - video will seek exactly to the position requested - EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), - //Set the start position to newTime - SeekType.SET, newTime, - //Do nothing for the end position - SeekType.NONE, -1); + Gst.getExecutor().submit(() -> { + if (gstPlayBin != null) { + long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + //Skip 30 seconds. + long rewindDelta = TimeUnit.NANOSECONDS.convert(SKIP_IN_SECONDS, TimeUnit.SECONDS); + //Ensure new video position is within bounds + long newTime = Math.max(currentTime - rewindDelta, 0); + double playBackRate = getPlayBackRate(); + gstPlayBin.seek(playBackRate, + Format.TIME, + //FLUSH - flushes the pipeline + //ACCURATE - video will seek exactly to the position requested + EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), + //Set the start position to newTime + SeekType.SET, newTime, + //Do nothing for the end position + SeekType.NONE, -1); + } + }); }//GEN-LAST:event_rewindButtonActionPerformed - private void fastForwardButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fastForwardButtonActionPerformed - long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); - long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - //Skip 30 seconds. - long fastForwardDelta = TimeUnit.NANOSECONDS.convert(SKIP_IN_SECONDS, TimeUnit.SECONDS); - //Don't allow skipping within 2 seconds of video ending. Skipping right to - //the end causes undefined behavior for some gstreamer plugins. - long twoSecondsInNano = TimeUnit.NANOSECONDS.convert(2, TimeUnit.SECONDS); - if((duration - currentTime) <= twoSecondsInNano) { - return; - } - - long newTime; - if (currentTime + fastForwardDelta >= duration) { - //If there are less than 30 seconds left, only fast forward to the midpoint. - newTime = currentTime + (duration - currentTime)/2; - } else { - newTime = currentTime + fastForwardDelta; - } + private void fastForwardButtonActionPerformed(java.awt.event.ActionEvent evt) { + Gst.getExecutor().submit(() -> { + if (gstPlayBin != null) { + long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); + long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + //Skip 30 seconds. + long fastForwardDelta = TimeUnit.NANOSECONDS.convert(SKIP_IN_SECONDS, TimeUnit.SECONDS); + //Don't allow skipping within 2 seconds of video ending. Skipping right to + //the end causes undefined behavior for some gstreamer plugins. + long twoSecondsInNano = TimeUnit.NANOSECONDS.convert(2, TimeUnit.SECONDS); + if ((duration - currentTime) <= twoSecondsInNano) { + return; + } - double playBackRate = getPlayBackRate(); - gstPlayBin.seek(playBackRate, - Format.TIME, - //FLUSH - flushes the pipeline - //ACCURATE - video will seek exactly to the position requested - EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), - //Set the start position to newTime - SeekType.SET, newTime, - //Do nothing for the end position - SeekType.NONE, -1); - }//GEN-LAST:event_fastForwardButtonActionPerformed + long newTime; + if (currentTime + fastForwardDelta >= duration) { + //If there are less than 30 seconds left, only fast forward to the midpoint. + newTime = currentTime + (duration - currentTime) / 2; + } else { + newTime = currentTime + fastForwardDelta; + } - private void playButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playButtonActionPerformed - if (gstPlayBin.isPlaying()) { - gstPlayBin.pause(); - } else { - double playBackRate = getPlayBackRate(); - long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - //Set playback rate before play. - gstPlayBin.seek(playBackRate, - Format.TIME, - //FLUSH - flushes the pipeline - //ACCURATE - video will seek exactly to the position requested - EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), - //Set the start position to newTime - SeekType.SET, currentTime, - //Do nothing for the end position - SeekType.NONE, -1); - gstPlayBin.play(); - } - }//GEN-LAST:event_playButtonActionPerformed + double playBackRate = getPlayBackRate(); + gstPlayBin.seek(playBackRate, + Format.TIME, + //FLUSH - flushes the pipeline + //ACCURATE - video will seek exactly to the position requested + EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), + //Set the start position to newTime + SeekType.SET, newTime, + //Do nothing for the end position + SeekType.NONE, -1); + } + }); + } - private void playBackSpeedComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playBackSpeedComboBoxActionPerformed - double playBackRate = getPlayBackRate(); - long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - gstPlayBin.seek(playBackRate, - Format.TIME, - //FLUSH - flushes the pipeline - //ACCURATE - video will seek exactly to the position requested - EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), - //Set the position to the currentTime, we are only adjusting the - //playback rate. - SeekType.SET, currentTime, - SeekType.NONE, 0); - }//GEN-LAST:event_playBackSpeedComboBoxActionPerformed + private void playButtonActionPerformed(java.awt.event.ActionEvent evt) { + Gst.getExecutor().submit(() -> { + if (gstPlayBin != null) { + if (gstPlayBin.isPlaying()) { + gstPlayBin.pause(); + } else { + double playBackRate = getPlayBackRate(); + long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + //Set playback rate before play. + gstPlayBin.seek(playBackRate, + Format.TIME, + //FLUSH - flushes the pipeline + //ACCURATE - video will seek exactly to the position requested + EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), + //Set the start position to newTime + SeekType.SET, currentTime, + //Do nothing for the end position + SeekType.NONE, -1); + gstPlayBin.play(); + } + } + }); + } + + private void playBackSpeedComboBoxActionPerformed(java.awt.event.ActionEvent evt) { + Gst.getExecutor().submit(() -> { + if (gstPlayBin != null) { + double playBackRate = getPlayBackRate(); + long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + gstPlayBin.seek(playBackRate, + Format.TIME, + //FLUSH - flushes the pipeline + //ACCURATE - video will seek exactly to the position requested + EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), + //Set the position to the currentTime, we are only adjusting the + //playback rate. + SeekType.SET, currentTime, + SeekType.NONE, 0); + } + }); + } // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JLabel VolumeIcon; diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/Bundle.properties b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/Bundle.properties index 6a74bbcb06..45a1603c17 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/Bundle.properties @@ -1,8 +1,9 @@ -ContextViewer.jSourceGoToResultButton.text=Go to Result -ContextViewer.jSourceTextLabel.text=jLabel2 -ContextViewer.jSourceNameLabel.text=jSourceNameLabel -ContextViewer.jSourceLabel.text=Source -ContextViewer.jUsageGoToResultButton.text=Go to Result -ContextViewer.jUsageLabel.text=Usage -ContextViewer.jUsageNameLabel.text=jSourceNameLabel -ContextViewer.jUsageTextLabel.text=jLabel2 +ContextUsagePanel.jUsageGoToResultButton.text=Go to Result +ContextSourcePanel.jSourceGoToResultButton.text=Go to Result +ContextUsagePanel.jUsageTextLabel.text=Label2 +ContextUsagePanel.jUsageNameLabel.text=jUsageLabel +ContextSourcePanel.jSourceTextLabel.text=Label2 +ContextSourcePanel.jSourceNameLabel.text=jSourceNameLabel +ContextViewer.jSourceLabel.text=Usage +ContextViewer.jUsageLabel.text=Source +ContextViewer.jUnknownLabel.text=Unknown diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/Bundle.properties-MERGED index 76df8c7b3c..c8cebe69fc 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/Bundle.properties-MERGED @@ -1,16 +1,17 @@ +ContextUsagePanel.jUsageGoToResultButton.text=Go to Result +ContextSourcePanel.jSourceGoToResultButton.text=Go to Result +ContextUsagePanel.jUsageTextLabel.text=Label2 +ContextUsagePanel.jUsageNameLabel.text=jUsageLabel +ContextSourcePanel.jSourceTextLabel.text=Label2 +ContextSourcePanel.jSourceNameLabel.text=jSourceNameLabel ContextViewer.attachmentSource=Attached to: ContextViewer.downloadedOn=On ContextViewer.downloadSource=Downloaded from: ContextViewer.downloadURL=URL ContextViewer.email=Email -ContextViewer.jSourceGoToResultButton.text=Go to Result -ContextViewer.jSourceTextLabel.text=jLabel2 -ContextViewer.jSourceNameLabel.text=jSourceNameLabel -ContextViewer.jSourceLabel.text=Source -ContextViewer.jUsageGoToResultButton.text=Go to Result -ContextViewer.jUsageLabel.text=Usage -ContextViewer.jUsageNameLabel.text=jSourceNameLabel -ContextViewer.jUsageTextLabel.text=jLabel2 +ContextViewer.jSourceLabel.text=Usage +ContextViewer.jUsageLabel.text=Source +ContextViewer.jUnknownLabel.text=Unknown ContextViewer.message=Message ContextViewer.messageFrom=From ContextViewer.messageOn=On @@ -20,3 +21,4 @@ ContextViewer.recentDocs=Recent Documents: ContextViewer.title=Context ContextViewer.toolTip=Displays context for selected file. ContextViewer.unknown=Opened at unknown time +ContextViewer.unknownSource=Unknown diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextSourcePanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextSourcePanel.form new file mode 100644 index 0000000000..c57a0990a8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextSourcePanel.form @@ -0,0 +1,82 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextSourcePanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextSourcePanel.java new file mode 100644 index 0000000000..284a66cf04 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextSourcePanel.java @@ -0,0 +1,154 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers.contextviewer; + +import java.util.ArrayList; +import java.util.List; +import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent; +import org.sleuthkit.datamodel.BlackboardArtifact; +import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT; + +/** + * Displays additional context for the selected file, such as its source, and + * usage, if known. + * + */ +public final class ContextSourcePanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + + // defines a list of artifacts that provide context for a file + private static final List SOURCE_CONTEXT_ARTIFACTS = new ArrayList<>(); + static { + SOURCE_CONTEXT_ARTIFACTS.add(TSK_ASSOCIATED_OBJECT); + } + + private final BlackboardArtifact sourceContextArtifact; + + /** + * Creates new form ContextViewer + */ + public ContextSourcePanel(String sourceName, String sourceText, BlackboardArtifact associatedArtifact) { + + initComponents(); + sourceContextArtifact = associatedArtifact; + setSourceName(sourceName); + setSourceText(sourceText); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + jSourceGoToResultButton = new javax.swing.JButton(); + jSourceNameLabel = new javax.swing.JLabel(); + jSourceTextLabel = new javax.swing.JLabel(); + + setBackground(new java.awt.Color(255, 255, 255)); + setPreferredSize(new java.awt.Dimension(495, 75)); + + org.openide.awt.Mnemonics.setLocalizedText(jSourceGoToResultButton, org.openide.util.NbBundle.getMessage(ContextSourcePanel.class, "ContextSourcePanel.jSourceGoToResultButton.text")); // NOI18N + jSourceGoToResultButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jSourceGoToResultButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jSourceNameLabel, org.openide.util.NbBundle.getMessage(ContextSourcePanel.class, "ContextSourcePanel.jSourceNameLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(jSourceTextLabel, org.openide.util.NbBundle.getMessage(ContextSourcePanel.class, "ContextSourcePanel.jSourceTextLabel.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(50, 50, 50) + .addComponent(jSourceNameLabel) + .addGap(36, 36, 36) + .addComponent(jSourceTextLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(260, 260, 260)) + .addGroup(layout.createSequentialGroup() + .addGap(90, 90, 90) + .addComponent(jSourceGoToResultButton) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(2, 2, 2) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jSourceNameLabel) + .addComponent(jSourceTextLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jSourceGoToResultButton) + .addGap(0, 0, 0)) + ); + }// //GEN-END:initComponents + + private void jSourceGoToResultButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jSourceGoToResultButtonActionPerformed + + final DirectoryTreeTopComponent dtc = DirectoryTreeTopComponent.findInstance(); + + // Navigate to the source context artifact. + if (sourceContextArtifact != null) { + dtc.viewArtifact(sourceContextArtifact); + } + }//GEN-LAST:event_jSourceGoToResultButtonActionPerformed + + /** + * Sets the source label string. + * + * @param nameLabel String value for source label. + */ + private void setSourceName(String nameLabel) { + jSourceNameLabel.setText(nameLabel); + } + + /** + * Sets the source text string. + * + * @param text String value for source text. + */ + private void setSourceText(String text) { + jSourceTextLabel.setText(text); + showSourceButton(!text.isEmpty()); + showSourceText(true); + } + + private void showSourceText(boolean show) { + jSourceTextLabel.setVisible(show); + } + + private void showSourceButton(boolean show) { + jSourceGoToResultButton.setVisible(show); + jSourceGoToResultButton.setEnabled(show); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton jSourceGoToResultButton; + private javax.swing.JLabel jSourceNameLabel; + private javax.swing.JLabel jSourceTextLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextUsagePanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextUsagePanel.form new file mode 100644 index 0000000000..a3e2a90f84 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextUsagePanel.form @@ -0,0 +1,81 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextUsagePanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextUsagePanel.java new file mode 100644 index 0000000000..de87d7a5e5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextUsagePanel.java @@ -0,0 +1,152 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers.contextviewer; + +import java.util.ArrayList; +import java.util.List; +import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent; +import org.sleuthkit.datamodel.BlackboardArtifact; +import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT; + +/** + * Displays additional context for the selected file, such as its source, and + * usage, if known. + * + */ +public final class ContextUsagePanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + + // defines a list of artifacts that provide context for a file + private static final List SOURCE_CONTEXT_ARTIFACTS = new ArrayList<>(); + static { + SOURCE_CONTEXT_ARTIFACTS.add(TSK_ASSOCIATED_OBJECT); + } + + private final BlackboardArtifact sourceContextArtifact; + + /** + * Creates new form ContextViewer + */ + public ContextUsagePanel(String sourceName, String sourceText, BlackboardArtifact associatedArtifact) { + + initComponents(); + sourceContextArtifact = associatedArtifact; + setUsageName(sourceName); + setUsageText(sourceText); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + jUsageGoToResultButton = new javax.swing.JButton(); + jUsageNameLabel = new javax.swing.JLabel(); + jUsageTextLabel = new javax.swing.JLabel(); + + setBackground(new java.awt.Color(255, 255, 255)); + setPreferredSize(new java.awt.Dimension(495, 75)); + + org.openide.awt.Mnemonics.setLocalizedText(jUsageGoToResultButton, org.openide.util.NbBundle.getMessage(ContextUsagePanel.class, "ContextUsagePanel.jUsageGoToResultButton.text")); // NOI18N + jUsageGoToResultButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jUsageGoToResultButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jUsageNameLabel, org.openide.util.NbBundle.getMessage(ContextUsagePanel.class, "ContextUsagePanel.jUsageNameLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(jUsageTextLabel, org.openide.util.NbBundle.getMessage(ContextUsagePanel.class, "ContextUsagePanel.jUsageTextLabel.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(50, 50, 50) + .addComponent(jUsageNameLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jUsageTextLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 240, Short.MAX_VALUE) + .addGap(36, 36, 36)) + .addGroup(layout.createSequentialGroup() + .addGap(90, 90, 90) + .addComponent(jUsageGoToResultButton) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(2, 2, 2) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jUsageTextLabel) + .addComponent(jUsageNameLabel, javax.swing.GroupLayout.Alignment.TRAILING)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jUsageGoToResultButton)) + ); + }// //GEN-END:initComponents + + private void jUsageGoToResultButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jUsageGoToResultButtonActionPerformed + final DirectoryTreeTopComponent dtc = DirectoryTreeTopComponent.findInstance(); + + // Navigate to the source context artifact. + if (sourceContextArtifact != null) { + dtc.viewArtifact(sourceContextArtifact); + } + }//GEN-LAST:event_jUsageGoToResultButtonActionPerformed + + /** + * Sets the usage label string. + * + * @param nameLabel String value for usage label. + */ + private void setUsageName(String nameLabel) { + jUsageNameLabel.setText(nameLabel); + } + + /** + * Sets the Usage text string. + * + * @param text String value for Usage text. + */ + private void setUsageText(String text) { + jUsageTextLabel.setText(text); + showUsageButton(!text.isEmpty()); + showUsageText(true); + } + + private void showUsageText(boolean show) { + jUsageTextLabel.setVisible(show); + } + + private void showUsageButton(boolean show) { + jUsageGoToResultButton.setVisible(show); + jUsageGoToResultButton.setEnabled(show); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton jUsageGoToResultButton; + private javax.swing.JLabel jUsageNameLabel; + private javax.swing.JLabel jUsageTextLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextViewer.form b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextViewer.form index 020705e938..42c7b19f1a 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextViewer.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextViewer.form @@ -1,10 +1,144 @@
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -21,138 +155,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextViewer.java index 0c457be704..f6d7e8f366 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/contextviewer/ContextViewer.java @@ -24,6 +24,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; +import javax.swing.BoxLayout; +import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED; import org.apache.commons.lang.StringUtils; import org.openide.nodes.Node; import org.openide.util.NbBundle; @@ -32,7 +34,6 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT; @@ -55,19 +56,20 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte // defines a list of artifacts that provide context for a file private static final List SOURCE_CONTEXT_ARTIFACTS = new ArrayList<>(); + private final List contextSourcePanels = new ArrayList<>(); + private final List contextUsagePanels = new ArrayList<>(); static { SOURCE_CONTEXT_ARTIFACTS.add(TSK_ASSOCIATED_OBJECT); } - private BlackboardArtifact sourceContextArtifact; - /** * Creates new form ContextViewer */ public ContextViewer() { initComponents(); + jScrollPane.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED); } /** @@ -79,119 +81,96 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte // //GEN-BEGIN:initComponents private void initComponents() { - jSourceGoToResultButton = new javax.swing.JButton(); - jSourceLabel = new javax.swing.JLabel(); - jSourceNameLabel = new javax.swing.JLabel(); - jSourceTextLabel = new javax.swing.JLabel(); - jUsageGoToResultButton = new javax.swing.JButton(); - jUsageLabel = new javax.swing.JLabel(); - jUsageNameLabel = new javax.swing.JLabel(); - jUsageTextLabel = new javax.swing.JLabel(); + jSourcePanel = new javax.swing.JPanel(); + javax.swing.JLabel jSourceLabel = new javax.swing.JLabel(); + jUsagePanel = new javax.swing.JPanel(); + javax.swing.JLabel jUsageLabel = new javax.swing.JLabel(); + jUnknownPanel = new javax.swing.JPanel(); + javax.swing.JLabel jUnknownLabel = new javax.swing.JLabel(); + jScrollPane = new javax.swing.JScrollPane(); - setBackground(new java.awt.Color(255, 255, 255)); - - org.openide.awt.Mnemonics.setLocalizedText(jSourceGoToResultButton, org.openide.util.NbBundle.getMessage(ContextViewer.class, "ContextViewer.jSourceGoToResultButton.text")); // NOI18N - jSourceGoToResultButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - jSourceGoToResultButtonActionPerformed(evt); - } - }); + jSourcePanel.setBackground(javax.swing.UIManager.getDefaults().getColor("window")); jSourceLabel.setFont(new java.awt.Font("Dialog", 1, 14)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(jSourceLabel, org.openide.util.NbBundle.getMessage(ContextViewer.class, "ContextViewer.jSourceLabel.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(jSourceNameLabel, org.openide.util.NbBundle.getMessage(ContextViewer.class, "ContextViewer.jSourceNameLabel.text")); // NOI18N + javax.swing.GroupLayout jSourcePanelLayout = new javax.swing.GroupLayout(jSourcePanel); + jSourcePanel.setLayout(jSourcePanelLayout); + jSourcePanelLayout.setHorizontalGroup( + jSourcePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jSourcePanelLayout.createSequentialGroup() + .addGap(40, 40, 40) + .addComponent(jSourceLabel) + .addContainerGap(304, Short.MAX_VALUE)) + ); + jSourcePanelLayout.setVerticalGroup( + jSourcePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jSourcePanelLayout.createSequentialGroup() + .addGap(5, 5, 5) + .addComponent(jSourceLabel) + .addGap(2, 2, 2)) + ); - org.openide.awt.Mnemonics.setLocalizedText(jSourceTextLabel, org.openide.util.NbBundle.getMessage(ContextViewer.class, "ContextViewer.jSourceTextLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(jUsageGoToResultButton, org.openide.util.NbBundle.getMessage(ContextViewer.class, "ContextViewer.jUsageGoToResultButton.text")); // NOI18N - jUsageGoToResultButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - jUsageGoToResultButtonActionPerformed(evt); - } - }); + jUsagePanel.setBackground(javax.swing.UIManager.getDefaults().getColor("window")); jUsageLabel.setFont(new java.awt.Font("Dialog", 1, 14)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(jUsageLabel, org.openide.util.NbBundle.getMessage(ContextViewer.class, "ContextViewer.jUsageLabel.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(jUsageNameLabel, org.openide.util.NbBundle.getMessage(ContextViewer.class, "ContextViewer.jUsageNameLabel.text")); // NOI18N + javax.swing.GroupLayout jUsagePanelLayout = new javax.swing.GroupLayout(jUsagePanel); + jUsagePanel.setLayout(jUsagePanelLayout); + jUsagePanelLayout.setHorizontalGroup( + jUsagePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jUsagePanelLayout.createSequentialGroup() + .addGap(40, 40, 40) + .addComponent(jUsageLabel) + .addContainerGap(298, Short.MAX_VALUE)) + ); + jUsagePanelLayout.setVerticalGroup( + jUsagePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jUsagePanelLayout.createSequentialGroup() + .addGap(2, 2, 2) + .addComponent(jUsageLabel) + .addGap(2, 2, 2)) + ); - org.openide.awt.Mnemonics.setLocalizedText(jUsageTextLabel, org.openide.util.NbBundle.getMessage(ContextViewer.class, "ContextViewer.jUsageTextLabel.text")); // NOI18N + jUnknownPanel.setBackground(new java.awt.Color(255, 255, 255)); + + org.openide.awt.Mnemonics.setLocalizedText(jUnknownLabel, org.openide.util.NbBundle.getMessage(ContextViewer.class, "ContextViewer.jUnknownLabel.text")); // NOI18N + + javax.swing.GroupLayout jUnknownPanelLayout = new javax.swing.GroupLayout(jUnknownPanel); + jUnknownPanel.setLayout(jUnknownPanelLayout); + jUnknownPanelLayout.setHorizontalGroup( + jUnknownPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jUnknownPanelLayout.createSequentialGroup() + .addGap(50, 50, 50) + .addComponent(jUnknownLabel) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jUnknownPanelLayout.setVerticalGroup( + jUnknownPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jUnknownPanelLayout.createSequentialGroup() + .addGap(2, 2, 2) + .addComponent(jUnknownLabel) + .addGap(2, 2, 2)) + ); + + setBackground(new java.awt.Color(255, 255, 255)); + setPreferredSize(new java.awt.Dimension(495, 358)); + + jScrollPane.setBackground(new java.awt.Color(255, 255, 255)); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jSourceLabel) - .addComponent(jUsageLabel) - .addGroup(layout.createSequentialGroup() - .addGap(6, 6, 6) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(jSourceNameLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jSourceTextLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 192, Short.MAX_VALUE)) - .addGroup(layout.createSequentialGroup() - .addComponent(jUsageNameLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jUsageTextLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 192, Short.MAX_VALUE))))) - .addGap(36, 36, 36)) - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(45, 45, 45) - .addComponent(jUsageGoToResultButton)) - .addGroup(layout.createSequentialGroup() - .addGap(46, 46, 46) - .addComponent(jSourceGoToResultButton))) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(jScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 509, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(jSourceLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jSourceNameLabel) - .addComponent(jSourceTextLabel)) - .addGap(18, 18, 18) - .addComponent(jSourceGoToResultButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jUsageLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jUsageNameLabel) - .addComponent(jUsageTextLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jUsageGoToResultButton) - .addGap(0, 62, Short.MAX_VALUE)) + .addComponent(jScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 335, Short.MAX_VALUE) ); }// //GEN-END:initComponents - private void jSourceGoToResultButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jSourceGoToResultButtonActionPerformed - - final DirectoryTreeTopComponent dtc = DirectoryTreeTopComponent.findInstance(); - - // Navigate to the source context artifact. - if (sourceContextArtifact != null) { - dtc.viewArtifact(sourceContextArtifact); - } - - }//GEN-LAST:event_jSourceGoToResultButtonActionPerformed - - private void jUsageGoToResultButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jUsageGoToResultButtonActionPerformed - final DirectoryTreeTopComponent dtc = DirectoryTreeTopComponent.findInstance(); - - // Navigate to the source context artifact. - if (sourceContextArtifact != null) { - dtc.viewArtifact(sourceContextArtifact); - } - }//GEN-LAST:event_jUsageGoToResultButtonActionPerformed - @Override public void setNode(Node selectedNode) { if ((selectedNode == null) || (!isSupported(selectedNode))) { @@ -234,9 +213,8 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte @Override public void resetComponent() { - jSourceGoToResultButton.setVisible(false); - setSourceName(""); - setSourceText(""); + contextSourcePanels.clear(); + contextUsagePanels.clear(); } @Override @@ -268,6 +246,9 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte return 1; } + @NbBundle.Messages({ + "ContextViewer.unknownSource=Unknown ", + }) /** * Looks for context providing artifacts for the given file and populates * the source context. @@ -280,7 +261,7 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte private void populateSourceContextData(AbstractFile sourceFile) throws NoCurrentCaseException, TskCoreException { SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - + // Check for all context artifacts boolean foundASource = false; for (BlackboardArtifact.ARTIFACT_TYPE artifactType : SOURCE_CONTEXT_ARTIFACTS) { @@ -291,15 +272,33 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte addSourceEntry(contextArtifact); } } - jSourceGoToResultButton.setVisible(true); - jUsageGoToResultButton.setVisible(true); - if (foundASource == false) { - setSourceName("Unknown"); - showSourceText(false); - setUsageName("Unknown"); - showUsageText(false); + javax.swing.JPanel contextContainer = new javax.swing.JPanel(); + contextContainer.add(jSourcePanel); + contextContainer.setLayout(new BoxLayout(contextContainer, BoxLayout.Y_AXIS)); + if (contextSourcePanels.isEmpty()) { + contextContainer.add(jUnknownPanel); + } else { + for (javax.swing.JPanel sourcePanel : contextSourcePanels) { + contextContainer.add(sourcePanel); + } } - + contextContainer.add(jUsagePanel); + if (contextUsagePanels.isEmpty()) { + contextContainer.add(jUnknownPanel); + } else { + for (javax.swing.JPanel usagePanel : contextUsagePanels) { + contextContainer.add(usagePanel); + } + } + contextContainer.setEnabled(foundASource); + contextContainer.setVisible(foundASource); + jScrollPane.getViewport().setView(contextContainer); + jScrollPane.setEnabled(foundASource); + jScrollPane.setVisible(foundASource); + jScrollPane.repaint(); + jScrollPane.revalidate(); + + } /** @@ -312,10 +311,6 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte * @throws TskCoreException */ private void addSourceEntry(BlackboardArtifact artifact) throws TskCoreException { - setSourceName("Unknown"); - showSourceText(false); - setUsageName("Unknown"); - showUsageText(false); if (BlackboardArtifact.ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT.getTypeID() == artifact.getArtifactTypeID()) { BlackboardAttribute associatedArtifactAttribute = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT)); @@ -323,9 +318,6 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte long artifactId = associatedArtifactAttribute.getValueLong(); BlackboardArtifact associatedArtifact = artifact.getSleuthkitCase().getBlackboardArtifact(artifactId); - //save the artifact for "Go to Result" button - sourceContextArtifact = associatedArtifact; - setSourceFields(associatedArtifact); } } @@ -347,74 +339,27 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte private void setSourceFields(BlackboardArtifact associatedArtifact) throws TskCoreException { if (BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE.getTypeID() == associatedArtifact.getArtifactTypeID() || BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID() == associatedArtifact.getArtifactTypeID()) { - - setSourceName(Bundle.ContextViewer_attachmentSource()); - setSourceText(msgArtifactToAbbreviatedString(associatedArtifact)); + String sourceName = Bundle.ContextViewer_attachmentSource(); + String sourceText = msgArtifactToAbbreviatedString(associatedArtifact); + javax.swing.JPanel sourcePanel = new ContextSourcePanel(sourceName, sourceText, associatedArtifact); + contextSourcePanels.add(sourcePanel); } else if (BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID() == associatedArtifact.getArtifactTypeID() || BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE.getTypeID() == associatedArtifact.getArtifactTypeID()) { + String sourceName = Bundle.ContextViewer_downloadSource(); + String sourceText = webDownloadArtifactToString(associatedArtifact); + javax.swing.JPanel sourcePanel = new ContextSourcePanel(sourceName, sourceText, associatedArtifact); + contextSourcePanels.add(sourcePanel); - setSourceName(Bundle.ContextViewer_downloadSource()); - setSourceText(webDownloadArtifactToString(associatedArtifact)); } else if (BlackboardArtifact.ARTIFACT_TYPE.TSK_RECENT_OBJECT.getTypeID() == associatedArtifact.getArtifactTypeID()) { - setUsageName(Bundle.ContextViewer_recentDocs()); - setUsageText(recentDocArtifactToString(associatedArtifact)); + String sourceName = Bundle.ContextViewer_recentDocs(); + String sourceText = recentDocArtifactToString(associatedArtifact); + javax.swing.JPanel usagePanel = new ContextUsagePanel(sourceName, sourceText, associatedArtifact); + contextUsagePanels.add(usagePanel); } } - /** - * Sets the source label string. - * - * @param nameLabel String value for source label. - */ - private void setSourceName(String nameLabel) { - jSourceNameLabel.setText(nameLabel); - } - - /** - * Sets the usage label string. - * - * @param nameLabel String value for usage label. - */ - private void setUsageName(String nameLabel) { - jUsageNameLabel.setText(nameLabel); - } - - /** - * Sets the source text string. - * - * @param text String value for source text. - */ - private void setSourceText(String text) { - jSourceTextLabel.setText(text); - showSourceText(!text.isEmpty()); - } - - private void showSourceText(boolean show) { - jSourceTextLabel.setVisible(show); - jSourceGoToResultButton.setEnabled(show); - jSourceLabel.setVisible(show); - jUsageLabel.setVisible(show); - } - - /** - * Sets the Usage text string. - * - * @param text String value for Usage text. - */ - private void setUsageText(String text) { - jUsageTextLabel.setText(text); - showUsageText(!text.isEmpty()); - } - - private void showUsageText(boolean show) { - jUsageTextLabel.setVisible(show); - jUsageGoToResultButton.setEnabled(show); - jUsageLabel.setVisible(show); - jSourceLabel.setVisible(show); - } - /** * Returns a display string with download source URL from the given * artifact. @@ -554,13 +499,9 @@ public final class ContextViewer extends javax.swing.JPanel implements DataConte // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton jSourceGoToResultButton; - private javax.swing.JLabel jSourceLabel; - private javax.swing.JLabel jSourceNameLabel; - private javax.swing.JLabel jSourceTextLabel; - private javax.swing.JButton jUsageGoToResultButton; - private javax.swing.JLabel jUsageLabel; - private javax.swing.JLabel jUsageNameLabel; - private javax.swing.JLabel jUsageTextLabel; + private javax.swing.JScrollPane jScrollPane; + private javax.swing.JPanel jSourcePanel; + private javax.swing.JPanel jUnknownPanel; + private javax.swing.JPanel jUsagePanel; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/utils/GstLoader.java b/Core/src/org/sleuthkit/autopsy/contentviewers/utils/GstLoader.java new file mode 100755 index 0000000000..b6804d488a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/utils/GstLoader.java @@ -0,0 +1,74 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sleuthkit.autopsy.contentviewers.utils; + +import java.util.logging.Level; +import org.freedesktop.gstreamer.Gst; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * A utility class that loads the gstreamer bindings. + */ +public final class GstLoader { + + private static final Logger logger = Logger.getLogger(GstLoader.class.getName()); + private static GstStatus status; + + /** + * Attempts to load the gstreamer bindings. Only one attempt will be + * performed per Autopsy process. Clients should not attempt to interact + * with the gstreamer bindings unless the load was successful. + * + * @return Status - SUCCESS or FAILURE + */ + public synchronized static GstStatus tryLoad() { + // Null is our 'unknown' status. Prior to the first call, the status + // is unknown. + if (status != null) { + return status; + } + + try { + // Setting the following property causes the GST + // Java bindings to call dispose() on the GST + // service thread instead of running it in the GST + // Native Object Reaper thread. + System.setProperty("glib.reapOnEDT", "true"); + Gst.init(); + status = GstStatus.SUCCESS; + } catch (Throwable ex) { + status = GstStatus.FAILURE; + logger.log(Level.WARNING, "Failed to load gsteamer bindings", ex); + } + + return status; + } + + /** + * The various init statuses that tryLoad can return. + */ + public enum GstStatus { + SUCCESS, FAILURE + } + + private GstLoader() { + + } +} diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java index 8747d49795..67ce973d3b 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java @@ -27,6 +27,7 @@ import javax.swing.event.DocumentListener; import org.openide.util.NbBundle; import org.sleuthkit.datamodel.CaseDbConnectionInfo; import org.sleuthkit.datamodel.TskData.DbType; +import org.sleuthkit.autopsy.centralrepository.optionspanel.GlobalSettingsPanel; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.events.MessageServiceConnectionInfo; import org.sleuthkit.autopsy.coreutils.Logger; @@ -683,56 +684,93 @@ public final class MultiUserSettingsPanel extends javax.swing.JPanel { boolean isPwSet = (tbMsgPassword.getPassword().length != 0); return (isUserSet == isPwSet); } + + void store() { + boolean prevSelected = UserPreferences.getIsMultiUserModeEnabled(); + CaseDbConnectionInfo prevConn = null; + try { + prevConn = UserPreferences.getDatabaseConnectionInfo(); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "There was an error while fetching previous connection settings while trying to save", ex); //NON-NLS + } boolean multiUserCasesEnabled = cbEnableMultiUser.isSelected(); UserPreferences.setIsMultiUserModeEnabled(multiUserCasesEnabled); - if (multiUserCasesEnabled == false) { - return; + + CaseDbConnectionInfo info = null; + + if (multiUserCasesEnabled == true) { + + /* + * Currently only supporting multi-user cases with PostgreSQL case + * databases. + */ + DbType dbType = DbType.POSTGRESQL; + info = new CaseDbConnectionInfo( + tbDbHostname.getText().trim(), + tbDbPort.getText().trim(), + tbDbUsername.getText().trim(), + new String(tbDbPassword.getPassword()), + dbType); + try { + UserPreferences.setDatabaseConnectionInfo(info); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error saving case database connection info", ex); //NON-NLS + } + + int msgServicePort = 0; + try { + msgServicePort = Integer.parseInt(this.tbMsgPort.getText().trim()); + } catch (NumberFormatException ex) { + logger.log(Level.SEVERE, "Could not parse messaging service port setting", ex); + } + + MessageServiceConnectionInfo msgServiceInfo = new MessageServiceConnectionInfo( + tbMsgHostname.getText().trim(), + msgServicePort, + tbMsgUsername.getText().trim(), + new String(tbMsgPassword.getPassword())); + + try { + UserPreferences.setMessageServiceConnectionInfo(msgServiceInfo); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error saving messaging service connection info", ex); //NON-NLS + } + + UserPreferences.setIndexingServerHost(tbSolrHostname.getText().trim()); + UserPreferences.setIndexingServerPort(Integer.parseInt(tbSolrPort.getText().trim())); } - /* - * Currently only supporting multi-user cases with PostgreSQL case - * databases. - */ - DbType dbType = DbType.POSTGRESQL; - CaseDbConnectionInfo info = new CaseDbConnectionInfo( - tbDbHostname.getText().trim(), - tbDbPort.getText().trim(), - tbDbUsername.getText().trim(), - new String(tbDbPassword.getPassword()), - dbType); - try { - UserPreferences.setDatabaseConnectionInfo(info); - } catch (UserPreferencesException ex) { - logger.log(Level.SEVERE, "Error saving case database connection info", ex); //NON-NLS - } - - int msgServicePort = 0; - try { - msgServicePort = Integer.parseInt(this.tbMsgPort.getText().trim()); - } catch (NumberFormatException ex) { - logger.log(Level.SEVERE, "Could not parse messaging service port setting", ex); - } - - MessageServiceConnectionInfo msgServiceInfo = new MessageServiceConnectionInfo( - tbMsgHostname.getText().trim(), - msgServicePort, - tbMsgUsername.getText().trim(), - new String(tbMsgPassword.getPassword())); - - try { - UserPreferences.setMessageServiceConnectionInfo(msgServiceInfo); - } catch (UserPreferencesException ex) { - logger.log(Level.SEVERE, "Error saving messaging service connection info", ex); //NON-NLS - } - - UserPreferences.setIndexingServerHost(tbSolrHostname.getText().trim()); - UserPreferences.setIndexingServerPort(Integer.parseInt(tbSolrPort.getText().trim())); - + // trigger changes to whether or not user can use multi user settings for central repository + if (prevSelected != multiUserCasesEnabled || !areCaseDbConnectionEqual(prevConn, info)) + GlobalSettingsPanel.onMultiUserChange(this, prevSelected, multiUserCasesEnabled); } + private static boolean arePropsEqual(Object a, Object b) { + if (a == null || b == null) { + return (a == null && b == null); + } + else { + return a.equals(b); + } + } + + private static boolean areCaseDbConnectionEqual(CaseDbConnectionInfo a, CaseDbConnectionInfo b) { + if (a == null || b == null) { + return (a == null && b == null); + } + + return + arePropsEqual(a.getDbType(), b.getDbType()) && + arePropsEqual(a.getHost(), b.getHost()) && + arePropsEqual(a.getPassword(), b.getPassword()) && + arePropsEqual(a.getPort(), b.getPort()) && + arePropsEqual(a.getUserName(), b.getUserName()); + } + + /** * Validates that the form is filled out correctly for our usage. * diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index 1d53e46ca6..74ac603e0b 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.datamodel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; @@ -28,7 +29,6 @@ import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.stream.Collectors; -import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Sheet; @@ -66,6 +66,7 @@ import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.texttranslation.utils.FileNameTranslationUtil; /** * An abstract node that encapsulates AbstractFile data @@ -94,15 +95,15 @@ public abstract class AbstractAbstractFileNode extends A IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, weakPcl); } } - + try { //See JIRA-5971 //Attempt to cache file path during construction of this UI component. this.content.getUniquePath(); } catch (TskCoreException ex) { logger.log(Level.SEVERE, String.format("Failed attempt to cache the " - + "unique path of the abstract file instance. Name: %s (objID=%d)", - this.content.getName(), this.content.getId()), ex); + + "unique path of the abstract file instance. Name: %s (objID=%d)", + this.content.getName(), this.content.getId()), ex); } if (TextTranslationService.getInstance().hasProvider() && UserPreferences.displayTranslatedFileNames()) { @@ -490,39 +491,18 @@ public abstract class AbstractAbstractFileNode extends A } /** - * Translates this nodes content name. Doesn't attempt translation if the - * name is in english or if there is now translation service available. + * Translates the name of the file this node represents. An empty string + * will be returned if the translation fails for any reason. + * + * @return The translated file name or the empty string. */ String getTranslatedFileName() { - //If already in complete English, don't translate. - if (content.getName().matches("^\\p{ASCII}+$")) { + try { + return FileNameTranslationUtil.translate(content.getName()); + } catch (NoServiceProviderException | TranslationException ex) { + logger.log(Level.WARNING, MessageFormat.format("Error translating file name (objID={0}))", content.getId()), ex); return ""; } - TextTranslationService tts = TextTranslationService.getInstance(); - if (tts.hasProvider()) { - //Seperate out the base and ext from the contents file name. - String base = FilenameUtils.getBaseName(content.getName()); - try { - String translation = tts.translate(base); - String ext = FilenameUtils.getExtension(content.getName()); - - //If we have no extension, then we shouldn't add the . - String extensionDelimiter = (ext.isEmpty()) ? "" : "."; - - //Talk directly to this nodes pcl, fire an update when the translation - //is complete. - if (!translation.isEmpty()) { - return translation + extensionDelimiter + ext; - } - } catch (NoServiceProviderException noServiceEx) { - logger.log(Level.WARNING, "Translate unsuccessful because no TextTranslator " - + "implementation was provided.", noServiceEx.getMessage()); - } catch (TranslationException noTranslationEx) { - logger.log(Level.WARNING, "Could not successfully translate file name " - + content.getName(), noTranslationEx.getMessage()); - } - } - return ""; } /** diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index 17b8248f06..8358dd7d34 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -60,7 +60,6 @@ import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.Score; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import static org.sleuthkit.autopsy.datamodel.DisplayableItemNode.findLinked; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.HasCommentStatus; import static org.sleuthkit.autopsy.datamodel.AbstractContentNode.backgroundTasksPool; @@ -77,47 +76,68 @@ import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import static org.sleuthkit.autopsy.datamodel.AbstractContentNode.NO_DESCR; +import org.sleuthkit.autopsy.texttranslation.TextTranslationService; +import org.sleuthkit.autopsy.datamodel.utils.FileNameTransTask; /** - * Node wrapping a blackboard artifact object. This is generated from several - * places in the tree. + * A BlackboardArtifactNode is an AbstractNode implementation that can be used + * to represent an artifact of any type. */ public class BlackboardArtifactNode extends AbstractContentNode { private static final Logger logger = Logger.getLogger(BlackboardArtifactNode.class.getName()); - private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED, + + /* + * Cache of Content objects used to avoid repeated trips to the case + * database to retrieve Content objects that are the source of multiple + * artifacts. + */ + private static final Cache contentCache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(); + + /* + * Case events that indicate an update to the node's property sheet may be + * required. + */ + private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of( + Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED, Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED, Case.Events.CONTENT_TAG_ADDED, Case.Events.CONTENT_TAG_DELETED, - Case.Events.CURRENT_CASE, - Case.Events.CR_COMMENT_CHANGED); - - private static Cache contentCache = CacheBuilder.newBuilder() - .expireAfterWrite(1, TimeUnit.MINUTES). - build(); - - private final BlackboardArtifact artifact; - private Content associated = null; - - private List> customProperties; + Case.Events.CR_COMMENT_CHANGED, + Case.Events.CURRENT_CASE); /* - * Artifact types which should have the full unique path of the associated - * content as a property. + * Artifact types for which the unique path of the artifact's source content + * should be displayed in the node's property sheet. */ private static final Integer[] SHOW_UNIQUE_PATH = new Integer[]{ BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID(), BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID(), BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(), - BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID(),}; + BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID() + }; - // TODO (RC): This is an unattractive alternative to subclassing BlackboardArtifactNode, - // cut from the same cloth as the equally unattractive SHOW_UNIQUE_PATH array - // above. It should be removed when and if the subclassing is implemented. + /* + * Artifact types for which the file metadata of the artifact's source file + * should be displayed in the node's property sheet. + */ private static final Integer[] SHOW_FILE_METADATA = new Integer[]{ - BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(),}; + BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() + }; - private final PropertyChangeListener pcl = new PropertyChangeListener() { + private final BlackboardArtifact artifact; + private Content srcContent; + private volatile String translatedSourceName; + + /* + * A method has been provided to allow the injection of properties into this + * node for display in the node's property sheet, independent of the + * artifact the node represents. + */ + private List> customProperties; + + private final PropertyChangeListener listener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); @@ -133,154 +153,218 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), scoData.getScoreAndDescription().getRight(), scoData.getScoreAndDescription().getLeft())); + updateSheet(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_score_name(), + Bundle.BlackboardArtifactNode_createSheet_score_displayName(), + scoData.getScoreAndDescription().getRight(), + scoData.getScoreAndDescription().getLeft())); } if (scoData.getComment() != null) { - updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, scoData.getComment())); + updateSheet(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_comment_name(), + Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), + NO_DESCR, scoData.getComment())); } if (scoData.getCountAndDescription() != null) { - updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft())); + updateSheet(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_count_name(), + Bundle.BlackboardArtifactNode_createSheet_count_displayName(), + scoData.getCountAndDescription().getRight(), + scoData.getCountAndDescription().getLeft())); } + } else if (eventType.equals(FileNameTransTask.getPropertyName())) { + /* + * Replace the value of the Source File property with the + * translated name via setDisplayName (see note in createSheet), + * and put the untranslated name in the Original Name property + * and in the tooltip. + */ + String originalName = evt.getOldValue().toString(); + translatedSourceName = evt.getNewValue().toString(); + setDisplayName(translatedSourceName); + setShortDescription(originalName); + updateSheet(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_srcFile_origName(), + Bundle.BlackboardArtifactNode_createSheet_srcFile_origDisplayName(), + NO_DESCR, + originalName)); } } }; - /** - * We pass a weak reference wrapper around the listener to the event - * publisher. This allows Netbeans to delete the node when the user - * navigates to another part of the tree (previously, nodes were not being - * deleted because the event publisher was holding onto a strong reference - * to the listener. We need to hold onto the weak reference here to support - * unregistering of the listener in removeListeners() below. + /* + * The node's event listener is wrapped in a weak reference that allows the + * node to be garbage collected when the NetBeans infrastructure discards + * it. If this is not done, it has been shown that strong references to the + * listener held by event publishers prevents garbage collection of this + * node. */ - private final PropertyChangeListener weakPcl = WeakListeners.propertyChange(pcl, null); + private final PropertyChangeListener weakListener = WeakListeners.propertyChange(listener, null); /** - * Construct blackboard artifact node from an artifact, overriding the - * standard icon with the one at the path provided. + * Constructs a BlackboardArtifactNode, an AbstractNode implementation that + * can be used to represent an artifact of any type. * - * - * @param artifact artifact to encapsulate - * @param iconPath icon to use for the artifact + * @param artifact The artifact to represent. + * @param iconPath The path to the icon for the artifact type. */ public BlackboardArtifactNode(BlackboardArtifact artifact, String iconPath) { super(artifact, createLookup(artifact)); - this.artifact = artifact; - - // Look for associated Content i.e. the source file for the artifact for (Content lookupContent : this.getLookup().lookupAll(Content.class)) { if ((lookupContent != null) && (!(lookupContent instanceof BlackboardArtifact))) { - this.associated = lookupContent; - + srcContent = lookupContent; try { - //See JIRA-5971 - //Attempt to cache file path during construction of this UI component. - this.associated.getUniquePath(); + /* + * Calling this getter causes the unique path of the source + * content to be cached in the Content object. This is + * advantageous as long as this node is constructed in a + * background thread instead of a UI thread. + */ + srcContent.getUniquePath(); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Failed attempt to cache the " - + "unique path of the associated content instance. Name: %s (objID=%d)", - this.associated.getName(), this.associated.getId()), ex); + logger.log(Level.WARNING, MessageFormat.format("Error getting the unique path of the source content (artifact objID={0})", artifact.getId()), ex); } break; } } - - this.setName(Long.toString(artifact.getArtifactID())); - this.setDisplayName(); - this.setIconBaseWithExtension(iconPath); - Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); + if (srcContent == null) { + throw new IllegalArgumentException(MessageFormat.format("Artifact missing source content (artifact objID={0})", artifact)); + } + setName(Long.toString(artifact.getArtifactID())); + String displayName = srcContent.getName(); + setDisplayName(displayName); + setShortDescription(displayName); + setIconBaseWithExtension(iconPath); + Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakListener); } /** - * Construct blackboard artifact node from an artifact and using default - * icon for artifact type + * Constructs a BlackboardArtifactNode, an AbstractNode implementation that + * can be used to represent an artifact of any type. * - * @param artifact artifact to encapsulate + * @param artifact The artifact to represent. */ public BlackboardArtifactNode(BlackboardArtifact artifact) { this(artifact, ExtractedContent.getIconFilePath(artifact.getArtifactTypeID())); } /** - * The finalizer removes event listeners as the BlackboardArtifactNode is - * being garbage collected. Yes, we know that finalizers are considered to - * be "bad" but since the alternative also relies on garbage collection - * being run and we know that finalize will be called when the object is - * being GC'd it seems like this is a reasonable solution. + * Creates a Lookup object for this node and populates it with both the + * artifact this node represents and its source content. + * + * @param artifact The artifact this node represents. + * + * @return The Lookup. + */ + private static Lookup createLookup(BlackboardArtifact artifact) { + final long objectID = artifact.getObjectID(); + try { + Content content = contentCache.get(objectID, () -> artifact.getSleuthkitCase().getContentById(objectID)); + if (content == null) { + return Lookups.fixed(artifact); + } else { + return Lookups.fixed(artifact, content); + } + } catch (ExecutionException ex) { + logger.log(Level.SEVERE, MessageFormat.format("Error getting source content (artifact objID={0}", artifact.getId()), ex); //NON-NLS + return Lookups.fixed(artifact); + } + } + + /** + * Unregisters the application event listener when this node is garbage + * collected, if this finalizer is actually called. + * + * RC: Isn't there some node lifecycle property change event that could be + * used to unregister the listener instead? * * @throws Throwable */ @Override protected void finalize() throws Throwable { super.finalize(); - removeListeners(); + unregisterListener(); } - private void removeListeners() { - Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); + /** + * Unregisters this node's application event listener. + */ + private void unregisterListener() { + Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakListener); } + /** + * Gets the artifact represented by this node. + * + * @return The artifact. + */ public BlackboardArtifact getArtifact() { return this.artifact; } @Override - @NbBundle.Messages({ - "BlackboardArtifactNode.getAction.errorTitle=Error getting actions", - "BlackboardArtifactNode.getAction.resultErrorMessage=There was a problem getting actions for the selected result." - + " The 'View Result in Timeline' action will not be available.", - "BlackboardArtifactNode.getAction.linkedFileMessage=There was a problem getting actions for the selected result. " - + " The 'View File in Timeline' action will not be available."}) public Action[] getActions(boolean context) { List actionsList = new ArrayList<>(); actionsList.addAll(Arrays.asList(super.getActions(context))); - AbstractFile file = getLookup().lookup(AbstractFile.class); - //if this artifact has a time stamp add the action to view it in the timeline + /* + * If the artifact represented by this node has a timestamp, add an + * action to view it in the timeline. + */ try { if (ViewArtifactInTimelineAction.hasSupportedTimeStamp(artifact)) { actionsList.add(new ViewArtifactInTimelineAction(artifact)); } } catch (TskCoreException ex) { - logger.log(Level.SEVERE, MessageFormat.format("Error getting arttribute(s) from blackboard artifact{0}.", artifact.getArtifactID()), ex); //NON-NLS - MessageNotifyUtil.Notify.error(Bundle.BlackboardArtifactNode_getAction_errorTitle(), Bundle.BlackboardArtifactNode_getAction_resultErrorMessage()); + logger.log(Level.SEVERE, MessageFormat.format("Error getting artifact timestamp (artifact objID={0})", artifact.getId()), ex); //NON-NLS } - // if the artifact links to another file, add an action to go to that file + /* + * If the artifact represented by this node is linked to a file via a + * TSK_PATH_ID attribute, add an action to view the file in the + * timeline. + */ try { - AbstractFile c = findLinked(artifact); - if (c != null) { - actionsList.add(ViewFileInTimelineAction.createViewFileAction(c)); + AbstractFile linkedFile = findLinked(artifact); + if (linkedFile != null) { + actionsList.add(ViewFileInTimelineAction.createViewFileAction(linkedFile)); } } catch (TskCoreException ex) { - logger.log(Level.SEVERE, MessageFormat.format("Error getting linked file from blackboard artifact{0}.", artifact.getArtifactID()), ex); //NON-NLS - MessageNotifyUtil.Notify.error(Bundle.BlackboardArtifactNode_getAction_errorTitle(), Bundle.BlackboardArtifactNode_getAction_linkedFileMessage()); + logger.log(Level.SEVERE, MessageFormat.format("Error getting linked file of artifact (artifact objID={0})", artifact.getId()), ex); //NON-NLS + } - //if the artifact has associated content, add the action to view the content in the timeline + /* + * If the source content of the artifact represented by this node is a + * file, add an action to view the file in the data source tree. + */ + AbstractFile file = getLookup().lookup(AbstractFile.class + ); if (null != file) { actionsList.add(ViewFileInTimelineAction.createViewSourceFileAction(file)); } @@ -288,67 +372,21 @@ public class BlackboardArtifactNode extends AbstractContentNode map = new LinkedHashMap<>(); - fillPropertyMap(map, artifact); - - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.srcFile.name"), - NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.srcFile.displayName"), + /* + * Add the name of the source content of the artifact represented by + * this node to the sheet. The value of this property is the same as the + * display name of the node and this a "special" property that displays + * the node's icon as well as the display name. + */ + sheetSet.put(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_srcFile_name(), + Bundle.BlackboardArtifactNode_createSheet_srcFile_displayName(), NO_DESCR, - this.getSourceName())); + getDisplayName())); - // Create place holders for S C O - if (!UserPreferences.getHideSCOColumns()) { - sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), VALUE_LOADING, "")); - sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), VALUE_LOADING, "")); - if (CentralRepository.isEnabled()) { - sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), VALUE_LOADING, "")); + if (TextTranslationService.getInstance().hasProvider() && UserPreferences.displayTranslatedFileNames()) { + /* + * If machine translation is configured, add the original name of + * the of the source content of the artifact represented by this + * node to the sheet. + */ + sheetSet.put(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_srcFile_origName(), + Bundle.BlackboardArtifactNode_createSheet_srcFile_origDisplayName(), + NO_DESCR, + translatedSourceName != null ? srcContent.getName() : "")); + if (translatedSourceName == null) { + /* + * NOTE: The task makes its own weak reference to the listener. + */ + new FileNameTransTask(srcContent.getName(), this, listener).submit(); } - // Get the SCO columns data in a background task - backgroundTasksPool.submit(new GetSCOTask( - new WeakReference<>(this), weakPcl)); } + if (!UserPreferences.getHideSCOColumns()) { + /* + * Add S(core), C(omments), and O(ther occurences) columns to the + * sheet and start a background task to compute the value of these + * properties for the artifact represented by this node. The task + * will fire a PropertyChangeEvent when the computation is completed + * and this node's PropertyChangeListener will update the sheet. + */ + sheetSet.put(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_score_name(), + Bundle.BlackboardArtifactNode_createSheet_score_displayName(), + VALUE_LOADING, + "")); + sheetSet.put(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_comment_name(), + Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), + VALUE_LOADING, + "")); + if (CentralRepository.isEnabled()) { + sheetSet.put(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_count_name(), + Bundle.BlackboardArtifactNode_createSheet_count_displayName(), + VALUE_LOADING, + "")); + } + backgroundTasksPool.submit(new GetSCOTask(new WeakReference<>(this), weakListener)); + } + + /* + * If the artifact represented by this node is an interesting artifact + * hit, add the type and description of the interesting artifact to the + * sheet. + */ if (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID()) { try { BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT)); if (attribute != null) { BlackboardArtifact associatedArtifact = Case.getCurrentCaseThrows().getSleuthkitCase().getBlackboardArtifact(attribute.getValueLong()); - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactType.name"), + sheetSet.put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactType.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactType.displayName"), NO_DESCR, associatedArtifact.getDisplayName())); - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactDetails.name"), + sheetSet.put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactDetails.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactDetails.displayName"), NO_DESCR, associatedArtifact.getShortDescription())); } } catch (TskCoreException | NoCurrentCaseException ex) { - // Do nothing since the display name will be set to the file name. + logger.log(Level.SEVERE, MessageFormat.format("Error getting associated artifact of TSK_INTERESTING_ARTIFACT_HIT artifact (objID={0}))", artifact.getId()), ex); //NON-NLS } } + /* + * Add the attributes of the artifact represented by this node to the + * sheet. + */ + Map map = new LinkedHashMap<>(); + fillPropertyMap(map, artifact); for (Map.Entry entry : map.entrySet()) { sheetSet.put(new NodeProperty<>(entry.getKey(), entry.getKey(), @@ -415,28 +508,35 @@ public class BlackboardArtifactNode extends AbstractContentNode np : customProperties) { sheetSet.put(np); } } + /* + * If the artifact represented by this node is a file extension mismatch + * artifact, add the extension and type of the artifact's source file to + * the sheet. + */ final int artifactTypeId = artifact.getArtifactTypeID(); - - // If mismatch, add props for extension and file type if (artifactTypeId == BlackboardArtifact.ARTIFACT_TYPE.TSK_EXT_MISMATCH_DETECTED.getTypeID()) { String ext = ""; //NON-NLS String actualMimeType = ""; //NON-NLS - if (associated instanceof AbstractFile) { - AbstractFile af = (AbstractFile) associated; - ext = af.getNameExtension(); - actualMimeType = af.getMIMEType(); + if (srcContent instanceof AbstractFile) { + AbstractFile file = (AbstractFile) srcContent; + ext = file.getNameExtension(); + actualMimeType = file.getMIMEType(); if (actualMimeType == null) { actualMimeType = ""; //NON-NLS + } } - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.ext.name"), + sheetSet.put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.ext.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.ext.displayName"), NO_DESCR, ext)); @@ -447,12 +547,17 @@ public class BlackboardArtifactNode extends AbstractContentNode(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileModifiedTime.name"), + AbstractFile file = srcContent instanceof AbstractFile ? (AbstractFile) srcContent : null; + sheetSet.put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileModifiedTime.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileModifiedTime.displayName"), "", file == null ? "" : ContentUtils.getStringTime(file.getMtime(), file))); - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileChangedTime.name"), + sheetSet.put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileChangedTime.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileChangedTime.displayName"), "", file == null ? "" : ContentUtils.getStringTime(file.getCtime(), file))); - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileAccessedTime.name"), + sheetSet.put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileAccessedTime.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileAccessedTime.displayName"), "", file == null ? "" : ContentUtils.getStringTime(file.getAtime(), file))); - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileCreatedTime.name"), + sheetSet.put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileCreatedTime.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileCreatedTime.displayName"), "", file == null ? "" : ContentUtils.getStringTime(file.getCrtime(), file))); - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileSize.name"), + sheetSet.put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileSize.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileSize.displayName"), "", - associated.getSize())); - sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_artifactMD5_name(), + file == null ? "" : file.getSize())); + sheetSet.put(new NodeProperty<>( + Bundle.BlackboardArtifactNode_createSheet_artifactMD5_name(), Bundle.BlackboardArtifactNode_createSheet_artifactMD5_displayName(), "", file == null ? "" : StringUtils.defaultString(file.getMd5Hash()))); @@ -493,14 +609,15 @@ public class BlackboardArtifactNode extends AbstractContentNode(NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.fileSize.name"), + sheetSet.put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.fileSize.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.fileSize.displayName"), NO_DESCR, size)); - sheetSet.put(new NodeProperty<>( - NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.path.name"), - NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.path.displayName"), - NO_DESCR, - path)); + sheetSet + .put(new NodeProperty<>( + NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.path.name"), + NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.path.displayName"), + NO_DESCR, + path)); } return sheet; } /** - * Get all tags from the case database relating to the artifact and the file - * it is associated with. + * Gets all of the tags applied to the artifact represented by this node and + * its source content. * - * @return a list of tags which on the artifact or the file it is associated - * with + * @return The tags. */ @Override protected final List getAllTagsFromDatabase() { List tags = new ArrayList<>(); try { tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact)); - tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByContent(associated)); + tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByContent(srcContent)); } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Failed to get tags for artifact " + artifact.getDisplayName(), ex); + logger.log(Level.SEVERE, MessageFormat.format("Error getting tags for artifact and its source content (artifact objID={0})", artifact.getId()), ex); } return tags; } /** - * Used by (subclasses of) BlackboardArtifactNode to add the tags property - * to their sheets. + * Gets the correlation attribute for the MD5 hash of the source file of the + * artifact represented by this node. The correlation attribute instance can + * only be returned if the central repository is enabled and the source + * content is a file. * - * @param sheetSet the modifiable Sheet.Set returned by - * Sheet.get(Sheet.PROPERTIES) - */ - @NbBundle.Messages({ - "BlackboardArtifactNode.createSheet.tags.displayName=Tags"}) - @Deprecated - protected void addTagProperty(Sheet.Set sheetSet) throws MissingResourceException { - // add properties for tags - List tags = new ArrayList<>(); - try { - tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact)); - tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByContent(associated)); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Failed to get tags for artifact " + artifact.getDisplayName(), ex); - } - sheetSet.put(new NodeProperty<>("Tags", Bundle.BlackboardArtifactNode_createSheet_tags_displayName(), - NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()).collect(Collectors.joining(", ")))); - } - - /** - * Used by (subclasses of) BlackboardArtifactNode to add the tags property - * to their sheets. - * - * @param sheetSet the modifiable Sheet.Set returned by - * Sheet.get(Sheet.PROPERTIES) - * @param tags the list of tags which should appear as the value for the - * property - */ - @Deprecated - protected final void addTagProperty(Sheet.Set sheetSet, List tags) { - sheetSet.put(new NodeProperty<>("Tags", Bundle.BlackboardArtifactNode_createSheet_tags_displayName(), - NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()).collect(Collectors.joining(", ")))); - } - - /** - * Gets the correlation attribute for the associated file - * - * @return the correlation attribute for the file associated with this - * BlackboardArtifactNode + * @return The correlation attribute instance, may be null. */ @Override protected final CorrelationAttributeInstance getCorrelationAttributeInstance() { CorrelationAttributeInstance correlationAttribute = null; - if (CentralRepository.isEnabled() && associated instanceof AbstractFile) { - correlationAttribute = CorrelationAttributeUtil.getCorrAttrForFile((AbstractFile)associated); + if (CentralRepository.isEnabled() && srcContent instanceof AbstractFile) { + correlationAttribute = CorrelationAttributeUtil.getCorrAttrForFile((AbstractFile) srcContent); } return correlationAttribute; } /** - * Used by (subclasses of) BlackboardArtifactNode to add the comment - * property to their sheets. + * Computes the value of the comment property ("C" in S, C, O) for the + * artifact represented by this node. * - * @param sheetSet the modifiable Sheet.Set to add the property to - * @param tags the list of tags associated with the file - * @param attribute the correlation attribute associated with this - * artifact's associated file, null if central repo is not - * enabled + * An icon is displayed in the property sheet if a commented tag has been + * applied to the artifact or its source content, or if there is a + * corresponding commented correlation attribute instance in the central + * repository. * - * @deprecated Use the GetSCOTask to get this data on a background - * thread..., and then update the property sheet asynchronously - */ - @NbBundle.Messages({"BlackboardArtifactNode.createSheet.comment.name=C", - "BlackboardArtifactNode.createSheet.comment.displayName=C"}) - @Deprecated - protected final void addCommentProperty(Sheet.Set sheetSet, List tags, CorrelationAttributeInstance attribute) { - HasCommentStatus status = getCommentProperty(tags, attribute); - sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, - status)); - } - - /** - * Gets the comment property for the node + * @param tags The tags applied to the artifact and its source content. + * @param attribute A correlation attribute instance Ffor the central + * repository lookup. * - * @param tags the list of tags associated with the file - * @param attribute the correlation attribute associated with this - * artifact's associated file, null if central repo is not - * enabled - * - * @return comment property + * @return The value of the comment property. */ @Override protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + /* + * Has a tag with a comment been applied to the artifact or its source + * content? + */ HasCommentStatus status = tags.size() > 0 ? HasCommentStatus.TAG_NO_COMMENT : HasCommentStatus.NO_COMMENT; for (Tag tag : tags) { if (!StringUtils.isBlank(tag.getComment())) { - //if the tag is null or empty or contains just white space it will indicate there is not a comment status = HasCommentStatus.TAG_COMMENT; break; } } - //currently checks for a comment on the associated file in the central repo not the artifact itself - //what we want the column property to reflect should be revisted when we have added a way to comment - //on the artifact itself + + /* + * Does the given correlation attribute instance have a comment in the + * central repository? + */ if (attribute != null && !StringUtils.isBlank(attribute.getComment())) { if (status == HasCommentStatus.TAG_COMMENT) { status = HasCommentStatus.CR_AND_TAG_COMMENTS; @@ -665,53 +738,51 @@ public class BlackboardArtifactNode extends AbstractContentNode tags) { - Pair scoreAndDescription = getScorePropertyAndDescription(tags); - sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), scoreAndDescription.getRight(), scoreAndDescription.getLeft())); - } - - /** - * Get the score property for the node. + * A red icon will be displayed in the property sheet if the hash of the + * source file has been found in a notable hash set or if either the + * artifact or its source content has been tagged with a notable tag. A + * yellow icon will be displayed if the source file belongs to an + * interesting file set or either the artifact or its source content has + * been tagged with a non-notable tag. * - * @param tags the list of tags associated with the file + * @param tags The tags that have been applied to the artifact and its + * source content. * - * @return score property and description + * @return The value of the score property as an enum element and a + * description string for dislpay in a tool tip. */ @Override protected Pair getScorePropertyAndDescription(List tags) { + /* + * Is the artifact's source content marked as notable? + */ Score score = Score.NO_SCORE; String description = Bundle.BlackboardArtifactNode_createSheet_noScore_description(); - if (associated instanceof AbstractFile) { - if (((AbstractFile) associated).getKnown() == TskData.FileKnown.BAD) { + if (srcContent instanceof AbstractFile) { + if (((AbstractFile) srcContent).getKnown() == TskData.FileKnown.BAD) { score = Score.NOTABLE_SCORE; description = Bundle.BlackboardArtifactNode_createSheet_notableFile_description(); } } - //if the artifact being viewed is a hashhit check if the hashset is notable - if ((score == Score.NO_SCORE || score == Score.INTERESTING_SCORE) && content.getArtifactTypeID() == ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) { + + /* + * If the artifact is a hash set hit, is the hash set a notable hashes + * hash set? + */ + if (score == Score.NO_SCORE && artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) { try { - BlackboardAttribute attr = content.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_SET_NAME)); + BlackboardAttribute attr = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_SET_NAME)); List notableHashsets = HashDbManager.getInstance().getKnownBadFileHashSets(); for (HashDbManager.HashDb hashDb : notableHashsets) { if (hashDb.getHashSetName().equals(attr.getValueString())) { @@ -721,18 +792,29 @@ public class BlackboardArtifactNode extends AbstractContentNode 0 && (score == Score.NO_SCORE || score == Score.INTERESTING_SCORE)) { score = Score.INTERESTING_SCORE; description = Bundle.BlackboardArtifactNode_createSheet_taggedItem_description(); @@ -749,124 +831,117 @@ public class BlackboardArtifactNode extends AbstractContentNode countAndDescription = getCountPropertyAndDescription(attribute.getCorrelationType(), attribute.getCorrelationValue(), Bundle.BlackboardArtifactNode_createSheet_count_noCorrelationAttributes_description()); - sheetSet.put( - new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), countAndDescription.getRight(), countAndDescription.getLeft())); - } - - /** - * Gets the Occurrences property for the node. - * - * @param attributeType the type of the attribute to count - * @param attributeValue the value of the attribute to count - * @param defaultDescription a description to use when none is determined by - * the getCountPropertyAndDescription method - * - * @return count and description + * @return The value of the occurrences property as a data sources count and + * a description string. * */ @Override - protected Pair getCountPropertyAndDescription(Type attributeType, String attributeValue, String defaultDescription) { + protected Pair getCountPropertyAndDescription(Type corrAttrType, String attributeValue, String defaultDescription) { Long count = -1L; String description = defaultDescription; try { - //don't perform the query if there is no correlation value - if (attributeType != null && StringUtils.isNotBlank(attributeValue)) { - count = CentralRepository.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(attributeType, attributeValue); - description = Bundle.BlackboardArtifactNode_createSheet_count_description(count, attributeType.getDisplayName()); - } else if (attributeType != null) { + if (corrAttrType != null && StringUtils.isNotBlank(attributeValue)) { + count = CentralRepository.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(corrAttrType, attributeValue); + description = Bundle.BlackboardArtifactNode_createSheet_count_description(count, corrAttrType.getDisplayName()); + } else if (corrAttrType != null) { description = Bundle.BlackboardArtifactNode_createSheet_count_noCorrelationValues_description(); } } catch (CentralRepoException ex) { - logger.log(Level.WARNING, "Error getting count of datasources with correlation attribute", ex); + logger.log(Level.SEVERE, MessageFormat.format("Error querying central repository for other occurences count (artifact objID={0}, corrAttrType={1}, corrAttrValue={2})", artifact.getId(), corrAttrType, attributeValue), ex); } catch (CorrelationAttributeNormalizationException ex) { - logger.log(Level.WARNING, "Unable to normalize data to get count of datasources with correlation attribute", ex); + logger.log(Level.SEVERE, MessageFormat.format("Error normalizing correlation attribute for central repository query (artifact objID={0}, corrAttrType={2}, corrAttrValue={3})", artifact.getId(), corrAttrType, attributeValue), ex); } return Pair.of(count, description); } + /** + * Refreshes this node's property sheet. + */ private void updateSheet() { this.setSheet(createSheet()); } - private String getRootParentName() { - String parentName = associated.getName(); - Content parent = associated; + /** + * Gets the name of the root ancestor of the source content for the artifact + * represented by this node. + * + * @return The root ancestor name or the empty string if an error occurs. + */ + private String getRootAncestorName() { + String parentName = srcContent.getName(); + Content parent = srcContent; try { while ((parent = parent.getParent()) != null) { parentName = parent.getName(); } } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Failed to get parent name from {0}", associated.getName()); //NON-NLS + logger.log(Level.SEVERE, MessageFormat.format("Error getting root ancestor name for source content (artifact objID={0})", artifact.getId()), ex); //NON-NLS return ""; } return parentName; } /** - * Add an additional custom node property to that node before it is - * displayed + * Adds a "custom" property to the property sheet of this node, indepoendent + * of the artifact this node represents or its source content. * - * @param np NodeProperty to add + * @param property The custom property. */ - public void addNodeProperty(NodeProperty np) { - if (null == customProperties) { - //lazy create the list + public void addNodeProperty(NodeProperty property) { + if (customProperties == null) { customProperties = new ArrayList<>(); } - customProperties.add(np); + customProperties.add(property); } /** - * Fill map with Artifact properties + * Converts the attributes of the artifact this node represents to a map of + * name-value pairs, where the names are attribute type display names. * - * @param map map with preserved ordering, where property names/values - * are put - * @param artifact to extract properties from + * @param map The map to be populated with the artifact attribute + * name-value pairs. + * @param artifact The artifact. */ @SuppressWarnings("deprecation") private void fillPropertyMap(Map map, BlackboardArtifact artifact) { try { for (BlackboardAttribute attribute : artifact.getAttributes()) { final int attributeTypeID = attribute.getAttributeType().getTypeID(); - //skip some internal attributes that user shouldn't see if (attributeTypeID == ATTRIBUTE_TYPE.TSK_PATH_ID.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_TAGGED_ARTIFACT.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() - || attributeTypeID == ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_TYPE.getTypeID() + || attributeTypeID == ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_TYPE.getTypeID() || attribute.getValueType() == BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) { - continue; + /* + * Do nothing. + */ } else if (artifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID()) { addEmailMsgProperty(map, attribute); } else if (attribute.getAttributeType().getValueType() == BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME) { - map.put(attribute.getAttributeType().getDisplayName(), ContentUtils.getStringTime(attribute.getValueLong(), associated)); + map.put(attribute.getAttributeType().getDisplayName(), ContentUtils.getStringTime(attribute.getValueLong(), srcContent)); } else if (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_TOOL_OUTPUT.getTypeID() && attributeTypeID == ATTRIBUTE_TYPE.TSK_TEXT.getTypeID()) { /* - * This was added because the RegRipper output would often - * cause the UI to get a black line accross it and hang if - * you hovered over large output or selected it. This - * reduces the amount of data in the table. Could consider - * doing this for all fields in the UI. + * The truncation of text attributes appears to have been + * motivated by the statement that "RegRipper output would + * often cause the UI to get a black line accross it and + * hang if you hovered over large output or selected it. + * This reduces the amount of data in the table. Could + * consider doing this for all fields in the UI." */ String value = attribute.getDisplayString(); if (value.length() > 512) { @@ -878,43 +953,41 @@ public class BlackboardArtifactNode extends AbstractContentNode map, BlackboardAttribute attribute) { - final int attributeTypeID = attribute.getAttributeType().getTypeID(); - - // Skip certain Email msg attributes if (attributeTypeID == ATTRIBUTE_TYPE.TSK_DATETIME_SENT.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_HTML.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_RTF.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_EMAIL_BCC.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_EMAIL_CC.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_HEADERS.getTypeID()) { - - // do nothing + /* + * Do nothing. + */ } else if (attributeTypeID == ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_PLAIN.getTypeID()) { - String value = attribute.getDisplayString(); if (value.length() > 160) { value = value.substring(0, 160) + "..."; } map.put(attribute.getAttributeType().getDisplayName(), value); } else if (attribute.getAttributeType().getValueType() == BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME) { - map.put(attribute.getAttributeType().getDisplayName(), ContentUtils.getStringTime(attribute.getValueLong(), associated)); + map.put(attribute.getAttributeType().getDisplayName(), ContentUtils.getStringTime(attribute.getValueLong(), srcContent)); } else { map.put(attribute.getAttributeType().getDisplayName(), attribute.getDisplayString()); } - } @Override @@ -922,30 +995,6 @@ public class BlackboardArtifactNode extends AbstractContentNode artifact.getSleuthkitCase().getContentById(objectID)); - if (content == null) { - return Lookups.fixed(artifact); - } else { - return Lookups.fixed(artifact, content); - } - } catch (ExecutionException ex) { - logger.log(Level.WARNING, "Getting associated content for artifact failed", ex); //NON-NLS - return Lookups.fixed(artifact); - } - } - @Override public boolean isLeafTypeNode() { return true; @@ -960,4 +1009,117 @@ public class BlackboardArtifactNode extends AbstractContentNode T accept(ContentNodeVisitor visitor) { return visitor.visit(this); } + + /** + * Adds the score property for the artifact represented by this node to the + * node property sheet. + * + * @param sheetSet The property sheet. + * @param tags The tags that have been applied to the artifact and its + * source content. + * + * @deprecated Do not use. The score property is now computed in a + * background thread and added to the property sheet via property change + * event. + */ + @NbBundle.Messages({"BlackboardArtifactNode.createSheet.score.name=S", + "BlackboardArtifactNode.createSheet.score.displayName=S", + "BlackboardArtifactNode.createSheet.notableFile.description=Associated file recognized as notable.", + "BlackboardArtifactNode.createSheet.interestingResult.description=Result has an interesting result associated with it.", + "BlackboardArtifactNode.createSheet.taggedItem.description=Result or associated file has been tagged.", + "BlackboardArtifactNode.createSheet.notableTaggedItem.description=Result or associated file tagged with notable tag.", + "BlackboardArtifactNode.createSheet.noScore.description=No score"}) + @Deprecated + protected final void addScorePropertyAndDescription(Sheet.Set sheetSet, List tags) { + Pair scoreAndDescription = getScorePropertyAndDescription(tags); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), scoreAndDescription.getRight(), scoreAndDescription.getLeft())); + } + + /** + * Adds the tags property for the artifact represented by this node to the + * node property sheet. + * + * @param sheetSet The property sheet. + * + * @deprecated Do not use. The tags property is now computed in a background + * thread and added to the property sheet via property change event. + */ + @NbBundle.Messages({ + "BlackboardArtifactNode.createSheet.tags.displayName=Tags"} + ) + @Deprecated + protected void addTagProperty(Sheet.Set sheetSet) throws MissingResourceException { + List tags = new ArrayList<>(); + try { + tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact)); + tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByContent(srcContent)); + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.SEVERE, MessageFormat.format("Error getting tags for artifact and source content (artifact objID={0})", artifact.getId()), ex); + } + sheetSet.put(new NodeProperty<>("Tags", Bundle.BlackboardArtifactNode_createSheet_tags_displayName(), NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()).collect(Collectors.joining(", ")))); + } + + /** + * Adds the tags property for the artifact represented by this node to the + * node property sheet. + * + * @param sheetSet The property sheet. + * @param tags The tags that have been applied to the artifact and its + * source content. + * + * @deprecated Do not use. The tags property is now computed in a background + * thread and added to the property sheet via property change event. + */ + @Deprecated + protected final void addTagProperty(Sheet.Set sheetSet, List tags) { + sheetSet.put(new NodeProperty<>("Tags", Bundle.BlackboardArtifactNode_createSheet_tags_displayName(), NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()).collect(Collectors.joining(", ")))); + } + + /** + * Adds the count property for the artifact represented by this node to the + * node property sheet. + * + * @param sheetSet The property sheet. + * @param attribute The correlation attribute instance to use for the + * central repository lookup. + * + * @deprecated Do not use. The count property is now computed in a + * background thread and added to the property sheet via property change + * event. + */ + @NbBundle.Messages({"BlackboardArtifactNode.createSheet.count.name=O", + "BlackboardArtifactNode.createSheet.count.displayName=O", + "BlackboardArtifactNode.createSheet.count.noCorrelationAttributes.description=No correlation properties found", + "BlackboardArtifactNode.createSheet.count.noCorrelationValues.description=Unable to find other occurrences because no value exists for the available correlation property", + "# {0} - occurrenceCount", + "# {1} - attributeType", + "BlackboardArtifactNode.createSheet.count.description=There were {0} datasource(s) found with occurrences of the correlation value of type {1}"}) + @Deprecated + protected final void addCountProperty(Sheet.Set sheetSet, CorrelationAttributeInstance attribute) { + Pair countAndDescription = getCountPropertyAndDescription(attribute.getCorrelationType(), attribute.getCorrelationValue(), Bundle.BlackboardArtifactNode_createSheet_count_noCorrelationAttributes_description()); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), countAndDescription.getRight(), countAndDescription.getLeft())); + } + + /** + * Adds the other occurrences property for the artifact represented by this + * node to the node property sheet. + * + * @param sheetSet The property sheet. + * @param tags The tags that have been applied to the artifact and its + * source content. + * @param attribute The correlation attribute instance to use for the + * central repository lookup. + * + * @deprecated Do not use. The other occurrences property is now computed in + * a background thread and added to the property sheet via property change + * event. + */ + @NbBundle.Messages({"BlackboardArtifactNode.createSheet.comment.name=C", + "BlackboardArtifactNode.createSheet.comment.displayName=C"}) + @Deprecated + protected final void addCommentProperty(Sheet.Set sheetSet, List tags, CorrelationAttributeInstance attribute) { + HasCommentStatus status = getCommentProperty(tags, attribute); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, status)); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java index 16cf40680c..6068615592 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2018 Basis Technology Corp. + * Copyright 2013-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,13 +24,11 @@ import java.util.Arrays; import java.util.List; import java.util.logging.Level; import javax.swing.Action; -import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; @@ -46,14 +44,14 @@ import static org.sleuthkit.autopsy.datamodel.Bundle.*; * tag name nodes have tag type child nodes; tag type nodes are the parents of * either content or blackboard artifact tag nodes. */ -public class BlackboardArtifactTagNode extends DisplayableItemNode { +public class BlackboardArtifactTagNode extends TagNode { private static final Logger LOGGER = Logger.getLogger(BlackboardArtifactTagNode.class.getName()); private static final String ICON_PATH = "org/sleuthkit/autopsy/images/green-tag-icon-16.png"; //NON-NLS private final BlackboardArtifactTag tag; public BlackboardArtifactTagNode(BlackboardArtifactTag tag) { - super(Children.LEAF, Lookups.fixed(tag, tag.getArtifact(), tag.getContent())); + super(Lookups.fixed(tag, tag.getArtifact(), tag.getContent()), tag.getContent()); super.setName(tag.getContent().getName()); super.setDisplayName(tag.getContent().getName()); this.setIconBaseWithExtension(ICON_PATH); @@ -75,6 +73,7 @@ public class BlackboardArtifactTagNode extends DisplayableItemNode { NbBundle.getMessage(this.getClass(), "BlackboardArtifactTagNode.createSheet.srcFile.text"), "", tag.getContent().getName())); + addOriginalNameProp(properties); String contentPath; try { contentPath = tag.getContent().getUniquePath(); @@ -82,7 +81,6 @@ public class BlackboardArtifactTagNode extends DisplayableItemNode { Logger.getLogger(ContentTagNode.class.getName()).log(Level.SEVERE, "Failed to get path for content (id = " + tag.getContent().getId() + ")", ex); //NON-NLS contentPath = NbBundle.getMessage(this.getClass(), "BlackboardArtifactTagNode.createSheet.unavail.text"); } - properties.put(new NodeProperty<>( NbBundle.getMessage(this.getClass(), "BlackboardArtifactTagNode.createSheet.srcFilePath.text"), NbBundle.getMessage(this.getClass(), "BlackboardArtifactTagNode.createSheet.srcFilePath.text"), @@ -119,7 +117,6 @@ public class BlackboardArtifactTagNode extends DisplayableItemNode { } } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, MessageFormat.format("Error getting arttribute(s) from blackboard artifact{0}.", artifact.getArtifactID()), ex); //NON-NLS - MessageNotifyUtil.Notify.error(Bundle.BlackboardArtifactNode_getAction_errorTitle(), Bundle.BlackboardArtifactNode_getAction_resultErrorMessage()); } // if the artifact links to another file, add an action to go to that file @@ -130,7 +127,6 @@ public class BlackboardArtifactTagNode extends DisplayableItemNode { } } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, MessageFormat.format("Error getting linked file from blackboard artifact{0}.", artifact.getArtifactID()), ex); //NON-NLS - MessageNotifyUtil.Notify.error(Bundle.BlackboardArtifactNode_getAction_errorTitle(), Bundle.BlackboardArtifactNode_getAction_linkedFileMessage()); } //if this artifact has associated content, add the action to view the content in the timeline AbstractFile file = getLookup().lookup(AbstractFile.class); @@ -148,11 +144,6 @@ public class BlackboardArtifactTagNode extends DisplayableItemNode { return visitor.visit(this); } - @Override - public boolean isLeafTypeNode() { - return true; - } - @Override public String getItemType() { return getClass().getName(); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties index 3fa8cd0341..83225be1c2 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties @@ -11,8 +11,6 @@ ArtifactTypeNode.createSheet.childCnt.name=Child Count ArtifactTypeNode.createSheet.childCnt.displayName=Child Count ArtifactTypeNode.createSheet.childCnt.desc=no description BlackboardArtifactNode.noDesc.text=no description -BlackboardArtifactNode.createSheet.srcFile.name=Source File -BlackboardArtifactNode.createSheet.srcFile.displayName=Source File BlackboardArtifactNode.createSheet.ext.name=Extension BlackboardArtifactNode.createSheet.ext.displayName=Extension BlackboardArtifactNode.createSheet.mimeType.name=MIME Type diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED index 9ac85bd1e7..e7310882a3 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED @@ -74,13 +74,12 @@ BlackboardArtifactNode.createSheet.path.displayName=Path BlackboardArtifactNode.createSheet.path.name=Path BlackboardArtifactNode.createSheet.score.displayName=S BlackboardArtifactNode.createSheet.score.name=S +BlackboardArtifactNode.createSheet.srcFile.displayName=Source File +BlackboardArtifactNode.createSheet.srcFile.name=Source File +BlackboardArtifactNode.createSheet.srcFile.origDisplayName=Original Name +BlackboardArtifactNode.createSheet.srcFile.origName=Original Name BlackboardArtifactNode.createSheet.taggedItem.description=Result or associated file has been tagged. BlackboardArtifactNode.createSheet.tags.displayName=Tags -# {0} - artifactDisplayName -BlackboardArtifactNode.displayName.artifact={0} Artifact -BlackboardArtifactNode.getAction.errorTitle=Error getting actions -BlackboardArtifactNode.getAction.linkedFileMessage=There was a problem getting actions for the selected result. The 'View File in Timeline' action will not be available. -BlackboardArtifactNode.getAction.resultErrorMessage=There was a problem getting actions for the selected result. The 'View Result in Timeline' action will not be available. BlackboardArtifactTagNode.createSheet.userName.text=User Name BlackboardArtifactTagNode.viewSourceArtifact.text=View Source Result Category.five=CAT-5: Non-pertinent @@ -91,6 +90,7 @@ Category.two=CAT-2: Child Exploitation (Non-Illegal/Age Difficult) Category.zero=CAT-0: Uncategorized ContentTagNode.createSheet.artifactMD5.displayName=MD5 Hash ContentTagNode.createSheet.artifactMD5.name=MD5 Hash +ContentTagNode.createSheet.origFileName=Original Name ContentTagNode.createSheet.userName.text=User Name DeletedContent.allDelFilter.text=All DeletedContent.createSheet.filterType.desc=no description @@ -178,8 +178,6 @@ ArtifactTypeNode.createSheet.childCnt.name=Child Count ArtifactTypeNode.createSheet.childCnt.displayName=Child Count ArtifactTypeNode.createSheet.childCnt.desc=no description BlackboardArtifactNode.noDesc.text=no description -BlackboardArtifactNode.createSheet.srcFile.name=Source File -BlackboardArtifactNode.createSheet.srcFile.displayName=Source File BlackboardArtifactNode.createSheet.ext.name=Extension BlackboardArtifactNode.createSheet.ext.displayName=Extension BlackboardArtifactNode.createSheet.mimeType.name=MIME Type @@ -348,6 +346,8 @@ TagNameNode.bbArtTagTypeNodeKey.text=Result Tags TagNameNode.bookmark.text=Bookmark TagNameNode.createSheet.name.name=Name TagNameNode.createSheet.name.displayName=Name +TagNode.propertySheet.origName=Original Name +TagNode.propertySheet.origNameDisplayName=Original Name TagsNode.displayName.text=Tags TagsNode.createSheet.name.name=Name TagsNode.createSheet.name.displayName=Name diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java index e213e460cb..89d6c2fae2 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2016 Basis Technology Corp. + * Copyright 2013-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,6 @@ import java.util.List; import java.util.logging.Level; import javax.swing.Action; import org.apache.commons.lang3.StringUtils; -import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; @@ -39,18 +38,16 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Instances of this class wrap ContentTag objects. In the Autopsy presentation * of the SleuthKit data model, they are leaf nodes of a tree consisting of - * content and blackboard artifact tags, grouped first by tag type, then by tag - * name. + * content and artifact tags, grouped first by tag type, then by tag name. */ -class ContentTagNode extends DisplayableItemNode { +class ContentTagNode extends TagNode { private static final Logger LOGGER = Logger.getLogger(ContentTagNode.class.getName()); - private static final String ICON_PATH = "org/sleuthkit/autopsy/images/blue-tag-icon-16.png"; //NON-NLS private final ContentTag tag; - public ContentTagNode(ContentTag tag) { - super(Children.LEAF, Lookups.fixed(tag, tag.getContent())); + ContentTagNode(ContentTag tag) { + super(Lookups.fixed(tag, tag.getContent()), tag.getContent()); super.setName(tag.getContent().getName()); super.setDisplayName(tag.getContent().getName()); this.setIconBaseWithExtension(ICON_PATH); @@ -58,6 +55,7 @@ class ContentTagNode extends DisplayableItemNode { } @Messages({ + "ContentTagNode.createSheet.origFileName=Original Name", "ContentTagNode.createSheet.artifactMD5.displayName=MD5 Hash", "ContentTagNode.createSheet.artifactMD5.name=MD5 Hash", "ContentTagNode.createSheet.userName.text=User Name"}) @@ -79,15 +77,19 @@ class ContentTagNode extends DisplayableItemNode { properties = Sheet.createPropertiesSet(); propertySheet.put(properties); } - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.file.name"), + properties.put(new NodeProperty<>( + NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.file.name"), NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.file.displayName"), "", content.getName())); - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.filePath.name"), + addOriginalNameProp(properties); + properties.put(new NodeProperty<>( + NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.filePath.name"), NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.filePath.displayName"), "", contentPath)); - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.comment.name"), + properties.put(new NodeProperty<>( + NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.comment.name"), NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.comment.displayName"), "", tag.getComment())); @@ -95,23 +97,28 @@ class ContentTagNode extends DisplayableItemNode { NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileModifiedTime.displayName"), "", file != null ? ContentUtils.getStringTime(file.getMtime(), file) : "")); - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileChangedTime.name"), + properties.put(new NodeProperty<>( + NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileChangedTime.name"), NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileChangedTime.displayName"), "", file != null ? ContentUtils.getStringTime(file.getCtime(), file) : "")); - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileAccessedTime.name"), + properties.put(new NodeProperty<>( + NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileAccessedTime.name"), NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileAccessedTime.displayName"), "", file != null ? ContentUtils.getStringTime(file.getAtime(), file) : "")); - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileCreatedTime.name"), + properties.put(new NodeProperty<>( + NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileCreatedTime.name"), NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileCreatedTime.displayName"), "", file != null ? ContentUtils.getStringTime(file.getCrtime(), file) : "")); - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileSize.name"), + properties.put(new NodeProperty<>( + NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileSize.name"), NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileSize.displayName"), "", content.getSize())); - properties.put(new NodeProperty<>(Bundle.ContentTagNode_createSheet_artifactMD5_name(), + properties.put(new NodeProperty<>( + Bundle.ContentTagNode_createSheet_artifactMD5_name(), Bundle.ContentTagNode_createSheet_artifactMD5_displayName(), "", file != null ? StringUtils.defaultString(file.getMd5Hash()) : "")); @@ -128,8 +135,7 @@ class ContentTagNode extends DisplayableItemNode { List actions = new ArrayList<>(); actions.addAll(Arrays.asList(super.getActions(context))); - AbstractFile file = getLookup().lookup(AbstractFile.class - ); + AbstractFile file = getLookup().lookup(AbstractFile.class); if (file != null) { actions.add(ViewFileInTimelineAction.createViewFileAction(file)); } @@ -144,13 +150,9 @@ class ContentTagNode extends DisplayableItemNode { return visitor.visit(this); } - @Override - public boolean isLeafTypeNode() { - return true; - } - @Override public String getItemType() { return getClass().getName(); } + } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java index c723d99b55..c6bc88129a 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2012-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.datamodel; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; +import org.openide.nodes.Sheet; import org.openide.util.Lookup; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -142,4 +143,26 @@ public abstract class DisplayableItemNode extends AbstractNode { return selectedChildNodeInfo; } + /** + * Updates the node property sheet by replacing existing properties with new + * properties with the same property name. + * + * @param newProps The replacement property objects. + */ + protected synchronized final void updatePropertySheet(NodeProperty... newProps) { + Sheet currentSheet = this.getSheet(); + Sheet.Set currentPropsSet = currentSheet.get(Sheet.PROPERTIES); + Property[] currentProps = currentPropsSet.getProperties(); + for (NodeProperty newProp : newProps) { + for (int i = 0; i < currentProps.length; i++) { + if (currentProps[i].getName().equals(newProp.getName())) { + currentProps[i] = newProp; + } + } + } + currentPropsSet.put(currentProps); + currentSheet.put(currentPropsSet); + this.setSheet(currentSheet); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/TagNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/TagNode.java new file mode 100755 index 0000000000..9653c44d04 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/TagNode.java @@ -0,0 +1,128 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020-2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datamodel; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import org.openide.nodes.Children; +import org.openide.nodes.Sheet; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.datamodel.utils.FileNameTransTask; +import org.sleuthkit.autopsy.texttranslation.TextTranslationService; +import org.sleuthkit.datamodel.Content; + +/** + * An abstract superclass for a node that represents a tag, uses the name of a + * given Content object as its display name, and has a property sheet with an + * original name property when machine translation is enabled. + * + * The translation of the Content name is done in a background thread. The + * translated name is made the display name of the node and the untranslated + * name is put into both the original name property and into the node's tooltip. + * + * TODO (Jira-6174): Consider modifying this class to be able to use it more broadly + * within the Autopsy data model (i.e., AbstractNode suclasses). It's not really + * specific to a tag node. + */ +@NbBundle.Messages({ + "TagNode.propertySheet.origName=Original Name", + "TagNode.propertySheet.origNameDisplayName=Original Name" +}) +abstract class TagNode extends DisplayableItemNode { + + private final static String ORIG_NAME_PROP_NAME = Bundle.TagNode_propertySheet_origName(); + private final static String ORIG_NAME_PROP_DISPLAY_NAME = Bundle.TagNode_propertySheet_origNameDisplayName(); + + private final String originalName; + private volatile String translatedName; + + /** + * An abstract superclass for a node that represents a tag, uses the name of + * a given Content object as its display name, and has a property sheet with + * an untranslated file name property when machine translation is enabled. + * + * @param lookup The Lookup of the node. + * @param content The Content to use for the node display name. + */ + TagNode(Lookup lookup, Content content) { + super(Children.LEAF, lookup); + originalName = content.getName(); + } + + @Override + public boolean isLeafTypeNode() { + return true; + } + + @Override + abstract public String getItemType(); + + @Override + abstract public T accept(DisplayableItemNodeVisitor visitor); + + /** + * Adds an original name property to the node's property sheet and submits + * an original name translation task. + * + * The translation of the original name is done in a background thread. The + * translated name is made the display name of the node and the untranslated + * name is put into both the original name property and into the node's + * tooltip. + * + * @param properties The node's property sheet. + */ + protected void addOriginalNameProp(Sheet.Set properties) { + if (TextTranslationService.getInstance().hasProvider() && UserPreferences.displayTranslatedFileNames()) { + properties.put(new NodeProperty<>( + ORIG_NAME_PROP_NAME, + ORIG_NAME_PROP_DISPLAY_NAME, + "", + translatedName != null ? originalName : "")); + if (translatedName == null) { + new FileNameTransTask(originalName, this, new NameTranslationListener()).submit(); + } + } + } + + /** + * A listener for PropertyChangeEvents from a background task used to + * translate the original display name associated with the node. + */ + private class NameTranslationListener implements PropertyChangeListener { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + String eventType = evt.getPropertyName(); + if (eventType.equals(FileNameTransTask.getPropertyName())) { + translatedName = evt.getNewValue().toString(); + String originalName = evt.getOldValue().toString(); + setDisplayName(translatedName); + setShortDescription(originalName); + updatePropertySheet(new NodeProperty<>( + ORIG_NAME_PROP_NAME, + ORIG_NAME_PROP_DISPLAY_NAME, + "", + originalName)); + } + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/utils/AbstractNodePropertySheetTask.java b/Core/src/org/sleuthkit/autopsy/datamodel/utils/AbstractNodePropertySheetTask.java index 368b09b142..0a7e0a6f3f 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/utils/AbstractNodePropertySheetTask.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/utils/AbstractNodePropertySheetTask.java @@ -64,7 +64,7 @@ public abstract class AbstractNodePropertySheetTask impl * @return The Future of the task, may be used for task cancellation by * calling Future.cancel(true). */ - public static Future submitTask(AbstractNodePropertySheetTask task) { + private static Future submitTask(AbstractNodePropertySheetTask task) { return executor.submit(task); } @@ -104,12 +104,22 @@ public abstract class AbstractNodePropertySheetTask impl * * @param node The AbstractNode. * - * @return The result of the computation as a PropertyChangeEvent. + * @return The result of the computation as a PropertyChangeEvent, may be + * null. */ protected abstract PropertyChangeEvent computePropertyValue(T node) throws Exception; + /** + * Submits this task to the ExecutorService for the thread pool. + * + * @return The task's Future from the ExecutorService. + */ + public final Future submit() { + return submitTask(this); + } + @Override - final public void run() { + public final void run() { try { T node = this.weakNodeRef.get(); PropertyChangeListener listener = this.weakListenerRef.get(); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/utils/FileNameTransTask.java b/Core/src/org/sleuthkit/autopsy/datamodel/utils/FileNameTransTask.java new file mode 100755 index 0000000000..8b755ec3dc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/utils/FileNameTransTask.java @@ -0,0 +1,61 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datamodel.utils; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import org.openide.nodes.AbstractNode; +import org.sleuthkit.autopsy.texttranslation.utils.FileNameTranslationUtil; + +/** + * An AbstractNodePropertySheetTask that translates a file name for an + * AbstractNode's property sheet. + */ +public class FileNameTransTask extends AbstractNodePropertySheetTask { + + private final static String EVENT_SOURCE = FileNameTransTask.class.getName(); + private final static String PROPERTY_NAME = EVENT_SOURCE + ".TranslatedFileName"; + private final String fileName; + + public static String getPropertyName() { + return PROPERTY_NAME; + } + + /** + * Constructs an AbstractNodePropertySheetTask that translates a file name + * for an AbstractNode's property sheet. When the translation is complete, a + * PropertyChangeEvent will be fired to the node's PropertyChangeListener. + * Call getPropertyName() to identify the property. + * + * @param node The node. + * @param listener The node's PropertyChangeListener. + * @param fileName THe file name. + */ + public FileNameTransTask(String fileName, AbstractNode node, PropertyChangeListener listener) { + super(node, listener); + this.fileName = fileName; + } + + @Override + protected PropertyChangeEvent computePropertyValue(AbstractNode node) throws Exception { + String translatedFileName = FileNameTranslationUtil.translate(fileName); + return translatedFileName.isEmpty() ? null : new PropertyChangeEvent(EVENT_SOURCE, PROPERTY_NAME, fileName, translatedFileName); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java index 522d3836c3..a412bb5970 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2019 Basis Technology Corp. + * Copyright 2012-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -187,27 +187,6 @@ public class DataResultFilterNode extends FilterNode { return propertySets; } - /** - * Gets the display name for the wrapped node. - * - * OutlineView used in the DataResult table uses getDisplayName() to - * populate the first column, which is Source File. - * - * Hence this override to return the 'correct' displayName for the wrapped - * node. - * - * @return The display name for the node. - */ - @Override - public String getDisplayName() { - final Node orig = getOriginal(); - String name = orig.getDisplayName(); - if ((orig instanceof BlackboardArtifactNode)) { - name = ((BlackboardArtifactNode) orig).getSourceName(); - } - return name; - } - /** * Adds information about which child node of this node, if any, should be * selected. Can be null. diff --git a/Core/src/org/sleuthkit/autopsy/filequery/FileSearch.java b/Core/src/org/sleuthkit/autopsy/filequery/FileSearch.java index daa4124e8d..6a5543c9c9 100644 --- a/Core/src/org/sleuthkit/autopsy/filequery/FileSearch.java +++ b/Core/src/org/sleuthkit/autopsy/filequery/FileSearch.java @@ -23,7 +23,6 @@ import com.google.common.cache.CacheBuilder; import com.google.common.io.Files; import java.awt.Image; import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; import java.io.IOException; import java.io.Reader; import java.nio.file.Paths; @@ -95,6 +94,7 @@ class FileSearch { .build(); private static final int PREVIEW_SIZE = 256; private static volatile TextSummarizer summarizerToUse = null; + private static final BufferedImage VIDEO_DEFAULT_IMAGE = getDefaultVideoThumbnail(); /** * Run the file search and returns the SearchResults object for debugging. @@ -456,6 +456,20 @@ class FileSearch { + "AND blackboard_artifacts.obj_id IN (" + objIdList + ") "; // NON-NLS } + /** + * Get the default image to display when a thumbnail is not available. + * + * @return The default video thumbnail. + */ + private static BufferedImage getDefaultVideoThumbnail() { + try { + return ImageIO.read(ImageUtils.class.getResourceAsStream("/org/sleuthkit/autopsy/images/failedToCreateVideoThumb.png"));//NON-NLS + } catch (IOException ex) { + logger.log(Level.SEVERE, "Failed to load 'failed to create video' placeholder.", ex); //NON-NLS + } + return null; + } + /** * Get the video thumbnails for a file which exists in a * VideoThumbnailsWrapper and update the VideoThumbnailsWrapper to include @@ -476,7 +490,6 @@ class FileSearch { cacheDirectory = null; logger.log(Level.WARNING, "Unable to get cache directory, video thumbnails will not be saved", ex); } - if (cacheDirectory == null || file.getMd5Hash() == null || !Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash()).toFile().exists()) { java.io.File tempFile; try { @@ -488,7 +501,7 @@ class FileSearch { 0, 0, 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(), framePositions); + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); return; } if (tempFile.exists() == false || tempFile.length() < file.getSize()) { @@ -502,7 +515,7 @@ class FileSearch { 0, 0, 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(), framePositions); + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); return; } ContentUtils.writeToFile(file, tempFile, progress, null, true); @@ -523,7 +536,7 @@ class FileSearch { 0, 0, 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(), framePositions); + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); return; } double fps = videoFile.get(5); // gets frame per second @@ -535,7 +548,7 @@ class FileSearch { 0, 0, 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(), framePositions); + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); return; } if (Thread.interrupted()) { @@ -544,7 +557,7 @@ class FileSearch { 0, 0, 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(), framePositions); + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); return; } @@ -573,10 +586,10 @@ class FileSearch { logger.log(Level.WARNING, "Error seeking to " + framePositions[i] + "ms in {0}", file.getParentPath() + "/" + file.getName()); //NON-NLS // If we can't set the time, continue to the next frame position and try again. - videoThumbnails.add(ImageUtils.getDefaultThumbnail()); + videoThumbnails.add(VIDEO_DEFAULT_IMAGE); if (cacheDirectory != null) { try { - ImageIO.write((RenderedImage) ImageUtils.getDefaultThumbnail(), THUMBNAIL_FORMAT, + ImageIO.write(VIDEO_DEFAULT_IMAGE, THUMBNAIL_FORMAT, Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) } catch (IOException ex) { logger.log(Level.WARNING, "Unable to save default video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); @@ -588,10 +601,10 @@ class FileSearch { if (!videoFile.read(imageMatrix)) { logger.log(Level.WARNING, "Error reading frame at " + framePositions[i] + "ms from {0}", file.getParentPath() + "/" + file.getName()); //NON-NLS // If the image is bad for some reason, continue to the next frame position and try again. - videoThumbnails.add(ImageUtils.getDefaultThumbnail()); + videoThumbnails.add(VIDEO_DEFAULT_IMAGE); if (cacheDirectory != null) { try { - ImageIO.write((RenderedImage) ImageUtils.getDefaultThumbnail(), THUMBNAIL_FORMAT, + ImageIO.write(VIDEO_DEFAULT_IMAGE, THUMBNAIL_FORMAT, Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) } catch (IOException ex) { logger.log(Level.WARNING, "Unable to save default video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); @@ -602,10 +615,10 @@ class FileSearch { } // If the image is empty, return since no buffered image can be created. if (imageMatrix.empty()) { - videoThumbnails.add(ImageUtils.getDefaultThumbnail()); + videoThumbnails.add(VIDEO_DEFAULT_IMAGE); if (cacheDirectory != null) { try { - ImageIO.write((RenderedImage) ImageUtils.getDefaultThumbnail(), THUMBNAIL_FORMAT, + ImageIO.write(VIDEO_DEFAULT_IMAGE, THUMBNAIL_FORMAT, Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) } catch (IOException ex) { logger.log(Level.WARNING, "Unable to save default video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); @@ -660,7 +673,7 @@ class FileSearch { videoFile.release(); // close the file} } } else { - loadSavedThumbnails(cacheDirectory, thumbnailWrapper); + loadSavedThumbnails(cacheDirectory, thumbnailWrapper, VIDEO_DEFAULT_IMAGE); } } @@ -674,7 +687,7 @@ class FileSearch { * information about the file and the thumbnails * associated with it. */ - private static void loadSavedThumbnails(String cacheDirectory, VideoThumbnailsWrapper thumbnailWrapper) { + private static void loadSavedThumbnails(String cacheDirectory, VideoThumbnailsWrapper thumbnailWrapper, BufferedImage failedVideoThumbImage) { int[] framePositions = new int[4]; List videoThumbnails = new ArrayList<>(); int thumbnailNumber = 0; @@ -683,7 +696,7 @@ class FileSearch { try { videoThumbnails.add(ImageIO.read(Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, md5, fileName).toFile())); } catch (IOException ex) { - videoThumbnails.add(ImageUtils.getDefaultThumbnail()); + videoThumbnails.add(failedVideoThumbImage); logger.log(Level.WARNING, "Unable to read saved video thumbnail " + fileName + " for " + md5, ex); } int framePos = Integer.valueOf(FilenameUtils.getBaseName(fileName).substring(2)); @@ -699,12 +712,12 @@ class FileSearch { * * @return List containing the default thumbnail. */ - private static List createDefaultThumbnailList() { + private static List createDefaultThumbnailList(BufferedImage failedVideoThumbImage) { List videoThumbnails = new ArrayList<>(); - videoThumbnails.add(ImageUtils.getDefaultThumbnail()); - videoThumbnails.add(ImageUtils.getDefaultThumbnail()); - videoThumbnails.add(ImageUtils.getDefaultThumbnail()); - videoThumbnails.add(ImageUtils.getDefaultThumbnail()); + videoThumbnails.add(failedVideoThumbImage); + videoThumbnails.add(failedVideoThumbImage); + videoThumbnails.add(failedVideoThumbImage); + videoThumbnails.add(failedVideoThumbImage); return videoThumbnails; } diff --git a/Core/src/org/sleuthkit/autopsy/filequery/FileSearchData.java b/Core/src/org/sleuthkit/autopsy/filequery/FileSearchData.java index d86d470102..32a2cf19cb 100644 --- a/Core/src/org/sleuthkit/autopsy/filequery/FileSearchData.java +++ b/Core/src/org/sleuthkit/autopsy/filequery/FileSearchData.java @@ -273,7 +273,6 @@ final class FileSearchData { = new ImmutableSet.Builder() .add("text/html", //NON-NLS "text/csv", //NON-NLS - "text/x-log", //NON-NLS "application/rtf", //NON-NLS "application/pdf", //NON-NLS "application/xhtml+xml", //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java index 8c6e879c76..0462e77842 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java @@ -434,7 +434,7 @@ final public class MapPanel extends javax.swing.JPanel { /** * Find the waypoint that is closest to the given mouse click point. * - * @param mouseClickPoint The mouse click point + * @param clickPoint The mouse click point * * @return A waypoint that is within 10 pixels of the given point, or null * if none was found. diff --git a/Core/src/org/sleuthkit/autopsy/images/failedToCreateVideoThumb.png b/Core/src/org/sleuthkit/autopsy/images/failedToCreateVideoThumb.png new file mode 100644 index 0000000000..876402c150 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/failedToCreateVideoThumb.png differ diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslateTextTask.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslateTextTask.java index 9fe93b21ab..245052d4a4 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslateTextTask.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslateTextTask.java @@ -81,9 +81,9 @@ public abstract class TranslateTextTask extends SwingWorker sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.texttranslation.utils; + +import org.apache.commons.io.FilenameUtils; +import org.sleuthkit.autopsy.texttranslation.NoServiceProviderException; +import org.sleuthkit.autopsy.texttranslation.TextTranslationService; +import org.sleuthkit.autopsy.texttranslation.TranslationException; + +/** + * A utility to translate file names. + */ +public final class FileNameTranslationUtil { + + /** + * Translates a file name using the configured machine translation service. + * + * @param fileName The file name. + * + * @return The translation of the file name. + * + * @throws NoServiceProviderException If machine translation is not + * configured. + * @throws TranslationException If there is an error doing the + * translation. + */ + public static String translate(String fileName) throws NoServiceProviderException, TranslationException { + /* + * Don't attempt translation if the characters of the file name are all + * ASCII chars. + * + * TODO (Jira-6175): This filter prevents translation of many + * non-English file names composed entirely of Latin chars. + */ + if (fileName.matches("^\\p{ASCII}+$")) { + return ""; + } + + TextTranslationService translator = TextTranslationService.getInstance(); + String baseName = FilenameUtils.getBaseName(fileName); + String translation = translator.translate(baseName); + if (!translation.isEmpty()) { + String extension = FilenameUtils.getExtension(fileName); + if (!extension.isEmpty()) { + String extensionDelimiter = (extension.isEmpty()) ? "" : "."; + translation += extensionDelimiter + extension; + } + } + return translation; + } + + /** + * Prevent instantiation of this utility class + */ + private FileNameTranslationUtil() { + } + +} diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java index c95b58b874..0016428a0d 100755 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java @@ -130,8 +130,7 @@ public class CentralRepoDatamodelTest extends TestCase { dbSettingsSqlite.saveSettings(); CentralRepoDbUtil.setUseCentralRepo(true); - CentralRepoPlatforms.setSelectedPlatform(CentralRepoPlatforms.SQLITE.name()); - CentralRepoPlatforms.saveSelectedPlatform(); + CentralRepoDbManager.saveDbChoice(CentralRepoDbChoice.SQLITE); } catch (CentralRepoException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseTestUtils.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseTestUtils.java index 65b47bcb3b..bd297579a7 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseTestUtils.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseTestUtils.java @@ -47,6 +47,8 @@ import org.sleuthkit.autopsy.testutils.IngestUtils; import org.sleuthkit.datamodel.TskCoreException; import junit.framework.Assert; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbChoice; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbManager; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; import org.sleuthkit.autopsy.coreutils.TimeStampUtils; @@ -303,8 +305,7 @@ class InterCaseTestUtils { centralRepoSchemaFactory.insertDefaultDatabaseContent(); crSettings.saveSettings(); - CentralRepoPlatforms.setSelectedPlatform(CentralRepoPlatforms.SQLITE.name()); - CentralRepoPlatforms.saveSelectedPlatform(); + CentralRepoDbManager.saveDbChoice(CentralRepoDbChoice.SQLITE); } /** diff --git a/CoreLibs/ivy.xml b/CoreLibs/ivy.xml index 6819dac82d..4853d1f90e 100644 --- a/CoreLibs/ivy.xml +++ b/CoreLibs/ivy.xml @@ -14,8 +14,7 @@ - - + @@ -73,8 +72,5 @@ - - - diff --git a/CoreLibs/nbproject/project.properties b/CoreLibs/nbproject/project.properties index 677f823e67..42f8292d5b 100644 --- a/CoreLibs/nbproject/project.properties +++ b/CoreLibs/nbproject/project.properties @@ -21,7 +21,6 @@ file.reference.dom4j-1.6.1.jar=release/modules/ext/dom4j-1.6.1.jar file.reference.geronimo-jms_1.1_spec-1.0.jar=release/modules/ext/geronimo-jms_1.1_spec-1.0.jar file.reference.gson-2.8.5.jar=release/modules/ext/gson-2.8.5.jar file.reference.gst1-java-core-1.0.0.jar=release\\modules\\ext\\gst1-java-core-1.0.0.jar -file.reference.jna-3.4.0.jar=release/modules/ext/jna-3.4.0.jar file.reference.guava-19.0.jar=release/modules/ext/guava-19.0.jar file.reference.imageio-bmp-3.2.jar=release/modules/ext/imageio-bmp-3.2.jar file.reference.imageio-core-3.2.jar=release/modules/ext/imageio-core-3.2.jar @@ -44,6 +43,8 @@ file.reference.jfxtras-common-8.0-r4.jar=release/modules/ext/jfxtras-common-8.0- file.reference.jfxtras-controls-8.0-r4.jar=release/modules/ext/jfxtras-controls-8.0-r4.jar file.reference.jfxtras-fxml-8.0-r4.jar=release/modules/ext/jfxtras-fxml-8.0-r4.jar file.reference.jna-3.4.0.jar=release/modules/ext/jna-3.4.0.jar +file.reference.jna-5.5.0.jar=release\\modules\\ext\\jna-5.5.0.jar +file.reference.jna-platform-5.5.0.jar=release\\modules\\ext\\jna-platform-5.5.0.jar file.reference.joda-time-2.4.jar=release/modules/ext/joda-time-2.4.jar file.reference.jsr305-1.3.9.jar=release/modules/ext/jsr305-1.3.9.jar file.reference.LGoodDatePicker-10.3.1.jar=release/modules/ext/LGoodDatePicker-10.3.1.jar @@ -52,7 +53,6 @@ file.reference.logkit-1.0.1.jar=release/modules/ext/logkit-1.0.1.jar file.reference.mail-1.4.3.jar=release/modules/ext/mail-1.4.3.jar file.reference.opencv-248.jar=release/modules/ext/opencv-248.jar file.reference.openjfx-dialogs-1.0.2.jar=release/modules/ext/openjfx-dialogs-1.0.3.jar -file.reference.platform-3.4.0.jar=release/modules/ext/platform-3.4.0.jar file.reference.poi-4.0.1.jar=release\\modules\\ext\\poi-4.0.1.jar file.reference.poi-excelant-4.0.1.jar=release\\modules\\ext\\poi-excelant-4.0.1.jar file.reference.poi-ooxml-4.0.1.jar=release\\modules\\ext\\poi-ooxml-4.0.1.jar diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index 0498669b04..d5169a8965 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -806,10 +806,6 @@ ext/sigar-1.6.4.jar release/modules/ext/sigar-1.6.4.jar
- - ext/jna-3.4.0.jar - release/modules/ext/jna-3.4.0.jar - ext/gson-2.8.5.jar release/modules/ext/gson-2.8.5.jar @@ -902,6 +898,10 @@ ext/commons-csv-1.4.jar release/modules/ext/commons-csv-1.4.jar + + ext/jna-5.5.0.jar + release/modules/ext/jna-5.5.0.jar + ext/imageio-sgi-3.2.jar release/modules/ext/imageio-sgi-3.2.jar @@ -946,10 +946,6 @@ ext/imageio-bmp-3.2.jar release/modules/ext/imageio-bmp-3.2.jar - - ext/platform-3.4.0.jar - release/modules/ext/platform-3.4.0.jar - ext/commons-lang-2.6.jar release/modules/ext/commons-lang-2.6.jar @@ -1018,6 +1014,10 @@ ext/dom4j-1.6.1.jar release/modules/ext/dom4j-1.6.1.jar + + ext/jna-platform-5.5.0.jar + release/modules/ext/jna-platform-5.5.0.jar + ext/imageio-metadata-3.2.jar release/modules/ext/imageio-metadata-3.2.jar diff --git a/InternalPythonModules/android/browserlocation.py b/InternalPythonModules/android/browserlocation.py index 84c2a601e7..c202c36d2a 100644 --- a/InternalPythonModules/android/browserlocation.py +++ b/InternalPythonModules/android/browserlocation.py @@ -95,7 +95,7 @@ class BrowserLocationAnalyzer(general.AndroidComponentAnalyzer): longitude = Double.valueOf(resultSet.getString("longitude")) attributes = ArrayList() - artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT) + artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK) attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE, general.MODULE_NAME, latitude)) attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE, general.MODULE_NAME, longitude)) attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, general.MODULE_NAME, timestamp)) diff --git a/InternalPythonModules/android/cachelocation.py b/InternalPythonModules/android/cachelocation.py index 1fb162a817..3697fb44b0 100644 --- a/InternalPythonModules/android/cachelocation.py +++ b/InternalPythonModules/android/cachelocation.py @@ -41,6 +41,7 @@ from org.sleuthkit.datamodel import TskCoreException import traceback import general +import struct """ Parses cache files that Android maintains for Wifi and cell towers. Adds GPS points to blackboard. @@ -74,60 +75,24 @@ class CacheLocationAnalyzer(general.AndroidComponentAnalyzer): def __findGeoLocationsInFile(self, file, abstractFile): - tempBytes = bytearray([0] * 2) # will temporarily hold bytes to be converted into the correct data types - try: - inputStream = FileInputStream(file) - - inputStream.read(tempBytes) # version - - tempBytes = bytearray([0] * 2) - inputStream.read(tempBytes) # number of location entries - - iterations = BigInteger(tempBytes).intValue() - - for i in range(iterations): # loop through every entry - tempBytes = bytearray([0] * 2) - inputStream.read(tempBytes) - - tempBytes = bytearray([0]) - inputStream.read(tempBytes) - - while BigInteger(tempBytes).intValue() != 0: # pass through non important values until the start of accuracy(around 7-10 bytes) - if 0 > inputStream.read(tempBytes): - break # we've passed the end of the file, so stop - - tempBytes = bytearray([0] * 3) - inputStream.read(tempBytes) - if BigInteger(tempBytes).intValue() <= 0: # This refers to a location that could not be calculated - tempBytes = bytearray([0] * 28) # read rest of the row's bytes - inputStream.read(tempBytes) - continue - accuracy = "" + BigInteger(tempBytes).intValue() - - tempBytes = bytearray([0] * 4) - inputStream.read(tempBytes) - confidence = "" + BigInteger(tempBytes).intValue() - - tempBytes = bytearray([0] * 8) - inputStream.read(tempBytes) - latitude = CacheLocationAnalyzer.toDouble(bytes) - - tempBytes = bytearray([0] * 8) - inputStream.read(tempBytes) - longitude = CacheLocationAnalyzer.toDouble(bytes) - - tempBytes = bytearray([0] * 8) - inputStream.read(tempBytes) - timestamp = BigInteger(tempBytes).longValue() / 1000 + # code to parse the cache.wifi and cache.cell taken from https://forensics.spreitzenbarth.de/2011/10/28/decoding-cache-cell-and-cache-wifi-files/ + cacheFile = open(str(file), 'rb') + (version, entries) = struct.unpack('>hh', cacheFile.read(4)) + i = 0 + while i < entries: + key = cacheFile.read(struct.unpack('>h', cacheFile.read(2))[0]) + (accuracy, confidence, latitude, longitude, readtime) = struct.unpack('>iiddQ', cacheFile.read(32)) + timestamp = readtime/1000 + i = i + 1 attributes = ArrayList() - artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT) - attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE, AndroidAnalyzer.MODULE_NAME, latitude)) - attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE, AndroidAnalyzer.MODULE_NAME, longitude)) - attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, AndroidModuleFactorymodule.Name, timestamp)) - attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME, AndroidAnalyzer.MODULE_NAME, - file.getName() + "Location History")) + artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK) + attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE, general.MODULE_NAME, latitude)) + attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE, general.MODULE_NAME, longitude)) + attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, general.MODULE_NAME, timestamp)) + attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME, general.MODULE_NAME, + abstractFile.getName() + " Location History")) artifact.addAttributes(attributes) #Not storing these for now. @@ -136,15 +101,13 @@ class CacheLocationAnalyzer(general.AndroidComponentAnalyzer): try: # index the artifact for keyword search blackboard = Case.getCurrentCase().getSleuthkitCase().getBlackboard() - blackboard.postArtifact(artifact, MODULE_NAME) + blackboard.postArtifact(artifact, general.MODULE_NAME) except Blackboard.BlackboardException as ex: self._logger.log(Level.SEVERE, "Unable to index blackboard artifact " + str(artifact.getArtifactID()), ex) self._logger.log(Level.SEVERE, traceback.format_exc()) MessageNotifyUtil.Notify.error("Failed to index GPS trackpoint artifact for keyword search.", artifact.getDisplayName()) + cacheFile.close() - except SQLException as ex: - # Unable to execute Cached GPS locations SQL query against database. - pass except Exception as ex: self._logger.log(Level.SEVERE, "Error parsing Cached GPS locations to blackboard", ex) self._logger.log(Level.SEVERE, traceback.format_exc()) diff --git a/InternalPythonModules/android/viber.py b/InternalPythonModules/android/viber.py index a5edef4ce9..1ad418b478 100644 --- a/InternalPythonModules/android/viber.py +++ b/InternalPythonModules/android/viber.py @@ -117,13 +117,25 @@ class ViberAnalyzer(general.AndroidComponentAnalyzer): try: contacts_parser = ViberContactsParser(contacts_db) while contacts_parser.next(): - helper.addContact( - contacts_parser.get_contact_name(), - contacts_parser.get_phone(), - contacts_parser.get_home_phone(), - contacts_parser.get_mobile_phone(), - contacts_parser.get_email() - ) + if (not(not contacts_parser.get_phone() or contacts_parser.get_phone().isspace())): + helper.addContact( + contacts_parser.get_contact_name(), + contacts_parser.get_phone(), + contacts_parser.get_home_phone(), + contacts_parser.get_mobile_phone(), + contacts_parser.get_email() + ) + # Check if contact_name is blank and if it is not create a TSK_CONTACT otherwise ignore as not Contact Info + elif (not(not contacts_parser.get_contact_name() or contacts_parser.get_contact_name().isspace())): + current_case = Case.getCurrentCase().getSleuthkitCase() + attributes = ArrayList() + artifact = contacts_db.getDBFile().newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT) + attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME.getTypeID(), self._PARSER_NAME, contacts_parser.get_contact_name())) + artifact.addAttributes(attributes) + + # Post the artifact to blackboard + current_case.getBlackboard().postArtifact(artifact, self._PARSER_NAME) + contacts_parser.close() except SQLException as ex: self._logger.log(Level.WARNING, "Error querying the viber database for contacts.", ex) @@ -268,8 +280,8 @@ class ViberContactsParser(TskContactsParser): def __init__(self, contact_db): super(ViberContactsParser, self).__init__(contact_db.runQuery( """ - SELECT C.display_name AS name, - D.data2 AS number + SELECT C.display_name AS name, + coalesce(D.data2, D.data1, D.data3) AS number FROM phonebookcontact AS C JOIN phonebookdata AS D ON C._id = D.contact_id