2019-10-28 16:14:08 -04:00

978 lines
40 KiB
Java

/*
* Autopsy Forensic Browser
*
* Copyright 2015-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> 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.imagegallery;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.sql.SQLException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import org.apache.commons.collections4.CollectionUtils;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.Case.CaseType;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.casemodule.events.DataSourceDeletedEvent;
import org.sleuthkit.autopsy.coreutils.History;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.events.AutopsyEvent;
import org.sleuthkit.autopsy.imagegallery.actions.UndoRedoManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB.DrawableDbBuildStatusEnum;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.HashSetManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.ingest.ModuleDataEvent;
import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisEvent;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
/**
* Instances of this class are responsible for fulfilling the controller role in
* an MVC pattern implementation where the model is the drawables database for a
* case plus the image gallery tables in the case database, and the view is the
* image gallery top component.
*/
public final class ImageGalleryController {
private static final Logger logger = Logger.getLogger(ImageGalleryController.class.getName());
private static final Set<IngestManager.IngestJobEvent> INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_STARTED, IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED);
private static final Set<IngestManager.IngestModuleEvent> INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestModuleEvent.DATA_ADDED, IngestManager.IngestModuleEvent.FILE_DONE);
/*
* The file limit for image gallery. If the selected data source (or all
* data sources, if that option is selected) has more than this many files
* in the tsk_files table, the user cannot use the image gallery.
*/
private static final long FILE_LIMIT = 6_000_000;
private static final Set<Case.Events> CASE_EVENTS_OF_INTEREST = EnumSet.of(
Case.Events.CURRENT_CASE,
Case.Events.DATA_SOURCE_ADDED,
Case.Events.CONTENT_TAG_ADDED,
Case.Events.CONTENT_TAG_DELETED,
Case.Events.DATA_SOURCE_DELETED
);
/*
* There is an image gallery controller per case. It is created during the
* opening of case resources and destroyed during the closing of case
* resources.
*/
private static final Object controllersByCaseLock = new Object();
@GuardedBy("controllersByCaseLock")
private static final Map<String, ImageGalleryController> controllersByCase = new HashMap<>();
/**
* A flag that controls whether or not the image gallery controller is
* handling various application events. Set to true by default.
*/
private final SimpleBooleanProperty listeningEnabled;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final ReadOnlyBooleanWrapper modelStale;
private final ReadOnlyBooleanWrapper metaDataCollapsed;
private final SimpleDoubleProperty thumbnailSizeProp;
private final ReadOnlyBooleanWrapper regroupDisabled;
private final ReadOnlyIntegerWrapper dbTaskQueueSize;
private final History<GroupViewState> historyManager;
private final UndoRedoManager undoManager;
private final Case theCase;
private final SleuthkitCase caseDb;
private final CaseEventListener caseEventListener;
private final IngestJobEventListener ingestJobEventListener;
private final IngestModuleEventListener ingestModuleEventListener;
private volatile ImageGalleryTopComponent topComponent;
private FileIDSelectionModel selectionModel;
private ThumbnailCache thumbnailCache;
private DrawableDB drawableDB;
private GroupManager groupManager;
private HashSetManager hashSetManager;
private CategoryManager categoryManager;
private DrawableTagsManager tagsManager;
private ListeningExecutorService dbExecutor;
/**
* Creates an image gallery controller for a case. The controller will
* create/open the model for the case: a local drawables database and the
* image gallery tables in the case database.
*
* @param theCase The case.
*
* @throws TskCoreException If there is an issue creating/opening a local
* drawables database for the case or the image
* gallery tables in the case database.
*/
static void createController(Case theCase) throws TskCoreException {
synchronized (controllersByCaseLock) {
if (!controllersByCase.containsKey(theCase.getName())) {
ImageGalleryController controller = new ImageGalleryController(theCase);
controller.startUp();
controllersByCase.put(theCase.getName(), controller);
}
}
}
/**
* Gets the image gallery controller for a case.
*
* @param theCase The case.
*
* @return The controller or null if it does not exist.
*/
public static ImageGalleryController getController(Case theCase) {
synchronized (controllersByCaseLock) {
return controllersByCase.get(theCase.getName());
}
}
/**
* Shuts down the image gallery controller for a case. The controller closes
* the model for the case: a local drawables database and the image gallery
* tables in the case database.
*
* @param theCase The case.
*/
static void shutDownController(Case theCase) {
synchronized (controllersByCaseLock) {
if (controllersByCase.containsKey(theCase.getName())) {
ImageGalleryController controller = controllersByCase.remove(theCase.getName());
controller.shutDown();
}
}
}
/**
* Constructs an object that is responsible for fulfilling the controller
* role in an MVC pattern implementation where the model is the drawables
* database for a case plus the image gallery tables in the case database,
* and the view is the image gallery top component.
*
* @param theCase The case.
*
* @throws TskCoreException If there is an error constructing the
* controller.
*/
ImageGalleryController(@Nonnull Case theCase) throws TskCoreException {
this.theCase = Objects.requireNonNull(theCase);
caseDb = theCase.getSleuthkitCase();
listeningEnabled = new SimpleBooleanProperty(false);
modelStale = new ReadOnlyBooleanWrapper(false);
metaDataCollapsed = new ReadOnlyBooleanWrapper(false);
thumbnailSizeProp = new SimpleDoubleProperty(100);
regroupDisabled = new ReadOnlyBooleanWrapper(false);
dbTaskQueueSize = new ReadOnlyIntegerWrapper(0);
historyManager = new History<>();
undoManager = new UndoRedoManager();
setListeningEnabled(ImageGalleryModule.isEnabledforCase(theCase));
caseEventListener = new CaseEventListener();
ingestJobEventListener = new IngestJobEventListener();
ingestModuleEventListener = new IngestModuleEventListener();
}
void startUp() throws TskCoreException {
selectionModel = new FileIDSelectionModel(this);
thumbnailCache = new ThumbnailCache(this);
/*
* TODO (JIRA-5212): The next two lines need to be executed in this
* order. Why? This suggests there is some inappropriate coupling
* between the DrawableDB and GroupManager classes.
*/
groupManager = new GroupManager(this);
drawableDB = DrawableDB.getDrawableDB(this);
categoryManager = new CategoryManager(this);
tagsManager = new DrawableTagsManager(this);
tagsManager.registerListener(groupManager);
tagsManager.registerListener(categoryManager);
hashSetManager = new HashSetManager(drawableDB);
setModelIsStale(isDataSourcesTableStale());
dbExecutor = getNewDBExecutor();
listeningEnabled.addListener((observable, wasPreviouslyEnabled, isEnabled) -> {
try {
/*
* For multi-user cases, this listener does nothing because
* rebuilding the drawables database is deferred until the Image
* Gallery tool is opened.
*/
if (isEnabled && !wasPreviouslyEnabled
&& (Case.getCurrentCaseThrows().getCaseType() == CaseType.SINGLE_USER_CASE)
&& isDataSourcesTableStale()) {
rebuildDrawablesDb();
}
} catch (NoCurrentCaseException ex) {
logger.log(Level.WARNING, "Exception while getting open case.", ex);
}
});
viewStateProperty().addListener((Observable observable) -> {
selectionModel.clearSelection();
undoManager.clear();
});
/*
* Disable regrouping when drawables database tasks are enqueued.
*/
dbTaskQueueSize.addListener(obs -> this.updateRegroupDisabled());
Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, caseEventListener);
IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, ingestJobEventListener);
IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, ingestModuleEventListener);
SwingUtilities.invokeLater(() -> {
topComponent = ImageGalleryTopComponent.getTopComponent();
});
}
/**
* Shuts down this image gallery controller.
*/
public synchronized void shutDown() {
logger.log(Level.INFO, String.format("Shutting down image gallery controller for case %s (%s)", theCase.getDisplayName(), theCase.getName()));
Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, caseEventListener);
IngestManager.getInstance().removeIngestJobEventListener(ingestJobEventListener);
IngestManager.getInstance().removeIngestModuleEventListener(ingestModuleEventListener);
selectionModel.clearSelection();
thumbnailCache.clearCache();
historyManager.clear();
groupManager.reset();
shutDownDBExecutor();
drawableDB.close();
logger.log(Level.INFO, String.format("Completed shut down of image gallery controller for case %s (%s)", theCase.getDisplayName(), theCase.getName()));
}
/**
* Gets the case that provides the model (the local drawables database and
* the image gallery tables in the case database) for this controller.
*
* @return The case.
*/
public Case getCase() {
return theCase;
}
/**
* Gets the drawables database that is part of the model for this
* controller.
*
* @return The drawables database.
*/
public DrawableDB getDrawablesDatabase() {
return drawableDB;
}
/**
* Gets the case database that provides part of the model for this
* controller.
*
* @return The case database.
*/
public SleuthkitCase getCaseDatabase() {
return caseDb;
}
public ReadOnlyBooleanProperty metaDataCollapsedProperty() {
return metaDataCollapsed.getReadOnlyProperty();
}
public void setMetaDataCollapsed(Boolean metaDataCollapsed) {
this.metaDataCollapsed.set(metaDataCollapsed);
}
public DoubleProperty thumbnailSizeProperty() {
return thumbnailSizeProp;
}
public ReadOnlyBooleanProperty regroupDisabledProperty() {
return regroupDisabled.getReadOnlyProperty();
}
public FileIDSelectionModel getSelectionModel() {
return selectionModel;
}
public GroupManager getGroupManager() {
return groupManager;
}
public void setListeningEnabled(boolean enabled) {
synchronized (listeningEnabled) {
listeningEnabled.set(enabled);
}
}
public boolean isListeningEnabled() {
synchronized (listeningEnabled) {
return listeningEnabled.get();
}
}
/**
* Sets a flag indicating whether the model is "stale" for any data source
* in the current case. The model is a local drawables database and the
* image gallery tables in the case database.
*
* @param isStale True if the model is "stale" for any data source in the
* current case.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
void setModelIsStale(Boolean isStale) {
Platform.runLater(() -> {
modelStale.set(isStale);
});
}
/**
* Gets the boolean property that is set to true if the model is "stale" for
* any data source in the current case. The model is a local drawables
* database and the image gallery tables in the case database.
*
* @return The property that is set to true if the model is "stale" for any
* data source in the current case.
*/
public ReadOnlyBooleanProperty modelIsStaleProperty() {
return modelStale.getReadOnlyProperty();
}
/**
* Gets the state of the flag that is set if the Model is "stale" for any
* data source in the case. The model is a local drawables database and the
* image gallery tables in the case database.
*
* @return True if the model is "stale" for any data source in the current
* case.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
boolean modelIsStale() {
return modelStale.get();
}
/**
* Gets the state of the image group display area in the UI.
*
* @return The current state.
*/
public GroupViewState getViewState() {
return historyManager.getCurrentState();
}
/**
* Gets the state of the image group display area in the UI.
*
* @return The current state.
*/
public ReadOnlyObjectProperty<GroupViewState> viewStateProperty() {
return historyManager.currentState();
}
/**
* Should the "forward" button on the history be enabled?
*
* @return True or false.
*/
public ReadOnlyBooleanProperty getCanAdvance() {
return historyManager.getCanAdvance();
}
/**
* Should the "Back" button on the history be enabled?
*
* @return True or false.
*/
public ReadOnlyBooleanProperty getCanRetreat() {
return historyManager.getCanRetreat();
}
/**
* Displays the passed in image group. Causes this group to get recorded in
* the history queue and observers of the current state will be notified and
* update their panels/widgets appropriately.
*
* @param newState
*/
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
public void advance(GroupViewState newState) {
historyManager.advance(newState);
}
/**
* Display the next group in the "forward" history stack
*
* @return
*/
public GroupViewState advance() {
return historyManager.advance();
}
/**
* Display the previous group in the "back" history stack
*
* @return
*/
public GroupViewState retreat() {
return historyManager.retreat();
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void updateRegroupDisabled() {
regroupDisabled.set((dbTaskQueueSize.get() > 0) || IngestManager.getInstance().isIngestRunning());
}
/**
* Rebuilds the DrawableDB database.
*
*/
public void rebuildDrawablesDb() {
// queue a rebuild task for each stale data source
getStaleDataSourceIds().forEach(dataSourceObjId -> queueDBTask(new AddDrawableFilesTask(dataSourceObjId, this)));
}
/**
* Checks if the datasources table in drawable DB is stale.
*
* @return true if datasources table is stale
*/
public boolean isDataSourcesTableStale() {
return isNotEmpty(getStaleDataSourceIds());
}
/**
* Returns a set of data source object ids that are stale.
*
* This includes any data sources already in the table, that are not in
* COMPLETE or IN_PROGRESS status, or any data sources that might have been
* added to the case, but are not in the datasources table.
*
* @return list of data source object ids that are stale.
*/
Set<Long> getStaleDataSourceIds() {
Set<Long> staleDataSourceIds = new HashSet<>();
// no current case open to check
if ((null == getDrawablesDatabase()) || (null == getCaseDatabase())) {
return staleDataSourceIds;
}
try {
Map<Long, DrawableDbBuildStatusEnum> knownDataSourceIds = getDrawablesDatabase().getDataSourceDbBuildStatus();
List<DataSource> dataSources = getCaseDatabase().getDataSources();
Set<Long> caseDataSourceIds = new HashSet<>();
dataSources.stream().map(DataSource::getId).forEach(caseDataSourceIds::add);
// collect all data sources already in the table, that are not yet COMPLETE
knownDataSourceIds.entrySet().stream().forEach((Map.Entry<Long, DrawableDbBuildStatusEnum> t) -> {
DrawableDbBuildStatusEnum status = t.getValue();
switch (status) {
case COMPLETE:
case IN_PROGRESS:
// not stale
break;
case REBUILT_STALE:
staleDataSourceIds.add(t.getKey());
break;
case UNKNOWN:
try {
// stale if there are files in CaseDB with MIME types
if (hasFilesWithMimeType(t.getKey())) {
staleDataSourceIds.add(t.getKey());
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error getting MIME types", ex);
}
break;
}
});
// collect any new data sources in the case.
caseDataSourceIds.forEach((Long id) -> {
if (!knownDataSourceIds.containsKey(id)) {
staleDataSourceIds.add(id);
}
});
return staleDataSourceIds;
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Image Gallery failed to check if datasources table is stale.", ex);
return staleDataSourceIds;
}
}
/**
* Returns a map of all data source object ids, along with their DB build
* status.
*
* This includes any data sources already in the table, and any data sources
* that might have been added to the case, but are not in the datasources
* table.
*
* @return map of data source object ids and their Db build status.
*/
public Map<Long, DrawableDbBuildStatusEnum> getAllDataSourcesDrawableDBStatus() {
Map<Long, DrawableDbBuildStatusEnum> dataSourceStatusMap = new HashMap<>();
// no current case open to check
if ((null == getDrawablesDatabase()) || (null == getCaseDatabase())) {
return dataSourceStatusMap;
}
try {
Map<Long, DrawableDbBuildStatusEnum> knownDataSourceIds = getDrawablesDatabase().getDataSourceDbBuildStatus();
List<DataSource> dataSources = getCaseDatabase().getDataSources();
Set<Long> caseDataSourceIds = new HashSet<>();
dataSources.stream().map(DataSource::getId).forEach(caseDataSourceIds::add);
// collect all data sources already in the table
knownDataSourceIds.entrySet().stream().forEach((Map.Entry<Long, DrawableDbBuildStatusEnum> t) -> {
dataSourceStatusMap.put(t.getKey(), t.getValue());
});
// collect any new data sources in the case.
caseDataSourceIds.forEach((Long id) -> {
if (!knownDataSourceIds.containsKey(id)) {
dataSourceStatusMap.put(id, DrawableDbBuildStatusEnum.UNKNOWN);
}
});
return dataSourceStatusMap;
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Image Gallery failed to get data source DB status.", ex);
return dataSourceStatusMap;
}
}
public boolean hasTooManyFiles(DataSource datasource) throws TskCoreException {
String whereClause = (datasource == null)
? "1 = 1"
: "data_source_obj_id = " + datasource.getId();
return caseDb.countFilesWhere(whereClause) > FILE_LIMIT;
}
/**
* Checks if the given data source has any files with no mimetype
*
* @param datasource
*
* @return true if the datasource has any files with no mime type
*
* @throws TskCoreException
*/
public boolean hasFilesWithNoMimeType(long dataSourceId) throws TskCoreException {
// There are some special files/attributes in the root folder, like $BadClus:$Bad and $Security:$SDS
// The IngestTasksScheduler does not push them down to the ingest modules,
// and hence they do not have any assigned mimetype
String whereClause = "data_source_obj_id = " + dataSourceId
+ " AND ( meta_type = " + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getValue() + ")"
+ " AND ( mime_type IS NULL )"
+ " AND ( meta_addr >= 32 ) "
+ " AND ( parent_path <> '/' )"
+ " AND ( name NOT like '$%:%' )";
return caseDb.countFilesWhere(whereClause) > 0;
}
public boolean hasFilesWithMimeType(long dataSourceId) throws TskCoreException {
String whereClause = "data_source_obj_id = " + dataSourceId
+ " AND ( meta_type = " + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getValue() + ")"
+ " AND ( mime_type IS NOT NULL )";
return caseDb.countFilesWhere(whereClause) > 0;
}
synchronized private void shutDownDBExecutor() {
dbExecutor.shutdownNow();
try {
dbExecutor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
logger.log(Level.WARNING, "Image Gallery failed to shutdown DB Task Executor in a timely fashion.", ex);
}
}
private static ListeningExecutorService getNewDBExecutor() {
return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder().setNameFormat("DB-Worker-Thread-%d").build()));
}
/**
* add InnerTask to the queue that the worker thread gets its work from
*
* @param bgTask
*/
public synchronized void queueDBTask(DrawableDbTask bgTask) {
if (!dbExecutor.isShutdown()) {
incrementQueueSize();
dbExecutor.submit(bgTask).addListener(this::decrementQueueSize, MoreExecutors.directExecutor());
}
}
private void incrementQueueSize() {
Platform.runLater(() -> dbTaskQueueSize.set(dbTaskQueueSize.get() + 1));
}
private void decrementQueueSize() {
Platform.runLater(() -> dbTaskQueueSize.set(dbTaskQueueSize.get() - 1));
}
public DrawableFile getFileFromID(Long fileID) throws TskCoreException {
return drawableDB.getFileFromID(fileID);
}
public HashSetManager getHashSetManager() {
return hashSetManager;
}
public CategoryManager getCategoryManager() {
return categoryManager;
}
public DrawableTagsManager getTagsManager() {
return tagsManager;
}
public UndoRedoManager getUndoManager() {
return undoManager;
}
public ReadOnlyIntegerProperty getDBTasksQueueSizeProperty() {
return dbTaskQueueSize.getReadOnlyProperty();
}
public ThumbnailCache getThumbsCache() {
return thumbnailCache;
}
/**
* Indicates whether or not a given file is of interest to the image gallery
* module (is "drawable") and is not marked as a "known" file (e.g., is not
* a file in the NSRL hash set).
*
* @param file The file.
*
* @return True if the file is "drawable" and not "known", false otherwise.
*
* @throws FileTypeDetectorInitException If there is an error determining
* the type of the file.
*/
private static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws FileTypeDetector.FileTypeDetectorInitException {
return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && FileTypeUtils.isDrawable(abstractFile);
}
/**
* A listener for ingest module application events.
*/
private class IngestModuleEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent event) {
/*
* Disable regrouping when ingest is running.
*/
Platform.runLater(ImageGalleryController.this::updateRegroupDisabled);
if (isListeningEnabled() == false) {
return;
}
/*
* Updates when individual files are fully analyzed and artifacts
* are added to a case are only done in "real time" on the host that
* is running the ingest job. On a remote host, the updates are
* deferred until the ingest job is complete.
*/
if (((AutopsyEvent) event).getSourceType() != AutopsyEvent.SourceType.LOCAL) {
return;
}
String eventType = event.getPropertyName();
switch (IngestManager.IngestModuleEvent.valueOf(eventType)) {
case FILE_DONE:
AbstractFile file = (AbstractFile) event.getNewValue();
if (!file.isFile()) {
return;
}
try {
if (isDrawableAndNotKnown(file)) {
queueDBTask(new UpdateDrawableFileTask(file, drawableDB));
}
} catch (FileTypeDetector.FileTypeDetectorInitException ex) {
logger.log(Level.SEVERE, String.format("Failed to determine if file is of interest to the image gallery module, ignoring file (obj_id=%d)", file.getId()), ex); //NON-NLS
}
break;
case DATA_ADDED:
ModuleDataEvent artifactAddedEvent = (ModuleDataEvent) event.getOldValue();
if (CollectionUtils.isNotEmpty(artifactAddedEvent.getArtifacts())) {
for (BlackboardArtifact art : artifactAddedEvent.getArtifacts()) {
if (artifactAddedEvent.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID()) {
drawableDB.addExifCache(art.getObjectID());
} else if (artifactAddedEvent.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) {
drawableDB.addHashSetCache(art.getObjectID());
}
}
}
break;
default:
break;
}
}
}
/**
* A listener for case application events.
*/
private class CaseEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent event) {
Case.Events eventType = Case.Events.valueOf(event.getPropertyName());
if (eventType == Case.Events.CURRENT_CASE) {
if (event.getOldValue() != null) { // Case closed event
if (topComponent != null) {
topComponent.closeForCurrentCase();
}
SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent);
}
} else {
switch (eventType) {
case DATA_SOURCE_ADDED:
if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) {
Content newDataSource = (Content) event.getNewValue();
if (isListeningEnabled()) {
drawableDB.insertOrUpdateDataSource(newDataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.UNKNOWN);
}
}
break;
case DATA_SOURCE_DELETED:
if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) {
final DataSourceDeletedEvent dataSourceDeletedEvent = (DataSourceDeletedEvent) event;
long dataSourceObjId = dataSourceDeletedEvent.getDataSourceId();
try {
drawableDB.deleteDataSource(dataSourceObjId);
} catch (SQLException ex) {
logger.log(Level.SEVERE, String.format("Failed to delete data source (obj_id = %d)", dataSourceObjId), ex); //NON-NLS
} catch (TskCoreException ex) {
Exceptions.printStackTrace(ex);
}
}
break;
case CONTENT_TAG_ADDED:
final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) event;
long objId = tagAddedEvent.getAddedTag().getContent().getId();
drawableDB.addTagCache(objId); // TODO (JIRA-5216): Why add the tag to the cache before doing the in DB check?
if (drawableDB.isInDB(objId)) {
tagsManager.fireTagAddedEvent(tagAddedEvent);
}
break;
case CONTENT_TAG_DELETED:
final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) event;
if (drawableDB.isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) {
tagsManager.fireTagDeletedEvent(tagDeletedEvent);
} // TODO (JIRA-5216): Why not remove the tag from the cache?
break;
default:
logger.log(Level.WARNING, String.format("Received %s event with no subscription", event.getPropertyName())); //NON-NLS
break;
}
}
}
}
/**
* A listener for ingest job application events.
*/
private class IngestJobEventListener implements PropertyChangeListener {
@NbBundle.Messages({
"ImageGalleryController.dataSourceAnalyzed.confDlg.msg= A new data source was added and finished ingest.\n"
+ "The image / video database may be out of date. "
+ "Do you want to update the database with ingest results?\n",
"ImageGalleryController.dataSourceAnalyzed.confDlg.title=Image Gallery"
})
@Override
public void propertyChange(PropertyChangeEvent event) {
/*
* Disable regrouping when ingest is running.
*/
Platform.runLater(ImageGalleryController.this::updateRegroupDisabled);
/*
* Only handling data source analysis events.
*/
if (!(event instanceof DataSourceAnalysisEvent)) {
return;
}
DataSourceAnalysisEvent dataSourceEvent = (DataSourceAnalysisEvent) event;
Content dataSource = dataSourceEvent.getDataSource();
if (dataSource == null) {
logger.log(Level.WARNING, String.format("Failed to handle %s event", event.getPropertyName())); //NON-NLS
return;
}
long dataSourceObjId = dataSource.getId();
String eventType = dataSourceEvent.getPropertyName();
try {
switch (IngestManager.IngestJobEvent.valueOf(eventType)) {
case DATA_SOURCE_ANALYSIS_STARTED:
handleDataSourceAnalysisStarted(dataSourceEvent);
break;
case DATA_SOURCE_ANALYSIS_COMPLETED:
handleDataSourceAnalysisCompleted(dataSourceEvent);
break;
default:
break;
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format("Failed to handle %s event for %s (objId=%d)", dataSourceEvent.getPropertyName(), dataSource.getName(), dataSourceObjId), ex);
}
}
}
/**
* Handles a data source analysis started event by adding the data source to
* the drawables database.
*
* @param event The event.
*
* @throws TskCoreException If there is an error adding the data source to
* the database.
*/
private void handleDataSourceAnalysisStarted(DataSourceAnalysisEvent event) throws TskCoreException {
if (event.getSourceType() == AutopsyEvent.SourceType.LOCAL && isListeningEnabled()) {
Content dataSource = event.getDataSource();
long dataSourceObjId = dataSource.getId();
if (drawableDB.getDataSourceDbBuildStatus(dataSourceObjId) != DrawableDB.DrawableDbBuildStatusEnum.COMPLETE) {
drawableDB.insertOrUpdateDataSource(dataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS);
}
drawableDB.buildFileMetaDataCache();
}
}
/**
* Handles a data source analysis completed event by updating the state of
* the data source stored in the drawables database if the event is local or
* prompting the user to do a refresh if the event is remote.
*
* @param event The event.
*
* @throws TskCoreException If there is an error updating the state ot the
* data source in the database.
*/
private void handleDataSourceAnalysisCompleted(DataSourceAnalysisEvent event) throws TskCoreException {
if (event.getSourceType() == AutopsyEvent.SourceType.LOCAL) {
Content dataSource = event.getDataSource();
long dataSourceObjId = dataSource.getId();
/*
* This node just completed analysis of a data source. Set the state
* of the local drawables database.
*/
if (isListeningEnabled()) {
groupManager.resetCurrentPathGroup();
if (drawableDB.getDataSourceDbBuildStatus(dataSourceObjId) == DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS) {
// If at least one file in CaseDB has mime type, then set to COMPLETE
// Otherwise, back to UNKNOWN since we assume file type module was not run
DrawableDB.DrawableDbBuildStatusEnum datasourceDrawableDBStatus
= hasFilesWithMimeType(dataSourceObjId)
? DrawableDB.DrawableDbBuildStatusEnum.COMPLETE
: DrawableDB.DrawableDbBuildStatusEnum.UNKNOWN;
drawableDB.insertOrUpdateDataSource(dataSource.getId(), datasourceDrawableDBStatus);
}
drawableDB.freeFileMetaDataCache();
}
} else if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.REMOTE) {
/*
* A remote node just completed analysis of a data source. The local
* drawables database is therefore stale. If the image gallery top
* component is open, give the user an opportunity to update the
* drawables database now.
*/
setModelIsStale(true);
if (isListeningEnabled()) {
SwingUtilities.invokeLater(() -> {
if (ImageGalleryTopComponent.isImageGalleryOpen()) {
int showAnswer = JOptionPane.showConfirmDialog(ImageGalleryTopComponent.getTopComponent(),
Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_msg(),
Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_title(),
JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
switch (showAnswer) {
case JOptionPane.YES_OPTION:
rebuildDrawablesDb();
break;
case JOptionPane.NO_OPTION:
case JOptionPane.CANCEL_OPTION:
default:
break;
}
}
});
}
}
}
}