/*
* Autopsy Forensic Browser
*
* Copyright 2011-2016 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.datamodel;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Level;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.core.UserPreferences;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ContentVisitor;
import org.sleuthkit.datamodel.DerivedFile;
import org.sleuthkit.datamodel.Directory;
import org.sleuthkit.datamodel.File;
import org.sleuthkit.datamodel.LayoutFile;
import org.sleuthkit.datamodel.LocalFile;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
/**
* Class which contains the Nodes for the 'By Mime Type' view located in the
* File Types view, shows all files with a mime type. Will initially be empty
* until file type identification has been performed. Contains a Property Change
* Listener which is checking for changes in IngestJobEvent Completed or
* Cancelled and IngestModuleEvent Content Changed.
*/
public final class FileTypesByMimeType extends Observable implements AutopsyVisitableItem {
private final SleuthkitCase skCase;
/**
* The nodes of this tree will be determined dynamically by the mimetypes
* which exist in the database. This hashmap will store them with the media
* type as the key and a list of media subtypes as the value.
*/
private final HashMap<String, List<String>> existingMimeTypes = new HashMap<>();
private static final Logger LOGGER = Logger.getLogger(FileTypesByMimeType.class.getName());
private void removeListeners() {
deleteObservers();
IngestManager.getInstance().removeIngestJobEventListener(pcl);
Case.removePropertyChangeListener(pcl);
}
/*
* The pcl is in the class because it has the easiest mechanisms to add
* and remove itself during its life cycles.
*/
private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> {
String eventType = evt.getPropertyName();
if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString())
|| eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) {
/**
* Checking for a current case is a stop gap measure until a
* different way of handling the closing of cases is worked out.
* Currently, remote events may be received for a case that is
* already closed.
*/
try {
Case.getCurrentCase();
populateHashMap();
} catch (IllegalStateException notUsed) {
/**
* Case is closed, do nothing.
*/
}
} else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) {
if (evt.getNewValue() == null) {
removeListeners();
}
}
};
/**
* Retrieve the media types by retrieving the keyset from the hashmap.
*
* @return mediaTypes - a list of strings representing all distinct media
* types of files for this case
*/
private List<String> getMediaTypeList() {
synchronized (existingMimeTypes) {
List<String> mediaTypes = new ArrayList<>(existingMimeTypes.keySet());
Collections.sort(mediaTypes);
return mediaTypes;
}
}
/**
* Performs the query on the database to get all distinct MIME types of
* files in it, and populate the hashmap with those results.
*/
private void populateHashMap() {
StringBuilder allDistinctMimeTypesQuery = new StringBuilder();
allDistinctMimeTypesQuery.append("SELECT DISTINCT mime_type from tsk_files where mime_type IS NOT null"); //NON-NLS
allDistinctMimeTypesQuery.append(" AND dir_type = ").append(TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue()); //NON-NLS
allDistinctMimeTypesQuery.append(" AND (type IN (").append(TskData.TSK_DB_FILES_TYPE_ENUM.FS.ordinal()).append(","); //NON-NLS
allDistinctMimeTypesQuery.append(TskData.TSK_DB_FILES_TYPE_ENUM.CARVED.ordinal()).append(",");
allDistinctMimeTypesQuery.append(TskData.TSK_DB_FILES_TYPE_ENUM.DERIVED.ordinal()).append(",");
allDistinctMimeTypesQuery.append(TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL.ordinal()).append("))");
synchronized (existingMimeTypes) {
existingMimeTypes.clear();
if (skCase == null) {
return;
}
try (SleuthkitCase.CaseDbQuery dbQuery = skCase.executeQuery(allDistinctMimeTypesQuery.toString())) {
ResultSet resultSet = dbQuery.getResultSet();
while (resultSet.next()) {
final String mime_type = resultSet.getString("mime_type"); //NON-NLS
if (!mime_type.isEmpty()) {
String mimeType[] = mime_type.split("/");
if (!mimeType[0].isEmpty() && !mimeType[1].isEmpty()) {
if (!existingMimeTypes.containsKey(mimeType[0])) {
existingMimeTypes.put(mimeType[0], new ArrayList<>());
}
existingMimeTypes.get(mimeType[0]).add(mimeType[1]);
}
}
}
} catch (TskCoreException | SQLException ex) {
LOGGER.log(Level.SEVERE, "Unable to populate File Types by MIME Type tree view from DB: ", ex); //NON-NLS
}
}
setChanged();
notifyObservers();
}
FileTypesByMimeType(SleuthkitCase skCase) {
IngestManager.getInstance().addIngestJobEventListener(pcl);
Case.addPropertyChangeListener(pcl);
this.skCase = skCase;
populateHashMap();
}
@Override
public <T> T accept(AutopsyItemVisitor<T> v) {
return v.visit(this);
}
/**
* Method to check if the node in question is a ByMimeTypeNode which is
* empty.
*
* @param node the Node which you wish to check.
* @return True if originNode is an instance of ByMimeTypeNode and is empty,
* false otherwise.
*/
public static boolean isEmptyMimeTypeNode(Node node) {
boolean isEmptyMimeNode = false;
if (node instanceof FileTypesByMimeType.ByMimeTypeNode && ((FileTypesByMimeType.ByMimeTypeNode) node).isEmpty()) {
isEmptyMimeNode = true;
}
return isEmptyMimeNode;
}
/**
* Class which represents the root node of the "By MIME Type" tree, will
* have children of each media type present in the database or no children
* when the file detection module has not been run and MIME type is
* currently unknown.
*/
class ByMimeTypeNode extends DisplayableItemNode {
@NbBundle.Messages("FileTypesByMimeType.name.text=By MIME Type")
final String NAME = Bundle.FileTypesByMimeType_name_text();
ByMimeTypeNode() {
super(Children.create(new ByMimeTypeNodeChildren(), true));
super.setName(NAME);
super.setDisplayName(NAME);
this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file_types.png");
}
@Override
public boolean isLeafTypeNode() {
return false;
}
@Override
public <T> T accept(DisplayableItemNodeVisitor<T> v) {
return v.visit(this);
}
@Override
public String getItemType() {
return getClass().getName();
}
boolean isEmpty() {
return existingMimeTypes.isEmpty();
}
}
/**
* Creates the children for the "By MIME Type" node these children will each
* represent a distinct media type present in the DB
*/
private class ByMimeTypeNodeChildren extends ChildFactory<String> implements Observer {
private ByMimeTypeNodeChildren() {
super();
addObserver(this);
}
@Override
protected boolean createKeys(List<String> mediaTypeNodes) {
if (!existingMimeTypes.isEmpty()) {
mediaTypeNodes.addAll(getMediaTypeList());
}
return true;
}
@Override
protected Node createNodeForKey(String key) {
return new MediaTypeNode(key);
}
@Override
public void update(Observable o, Object arg) {
refresh(true);
}
}
/**
* The Media type node created by the ByMimeTypeNodeChildren and contains
* one of the unique media types present in the database for this case.
*/
class MediaTypeNode extends DisplayableItemNode {
MediaTypeNode(String name) {
super(Children.create(new MediaTypeNodeChildren(name), true));
setName(name);
setDisplayName(name);
this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file_types.png");
}
@Override
public boolean isLeafTypeNode() {
return false;
}
@Override
public <T> T accept(DisplayableItemNodeVisitor<T> v) {
return v.visit(this);
}
@Override
public String getItemType() {
return getClass().getName();
}
}
/**
* Creates children for media type nodes, children will be MediaSubTypeNodes
* and represent one of the subtypes which are present in the database of
* their media type.
*/
private class MediaTypeNodeChildren extends ChildFactory<String> implements Observer {
String mediaType;
MediaTypeNodeChildren(String name) {
addObserver(this);
this.mediaType = name;
}
@Override
protected boolean createKeys(List<String> mediaTypeNodes) {
mediaTypeNodes.addAll(existingMimeTypes.get(mediaType));
return true;
}
@Override
protected Node createNodeForKey(String subtype) {
String mimeType = mediaType + "/" + subtype;
return new MediaSubTypeNode(mimeType);
}
@Override
public void update(Observable o, Object arg) {
refresh(true);
}
}
/**
* Node which represents the media sub type in the By MIME type tree, the
* media subtype is the portion of the MIME type following the /.
*/
class MediaSubTypeNode extends DisplayableItemNode implements Observer {
private MediaSubTypeNode(String mimeType) {
super(Children.create(new MediaSubTypeNodeChildren(mimeType), true));
addObserver(this);
init(mimeType);
}
private void init(String mimeType) {
super.setName(mimeType);
updateDisplayName(mimeType);
this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file-filter-icon.png"); //NON-NLS
}
/**
* Updates the display name of the mediaSubTypeNode to include the count
* of files which it represents.
*
* @param mimeType - the complete MimeType, needed for accurate query
* results
*/
private void updateDisplayName(String mimeType) {
final long count = new MediaSubTypeNodeChildren(mimeType).calculateItems(skCase, mimeType);
super.setDisplayName(mimeType.split("/")[1] + " (" + count + ")");
}
/**
* This returns true because any MediaSubTypeNode that exists is going
* to be a bottom level node in the Tree view on the left of Autopsy.
*
* @return true
*/
@Override
public boolean isLeafTypeNode() {
return true;
}
@Override
public <T> T accept(DisplayableItemNodeVisitor<T> v) {
return v.visit(this);
}
@Override
public String getItemType() {
return getClass().getName();
}
@Override
public void update(Observable o, Object arg) {
updateDisplayName(getName());
}
}
/**
* Factory for populating the contents of the Media Sub Type Node with the
* files that match MimeType which is represented by this position in the
* tree.
*/
private class MediaSubTypeNodeChildren extends ChildFactory.Detachable<Content> implements Observer {
private final String mimeType;
private MediaSubTypeNodeChildren(String mimeType) {
super();
addObserver(this);
this.mimeType = mimeType;
}
/**
* Get children count without actually loading all nodes
*
* @return count(*) - the number of items that will be shown in this
* items Directory Listing
*/
private long calculateItems(SleuthkitCase sleuthkitCase, String mime_type) {
try {
return sleuthkitCase.countFilesWhere(createQuery(mime_type));
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Error getting file search view count", ex); //NON-NLS
return 0;
}
}
/**
* Uses the createQuery method to complete the query, Select * from
* tsk_files WHERE. The results from the database will contain the files
* which match this mime type and their information.
*
* @param list - will contain all files and their attributes from the
* tsk_files table where mime_type matches the one specified
* @return true
*/
@Override
protected boolean createKeys(List<Content> list) {
try {
List<AbstractFile> files = skCase.findAllFilesWhere(createQuery(mimeType));
list.addAll(files);
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Couldn't get search results", ex); //NON-NLS
}
return true;
}
/**
* Create the portion of the query following WHERE for a query of the
* database for each file which matches the complete MIME type
* represented by this node. Matches against the mime_type column in
* tsk_files.
*
* @param mimeType - the complete mimetype of the file mediatype/subtype
* @return query.toString - portion of SQL query which will follow a
* WHERE clause.
*/
private String createQuery(String mime_type) {
StringBuilder query = new StringBuilder();
query.append("(dir_type = ").append(TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue()).append(")"); //NON-NLS
query.append(" AND (type IN (").append(TskData.TSK_DB_FILES_TYPE_ENUM.FS.ordinal()).append(","); //NON-NLS
query.append(TskData.TSK_DB_FILES_TYPE_ENUM.CARVED.ordinal()).append(",");
query.append(TskData.TSK_DB_FILES_TYPE_ENUM.DERIVED.ordinal()).append(",");
query.append(TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL.ordinal()).append("))");
if (UserPreferences.hideKnownFilesInViewsTree()) {
query.append(" AND (known IS NULL OR known != ").append(TskData.FileKnown.KNOWN.getFileKnownValue()).append(")"); //NON-NLS
}
query.append(" AND mime_type = '").append(mime_type).append("'"); //NON-NLS
return query.toString();
}
@Override
public void update(Observable o, Object arg) {
refresh(true);
}
/**
* Creates the content to populate the Directory Listing Table view for
* each file
*
* @param key
* @return
*/
@Override
protected Node createNodeForKey(Content key) {
return key.accept(new ContentVisitor.Default<AbstractNode>() {
@Override
public FileNode visit(File f) {
return new FileNode(f, false);
}
@Override
public DirectoryNode visit(Directory d) {
return new DirectoryNode(d);
}
@Override
public LayoutFileNode visit(LayoutFile lf) {
return new LayoutFileNode(lf);
}
@Override
public LocalFileNode visit(DerivedFile df) {
return new LocalFileNode(df);
}
@Override
public LocalFileNode visit(LocalFile lf) {
return new LocalFileNode(lf);
}
@Override
protected AbstractNode defaultVisit(Content di) {
throw new UnsupportedOperationException(NbBundle.getMessage(this.getClass(), "FileTypeChildren.exception.notSupported.msg", di.toString()));
}
});
}
}
}