/*
* 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.keywordsearch;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import org.apache.commons.lang.StringUtils;
import org.openide.nodes.Node;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.lookup.ServiceProvider;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.datamodel.BlackboardArtifact;
import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT;
import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.TskCoreException;
/**
* A content viewer that displays the indexed text associated with a file or an
* artifact, possibly marked up with HTML to highlight keyword hits.
*/
@ServiceProvider(service = DataContentViewer.class, position = 4)
public class ExtractedContentViewer implements DataContentViewer {
private static final Logger logger = Logger.getLogger(ExtractedContentViewer.class.getName());
private static final long INVALID_DOCUMENT_ID = 0L;
private static final BlackboardAttribute.Type TSK_ASSOCIATED_ARTIFACT_TYPE = new BlackboardAttribute.Type(TSK_ASSOCIATED_ARTIFACT);
private ExtractedContentPanel panel;
private volatile Node currentNode = null;
private IndexedText currentSource = null;
/**
* Constructs a content viewer that displays the indexed text associated
* with a file or an artifact, possibly marked up with HTML to highlight
* keyword hits.
*/
public ExtractedContentViewer() {
}
/**
* Sets the node displayed by the content viewer.
*
* @param node The node to display
*/
@Override
public void setNode(final Node node) {
// Clear the viewer.
if (node == null) {
currentNode = null;
resetComponent();
return;
}
/*
* This deals with the known bug with an unknown cause where setNode is
* sometimes called twice for the same node.
*/
if (node == currentNode) {
return;
} else {
currentNode = node;
}
Lookup nodeLookup = node.getLookup();
Content content = nodeLookup.lookup(Content.class);
/*
* Assemble a collection of all of the indexed text "sources" associated
* with the node.
*/
List<IndexedText> sources = new ArrayList<>();
IndexedText highlightedHitText = null;
IndexedText rawContentText = null;
IndexedText rawArtifactText = null;
/*
* First add the text marked up with HTML to highlight keyword hits that
* will be present in the selected node's lookup if the node is for a
* keyword hit artifact or account.
*/
sources.addAll(nodeLookup.lookupAll(IndexedText.class));
if (!sources.isEmpty()) {
//if the look up had any sources use them and don't make a new one.
highlightedHitText = sources.get(0);
} else if (null != content && solrHasContent(content.getId())) {//if the lookup didn't have any sources, and solr has indexed the content...
/*
* get all the credit card artifacts and make a AccountsText object
* that will highlight them.
*/
String solrDocumentID = String.valueOf(content.getId()); //grab the object id as the solrDocumentID
Set<String> accountNumbers = new HashSet<>();
try {
//if the node had artifacts in the lookup use them, other wise look up all credit card artifacts for the content.
Collection<? extends BlackboardArtifact> artifacts = nodeLookup.lookupAll(BlackboardArtifact.class);
artifacts = (artifacts == null || artifacts.isEmpty())
? content.getArtifacts(TSK_ACCOUNT)
: artifacts;
/*
* For each artifact add the account number to the list of
* accountNumbers to highlight, and use the solrDocumentId
* attribute(in place of the content's object Id) if it exists
*
* NOTE: this assumes all the artifacts will be from the same
* solrDocumentId
*/
for (BlackboardArtifact artifact : artifacts) {
try {
BlackboardAttribute solrIDAttr = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_DOCUMENT_ID));
if (solrIDAttr != null) {
String valueString = solrIDAttr.getValueString();
if (StringUtils.isNotBlank(valueString)) {
solrDocumentID = valueString;
}
}
BlackboardAttribute keyWordAttr = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_CARD_NUMBER));
if (keyWordAttr != null) {
String valueString = keyWordAttr.getValueString();
if (StringUtils.isNotBlank(valueString)) {
accountNumbers.add(valueString);
}
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Failed to retrieve Blackboard Attributes", ex); //NON-NLS
}
}
if (accountNumbers.isEmpty() == false) {
highlightedHitText = new AccountsText(solrDocumentID, accountNumbers);
sources.add(highlightedHitText);
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Failed to retrieve Blackboard Artifacts", ex); //NON-NLS
}
}
/*
* Next, add the "raw" (not highlighted) text, if any, for any content
* associated with the node.
*/
if (null != content && solrHasContent(content.getId())) {
rawContentText = new RawText(content, content.getId());
sources.add(rawContentText);
}
/*
* Finally, add the "raw" (not highlighted) text, if any, for any
* artifact associated with the node.
*/
BlackboardArtifact artifact = nodeLookup.lookup(BlackboardArtifact.class);
if (null != artifact) {
/*
* For keyword hit artifacts, add the text of the artifact that hit,
* not the hit artifact; otherwise add the text for the artifact.
*/
if (artifact.getArtifactTypeID() == TSK_KEYWORD_HIT.getTypeID() || artifact.getArtifactTypeID() == TSK_ACCOUNT.getTypeID()) {
try {
BlackboardAttribute attribute = artifact.getAttribute(TSK_ASSOCIATED_ARTIFACT_TYPE);
if (attribute != null) {
long artifactId = attribute.getValueLong();
BlackboardArtifact associatedArtifact = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifact(artifactId);
rawArtifactText = new RawText(associatedArtifact, associatedArtifact.getArtifactID());
sources.add(rawArtifactText);
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error getting associated artifact attributes", ex); //NON-NLS
}
} else {
rawArtifactText = new RawText(artifact, artifact.getArtifactID());
sources.add(rawArtifactText);
}
}
// Now set the default source to be displayed.
if (null != highlightedHitText) {
currentSource = highlightedHitText;
} else if (null != rawContentText) {
currentSource = rawContentText;
} else {
currentSource = rawArtifactText;
}
// Push the text sources into the panel.
for (IndexedText source : sources) {
int currentPage = source.getCurrentPage();
if (currentPage == 0 && source.hasNextPage()) {
source.nextPage();
}
}
updatePageControls();
setPanel(sources);
}
private void scrollToCurrentHit() {
final IndexedText source = panel.getSelectedSource();
if (source == null || !source.isSearchable()) {
return;
}
panel.scrollToAnchor(source.getAnchorPrefix() + Integer.toString(source.currentItem()));
}
@Override
public String getTitle() {
return NbBundle.getMessage(this.getClass(), "ExtractedContentViewer.getTitle");
}
@Override
public String getToolTip() {
return NbBundle.getMessage(this.getClass(), "ExtractedContentViewer.toolTip");
}
@Override
public DataContentViewer createInstance() {
return new ExtractedContentViewer();
}
@Override
public synchronized Component getComponent() {
if (panel == null) {
panel = new ExtractedContentPanel();
panel.addPrevMatchControlListener(new PrevFindActionListener());
panel.addNextMatchControlListener(new NextFindActionListener());
panel.addPrevPageControlListener(new PrevPageActionListener());
panel.addNextPageControlListener(new NextPageActionListener());
panel.addSourceComboControlListener(new SourceChangeActionListener());
}
return panel;
}
@Override
public void resetComponent() {
setPanel(new ArrayList<>());
panel.resetDisplay();
currentNode = null;
currentSource = null;
}
@Override
public boolean isSupported(Node node) {
if (node == null) {
return false;
}
/**
* Is there any marked up indexed text in the look up of this node? This
* will be the case if the node is for a keyword hit artifact produced
* by either an ad hoc keyword search result (keyword search toolbar
* widgets) or a keyword search by the keyword search ingest module.
*/
Collection<? extends IndexedText> sources = node.getLookup().lookupAll(IndexedText.class);
if (sources.isEmpty() == false) {
return true;
}
/*
* Is there a credit card artifact in the lookup
*/
Collection<? extends BlackboardArtifact> artifacts = node.getLookup().lookupAll(BlackboardArtifact.class);
if (artifacts != null) {
for (BlackboardArtifact art : artifacts) {
if (art.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT.getTypeID()) {
return true;
}
}
}
/*
* No highlighted text for a keyword hit, so is there any indexed text
* at all for this node?
*/
long documentID = getDocumentId(node);
if (INVALID_DOCUMENT_ID == documentID) {
return false;
}
return solrHasContent(documentID);
}
@Override
public int isPreferred(Node node) {
BlackboardArtifact art = node.getLookup().lookup(BlackboardArtifact.class);
if (art == null) {
return 4;
} else if (art.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()
|| art.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT.getTypeID()) {
return 6;
} else {
return 4;
}
}
/**
* Set the MarkupSources for the panel to display (safe to call even if the
* panel hasn't been created yet)
*
* @param sources
*/
private void setPanel(List<IndexedText> sources) {
if (panel != null) {
panel.setSources(sources);
}
}
/**
* Check if Solr has extracted content for a given node
*
* @param objectId
*
* @return true if Solr has content, else false
*/
private boolean solrHasContent(Long objectId) {
final Server solrServer = KeywordSearch.getServer();
try {
return solrServer.queryIsIndexed(objectId);
} catch (NoOpenCoreException | KeywordSearchModuleException ex) {
logger.log(Level.SEVERE, "Error querying Solr server", ex); //NON-NLS
return false;
}
}
/**
* Gets the object ID to use as the document ID for accessing any indexed
* text for the given node.
*
* @param node The node.
*
* @return The document ID or zero, which is an invalid document ID.
*/
private Long getDocumentId(Node node) {
/**
* If the node is a Blackboard artifact node for anything other than a
* keyword hit, the document ID for the text extracted from the artifact
* (the concatenation of its attributes) is the artifact ID, a large,
* negative integer. If it is a keyword hit, see if there is an
* associated artifact. If there is, get the associated artifact's ID
* and return it.
*/
BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class);
if (null != artifact) {
if (artifact.getArtifactTypeID() != BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) {
return artifact.getArtifactID();
} else {
try {
// Get the associated artifact attribute and return its value as the ID
BlackboardAttribute blackboardAttribute = artifact.getAttribute(TSK_ASSOCIATED_ARTIFACT_TYPE);
if (blackboardAttribute != null) {
return blackboardAttribute.getValueLong();
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error getting associated artifact attributes", ex); //NON-NLS
}
}
}
/*
* For keyword search hit artifact nodes and all other nodes, the
* document ID for the extracted text is the ID of the associated
* content, if any, unless there is an associated artifact, which is
* handled above.
*/
Content content = node.getLookup().lookup(Content.class);
if (content != null) {
return content.getId();
}
/*
* No extracted text, return an invalid docuemnt ID.
*/
return 0L;
}
private class NextFindActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
IndexedText source = panel.getSelectedSource();
if (source == null) {
// reset
panel.updateControls(null);
return;
}
final boolean hasNextItem = source.hasNextItem();
final boolean hasNextPage = source.hasNextPage();
int indexVal = 0;
if (hasNextItem || hasNextPage) {
if (!hasNextItem) {
//flip the page
nextPage();
indexVal = source.currentItem();
} else {
indexVal = source.nextItem();
}
//scroll
panel.scrollToAnchor(source.getAnchorPrefix() + Integer.toString(indexVal));
//update display
panel.updateCurrentMatchDisplay(source.currentItem());
panel.updateTotaMatcheslDisplay(source.getNumberHits());
//update controls if needed
if (!source.hasNextItem() && !source.hasNextPage()) {
panel.enableNextMatchControl(false);
}
if (source.hasPreviousItem() || source.hasPreviousPage()) {
panel.enablePrevMatchControl(true);
}
}
}
}
private class PrevFindActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
IndexedText source = panel.getSelectedSource();
final boolean hasPreviousItem = source.hasPreviousItem();
final boolean hasPreviousPage = source.hasPreviousPage();
int indexVal = 0;
if (hasPreviousItem || hasPreviousPage) {
if (!hasPreviousItem) {
//flip the page
previousPage();
indexVal = source.currentItem();
} else {
indexVal = source.previousItem();
}
//scroll
panel.scrollToAnchor(source.getAnchorPrefix() + Integer.toString(indexVal));
//update display
panel.updateCurrentMatchDisplay(source.currentItem());
panel.updateTotaMatcheslDisplay(source.getNumberHits());
//update controls if needed
if (!source.hasPreviousItem() && !source.hasPreviousPage()) {
panel.enablePrevMatchControl(false);
}
if (source.hasNextItem() || source.hasNextPage()) {
panel.enableNextMatchControl(true);
}
}
}
}
private class SourceChangeActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
currentSource = panel.getSelectedSource();
if (currentSource == null) {
//TODO might need to reset something
return;
}
updatePageControls();
updateSearchControls();
}
}
private void updateSearchControls() {
panel.updateSearchControls(currentSource);
}
private void updatePageControls() {
panel.updateControls(currentSource);
}
private void nextPage() {
// we should never have gotten here -- reset
if (currentSource == null) {
panel.updateControls(null);
return;
}
if (currentSource.hasNextPage()) {
currentSource.nextPage();
//set new text
panel.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
panel.refreshCurrentMarkup();
panel.setCursor(null);
//update display
panel.updateCurrentPageDisplay(currentSource.getCurrentPage());
//scroll to current selection
ExtractedContentViewer.this.scrollToCurrentHit();
//update controls if needed
if (!currentSource.hasNextPage()) {
panel.enableNextPageControl(false);
}
if (currentSource.hasPreviousPage()) {
panel.enablePrevPageControl(true);
}
updateSearchControls();
}
}
private void previousPage() {
// reset, we should have never gotten here if null
if (currentSource == null) {
panel.updateControls(null);
return;
}
if (currentSource.hasPreviousPage()) {
currentSource.previousPage();
//set new text
panel.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
panel.refreshCurrentMarkup();
panel.setCursor(null);
//update display
panel.updateCurrentPageDisplay(currentSource.getCurrentPage());
//scroll to current selection
ExtractedContentViewer.this.scrollToCurrentHit();
//update controls if needed
if (!currentSource.hasPreviousPage()) {
panel.enablePrevPageControl(false);
}
if (currentSource.hasNextPage()) {
panel.enableNextPageControl(true);
}
updateSearchControls();
}
}
class NextPageActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
nextPage();
}
}
private class PrevPageActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
previousPage();
}
}
}