/*
* 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.directorytree;
import java.awt.event.ActionEvent;
import java.beans.PropertyVetoException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import javax.swing.AbstractAction;
import javax.swing.Action;
import org.openide.explorer.ExplorerManager;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.nodes.Sheet;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.actions.AddBlackboardArtifactTagAction;
import org.sleuthkit.autopsy.actions.AddContentTagAction;
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType;
import org.sleuthkit.autopsy.datamodel.AbstractFsContentNode;
import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode;
import org.sleuthkit.autopsy.datamodel.DirectoryNode;
import org.sleuthkit.autopsy.datamodel.DisplayableItemNode;
import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor;
import org.sleuthkit.autopsy.datamodel.FileNode;
import org.sleuthkit.autopsy.datamodel.FileTypes.FileTypesNode;
import org.sleuthkit.autopsy.datamodel.LayoutFileNode;
import org.sleuthkit.autopsy.datamodel.LocalFileNode;
import org.sleuthkit.autopsy.datamodel.Reports;
import org.sleuthkit.autopsy.datamodel.SlackFileNode;
import org.sleuthkit.autopsy.datamodel.VirtualDirectoryNode;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.Content;
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.SlackFile;
import org.sleuthkit.datamodel.TskException;
import org.sleuthkit.datamodel.VirtualDirectory;
/**
* This class wraps nodes as they are passed to the DataResult viewers. It
* defines the actions that the node should have.
*/
public class DataResultFilterNode extends FilterNode {
private ExplorerManager sourceEm;
private final DisplayableItemNodeVisitor<List<Action>> getActionsDIV;
private final DisplayableItemNodeVisitor<AbstractAction> getPreferredActionsDIV;
/**
*
* @param node Root node to be passed to DataResult viewers
* @param em ExplorerManager for component that is creating the node
*/
public DataResultFilterNode(Node node, ExplorerManager em) {
super(node, new DataResultFilterChildren(node, em));
this.sourceEm = em;
getActionsDIV = new GetPopupActionsDisplayableItemNodeVisitor();
getPreferredActionsDIV = new GetPreferredActionsDisplayableItemNodeVisitor();
}
/**
* Right click action for the nodes that we want to pass to the directory
* table and the output view.
*
* @param popup
*
* @return actions
*/
@Override
public Action[] getActions(boolean popup) {
List<Action> actions = new ArrayList<>();
final DisplayableItemNode originalNode = (DisplayableItemNode) this.getOriginal();
List<Action> accept = originalNode.accept(getActionsDIV);
if (accept != null) {
actions.addAll(accept);
}
//actions.add(new IndexContentFilesAction(nodeContent, "Index"));
return actions.toArray(new Action[actions.size()]);
}
/**
* Double click action for the nodes that we want to pass to the directory
* table and the output view.
*
* @return action
*/
@Override
public Action getPreferredAction() {
final Node original = this.getOriginal();
// Once had a org.openide.nodes.ChildFactory$WaitFilterNode passed in
if ((original instanceof DisplayableItemNode) == false) {
return null;
}
final DisplayableItemNode originalNode = (DisplayableItemNode) this.getOriginal();
return originalNode.accept(getPreferredActionsDIV);
}
@Override
public Node.PropertySet[] getPropertySets() {
Node.PropertySet[] propertySets = super.getPropertySets();
for (int i = 0; i < propertySets.length; i++) {
Node.PropertySet ps = propertySets[i];
if (ps.getName().equals(Sheet.PROPERTIES)) {
Sheet.Set newPs = new Sheet.Set();
newPs.setName(ps.getName());
newPs.setDisplayName(ps.getDisplayName());
newPs.setShortDescription(ps.getShortDescription());
newPs.put(ps.getProperties());
if (newPs.remove(AbstractFsContentNode.HIDE_PARENT) != null) {
newPs.remove(AbstractFilePropertyType.LOCATION.toString());
}
propertySets[i] = newPs;
}
}
return propertySets;
}
/**
* Uses the default nodes actions per node, adds some custom ones and
* returns them per visited node type
*/
private static class GetPopupActionsDisplayableItemNodeVisitor extends DisplayableItemNodeVisitor.Default<List<Action>> {
@Override
public List<Action> visit(BlackboardArtifactNode ban) {
//set up actions for artifact node based on its Content object
//TODO all actions need to be consolidated in single place!
//they should be set in individual Node subclass and using a utility to get Actions per Content sub-type
// TODO UPDATE: There is now a DataModelActionsFactory utility;
List<Action> actions = new ArrayList<>();
//merge predefined specific node actions if bban subclasses have their own
for (Action a : ban.getActions(true)) {
actions.add(a);
}
BlackboardArtifact ba = ban.getLookup().lookup(BlackboardArtifact.class);
final int artifactTypeID = ba.getArtifactTypeID();
if (artifactTypeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()
|| artifactTypeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) {
actions.add(new ViewContextAction(
NbBundle.getMessage(this.getClass(), "DataResultFilterNode.action.viewFileInDir.text"), ban));
} else {
// if the artifact links to another file, add an action to go to
// that file
Content c = findLinked(ban);
if (c != null) {
actions.add(new ViewContextAction(
NbBundle.getMessage(this.getClass(), "DataResultFilterNode.action.viewFileInDir.text"), c));
}
// action to go to the source file of the artifact
actions.add(new ViewContextAction(
NbBundle.getMessage(this.getClass(), "DataResultFilterNode.action.viewSrcFileInDir.text"), ban));
}
Content c = ban.getLookup().lookup(File.class);
Node n = null;
boolean md5Action = false;
if (c != null) {
n = new FileNode((AbstractFile) c);
md5Action = true;
} else if ((c = ban.getLookup().lookup(Directory.class)) != null) {
n = new DirectoryNode((Directory) c);
} else if ((c = ban.getLookup().lookup(VirtualDirectory.class)) != null) {
n = new VirtualDirectoryNode((VirtualDirectory) c);
} else if ((c = ban.getLookup().lookup(LayoutFile.class)) != null) {
n = new LayoutFileNode((LayoutFile) c);
} else if ((c = ban.getLookup().lookup(LocalFile.class)) != null
|| (c = ban.getLookup().lookup(DerivedFile.class)) != null) {
n = new LocalFileNode((AbstractFile) c);
} else if ((c = ban.getLookup().lookup(SlackFile.class)) != null) {
n = new SlackFileNode((SlackFile) c);
}
if (n != null) {
actions.add(null); // creates a menu separator
actions.add(new NewWindowViewAction(
NbBundle.getMessage(this.getClass(), "DataResultFilterNode.action.viewInNewWin.text"), n));
actions.add(new ExternalViewerAction(
NbBundle.getMessage(this.getClass(), "DataResultFilterNode.action.openInExtViewer.text"), n));
actions.add(null); // creates a menu separator
actions.add(ExtractAction.getInstance());
if (md5Action) {
actions.add(new HashSearchAction(
NbBundle.getMessage(this.getClass(), "DataResultFilterNode.action.searchFilesSameMd5.text"), n));
}
actions.add(null); // creates a menu separator
actions.add(AddContentTagAction.getInstance());
actions.add(AddBlackboardArtifactTagAction.getInstance());
actions.addAll(ContextMenuExtensionPoint.getActions());
} else {
// There's no specific file associated with the artifact, but
// we can still tag the artifact itself
actions.add(null);
actions.add(AddBlackboardArtifactTagAction.getInstance());
}
return actions;
}
@Override
public List<Action> visit(Reports.ReportsListNode ditem) {
// The base class Action is "Collapse All", inappropriate.
return null;
}
@Override
public List<Action> visit(FileTypesNode fileTypes) {
return defaultVisit(fileTypes);
}
@Override
protected List<Action> defaultVisit(DisplayableItemNode ditem) {
//preserve the default node's actions
List<Action> actions = new ArrayList<>();
for (Action action : ditem.getActions(true)) {
actions.add(action);
}
return actions;
}
private Content findLinked(BlackboardArtifactNode ba) {
BlackboardArtifact art = ba.getLookup().lookup(BlackboardArtifact.class);
Content c = null;
try {
for (BlackboardAttribute attr : art.getAttributes()) {
if (attr.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID.getTypeID()) {
switch (attr.getAttributeType().getValueType()) {
case INTEGER:
int i = attr.getValueInt();
if (i != -1) {
c = art.getSleuthkitCase().getContentById(i);
}
break;
case LONG:
long l = attr.getValueLong();
if (l != -1) {
c = art.getSleuthkitCase().getContentById(l);
}
break;
}
}
}
} catch (TskException ex) {
Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Error getting linked file", ex); //NON-NLS
}
return c;
}
}
/*
* Action for double-click / preferred action on nodes.
*/
private class GetPreferredActionsDisplayableItemNodeVisitor extends DisplayableItemNodeVisitor.Default<AbstractAction> {
@Override
public AbstractAction visit(BlackboardArtifactNode ban) {
return new ViewContextAction(
NbBundle.getMessage(this.getClass(), "DataResultFilterNode.action.viewInDir.text"), ban);
}
@Override
public AbstractAction visit(DirectoryNode dn) {
if (dn.getDisplayName().equals(DirectoryNode.DOTDOTDIR)) {
return openParent(dn);
} else if (dn.getDisplayName().equals(DirectoryNode.DOTDIR) == false) {
return openChild(dn);
} else {
return null;
}
}
@Override
public AbstractAction visit(FileNode fn) {
if (fn.hasContentChildren()) {
return openChild(fn);
} else {
return null;
}
}
@Override
public AbstractAction visit(LocalFileNode dfn) {
if (dfn.hasContentChildren()) {
return openChild(dfn);
} else {
return null;
}
}
@Override
public AbstractAction visit(Reports.ReportNode reportNode) {
return reportNode.getPreferredAction();
}
@Override
protected AbstractAction defaultVisit(DisplayableItemNode c) {
return openChild(c);
}
@Override
public AbstractAction visit(FileTypesNode fileTypes) {
return openChild(fileTypes);
}
/**
* Tell the originating ExplorerManager to display the given
* dataModelNode.
*
* @param dataModelNode Original (non-filtered) dataModelNode to open
*
* @return
*/
private AbstractAction openChild(final AbstractNode dataModelNode) {
// get the current selection from the directory tree explorer manager,
// which is a DirectoryTreeFilterNode. One of that node's children
// is a DirectoryTreeFilterNode that wraps the dataModelNode. We need
// to set that wrapped node as the selection and root context of the
// directory tree explorer manager (sourceEm)
final Node currentSelectionInDirectoryTree = sourceEm.getSelectedNodes()[0];
return new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (currentSelectionInDirectoryTree != null) {
// Find the filter version of the passed in dataModelNode.
final org.openide.nodes.Children children = currentSelectionInDirectoryTree.getChildren();
// This call could break if the DirectoryTree is re-implemented with lazy ChildFactory objects.
Node newSelection = children.findChild(dataModelNode.getName());
/*
* We got null here when we were viewing a ZIP file in
* the Views -> Archives area and double clicking on it
* got to this code. It tried to find the child in the
* tree and didn't find it. An exception was then thrown
* from setting the selected node to be null.
*/
if (newSelection != null) {
try {
sourceEm.setExploredContextAndSelection(newSelection, new Node[]{newSelection});
} catch (PropertyVetoException ex) {
Logger logger = Logger.getLogger(DataResultFilterNode.class.getName());
logger.log(Level.WARNING, "Error: can't open the selected directory.", ex); //NON-NLS
}
}
}
}
};
}
/**
* Tell the originating ExplorerManager to display the parent of the
* given node.
*
* @param node Original (non-filtered) node to open
*
* @return
*/
private AbstractAction openParent(AbstractNode node) {
// @@@ Why do we ignore node?
Node[] selectedFilterNodes = sourceEm.getSelectedNodes();
Node selectedFilterNode = selectedFilterNodes[0];
final Node parentNode = selectedFilterNode.getParentNode();
return new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
try {
sourceEm.setSelectedNodes(new Node[]{parentNode});
} catch (PropertyVetoException ex) {
Logger logger = Logger.getLogger(DataResultFilterNode.class.getName());
logger.log(Level.WARNING, "Error: can't open the parent directory.", ex); //NON-NLS
}
}
};
}
}
}