diff --git a/Core/ivy.xml b/Core/ivy.xml index 9dfd0fcf85..54ed532feb 100644 --- a/Core/ivy.xml +++ b/Core/ivy.xml @@ -45,6 +45,9 @@ + + + diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 955079d04a..8209da0221 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -111,6 +111,10 @@ file.reference.grpc-context-1.19.0.jar=release/modules/ext/grpc-context-1.19.0.j file.reference.opencensus-api-0.19.2.jar=release/modules/ext/opencensus-api-0.19.2.jar file.reference.opencensus-contrib-http-util-0.19.2.jar=release/modules/ext/opencensus-contrib-http-util-0.19.2.jar file.reference.threetenbp-1.3.3.jar=release/modules/ext/threetenbp-1.3.3.jar +file.reference.okhttp-2.7.5-javadoc.jar=release/modules/ext/okhttp-2.7.5-javadoc.jar +file.reference.okhttp-2.7.5-sources.jar=release/modules/ext/okhttp-2.7.5-sources.jar +file.reference.okhttp-2.7.5.jar=release/modules/ext/okhttp-2.7.5.jar +file.reference.okio-1.6.0.jar=release/modules/ext/okio-1.6.0.jar javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial license.file=../LICENSE-2.0.txt diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 1ac3c9d2fd..3dbc3069e3 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -773,6 +773,14 @@ ext/google-api-services-translate-v2-rev20170525-1.27.0.jar release/modules/ext/google-api-services-translate-v2-rev20170525-1.27.0.jar + + ext/okhttp-2.7.5.jar + release/modules/ext/okhttp-2.7.5.jar + + + ext/okio-1.6.0.jar + release/modules/ext/okio-1.6.0.jar + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java index ff01e1c540..b817b77b21 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java @@ -186,13 +186,16 @@ public class LogicalImagerDSProcessor implements DataSourceProcessor { * associated with the data source that is * intended to be unique across multiple cases * (e.g., a UUID). - * @param imageFilePath Path to the image file. + * @param imagePath Path to the image file. + * @param sectorSize The sector size (use '0' for autodetect). * @param timeZone The time zone to use when processing dates * and times for the image, obtained from * java.util.TimeZone.getID. - * @param chunkSize The maximum size of each chunk of the raw - * data source as it is divided up into virtual - * unallocated space files. + * @param ignoreFatOrphanFiles Whether to parse orphans if the image has a + * FAT filesystem. + * @param md5 The MD5 hash of the image, may be null. + * @param sha1 The SHA-1 hash of the image, may be null. + * @param sha256 The SHA-256 hash of the image, may be null. * @param src The source directory of image. * @param dest The destination directory to copy the source. * @param progressMonitor Progress monitor for reporting progress diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED index 508e04b76a..fe998b0e57 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED @@ -66,6 +66,7 @@ DataSourceSummaryDialog.window.title=Data Sources Summary DataSourceSummaryNode.column.dataSourceName.header=Data Source Name DataSourceSummaryNode.column.files.header=Files DataSourceSummaryNode.column.results.header=Results +DataSourceSummaryNode.column.status.header=Ingest Status DataSourceSummaryNode.column.tags.header=Tags DataSourceSummaryNode.column.type.header=Type DataSourceSummaryNode.viewDataSourceAction.text=Go to Data Source diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceBrowser.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceBrowser.java index e3b9fa0e93..ac09010de1 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceBrowser.java @@ -37,8 +37,10 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.datasourcesummary.DataSourceSummaryNode.DataSourceSummaryEntryNode; import static javax.swing.SwingConstants.RIGHT; +import javax.swing.SwingUtilities; import javax.swing.table.TableColumn; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; @@ -50,9 +52,10 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(DataSourceBrowser.class.getName()); - private static final int COUNT_COLUMN_WIDTH = 25; - private static final int USAGE_COLUMN_WIDTH = 120; - private static final int DATA_SOURCE_COLUMN_WIDTH = 325; + private static final int COUNT_COLUMN_WIDTH = 20; + private static final int INGEST_STATUS_WIDTH = 50; + private static final int USAGE_COLUMN_WIDTH = 110; + private static final int DATA_SOURCE_COLUMN_WIDTH = 280; private final Outline outline; private final org.openide.explorer.view.OutlineView outlineView; private final ExplorerManager explorerManager; @@ -69,6 +72,7 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana outlineView = new org.openide.explorer.view.OutlineView(); this.setVisible(true); outlineView.setPropertyColumns( + Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_results_header(), Bundle.DataSourceSummaryNode_column_results_header(), @@ -90,6 +94,8 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana column.setPreferredWidth(COUNT_COLUMN_WIDTH); } else if (column.getHeaderValue().toString().equals(Bundle.DataSourceSummaryNode_column_type_header())) { column.setPreferredWidth(USAGE_COLUMN_WIDTH); + } else if (column.getHeaderValue().toString().equals(Bundle.DataSourceSummaryNode_column_status_header())) { + column.setPreferredWidth(INGEST_STATUS_WIDTH); } else { column.setPreferredWidth(DATA_SOURCE_COLUMN_WIDTH); } @@ -182,6 +188,47 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana return null; } + /** + * Update the DataSourceBrowser to display up to date status information for + * the data sources. + * + * @param dataSourceId the ID of the data source which should be updated + * @param newStatus the new status which the data source should have + */ + void refresh(long dataSourceId, IngestJobInfo.IngestJobStatusType newStatus) { + + //attempt to update the status of any datasources that had status which was STARTED + for (DataSourceSummary summary : dataSourceSummaryList) { + if (summary.getDataSource().getId() == dataSourceId) { + summary.setIngestStatus(newStatus); + } + } + //figure out which nodes were previously selected + Node[] selectedNodes = explorerManager.getSelectedNodes(); + SwingUtilities.invokeLater(() -> { + explorerManager.setRootContext(new DataSourceSummaryNode(dataSourceSummaryList)); + List nodesToSelect = new ArrayList<>(); + for (Node node : explorerManager.getRootContext().getChildren().getNodes()) { + if (node instanceof DataSourceSummaryEntryNode) { + //there should only be one selected node as multi-select is disabled + for (Node selectedNode : selectedNodes) { + if (((DataSourceSummaryEntryNode) node).getDataSource().equals(((DataSourceSummaryEntryNode) selectedNode).getDataSource())) { + nodesToSelect.add(node); + } + } + } + } + //reselect the previously selected Nodes + try { + explorerManager.setSelectedNodes(nodesToSelect.toArray(new Node[nodesToSelect.size()])); + } catch (PropertyVetoException ex) { + logger.log(Level.WARNING, "Error selecting previously selected nodes", ex); + } + + }); + + } + /** * 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 diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummary.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummary.java index 1aa55d95b5..390dce1afe 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummary.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummary.java @@ -18,15 +18,27 @@ */ package org.sleuthkit.autopsy.casemodule.datasourcesummary; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.datamodel.CaseDbAccessManager; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo.IngestJobStatusType; +import org.sleuthkit.datamodel.TskCoreException; /** * Wrapper object for a DataSource and the information associated with it. * */ class DataSourceSummary { - + + private static final Logger logger = Logger.getLogger(DataSourceSummary.class.getName()); + private static final String INGEST_JOB_STATUS_QUERY = "status_id FROM ingest_jobs WHERE obj_id="; private final DataSource dataSource; + private IngestJobStatusType status = null; private final String type; private final long filesCount; private final long resultsCount; @@ -45,12 +57,26 @@ class DataSourceSummary { */ DataSourceSummary(DataSource dSource, String typeValue, Long numberOfFiles, Long numberOfResults, Long numberOfTags) { dataSource = dSource; + getStatusFromDatabase(); type = typeValue == null ? "" : typeValue; filesCount = numberOfFiles == null ? 0 : numberOfFiles; resultsCount = numberOfResults == null ? 0 : numberOfResults; tagsCount = numberOfTags == null ? 0 : numberOfTags; } + /** + * Get the status of the ingest job from the case database + */ + private void getStatusFromDatabase() { + try { + IngestJobQueryCallback callback = new IngestJobQueryCallback(); + Case.getCurrentCaseThrows().getSleuthkitCase().getCaseDbAccessManager().select(INGEST_JOB_STATUS_QUERY + dataSource.getId(), callback); + status = callback.getStatus(); + } catch (NoCurrentCaseException | TskCoreException ex) { + logger.log(Level.WARNING, "Error getting status for data source from case database", ex); + } + } + /** * Get the DataSource * @@ -60,6 +86,16 @@ class DataSourceSummary { return dataSource; } + /** + * Manually set the ingest job status + * + * @param ingestStatus the status which the ingest job should have + * currently, null to display empty string + */ + void setIngestStatus(IngestJobStatusType ingestStatus) { + status = ingestStatus; + } + /** * Get the type of this DataSource * @@ -87,6 +123,16 @@ class DataSourceSummary { return resultsCount; } + /** + * Get the IngestJobStatusType associated with this data source. + * + * @return the IngestJobStatusType associated with this data source. Can be + * null if the IngestJobStatusType is not STARTED or COMPLETED. + */ + IngestJobStatusType getIngestStatus() { + return status; + } + /** * Get the number of tagged content objects in this DataSource * @@ -95,4 +141,40 @@ class DataSourceSummary { long getTagsCount() { return tagsCount; } + + /** + * Callback to parse result set, getting the status to be associated with + * this data source + */ + class IngestJobQueryCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { + + private IngestJobStatusType jobStatus = null; + + @Override + public void process(ResultSet rs) { + try { + while (rs.next()) { + IngestJobStatusType currentStatus = IngestJobStatusType.fromID(rs.getInt("status_id")); + if (currentStatus == IngestJobStatusType.COMPLETED) { + jobStatus = currentStatus; + } else if (currentStatus == IngestJobStatusType.STARTED) { + jobStatus = currentStatus; + return; + } + } + } catch (SQLException ex) { + logger.log(Level.WARNING, "Error getting status for ingest job", ex); + } + } + + /** + * Get the status which was determined for this callback + * + * @return the status of the data source which was queried for + */ + IngestJobStatusType getStatus() { + return jobStatus; + } + + } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java index 3fd86c8de8..1535cbefe0 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java @@ -19,14 +19,18 @@ package org.sleuthkit.autopsy.casemodule.datasourcesummary; import java.awt.Frame; +import java.beans.PropertyChangeEvent; import java.util.Map; import java.util.Observable; import java.util.Observer; -import java.util.logging.Logger; import javax.swing.event.ListSelectionEvent; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent.Reason; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo; /** * Dialog for displaying the Data Sources Summary information @@ -38,7 +42,6 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser private final DataSourceSummaryDetailsPanel detailsPanel; private final DataSourceBrowser dataSourcesPanel; private final IngestJobInfoPanel ingestHistoryPanel; - private static final Logger logger = Logger.getLogger(DataSourceSummaryDialog.class.getName()); /** * Creates new form DataSourceSummaryDialog for displaying a summary of the @@ -73,6 +76,17 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser this.repaint(); } }); + //add listener to refresh jobs with Started status when they complete + IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> { + if (evt instanceof DataSourceAnalysisCompletedEvent) { + DataSourceAnalysisCompletedEvent dsEvent = (DataSourceAnalysisCompletedEvent) evt; + if (dsEvent.getResult() == Reason.ANALYSIS_COMPLETED) { + dataSourcesPanel.refresh(dsEvent.getDataSource().getId(), IngestJobInfo.IngestJobStatusType.COMPLETED); + } else if (dsEvent.getResult() == Reason.ANALYSIS_CANCELLED) { + dataSourcesPanel.refresh(dsEvent.getDataSource().getId(), null); + } + } + }); this.pack(); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryNode.java index 8323061a66..0411d94bb2 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryNode.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryNode.java @@ -109,6 +109,7 @@ final class DataSourceSummaryNode extends AbstractNode { static final class DataSourceSummaryEntryNode extends AbstractNode { private final DataSource dataSource; + private final String status; private final String type; private final long filesCount; private final long resultsCount; @@ -124,6 +125,7 @@ final class DataSourceSummaryNode extends AbstractNode { DataSourceSummaryEntryNode(DataSourceSummary dataSourceSummary) { super(Children.LEAF); dataSource = dataSourceSummary.getDataSource(); + status = dataSourceSummary.getIngestStatus() == null ? "" : dataSourceSummary.getIngestStatus().getDisplayName(); type = dataSourceSummary.getType(); filesCount = dataSourceSummary.getFilesCount(); resultsCount = dataSourceSummary.getResultsCount(); @@ -143,6 +145,7 @@ final class DataSourceSummaryNode extends AbstractNode { } @Messages({"DataSourceSummaryNode.column.dataSourceName.header=Data Source Name", + "DataSourceSummaryNode.column.status.header=Ingest Status", "DataSourceSummaryNode.column.type.header=Type", "DataSourceSummaryNode.column.files.header=Files", "DataSourceSummaryNode.column.results.header=Results", @@ -157,6 +160,7 @@ final class DataSourceSummaryNode extends AbstractNode { } sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_dataSourceName_header(), Bundle.DataSourceSummaryNode_column_dataSourceName_header(), Bundle.DataSourceSummaryNode_column_dataSourceName_header(), dataSource)); + sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_status_header(), status)); sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_type_header(), type)); sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_files_header(), diff --git a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java index 22da4a81e9..a6cdf63b63 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java @@ -272,9 +272,11 @@ final public class FiltersPanel extends JPanel { } }); } - + /** * Populate the devices filter widgets + * + * @param initialState */ private void updateDeviceFilter(boolean initialState) { try { @@ -301,7 +303,7 @@ final public class FiltersPanel extends JPanel { * Given a list of subFilters, set the states of the panel controls * accordingly. * - * @param subFilters A list of subFilters + * @param commFilter Contains a list of subFilters */ public void setFilters(CommunicationsFilter commFilter) { List subFilters = commFilter.getAndFilters(); diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java index e6ac3f507f..d1cdcbabf2 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java @@ -18,13 +18,10 @@ */ package org.sleuthkit.autopsy.communications.relationships; -import java.util.List; import java.util.TimeZone; import java.util.logging.Level; import org.apache.commons.lang3.StringUtils; import org.openide.nodes.Sheet; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; -import org.sleuthkit.autopsy.core.UserPreferences; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; @@ -40,7 +37,6 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHO import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT; import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME; -import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TimeUtilities; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.autopsy.communications.Utils; @@ -72,7 +68,6 @@ final class MessageNode extends BlackboardArtifactNode { @Override protected Sheet createSheet() { Sheet sheet = super.createSheet(); - List tags = getAllTagsFromDatabase(); Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); if (sheetSet == null) { sheetSet = Sheet.createPropertiesSet(); @@ -81,17 +76,6 @@ final class MessageNode extends BlackboardArtifactNode { sheetSet.put(new NodeProperty<>("Type", Bundle.MessageNode_Node_Property_Type(), "", getDisplayName())); //NON-NLS - addScoreProperty(sheetSet, tags); - - CorrelationAttributeInstance correlationAttribute = null; - if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { - correlationAttribute = getCorrelationAttributeInstance(); - } - addCommentProperty(sheetSet, tags, correlationAttribute); - - if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { - addCountProperty(sheetSet, correlationAttribute); - } final BlackboardArtifact artifact = getArtifact(); BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact.getArtifactTypeID()); diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/RelationshipBrowser.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/RelationshipBrowser.java index 03729540df..b8c19f518e 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/RelationshipBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/RelationshipBrowser.java @@ -61,7 +61,7 @@ public final class RelationshipBrowser extends JPanel implements Lookup.Provider /** * Sets the value of currentSelection and passes the SelectionInfo onto the - * currently selected\visible tab. + * currently selected or visible tab. * * @param info Currently selected account nodes */ diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties index 652f8781ba..fd3bbc833f 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties @@ -83,7 +83,7 @@ MediaViewImagePanel.zoomResetButton.text=Reset MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= -HtmlPanel.showImagesToggleButton.text=Show Images +HtmlPanel.showImagesToggleButton.text=Download Images MediaPlayerPanel.audioSlider.toolTipText= MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED index 236fddfada..7b8fa6ad6b 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED @@ -22,7 +22,7 @@ GstVideoPanel.cannotProcFile.err=The media player cannot process this file. GstVideoPanel.noOpenCase.errMsg=No open case available. Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML. HtmlPanel_showImagesToggleButton_hide=Hide Images -HtmlPanel_showImagesToggleButton_show=Show Images +HtmlPanel_showImagesToggleButton_show=Download Images HtmlViewer_file_error=This file is missing or unreadable. MediaFileViewer.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled. GstVideoPanel.setupVideo.infoLabel.text=Playback of deleted videos is not supported, use an external player. @@ -145,7 +145,7 @@ MediaViewImagePanel.zoomResetButton.text=Reset MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= -HtmlPanel.showImagesToggleButton.text=Show Images +HtmlPanel.showImagesToggleButton.text=Download Images MediaPlayerPanel.audioSlider.toolTipText= MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java index a32dce1bbf..ead4199941 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java @@ -77,12 +77,12 @@ public class FileViewer extends javax.swing.JPanel implements DataContentViewer } /** - * Get the FileTypeViewer for a given mimetype + * Get the FileTypeViewer for a given file * - * @param mimeType + * @param file * * @return FileTypeViewer, null if no known content viewer supports the - * mimetype + * file */ private FileTypeViewer getSupportingViewer(AbstractFile file) { FileTypeViewer viewer = mimeTypeToViewerMap.get(file.getMIMEType()); diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form index 97b3c67f72..407f0e6ec7 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form @@ -18,7 +18,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java index f915b8d5a6..b53b1dc258 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java @@ -18,6 +18,11 @@ */ package org.sleuthkit.autopsy.contentviewers; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; @@ -25,8 +30,9 @@ import javafx.concurrent.Worker; import javafx.scene.web.WebView; import javafx.embed.swing.JFXPanel; import javafx.scene.Scene; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Node; +import net.htmlparser.jericho.Attribute; +import net.htmlparser.jericho.OutputDocument; +import net.htmlparser.jericho.Source; import org.openide.util.NbBundle.Messages; import org.w3c.dom.Document; import org.w3c.dom.NodeList; @@ -38,6 +44,7 @@ import org.w3c.dom.events.EventTarget; @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives final class HtmlPanel extends javax.swing.JPanel { + private static final Logger logger = Logger.getLogger(HtmlPanel.class.getName()); private static final long serialVersionUID = 1L; private static final String TEXT_TYPE = "text/plain"; private final JFXPanel jfxPanel = new JFXPanel(); @@ -92,26 +99,76 @@ final class HtmlPanel extends javax.swing.JPanel { } /** - * Cleans out input HTML string + * Cleans out input HTML string so it will not access resources over the internet * * @param htmlInString The HTML string to cleanse * * @return The cleansed HTML String */ private String cleanseHTML(String htmlInString) { - org.jsoup.nodes.Document doc = Jsoup.parse(htmlInString); - // remove all 'img' tags. - doc.select("img").stream().forEach(Node::remove); - // remove all 'span' tags, these are often images which are ads - doc.select("span").stream().forEach(Node::remove); - return doc.html(); + String returnString = ""; + try { + Source source = new Source(new StringReader(htmlInString)); + OutputDocument document = new OutputDocument(source); + //remove background images + source.getAllTags().stream().filter((tag) -> (tag.toString().contains("background-image"))).forEachOrdered((tag) -> { + document.remove(tag); + }); + //remove images + source.getAllElements("img").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove frames + source.getAllElements("frame").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove iframes + source.getAllElements("iframe").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove pictures + source.getAllElements("picture").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove svg + source.getAllElements("svg").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove audio + source.getAllElements("audio").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove video + source.getAllElements("video").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove tracks + source.getAllElements("track").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove embeded external elements + source.getAllElements("embed").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove linked elements + source.getAllElements("link").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove other URI elements such as input boxes + List attributesToRemove = source.getURIAttributes(); + document.remove(attributesToRemove); + returnString = document.toString(); + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to read html for cleaning out URI elements with Jericho", ex); + } + return returnString; } /** * Refresh the panel to reflect the current show/hide images setting. */ @Messages({ - "HtmlPanel_showImagesToggleButton_show=Show Images", + "HtmlPanel_showImagesToggleButton_show=Download Images", "HtmlPanel_showImagesToggleButton_hide=Hide Images", "Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML.",}) private void refresh() { @@ -164,7 +221,7 @@ final class HtmlPanel extends javax.swing.JPanel { layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(showImagesToggleButton) - .addGap(0, 95, Short.MAX_VALUE)) + .addGap(0, 75, Short.MAX_VALUE)) .addComponent(htmlJPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java index 81384acbaa..d7f853caab 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java @@ -23,6 +23,7 @@ import org.openide.nodes.Node; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; +import org.sleuthkit.datamodel.AbstractFile; /** * A DataContentViewer that displays text with the TextViewers available. @@ -90,6 +91,17 @@ public class TextContentViewer implements DataContentViewer { if (node == null) { return false; } + // get the node's File, if it has one + AbstractFile file = node.getLookup().lookup(AbstractFile.class); + if (file == null) { + return false; + } + + // disable the text content viewer for directories and empty files + if (file.isDir() || file.getSize() == 0) { + return false; + } + return panel.isSupported(node); } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java index e1af060cfc..7c80d606fd 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java @@ -79,7 +79,7 @@ public class TextContentViewerPanel extends javax.swing.JPanel implements DataCo } /** - * Deterime wether the content viewer which displays this panel isSupported. + * Determine whether the content viewer which displays this panel isSupported. * This panel is supported if any of the TextViewer's displayed in it are * supported. * diff --git a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java index d6f68086c4..c18d8b0f96 100644 --- a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java +++ b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java @@ -504,10 +504,10 @@ public final class UserPreferences { /** * Get the maximum number of results to display in a result table. * - * @return Saved value or default (0) which indicates no max. + * @return Saved value or default (10,000). */ public static int getResultsTablePageSize() { - return preferences.getInt(RESULTS_TABLE_PAGE_SIZE, 0); + return preferences.getInt(RESULTS_TABLE_PAGE_SIZE, 10_000); } /** diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties index ac5bccebc8..fae74a30ac 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -216,4 +216,5 @@ DataResultViewerTable.pagesLabel.text=Pages: DataResultViewerTable.pageNumLabel.text= DataResultViewerTable.pageLabel.text=Page: ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: -ViewPreferencesPanel.maxResultsLabel.toolTipText=\nAll results are shown in the results table if this value is 0 (default).\n
You may want to consider setting this value if you have large numbers of results that are taking a long time to display.\n +ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n +DataResultViewerTable.exportCSVButton.text=Save table as CSV diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED index 2c0af06f50..b0e36da986 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED @@ -32,6 +32,7 @@ DataResultViewerTable.commentRenderer.noComment.toolTip=No comments found DataResultViewerTable.commentRenderer.tagComment.toolTip=Comment exists on associated tag(s) DataResultViewerTable.countRender.name=O DataResultViewerTable.countRender.toolTip=O(ccurrences) indicates the number of data sources containing the item in the Central Repository +DataResultViewerTable.exportCSVButtonActionPerformed.empty=No data to export DataResultViewerTable.firstColLbl=Name DataResultViewerTable.goToPageTextField.err=Invalid page number # {0} - totalPages @@ -269,4 +270,5 @@ DataResultViewerTable.pagesLabel.text=Pages: DataResultViewerTable.pageNumLabel.text= DataResultViewerTable.pageLabel.text=Page: ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: -ViewPreferencesPanel.maxResultsLabel.toolTipText=\nAll results are shown in the results table if this value is 0 (default).\n
You may want to consider setting this value if you have large numbers of results that are taking a long time to display.\n +ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n +DataResultViewerTable.exportCSVButton.text=Save table as CSV diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form index 6b17cfd6dd..6aeda07298 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form @@ -16,13 +16,13 @@ - - - + + + - + - + @@ -32,14 +32,15 @@ - + + - + @@ -48,9 +49,10 @@ + - - + + @@ -164,5 +166,15 @@ + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index a5bbc461b8..e6c1110038 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -77,6 +77,7 @@ import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.datamodel.NodeSelectionInfo; @@ -176,6 +177,13 @@ public class DataResultViewerTable extends AbstractDataResultViewer { initComponents(); initializePagingSupport(); + + /* + * Disable the CSV export button for the common properties results + */ + if (this instanceof org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributesSearchResultsViewerTable) { + exportCSVButton.setEnabled(false); + } /* * Configure the child OutlineView (explorer view) component. @@ -1291,6 +1299,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outlineView = new OutlineView(DataResultViewerTable.FIRST_COLUMN_LABEL); gotoPageLabel = new javax.swing.JLabel(); gotoPageTextField = new javax.swing.JTextField(); + exportCSVButton = new javax.swing.JButton(); pageLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pageLabel.text")); // NOI18N @@ -1338,17 +1347,24 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } }); + exportCSVButton.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.exportCSVButton.text")); // NOI18N + exportCSVButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + exportCSVButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap(608, Short.MAX_VALUE) + .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 904, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addContainerGap() .addComponent(pageLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(pageNumLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 53, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGap(14, 14, 14) .addComponent(pagesLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -1358,7 +1374,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { .addComponent(gotoPageLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap()) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(exportCSVButton)) ); layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {pageNextButton, pagePrevButton}); @@ -1366,7 +1383,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap() + .addGap(3, 3, 3) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) .addComponent(pageLabel) .addComponent(pageNumLabel) @@ -1374,9 +1391,10 @@ public class DataResultViewerTable extends AbstractDataResultViewer { .addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(pageNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 15, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(gotoPageLabel) - .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 324, Short.MAX_VALUE) + .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(exportCSVButton)) + .addGap(3, 3, 3) + .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 321, Short.MAX_VALUE) .addContainerGap()) ); @@ -1397,7 +1415,19 @@ public class DataResultViewerTable extends AbstractDataResultViewer { pagingSupport.gotoPage(); }//GEN-LAST:event_gotoPageTextFieldActionPerformed + @NbBundle.Messages({"DataResultViewerTable.exportCSVButtonActionPerformed.empty=No data to export" + }) + private void exportCSVButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportCSVButtonActionPerformed + Node currentRoot = this.getExplorerManager().getRootContext(); + if (currentRoot != null && currentRoot.getChildren().getNodesCount() > 0) { + org.sleuthkit.autopsy.directorytree.ExportCSVAction.saveNodesToCSV(java.util.Arrays.asList(currentRoot.getChildren().getNodes()), this); + } else { + MessageNotifyUtil.Message.info(Bundle.DataResultViewerTable_exportCSVButtonActionPerformed_empty()); + } + }//GEN-LAST:event_exportCSVButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton exportCSVButton; private javax.swing.JLabel gotoPageLabel; private javax.swing.JTextField gotoPageTextField; private org.openide.explorer.view.OutlineView outlineView; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index fa4529e44b..158f2ef4d9 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.datamodel; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; @@ -27,8 +26,6 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.stream.Collectors; import org.apache.commons.io.FilenameUtils; @@ -65,6 +62,7 @@ import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; @@ -76,15 +74,10 @@ import org.sleuthkit.datamodel.TskData; public abstract class AbstractAbstractFileNode extends AbstractContentNode { private static final Logger logger = Logger.getLogger(AbstractAbstractFileNode.class.getName()); - @NbBundle.Messages("AbstractAbstractFileNode.addFileProperty.desc=no description") - private static final String NO_DESCR = AbstractAbstractFileNode_addFileProperty_desc(); private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.CURRENT_CASE, Case.Events.CONTENT_TAG_ADDED, Case.Events.CONTENT_TAG_DELETED, Case.Events.CR_COMMENT_CHANGED); - private static final ExecutorService translationPool; - private static final Integer MAX_POOL_SIZE = 10; - /** * @param abstractFile file to wrap */ @@ -101,7 +94,7 @@ public abstract class AbstractAbstractFileNode extends A } if (UserPreferences.displayTranslatedFileNames()) { - AbstractAbstractFileNode.translationPool.submit(new TranslationTask( + backgroundTasksPool.submit(new TranslationTask( new WeakReference<>(this), weakPcl)); } @@ -109,14 +102,7 @@ public abstract class AbstractAbstractFileNode extends A // or when tags are added. Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); } - - static { - //Initialize this pool only once! This will be used by every instance of AAFN - //to do their heavy duty SCO column and translation updates. - translationPool = Executors.newFixedThreadPool(MAX_POOL_SIZE, - new ThreadFactoryBuilder().setNameFormat("translation-task-thread-%d").build()); - } - + /** * The finalizer removes event listeners as the BlackboardArtifactNode is * being garbage collected. Yes, we know that finalizers are considered to @@ -137,16 +123,7 @@ public abstract class AbstractAbstractFileNode extends A Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); } - /** - * Event signals to indicate the background tasks have completed processing. - * Currently, we have one property task in the background: - * - * 1) Retreiving the translation of the file name - */ - enum NodeSpecificEvents { - TRANSLATION_AVAILABLE, - } - + private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { String eventType = evt.getPropertyName(); @@ -190,7 +167,7 @@ public abstract class AbstractAbstractFileNode extends A } else if (eventType.equals(Case.Events.CONTENT_TAG_ADDED.toString())) { ContentTagAddedEvent event = (ContentTagAddedEvent) evt; if (event.getAddedTag().getContent().equals(content)) { - List tags = getContentTagsFromDatabase(); + List tags = this.getAllTagsFromDatabase(); Pair scorePropAndDescr = getScorePropertyAndDescription(tags); Score value = scorePropAndDescr.getLeft(); String descr = scorePropAndDescr.getRight(); @@ -202,7 +179,7 @@ public abstract class AbstractAbstractFileNode extends A } else if (eventType.equals(Case.Events.CONTENT_TAG_DELETED.toString())) { ContentTagDeletedEvent event = (ContentTagDeletedEvent) evt; if (event.getDeletedTagInfo().getContentID() == content.getId()) { - List tags = getContentTagsFromDatabase(); + List tags = getAllTagsFromDatabase(); Pair scorePropAndDescr = getScorePropertyAndDescription(tags); Score value = scorePropAndDescr.getLeft(); String descr = scorePropAndDescr.getRight(); @@ -214,7 +191,7 @@ public abstract class AbstractAbstractFileNode extends A } else if (eventType.equals(Case.Events.CR_COMMENT_CHANGED.toString())) { CommentChangedEvent event = (CommentChangedEvent) evt; if (event.getContentID() == content.getId()) { - List tags = getContentTagsFromDatabase(); + List tags = getAllTagsFromDatabase(); CorrelationAttributeInstance attribute = getCorrelationAttributeInstance(); updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, getCommentProperty(tags, attribute))); } @@ -223,6 +200,18 @@ public abstract class AbstractAbstractFileNode extends A //Set the tooltip this.setShortDescription(content.getName()); updateSheet(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, content.getName())); + } else if (eventType.equals(NodeSpecificEvents.SCO_AVAILABLE.toString())) { + SCOData scoData = (SCOData)evt.getNewValue(); + if (scoData.getScoreAndDescription() != null) { + updateSheet(new NodeProperty<>(SCORE.toString(), SCORE.toString(), scoData.getScoreAndDescription().getRight(), scoData.getScoreAndDescription().getLeft())); + } + if (scoData.getComment() != null) { + updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, scoData.getComment())); + } + if (scoData.getCountAndDescription() != null && + !UserPreferences.hideCentralRepoCommentsAndOccurrences()) { + updateSheet(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft())); + } } }; /** @@ -235,38 +224,6 @@ public abstract class AbstractAbstractFileNode extends A */ private final PropertyChangeListener weakPcl = WeakListeners.propertyChange(pcl, null); - /** - * Updates the values of the properties in the current property sheet with - * the new properties being passed in. Only if that property exists in the - * current sheet will it be applied. That way, we allow for subclasses to - * add their own (or omit some!) properties and we will not accidentally - * disrupt their UI. - * - * Race condition if not synchronized. Only one update should be applied at - * a time. - * - * @param newProps New file property instances to be updated in the current - * sheet. - */ - private synchronized void updateSheet(NodeProperty... newProps) { - //Refresh ONLY those properties in the sheet currently. Subclasses may have - //only added a subset of our properties or their own props.s - Sheet visibleSheet = this.getSheet(); - Sheet.Set visibleSheetSet = visibleSheet.get(Sheet.PROPERTIES); - Property[] visibleProps = visibleSheetSet.getProperties(); - for (NodeProperty newProp : newProps) { - for (int i = 0; i < visibleProps.length; i++) { - if (visibleProps[i].getName().equals(newProp.getName())) { - visibleProps[i] = newProp; - } - } - } - visibleSheetSet.put(visibleProps); - visibleSheet.put(visibleSheetSet); - //setSheet() will notify Netbeans to update this node in the UI. - this.setSheet(visibleSheet); - } - /* * This is called when the node is first initialized. Any new updates or * changes happen by directly manipulating the sheet. That means we can fire @@ -368,18 +325,18 @@ public abstract class AbstractAbstractFileNode extends A properties.add(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, "")); } - //SCO column prereq info.. - List tags = getContentTagsFromDatabase(); - CorrelationAttributeInstance attribute = getCorrelationAttributeInstance(); - - Pair scoreAndDescription = getScorePropertyAndDescription(tags); - properties.add(new NodeProperty<>(SCORE.toString(), SCORE.toString(), scoreAndDescription.getRight(), scoreAndDescription.getLeft())); - DataResultViewerTable.HasCommentStatus comment = getCommentProperty(tags, attribute); - properties.add(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, comment)); - if (!UserPreferences.hideCentralRepoCommentsAndOccurrences()) { - Pair countAndDescription = getCountPropertyAndDescription(attribute); - properties.add(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), countAndDescription.getRight(), countAndDescription.getLeft())); + // Create place holders for S C O + properties.add(new NodeProperty<>(SCORE.toString(), SCORE.toString(), VALUE_LOADING, "")); + properties.add(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), VALUE_LOADING, "")); + if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { + properties.add(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), VALUE_LOADING, "")); } + + + // Get the SCO columns data in a background task + backgroundTasksPool.submit(new GetSCOTask( + new WeakReference<>(this), weakPcl)); + properties.add(new NodeProperty<>(LOCATION.toString(), LOCATION.toString(), NO_DESCR, getContentPath(content))); properties.add(new NodeProperty<>(MOD_TIME.toString(), MOD_TIME.toString(), NO_DESCR, ContentUtils.getStringTime(content.getMtime(), content))); properties.add(new NodeProperty<>(CHANGED_TIME.toString(), CHANGED_TIME.toString(), NO_DESCR, ContentUtils.getStringTime(content.getCtime(), content))); @@ -441,7 +398,8 @@ public abstract class AbstractAbstractFileNode extends A "AbstractAbstractFileNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this file when the column was populated", "# {0} - occuranceCount", "AbstractAbstractFileNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"}) - Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting String description = Bundle.AbstractAbstractFileNode_createSheet_count_noCentralRepo_description(); try { @@ -468,7 +426,8 @@ public abstract class AbstractAbstractFileNode extends A "AbstractAbstractFileNode.createSheet.taggedFile.description=File has been tagged.", "AbstractAbstractFileNode.createSheet.notableTaggedFile.description=File tagged with notable tag.", "AbstractAbstractFileNode.createSheet.noScore.description=No score"}) - Pair getScorePropertyAndDescription(List tags) { + @Override + protected Pair getScorePropertyAndDescription(List tags) { DataResultViewerTable.Score score = DataResultViewerTable.Score.NO_SCORE; String description = Bundle.AbstractAbstractFileNode_createSheet_noScore_description(); if (content.getKnown() == TskData.FileKnown.BAD) { @@ -486,7 +445,7 @@ public abstract class AbstractAbstractFileNode extends A if (!tags.isEmpty() && (score == DataResultViewerTable.Score.NO_SCORE || score == DataResultViewerTable.Score.INTERESTING_SCORE)) { score = DataResultViewerTable.Score.INTERESTING_SCORE; description = Bundle.AbstractAbstractFileNode_createSheet_taggedFile_description(); - for (ContentTag tag : tags) { + for (Tag tag : tags) { if (tag.getName().getKnownStatus() == TskData.FileKnown.BAD) { score = DataResultViewerTable.Score.NOTABLE_SCORE; description = Bundle.AbstractAbstractFileNode_createSheet_notableTaggedFile_description(); @@ -499,11 +458,12 @@ public abstract class AbstractAbstractFileNode extends A @NbBundle.Messages({ "AbstractAbstractFileNode.createSheet.comment.displayName=C"}) - HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + @Override + protected HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { DataResultViewerTable.HasCommentStatus status = !tags.isEmpty() ? DataResultViewerTable.HasCommentStatus.TAG_NO_COMMENT : DataResultViewerTable.HasCommentStatus.NO_COMMENT; - for (ContentTag tag : tags) { + 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 = DataResultViewerTable.HasCommentStatus.TAG_COMMENT; @@ -571,7 +531,13 @@ public abstract class AbstractAbstractFileNode extends A return tags; } - CorrelationAttributeInstance getCorrelationAttributeInstance() { + @Override + protected List getAllTagsFromDatabase() { + return new ArrayList<>(getContentTagsFromDatabase()); + } + + @Override + protected CorrelationAttributeInstance getCorrelationAttributeInstance() { CorrelationAttributeInstance attribute = null; if (EamDb.isEnabled() && !UserPreferences.hideCentralRepoCommentsAndOccurrences()) { attribute = EamArtifactUtil.getInstanceFromContent(content); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java index 4f6f2e5f47..b9e012e6af 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java @@ -18,20 +18,29 @@ */ package org.sleuthkit.autopsy.datamodel; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.logging.Level; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Children; +import org.openide.nodes.Sheet; import org.openide.util.lookup.Lookups; import org.openide.util.Lookup; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskException; @@ -50,6 +59,38 @@ public abstract class AbstractContentNode extends ContentNode T content; private static final Logger logger = Logger.getLogger(AbstractContentNode.class.getName()); + /** + * A pool of background tasks to run any long computation needed to + * populate this node. + */ + static final ExecutorService backgroundTasksPool; + private static final Integer MAX_POOL_SIZE = 10; + + /** + * Default no description string + */ + @NbBundle.Messages({"AbstractContentNode.nodescription=no description", + "AbstractContentNode.valueLoading=value loading"}) + protected static final String NO_DESCR = Bundle.AbstractContentNode_nodescription(); + protected static final String VALUE_LOADING = Bundle.AbstractContentNode_valueLoading(); + + /** + * Event signals to indicate the background tasks have completed processing. + * Currently, we have one property task in the background: + * + * 1) Retrieving the translation of the file name + */ + enum NodeSpecificEvents { + TRANSLATION_AVAILABLE, + SCO_AVAILABLE + } + + static { + //Initialize this pool only once! This will be used by every instance of AAFN + //to do their heavy duty SCO column and translation updates. + backgroundTasksPool = Executors.newFixedThreadPool(MAX_POOL_SIZE, + new ThreadFactoryBuilder().setNameFormat("content-node-background-task-%d").build()); + } /** * Handles aspects that depend on the Content object * @@ -240,4 +281,79 @@ public abstract class AbstractContentNode extends ContentNode public int read(byte[] buf, long offset, long len) throws TskException { return content.read(buf, offset, len); } + + + /** + * Updates the values of the properties in the current property sheet with + * the new properties being passed in. Only if that property exists in the + * current sheet will it be applied. That way, we allow for subclasses to + * add their own (or omit some!) properties and we will not accidentally + * disrupt their UI. + * + * Race condition if not synchronized. Only one update should be applied at + * a time. + * + * @param newProps New file property instances to be updated in the current + * sheet. + */ + protected synchronized void updateSheet(NodeProperty... newProps) { + //Refresh ONLY those properties in the sheet currently. Subclasses may have + //only added a subset of our properties or their own props.s + Sheet visibleSheet = this.getSheet(); + Sheet.Set visibleSheetSet = visibleSheet.get(Sheet.PROPERTIES); + Property[] visibleProps = visibleSheetSet.getProperties(); + for (NodeProperty newProp : newProps) { + for (int i = 0; i < visibleProps.length; i++) { + if (visibleProps[i].getName().equals(newProp.getName())) { + visibleProps[i] = newProp; + } + } + } + visibleSheetSet.put(visibleProps); + visibleSheet.put(visibleSheetSet); + //setSheet() will notify Netbeans to update this node in the UI. + this.setSheet(visibleSheet); + } + + /** + * Reads and returns a list of all tags associated with this content node. + * + * @return list of tags associated with the node. + */ + abstract protected List getAllTagsFromDatabase(); + + /** + * Returns correlation attribute instance for the underlying content of the node. + * + * @return correlation attribute instance for the underlying content of the node. + */ + abstract protected CorrelationAttributeInstance getCorrelationAttributeInstance(); + + /** + * Returns Score property for the node. + * + * @param tags list of tags. + * + * @return Score property for the underlying content of the node. + */ + abstract protected Pair getScorePropertyAndDescription(List tags); + + /** + * Returns comment property for the node. + * + * @param tags list of tags + * @param attribute correlation attribute instance + * + * @return Comment property for the underlying content of the node. + */ + abstract protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute); + + /** + * Returns occurrences/count property for the node. + * + * @param attribute correlation attribute instance + * + * @return count property for the underlying content of the node. + */ + abstract protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java index 4a1147494a..9727d83e39 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java @@ -286,7 +286,12 @@ public abstract class BaseChildFactory extends ChildFactory.D * If pageSize is set split keys into pages, otherwise create a * single page containing all keys. */ - pages = Lists.partition(keys, pageSize > 0 ? pageSize : keys.size()); + if (keys.isEmpty()) { + pages.clear(); + } else { + pages = Lists.partition(keys, pageSize > 0 ? pageSize : keys.size()); + } + if (pages.size() != oldPageCount) { try { // Number of pages has changed so we need to send out a notification. diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index 55e81acd17..23ab78fa01 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -37,6 +38,7 @@ import java.util.logging.Level; import java.util.stream.Collectors; import javax.swing.Action; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Sheet; import org.openide.util.Lookup; import org.openide.util.NbBundle; @@ -55,11 +57,13 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; 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; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; @@ -95,10 +99,7 @@ public class BlackboardArtifactNode extends AbstractContentNode> customProperties; - - private final static String NO_DESCR = NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.noDesc.text"); - - + /* * Artifact types which should have the full unique path of the associated * content as a property. @@ -151,6 +152,19 @@ public class BlackboardArtifactNode extends AbstractContentNode(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())); + } + if (scoData.getCountAndDescription() != null && + !UserPreferences.hideCentralRepoCommentsAndOccurrences()) { + updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft())); + } + } } }; @@ -335,8 +349,6 @@ public class BlackboardArtifactNode extends AbstractContentNode tags = getAllTagsFromDatabase(); - Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); if (sheetSet == null) { sheetSet = Sheet.createPropertiesSet(); @@ -351,17 +363,17 @@ public class BlackboardArtifactNode extends AbstractContentNode(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 (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { - correlationAttribute = getCorrelationAttributeInstance(); - } - addCommentProperty(sheetSet, tags, correlationAttribute); - - if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { - addCountProperty(sheetSet, correlationAttribute); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), VALUE_LOADING, "")); } + + // Get the SCO columns data in a background task + backgroundTasksPool.submit(new GetSCOTask( + new WeakReference<>(this), weakPcl)); + if (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID()) { try { BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT)); @@ -520,6 +532,7 @@ public class BlackboardArtifactNode extends AbstractContentNode getAllTagsFromDatabase() { List tags = new ArrayList<>(); try { @@ -569,6 +582,7 @@ public class BlackboardArtifactNode extends AbstractContentNode t.getName().getDisplayName()).collect(Collectors.joining(", ")))); } + @Override protected final CorrelationAttributeInstance getCorrelationAttributeInstance() { CorrelationAttributeInstance correlationAttribute = null; if (EamDb.isEnabled()) { @@ -581,16 +595,36 @@ public class BlackboardArtifactNode extends AbstractContentNode 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 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 + */ + @Override + protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + HasCommentStatus status = tags.size() > 0 ? HasCommentStatus.TAG_NO_COMMENT : HasCommentStatus.NO_COMMENT; for (Tag tag : tags) { if (!StringUtils.isBlank(tag.getComment())) { @@ -609,17 +643,17 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, - status)); + return status; } - /** * Used by (subclasses of) BlackboardArtifactNode to add the Score property * to their sheets. * - * @param sheetSet the modifiable Sheet.Set returned by - * Sheet.get(Sheet.PROPERTIES) + * @param sheetSet the modifiable Sheet.Set to add the property to * @param tags the list of tags associated with the file + * + * @deprecated Use the GetSCOTask to get this data on a background thread..., + * and then update the property sheet asynchronously */ @NbBundle.Messages({"BlackboardArtifactNode.createSheet.score.name=S", "BlackboardArtifactNode.createSheet.score.displayName=S", @@ -628,8 +662,22 @@ public class BlackboardArtifactNode extends AbstractContentNode tags) { - Score score = Score.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())); + } + + /** + * Get the score property for the node. + * + * @param tags the list of tags associated with the file + * + * @return score property and description + */ + @Override + protected Pair getScorePropertyAndDescription(List tags) { + Score score = Score.NO_SCORE; String description = Bundle.BlackboardArtifactNode_createSheet_noScore_description(); if (associated instanceof AbstractFile) { if (((AbstractFile) associated).getKnown() == TskData.FileKnown.BAD) { @@ -673,18 +721,43 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), description, score)); + + return Pair.of(score, description); } - + /** + * Used by (subclasses of) BlackboardArtifactNode to add the Occurrences property + * to their sheets. + * + * @param sheetSet the modifiable Sheet.Set to add the property to + * @param attribute correlation attribute instance + * + * @deprecated Use the GetSCOTask to get this data on a background thread..., + * and then update the property sheet asynchronously + */ @NbBundle.Messages({"BlackboardArtifactNode.createSheet.count.name=O", "BlackboardArtifactNode.createSheet.count.displayName=O", "BlackboardArtifactNode.createSheet.count.noCentralRepo.description=Central repository was not enabled when this column was populated", "BlackboardArtifactNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this artifact's associated file when the column was populated", "# {0} - occuranceCount", "BlackboardArtifactNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"}) - + @Deprecated protected final void addCountProperty(Sheet.Set sheetSet, CorrelationAttributeInstance attribute) { - Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting + Pair countAndDescription = getCountPropertyAndDescription(attribute); + 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 attribute correlation attribute instance + * + * @return count and description + * + */ + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { + Long count = -1L; String description = Bundle.BlackboardArtifactNode_createSheet_count_noCentralRepo_description(); try { //don't perform the query if there is no correlation value @@ -699,14 +772,13 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), description, count)); - } - + return Pair.of(count, description); + } + private void updateSheet() { this.setSheet(createSheet()); } - + private String getRootParentName() { String parentName = associated.getName(); Content parent = associated; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED index 1d8fb7eb14..0386b0fef2 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED @@ -1,5 +1,4 @@ AbstractAbstractFileNode.accessTimeColLbl=Access Time -AbstractAbstractFileNode.addFileProperty.desc=no description AbstractAbstractFileNode.attrAddrColLbl=Attr. Addr. AbstractAbstractFileNode.changeTimeColLbl=Change Time AbstractAbstractFileNode.createdTimeColLbl=Created Time @@ -37,6 +36,8 @@ AbstractAbstractFileNode.tagsProperty.displayName=Tags AbstractAbstractFileNode.typeDirColLbl=Type(Dir) AbstractAbstractFileNode.typeMetaColLbl=Type(Meta) AbstractAbstractFileNode.useridColLbl=UserID +AbstractContentNode.nodescription=no description +AbstractContentNode.valueLoading=value loading AbstractFsContentNode.noDesc.text=no description ArtifactStringContent.attrsTableHeader.sources=Source(s) ArtifactStringContent.attrsTableHeader.type=Type diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java index 13a0753ca1..bdb4a394a8 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java @@ -36,6 +36,7 @@ import org.sleuthkit.autopsy.actions.ReplaceBlackboardArtifactTagAction; import org.sleuthkit.autopsy.actions.ReplaceContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.datamodel.Reports.ReportNode; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -92,6 +93,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -119,6 +121,7 @@ public class DataModelActionsFactory { actionsList.add(new NewWindowViewAction(VIEW_IN_NEW_WINDOW, slackFileNode)); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -155,6 +158,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance());// + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -189,6 +193,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -223,6 +228,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -257,6 +263,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -291,6 +298,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -325,6 +333,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -379,6 +388,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -415,6 +425,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java index 97b0fd8216..5a3476f896 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java @@ -28,6 +28,7 @@ import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; @@ -89,6 +90,7 @@ public class DirectoryNode extends AbstractFsContentNode { actionsList.add(ViewFileInTimelineAction.createViewFileAction(content)); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(new RunIngestModulesAction(content)); actionsList.add(null); // creates a menu separator diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java index 1695b253b0..7b3a55c20b 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java @@ -32,6 +32,7 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -173,6 +174,7 @@ public class FileNode extends AbstractFsContentNode { actionsList.add(null); // Creates an item separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // Creates an item separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java index 152e8a6bac..b3005c28b4 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java @@ -390,6 +390,7 @@ public final class FileTypesByExtension implements AutopsyVisitableItem { * @param skCase * @param o Observable that will notify when there could be new * data to display + * @param nodeName */ private FileExtensionNodeChildren(FileTypesByExtension.SearchFilterInterface filter, SleuthkitCase skCase, Observable o, String nodeName) { super(nodeName, new ViewsKnownAndSlackFilter<>()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java b/Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java new file mode 100644 index 0000000000..b7f4e38a5e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java @@ -0,0 +1,73 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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 java.lang.ref.WeakReference; +import java.util.List; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.datamodel.Tag; + +/** + * Background task to get Score, Comment and Occurrences values for an + * Abstract content node. + * + */ +class GetSCOTask implements Runnable { + + private final WeakReference> weakNodeRef; + private final PropertyChangeListener listener; + + GetSCOTask(WeakReference> weakContentRef, PropertyChangeListener listener) { + this.weakNodeRef = weakContentRef; + this.listener = listener; + } + + @Override + public void run() { + AbstractContentNode contentNode = weakNodeRef.get(); + + //Check for stale reference + if (contentNode == null) { + return; + } + + // get the SCO column values + List tags = contentNode.getAllTagsFromDatabase(); + CorrelationAttributeInstance attribute = contentNode.getCorrelationAttributeInstance(); + + SCOData scoData = new SCOData(); + scoData.setScoreAndDescription(contentNode.getScorePropertyAndDescription(tags)); + scoData.setComment(contentNode.getCommentProperty(tags, attribute)); + if (!UserPreferences.hideCentralRepoCommentsAndOccurrences()) { + scoData.setCountAndDescription(contentNode.getCountPropertyAndDescription(attribute)); + } + + // signal SCO data is available. + if (listener != null) { + listener.propertyChange(new PropertyChangeEvent( + AutopsyEvent.SourceType.LOCAL.toString(), + AbstractAbstractFileNode.NodeSpecificEvents.SCO_AVAILABLE.toString(), + null, scoData)); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java index 584a405fcc..b2c80f1322 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java @@ -28,12 +28,15 @@ import java.util.EnumSet; import java.util.List; import java.util.logging.Level; import javax.swing.Action; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.datasourcesummary.ViewSummaryInformationAction; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.directorytree.ExplorerNodeActionVisitor; import org.sleuthkit.autopsy.directorytree.FileSearchAction; @@ -47,6 +50,7 @@ import org.sleuthkit.datamodel.SleuthkitCase.CaseDbQuery; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.VirtualDirectory; import org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException; +import org.sleuthkit.datamodel.Tag; /** * This class is used to represent the "Node" for the image. The children of @@ -250,4 +254,67 @@ public class ImageNode extends AbstractContentNode { } }; + /** + * Reads and returns a list of all tags associated with this content node. + * + * Null implementation of an abstract method. + * + * @return list of tags associated with the node. + */ + @Override + protected List getAllTagsFromDatabase() { + return new ArrayList<>(); + } + /** + * Returns correlation attribute instance for the underlying content of the node. + * + * Null implementation of an abstract method. + * + * @return correlation attribute instance for the underlying content of the node. + */ + @Override + protected CorrelationAttributeInstance getCorrelationAttributeInstance() { + return null; + } + + /** + * Returns Score property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags. + * + * @return Score property for the underlying content of the node. + */ + @Override + protected Pair getScorePropertyAndDescription(List tags) { + return Pair.of(DataResultViewerTable.Score.NO_SCORE, NO_DESCR); + } + /** + * Returns comment property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags + * @param attribute correlation attribute instance + * + * @return Comment property for the underlying content of the node. + */ + @Override + protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + return DataResultViewerTable.HasCommentStatus.NO_COMMENT; + } + /** + * Returns occurrences/count property for the node. + * + * Null implementation of an abstract method. + * + * @param attribute correlation attribute instance + * + * @return count property for the underlying content of the node. + */ + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { + return Pair.of(-1L, NO_DESCR); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java index bfd8f7bd91..41ecad0340 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java @@ -29,6 +29,7 @@ import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -105,6 +106,7 @@ public class LayoutFileNode extends AbstractAbstractFileNode { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java index 6d64aeb621..4a62cd9e4f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java @@ -31,6 +31,7 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -82,6 +83,7 @@ public class LocalFileNode extends AbstractAbstractFileNode { actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java b/Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java new file mode 100644 index 0000000000..a9dd99369d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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 org.apache.commons.lang3.tuple.Pair; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; + +/** + * Container to bag the S C & O data for an abstract file node. + * + */ +class SCOData { + + private Pair scoreAndDescription = null; + private DataResultViewerTable.HasCommentStatus comment = null; + private Pair countAndDescription = null; + + Pair getScoreAndDescription() { + return scoreAndDescription; + } + + DataResultViewerTable.HasCommentStatus getComment() { + return comment; + } + + Pair getCountAndDescription() { + return countAndDescription; + } + + void setScoreAndDescription(Pair scoreAndDescription) { + this.scoreAndDescription = scoreAndDescription; + } + void setComment(DataResultViewerTable.HasCommentStatus comment) { + this.comment = comment; + } + void setCountAndDescription(Pair countAndDescription) { + this.countAndDescription = countAndDescription; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java index 49d1b9da54..0e7bce56cc 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java @@ -28,6 +28,7 @@ import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; @@ -85,6 +86,7 @@ public class SlackFileNode extends AbstractFsContentNode { NbBundle.getMessage(this.getClass(), "SlackFileNode.getActions.viewInNewWin.text"), this)); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java index 47f616de12..ce5948ce68 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java @@ -25,6 +25,7 @@ import javax.swing.Action; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.datasourcesummary.ViewSummaryInformationAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.FileSearchAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; @@ -61,6 +62,7 @@ public abstract class SpecialDirectoryNode extends AbstractAbstractFileNode { public String getItemType() { return DisplayableItemNode.FILE_PARENT_NODE_KEY; } + /** + * Reads and returns a list of all tags associated with this content node. + * + * Null implementation of an abstract method. + * + * @return list of tags associated with the node. + */ + @Override + protected List getAllTagsFromDatabase() { + return new ArrayList<>(); + } + /** + * Returns correlation attribute instance for the underlying content of the node. + * + * Null implementation of an abstract method. + * + * @return correlation attribute instance for the underlying content of the node. + */ + @Override + protected CorrelationAttributeInstance getCorrelationAttributeInstance() { + return null; + } + + /** + * Returns Score property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags. + * + * @return Score property for the underlying content of the node. + */ + @Override + protected Pair getScorePropertyAndDescription(List tags) { + return Pair.of(DataResultViewerTable.Score.NO_SCORE, NO_DESCR); + } + /** + * Returns comment property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags + * @param attribute correlation attribute instance + * + * @return Comment property for the underlying content of the node. + */ + @Override + protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + return DataResultViewerTable.HasCommentStatus.NO_COMMENT; + } + /** + * Returns occurrences/count property for the node. + * + * Null implementation of an abstract method. + * + * @param attribute correlation attribute instance + * + * @return count property for the underlying content of the node. + */ + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { + return Pair.of(-1L, NO_DESCR); + } } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED index 0d04cc1c46..f70a374bc5 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED @@ -1,3 +1,8 @@ +CSVWriter.done.notifyMsg.error=Error exporting to CSV file +# {0} - Output file +CSVWriter.done.notifyMsg.success=Wrote to {0} +CSVWriter.progress.cancelling=Cancelling +CSVWriter.progress.extracting=Exporting to CSV file CTL_DirectoryTreeTopComponent=Directory Tree DataResultFilterNode.viewSourceArtifact.text=View Source Result # {0} - dataSourceCount @@ -5,6 +10,11 @@ DirectoryTreeTopComponent.componentOpened.groupDataSources.text=This case contai DirectoryTreeTopComponent.componentOpened.groupDataSources.title=Group by data source? DirectoryTreeTopComponent.emptyMimeNode.text=Data not available. Run file type identification module. DirectoryTreeTopComponent.resultsView.title=Listing +ExportCSV.saveNodesToCSV.empty=No data to export +# {0} - Output file +ExportCSV.saveNodesToCSV.fileExists=File {0} already exists +ExportCSV.saveNodesToCSV.noCurrentCase=No open case available +ExportCSV.title.text=Export selected rows to CSV ExternalViewerAction.actionPerformed.failure.exe.message=The file is an executable and will not be opened. ExternalViewerAction.actionPerformed.failure.IO.message=There is no associated editor for files of this type or the associated application failed to launch. ExternalViewerAction.actionPerformed.failure.missingFile.message=The file no longer exists. diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java index 0b3a6f698f..d438c11727 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java @@ -372,6 +372,7 @@ public class DataResultFilterNode extends FilterNode { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); actionsList.add(AddBlackboardArtifactTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java index 99692ea633..674eb6bf2e 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java @@ -125,6 +125,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final DerivedFile d) { List actionsList = new ArrayList<>(); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(AddContentTagAction.getInstance()); final Collection selectedFilesList = @@ -166,6 +169,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final LocalFile d) { List actionsList = new ArrayList<>(); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(AddContentTagAction.getInstance()); final Collection selectedFilesList = @@ -182,6 +186,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final org.sleuthkit.datamodel.File d) { List actionsList = new ArrayList<>(); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(AddContentTagAction.getInstance()); final Collection selectedFilesList = diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java new file mode 100644 index 0000000000..7761e82231 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java @@ -0,0 +1,360 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this content 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.Component; +import java.awt.event.ActionEvent; +import java.io.File; +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javax.swing.AbstractAction; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.netbeans.api.progress.ProgressHandle; +import org.openide.util.Cancellable; +import org.openide.util.NbBundle; +import org.openide.util.Utilities; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType; +import org.openide.nodes.Node; +import org.openide.nodes.Node.PropertySet; +import org.openide.nodes.Node.Property; + +/** + * Exports CSV version of result nodes to a location selected by the user. + */ +public final class ExportCSVAction extends AbstractAction { + + private static final Logger logger = Logger.getLogger(ExportCSVAction.class.getName()); + private final static String DEFAULT_FILENAME = "Results"; + private final static List columnsToSkip = Arrays.asList(AbstractFilePropertyType.SCORE.toString(), + AbstractFilePropertyType.COMMENT.toString(), AbstractFilePropertyType.OCCURRENCES.toString()); + + private static String userDefinedExportPath; + + // This class is a singleton to support multi-selection of nodes, since + // org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every + // node in the array returns a reference to the same action object from Node.getActions(boolean). + private static ExportCSVAction instance; + + /** + * Get an instance of the Action. See above for why + * the class is a singleton. + * + * @return the instance + */ + public static synchronized ExportCSVAction getInstance() { + if (null == instance) { + instance = new ExportCSVAction(); + } + return instance; + } + + /** + * Private constructor for the action. + */ + @NbBundle.Messages({"ExportCSV.title.text=Export selected rows to CSV"}) + private ExportCSVAction() { + super(Bundle.ExportCSV_title_text()); + } + + /** + * Asks user to choose destination, then extracts content to destination + * (recursing on directories). + * + * @param e The action event. + */ + + @Override + public void actionPerformed(ActionEvent e) { + Collection selectedNodes = Utilities.actionsGlobalContext().lookupAll(Node.class); + saveNodesToCSV(selectedNodes, (Component)e.getSource()); + } + + /** + * Save the selected nodes to a CSV file + * + * @param nodesToExport the nodes to save + * @param component + */ + @NbBundle.Messages({ + "# {0} - Output file", + "ExportCSV.saveNodesToCSV.fileExists=File {0} already exists", + "ExportCSV.saveNodesToCSV.noCurrentCase=No open case available", + "ExportCSV.saveNodesToCSV.empty=No data to export"}) + public static void saveNodesToCSV(Collection nodesToExport, Component component) { + + if (nodesToExport.isEmpty()) { + MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_empty()); + return; + } + + try { + // Set up the file chooser with a default name and either the Export + // folder or the last used folder. + String fileName = getDefaultOutputFileName(nodesToExport.iterator().next().getParentNode()); + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setCurrentDirectory(new File(getExportDirectory(Case.getCurrentCaseThrows()))); + fileChooser.setSelectedFile(new File(fileName)); + fileChooser.setFileFilter(new FileNameExtensionFilter("csv file", "csv")); + + int returnVal = fileChooser.showSaveDialog(component); + if (returnVal == JFileChooser.APPROVE_OPTION) { + + // Get the file name, appending .csv if necessary + File selectedFile = fileChooser.getSelectedFile(); + if (!selectedFile.getName().endsWith(".csv")) { // NON-NLS + selectedFile = new File(selectedFile.toString() + ".csv"); // NON-NLS + } + + // Save the directory used for next time + updateExportDirectory(selectedFile.getParent(), Case.getCurrentCaseThrows()); + + if (selectedFile.exists()) { + logger.log(Level.SEVERE, "File {0} already exists", selectedFile.getAbsolutePath()); //NON-NLS + MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_fileExists(selectedFile)); + return; + } + + CSVWriter writer = new CSVWriter(nodesToExport, selectedFile); + writer.execute(); + } + } catch (NoCurrentCaseException ex) { + JOptionPane.showMessageDialog(component, Bundle.ExportCSV_saveNodesToCSV_noCurrentCase()); + logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS + } + } + + /** + * Create a default name for the CSV output. + * + * @param parent The parent node for the selected nodes + * + * @return the default name + */ + private static String getDefaultOutputFileName(Node parent) { + String dateStr = String.format("%1$tY%1$tm%1$te%1$tI%1$tM%1$tS", Calendar.getInstance()); + + if (parent != null) { + // The first value in the property set is generally a reasonable name + for (PropertySet set : parent.getPropertySets()) { + for (Property prop : set.getProperties()) { + try { + String parentName = prop.getValue().toString(); + + // Strip off the count (if present) + parentName = parentName.replaceAll("\\([0-9]+\\)$", ""); + + // Strip out any invalid characters + parentName = parentName.replaceAll("[\\\\/:*?\"<>|]", "_"); + + return parentName + " " + dateStr; + } catch (IllegalAccessException | InvocationTargetException ex) { + logger.log(Level.WARNING, "Failed to get property set value as string", ex); + } + } + } + } + return DEFAULT_FILENAME + " " + dateStr; + } + + /** + * Get the export directory path. + * + * @param openCase The current case. + * + * @return The export directory path. + */ + private static String getExportDirectory(Case openCase) { + String caseExportPath = openCase.getExportDirectory(); + + if (userDefinedExportPath == null) { + return caseExportPath; + } + + File file = new File(userDefinedExportPath); + if (file.exists() == false || file.isDirectory() == false) { + return caseExportPath; + } + + return userDefinedExportPath; + } + + /** + * Update the default export directory. If the directory path matches the + * case export directory, then the directory used will always match the + * export directory of any given case. Otherwise, the path last used will be + * saved. + * + * @param exportPath The export path. + * @param openCase The current case. + */ + private static void updateExportDirectory(String exportPath, Case openCase) { + if (exportPath.equalsIgnoreCase(openCase.getExportDirectory())) { + userDefinedExportPath = null; + } else { + userDefinedExportPath = exportPath; + } + } + + + /** + * Thread that does the actual extraction work + */ + private static class CSVWriter extends SwingWorker { + + private static final Logger logger = Logger.getLogger(CSVWriter.class.getName()); + private ProgressHandle progress; + + private final Collection nodesToExport; + private final File outputFile; + + /** + * Create an instance of the CSVWriter. + * + * @param extractionTasks List of file extraction tasks. + */ + CSVWriter(Collection nodesToExport, File outputFile) { + this.nodesToExport = nodesToExport; + this.outputFile = outputFile; + } + + @NbBundle.Messages({"CSVWriter.progress.extracting=Exporting to CSV file", + "CSVWriter.progress.cancelling=Cancelling"}) + @Override + protected Object doInBackground() throws Exception { + if (nodesToExport.isEmpty()) { + return null; + } + + // Set up progress bar. + final String displayName = Bundle.CSVWriter_progress_extracting(); + progress = ProgressHandle.createHandle(displayName, new Cancellable() { + @Override + public boolean cancel() { + if (progress != null) { + progress.setDisplayName(Bundle.CSVWriter_progress_cancelling()); + } + return ExportCSVAction.CSVWriter.this.cancel(true); + } + }); + progress.start(); + progress.switchToIndeterminate(); + + try (BufferedWriter br = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8))) { + // Write BOM + br.write('\ufeff'); + + // Write the header + List headers = new ArrayList<>(); + PropertySet[] sets = nodesToExport.iterator().next().getPropertySets(); + for(PropertySet set : sets) { + for (Property prop : set.getProperties()) { + if ( ! columnsToSkip.contains(prop.getDisplayName())) { + headers.add(prop.getDisplayName()); + } + } + } + br.write(listToCSV(headers)); + + // Write each line + Iterator nodeIterator = nodesToExport.iterator(); + while (nodeIterator.hasNext()) { + if (this.isCancelled()) { + break; + } + + Node node = (Node)nodeIterator.next(); + List values = new ArrayList<>(); + sets = node.getPropertySets(); + for(PropertySet set : sets) { + for (Property prop : set.getProperties()) { + if ( ! columnsToSkip.contains(prop.getDisplayName())) { + values.add(escapeQuotes(prop.getValue().toString())); + } + } + } + br.write(listToCSV(values)); + } + } + + return null; + } + + /** + * Escape any quotes in the string + * + * @param original + * + * @return the string with quotes escaped + */ + private String escapeQuotes(String original) { + return original.replaceAll("\"", "\\\\\""); + } + + /** + * Convert list of values to a comma separated string. + * + * @param values Values to convert + * + * @return values as CSV + */ + private String listToCSV(List values) { + return "\"" + String.join("\",\"", values) + "\"\n"; + } + + @NbBundle.Messages({"CSVWriter.done.notifyMsg.error=Error exporting to CSV file", + "# {0} - Output file", + "CSVWriter.done.notifyMsg.success=Wrote to {0}"}) + @Override + protected void done() { + boolean msgDisplayed = false; + try { + super.get(); + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Fatal error during file extraction", ex); //NON-NLS + MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_error()); + msgDisplayed = true; + } catch (java.util.concurrent.CancellationException ex) { + // catch and ignore if we were cancelled + } finally { + progress.finish(); + if (!this.isCancelled() && !msgDisplayed) { + MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_success(outputFile)); + } + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java index e919bf189f..d226e6765b 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java @@ -278,6 +278,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp //enable the new button FilesSetDefsPanel.this.newSetButton.setEnabled(canBeEnabled); FilesSetDefsPanel.this.importSetButton.setEnabled(canBeEnabled); + // Get the selected interesting files set and populate the set // components. FilesSet selectedSet = FilesSetDefsPanel.this.setsList.getSelectedValue(); @@ -302,6 +303,12 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp if (!FilesSetDefsPanel.this.rulesListModel.isEmpty()) { FilesSetDefsPanel.this.rulesList.setSelectedIndex(0); } + } else { + // Disable the edit, delete, copy, and export buttons + FilesSetDefsPanel.this.editSetButton.setEnabled(false); + FilesSetDefsPanel.this.deleteSetButton.setEnabled(false); + FilesSetDefsPanel.this.copySetButton.setEnabled(false); + FilesSetDefsPanel.this.exportSetButton.setEnabled(false); } } diff --git a/Core/src/org/sleuthkit/autopsy/rejview/HexView.java b/Core/src/org/sleuthkit/autopsy/rejview/HexView.java index f78f9ac1ad..932bcf627a 100644 --- a/Core/src/org/sleuthkit/autopsy/rejview/HexView.java +++ b/Core/src/org/sleuthkit/autopsy/rejview/HexView.java @@ -251,10 +251,10 @@ final class HexView extends JPanel { this.setHighlight(startByte, endByte); if (startByte != endByte) { - /** - * @param 1 Start - * @param 2 End - * @param 3 Len + /* + * param 1 Start + * param 2 End + * param 3 Len */ int length = endByte - startByte; String text = Bundle.HexView_statusTemplate_nonZeroLength( @@ -266,8 +266,8 @@ final class HexView extends JPanel { String.format("0x%1$x", length)); statusLabel.setText(text); } else { - /** - * @param 1 Start + /* + * param 1 Start */ String text = Bundle.HexView_statusTemplate_zeroLength(startByte, String.format("0x%1$x", startByte)); statusLabel.setText(text); diff --git a/Core/src/org/sleuthkit/autopsy/rejview/RejTreeKeyView.java b/Core/src/org/sleuthkit/autopsy/rejview/RejTreeKeyView.java index dacb2e8177..a74d4d26b4 100644 --- a/Core/src/org/sleuthkit/autopsy/rejview/RejTreeKeyView.java +++ b/Core/src/org/sleuthkit/autopsy/rejview/RejTreeKeyView.java @@ -58,10 +58,10 @@ public final class RejTreeKeyView extends RejTreeNodeView { public RejTreeKeyView(RejTreeKeyNode node) { super(new BorderLayout()); - /** - * @param 1 Name - * @param 2 Number of subkeys - * @param 3 Number of values + /* + * param 1 Name + * param 2 Number of subkeys + * param 3 Number of values */ String metadataTemplate = "" + Bundle.RejTreeKeyView_template_name() diff --git a/Core/src/org/sleuthkit/autopsy/rejview/RejTreeValueView.java b/Core/src/org/sleuthkit/autopsy/rejview/RejTreeValueView.java index 8b18d38bf5..d2b4e7cf5c 100644 --- a/Core/src/org/sleuthkit/autopsy/rejview/RejTreeValueView.java +++ b/Core/src/org/sleuthkit/autopsy/rejview/RejTreeValueView.java @@ -51,9 +51,9 @@ public final class RejTreeValueView extends RejTreeNodeView { "RejTreeValueView.valueBorder.title=Value",}) public RejTreeValueView(RejTreeValueNode node) { super(new BorderLayout()); - /** - * @param 1 Name - * @param 2 Type + /* + * param 1 Name + * param 2 Type */ String metadataTemplate = "" + Bundle.RejTreeValueView_template_name() @@ -63,8 +63,8 @@ public final class RejTreeValueView extends RejTreeNodeView { String valueName; String valueType; - /** - * @param 1 Value + /* + * param 1 Value */ String valueTemplate = "%1$s"; try { diff --git a/Core/src/org/sleuthkit/autopsy/report/FileReportText.java b/Core/src/org/sleuthkit/autopsy/report/FileReportText.java index 9d3de2c945..ea0f3452c2 100644 --- a/Core/src/org/sleuthkit/autopsy/report/FileReportText.java +++ b/Core/src/org/sleuthkit/autopsy/report/FileReportText.java @@ -24,6 +24,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -63,9 +64,12 @@ class FileReportText implements FileReportModule { public void startReport(String baseReportDir) { this.reportPath = baseReportDir + FILE_NAME; try { - out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(this.reportPath))); + out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(this.reportPath), StandardCharsets.UTF_8)); + out.write('\ufeff'); } catch (FileNotFoundException ex) { logger.log(Level.WARNING, "Failed to create report text file", ex); //NON-NLS + } catch (IOException ex) { + logger.log(Level.WARNING, "Failed to write BOM to report text file", ex); //NON-NLS } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslationService.java b/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslationService.java index f1e6f084b7..b24dca2aac 100755 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslationService.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslationService.java @@ -120,4 +120,13 @@ public final class TextTranslationService { public boolean hasProvider() { return selectedTranslator.isPresent(); } + + /** + * Returns the hard limit for translation request sizes. + * + * @return + */ + public int getMaxPayloadSize() { + return selectedTranslator.get().getMaxPayloadSize(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslator.java b/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslator.java index 5e6ad8b19e..e996e6331e 100755 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslator.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslator.java @@ -56,4 +56,11 @@ public interface TextTranslator { * Save the settings as they have been modified in the component. */ void saveSettings(); + + /** + * Returns the hard limit for translation request sizes. + * + * @return + */ + int getMaxPayloadSize(); } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslator.java b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslator.java new file mode 100644 index 0000000000..3dfd984b0a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslator.java @@ -0,0 +1,179 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.texttranslation.translators; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; +import java.awt.Component; +import java.io.IOException; +import org.openide.util.NbBundle.Messages; +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.texttranslation.TextTranslator; +import org.sleuthkit.autopsy.texttranslation.TranslationException; + +/** + * Translates text by making HTTP requests to Bing Translator. This requires a + * valid subscription key for a Microsoft Azure account. + */ +@ServiceProvider(service = TextTranslator.class) +public class BingTranslator implements TextTranslator { + + //The target language follows the to= in the string below. You can include multiple target + //languages separated by commas. A full list of supported languages is here: + //https://docs.microsoft.com/en-us/azure/cognitive-services/translator/language-support + private static final String BASE_URL = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to="; + private static final int MAX_STRING_LENGTH = 5000; + private final BingTranslatorSettingsPanel settingsPanel; + private final BingTranslatorSettings settings = new BingTranslatorSettings(); + // This sends messages to Microsoft. + private final OkHttpClient CLIENT = new OkHttpClient(); + + /** + * Create a Bing Translator + */ + public BingTranslator() { + settingsPanel = new BingTranslatorSettingsPanel(settings.getAuthenticationKey(), settings.getTargetLanguageCode()); + } + + /** + * Get the tranlationurl for the specified language code + * + * + * + * @param languageCode language code for language to translate to + * + * @return a string representation of the url to request translation from + */ + static String getTranlatorUrl(String languageCode) { + return BASE_URL + languageCode; + } + + /** + * Converts an input text to the JSON format required by Bing Translator, + * posts it to Microsoft, and returns the JSON text response. + * + * @param string The input text to be translated. + * + * @return The translation response as a JSON string + * + * @throws IOException if the request could not be executed due to + * cancellation, a connectivity problem or timeout. + */ + public String postTranslationRequest(String string) throws IOException { + MediaType mediaType = MediaType.parse("application/json"); + + JsonArray jsonArray = new JsonArray(); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("Text", string); + jsonArray.add(jsonObject); + String bodyString = jsonArray.toString(); + + RequestBody body = RequestBody.create(mediaType, + bodyString); + Request request = new Request.Builder() + .url(getTranlatorUrl(settings.getTargetLanguageCode())).post(body) + .addHeader("Ocp-Apim-Subscription-Key", settings.getAuthenticationKey()) + .addHeader("Content-type", "application/json").build(); + Response response = CLIENT.newCall(request).execute(); + return response.body().string(); + } + + @Override + public String translate(String string) throws TranslationException { + if (settings.getAuthenticationKey() == null || settings.getAuthenticationKey().isEmpty()) { + throw new TranslationException("Bing Translator has not been configured, authentication key needs to be specified"); + } + String toTranslate = string.trim(); + //Translates some text into English, without specifying the source langauge. + + //Google Translate required us to replace (\r\n|\n) with
+ //but Bing Translator doesn not have that requirement. + //The free account has a maximum file size. If you have a paid account, + //you probably still want to limit file size to prevent accidentally + //translating very large documents. + if (toTranslate.length() > MAX_STRING_LENGTH) { + toTranslate = toTranslate.substring(0, MAX_STRING_LENGTH); + } + + try { + String response = postTranslationRequest(toTranslate); + return parseJSONResponse(response); + } catch (IOException | TranslationException ex) { + throw new TranslationException("Exception while attempting to translate using BingTranslator", ex); + } + } + + @Messages({"BingTranslator.name.text=Bing Translator"}) + @Override + public String getName() { + return Bundle.BingTranslator_name_text(); + } + + @Override + public Component getComponent() { + return settingsPanel; + } + + @Override + public void saveSettings() { + settings.setAuthenticationKey(settingsPanel.getAuthenticationKey()); + settings.setTargetLanguageCode(settingsPanel.getTargetLanguageCode()); + settings.saveSettings(); + } + + /** + * Parse the response to get the translated text + * + * @param json_text the json which was received as a response to a + * translation request + * + * @return the translated text + * + * @throws TranslationException + */ + private String parseJSONResponse(String json_text) throws TranslationException { + /* + * Here is an example of the text we get from Bing when input is "gato", + * the Spanish word for cat: [ { "detectedLanguage": { "language": "es", + * "score": 1.0 }, "translations": [ { "text": "cat", "to": "en" } ] } ] + */ + JsonParser parser = new JsonParser(); + try { + JsonArray responses = parser.parse(json_text).getAsJsonArray(); + //As far as I know, there's always exactly one item in the array. + JsonObject response0 = responses.get(0).getAsJsonObject(); + JsonArray translations = response0.getAsJsonArray("translations"); + JsonObject translation0 = translations.get(0).getAsJsonObject(); + return translation0.get("text").getAsString(); + } catch (IllegalStateException | ClassCastException | NullPointerException | IndexOutOfBoundsException e) { + throw new TranslationException("JSON text does not match Bing Translator scheme: " + e); + } + } + + @Override + public int getMaxPayloadSize() { + return MAX_STRING_LENGTH; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorSettings.java b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorSettings.java new file mode 100644 index 0000000000..d00ed61f77 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorSettings.java @@ -0,0 +1,115 @@ +/* + * Autopsy + * + * Copyright 2019 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.texttranslation.translators; + +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; + +/** + * Class to handle the settings associated with the BingTranslator + */ +public final class BingTranslatorSettings { + + private static final String AUTHENTICATION_KEY = "Credentials"; + private static final String BING_TRANSLATE_NAME = "BingTranslate"; + private static final String DEFAULT_AUTHENTICATION = ""; + private static final String DEFAULT_TARGET_LANGUAGE = "en"; + private static final String TARGET_LANGUAGE_CODE_KEY = "TargetLanguageCode"; + private String authenticationKey; + private String targetLanguageCode; + + /** + * Construct a new BingTranslatorSettings object + */ + BingTranslatorSettings() { + loadSettings(); + } + + /** + * Get the Authentication key to be used for the Microsoft translation + * service + * + * @return the Authentication key for the service + */ + String getAuthenticationKey() { + return authenticationKey; + } + + /** + * Set the Authentication key to be used for the Microsoft translation + * service + * + * @param authKey the Authentication key for the service + */ + void setAuthenticationKey(String authKey) { + authenticationKey = authKey; + } + + /** + * Load the settings into memory from their on disk storage + */ + void loadSettings() { + if (!ModuleSettings.configExists(BING_TRANSLATE_NAME)) { + ModuleSettings.makeConfigFile(BING_TRANSLATE_NAME); + } + if (ModuleSettings.settingExists(BING_TRANSLATE_NAME, AUTHENTICATION_KEY)) { + authenticationKey = ModuleSettings.getConfigSetting(BING_TRANSLATE_NAME, AUTHENTICATION_KEY); + } + if (authenticationKey == null || StringUtils.isBlank(authenticationKey)) { + authenticationKey = DEFAULT_AUTHENTICATION; + } + if (ModuleSettings.settingExists(BING_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY)) { + targetLanguageCode = ModuleSettings.getConfigSetting(BING_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY); + } + if (targetLanguageCode == null || StringUtils.isBlank(targetLanguageCode)) { + targetLanguageCode = DEFAULT_TARGET_LANGUAGE; + } + } + + /** + * Get the target language code + * + * @return the code used to identify the target language + */ + String getTargetLanguageCode() { + return targetLanguageCode; + } + + /** + * Set the target language code. If a blank code is specified it sets the + * default code instead. + * + * @param code the target language code to set + */ + void setTargetLanguageCode(String code) { + if (StringUtils.isBlank(code)) { + targetLanguageCode = DEFAULT_TARGET_LANGUAGE; + } else { + targetLanguageCode = code; + } + } + + /** + * Save the setting from memory to their location on disk + */ + void saveSettings() { + ModuleSettings.setConfigSetting(BING_TRANSLATE_NAME, AUTHENTICATION_KEY, authenticationKey); + ModuleSettings.setConfigSetting(BING_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY, targetLanguageCode); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorSettingsPanel.form new file mode 100644 index 0000000000..cd855ac419 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorSettingsPanel.form @@ -0,0 +1,172 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorSettingsPanel.java new file mode 100644 index 0000000000..1be932873c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorSettingsPanel.java @@ -0,0 +1,314 @@ +/* + * Autopsy + * + * Copyright 2019 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.texttranslation.translators; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.JsonObject; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.apache.commons.lang3.StringUtils; +import org.openide.util.NbBundle.Messages; + +/** + * Settings panel for the BingTranslator + */ +public class BingTranslatorSettingsPanel extends javax.swing.JPanel { + + private static final Logger logger = Logger.getLogger(BingTranslatorSettingsPanel.class.getName()); + private static final long serialVersionUID = 1L; + private static final String GET_TARGET_LANGUAGES_URL = "https://api.cognitive.microsofttranslator.com/languages?api-version=3.0&scope=translation"; + private static final String DEFUALT_TEST_STRING = "traducción exitoso"; //spanish which should translate to something along the lines of "successful translation" + private String targetLanguageCode = ""; + + /** + * Creates new form BingTranslatorSettingsPanel + */ + public BingTranslatorSettingsPanel(String authenticationKey, String code) { + initComponents(); + authenticationKeyField.setText(authenticationKey); + authenticationKeyField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + firePropertyChange("SettingChanged", true, false); + } + + @Override + public void removeUpdate(DocumentEvent e) { + firePropertyChange("SettingChanged", true, false); + } + + @Override + public void changedUpdate(DocumentEvent e) { + firePropertyChange("SettingChanged", true, false); + } + + }); + populateComboBox(); + selectLanguageByCode(code); + targetLanguageCode = code; + } + + /** + * Populate the target language combo box with available target languages + */ + @Messages({"BingTranslatorSettingsPanel.warning.targetLanguageFailure=Unable to get list of target languages or parse the result that was received"}) + private void populateComboBox() { + Request get_request = new Request.Builder() + .url(GET_TARGET_LANGUAGES_URL).build(); + try { + Response response = new OkHttpClient().newCall(get_request).execute(); + JsonParser parser = new JsonParser(); + String responseBody = response.body().string(); + JsonElement elementBody = parser.parse(responseBody); + JsonObject asObject = elementBody.getAsJsonObject(); + JsonElement translationElement = asObject.get("translation"); + JsonObject responses = translationElement.getAsJsonObject(); + responses.entrySet().forEach((entry) -> { + targetLanguageComboBox.addItem(new LanguageWrapper(entry.getKey(), entry.getValue().getAsJsonObject().get("name").getAsString())); + }); + targetLanguageComboBox.setEnabled(true); + } catch (IOException | IllegalStateException | ClassCastException | NullPointerException | IndexOutOfBoundsException ex) { + logger.log(Level.SEVERE, Bundle.BingTranslatorSettingsPanel_warning_targetLanguageFailure(), ex); + warningLabel.setText(Bundle.BingTranslatorSettingsPanel_warning_targetLanguageFailure()); + targetLanguageComboBox.setEnabled(false); + } + + } + + /** + * Given a language code select the corresponding language in the combo box + * if it is present + * + * @param code language code such as "en" for English + */ + private void selectLanguageByCode(String code) { + for (int i = 0; i < targetLanguageComboBox.getModel().getSize(); i++) { + if (targetLanguageComboBox.getModel().getElementAt(i).getLanguageCode().equals(code)) { + targetLanguageComboBox.setSelectedIndex(i); + break; + } + } + } + + /** + * 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() { + + authenticationKeyField = new javax.swing.JTextField(); + warningLabel = new javax.swing.JLabel(); + testButton = new javax.swing.JButton(); + targetLanguageLabel = new javax.swing.JLabel(); + targetLanguageComboBox = new javax.swing.JComboBox<>(); + testUntranslatedTextField = new javax.swing.JTextField(); + untranslatedLabel = new javax.swing.JLabel(); + resultLabel = new javax.swing.JLabel(); + testResultValueLabel = new javax.swing.JLabel(); + authenticationKeyLabel = new javax.swing.JLabel(); + + authenticationKeyField.setToolTipText(org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.authenticationKeyField.toolTipText")); // NOI18N + + warningLabel.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(warningLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.warningLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(testButton, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.testButton.text")); // NOI18N + testButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + testButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(targetLanguageLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.targetLanguageLabel.text")); // NOI18N + + targetLanguageComboBox.setEnabled(false); + targetLanguageComboBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + targetLanguageComboBoxSelected(evt); + } + }); + + testUntranslatedTextField.setText(DEFUALT_TEST_STRING); + + org.openide.awt.Mnemonics.setLocalizedText(untranslatedLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.untranslatedLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(resultLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.resultLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(testResultValueLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.testResultValueLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(authenticationKeyLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.authenticationKeyLabel.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() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 551, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createSequentialGroup() + .addComponent(targetLanguageLabel) + .addGap(18, 18, 18) + .addComponent(targetLanguageComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 192, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(authenticationKeyLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(authenticationKeyField, javax.swing.GroupLayout.PREFERRED_SIZE, 486, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 20, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addComponent(testButton, javax.swing.GroupLayout.PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(25, 25, 25) + .addComponent(untranslatedLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(testUntranslatedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 140, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(resultLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(testResultValueLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addContainerGap()))) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(authenticationKeyField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(authenticationKeyLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(targetLanguageLabel) + .addComponent(targetLanguageComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(testButton) + .addComponent(testUntranslatedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(untranslatedLabel) + .addComponent(resultLabel) + .addComponent(testResultValueLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + @Messages({"BingTranslatorSettingsPanel.warning.invalidKey=Invalid translation authentication key"}) + private void testButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_testButtonActionPerformed + if (testTranslationSetup()) { + warningLabel.setText(""); + } else { + warningLabel.setText(Bundle.BingTranslatorSettingsPanel_warning_invalidKey()); + } + }//GEN-LAST:event_testButtonActionPerformed + + private void targetLanguageComboBoxSelected(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_targetLanguageComboBoxSelected + String selectedCode = ((LanguageWrapper) targetLanguageComboBox.getSelectedItem()).getLanguageCode(); + if (!StringUtils.isBlank(selectedCode) && !selectedCode.equals(targetLanguageCode)) { + targetLanguageCode = selectedCode; + firePropertyChange("SettingChanged", true, false); + } + }//GEN-LAST:event_targetLanguageComboBoxSelected + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField authenticationKeyField; + private javax.swing.JLabel authenticationKeyLabel; + private javax.swing.JLabel resultLabel; + private javax.swing.JComboBox targetLanguageComboBox; + private javax.swing.JLabel targetLanguageLabel; + private javax.swing.JButton testButton; + private javax.swing.JLabel testResultValueLabel; + private javax.swing.JTextField testUntranslatedTextField; + private javax.swing.JLabel untranslatedLabel; + private javax.swing.JLabel warningLabel; + // End of variables declaration//GEN-END:variables + + /** + * Attempts to translate the text specified in the Untranslated field using + * the settings currently specified but not necessarily saved + * + * @return true if the translation was able to be performed, false otherwise + * + */ + private boolean testTranslationSetup() { + testResultValueLabel.setText(""); + MediaType mediaType = MediaType.parse("application/json"); + JsonArray jsonArray = new JsonArray(); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("Text", testUntranslatedTextField.getText()); + jsonArray.add(jsonObject); + String bodyString = jsonArray.toString(); + + RequestBody body = RequestBody.create(mediaType, + bodyString); + Request request = new Request.Builder() + .url(BingTranslator.getTranlatorUrl(targetLanguageCode)).post(body) + .addHeader("Ocp-Apim-Subscription-Key", authenticationKeyField.getText()) + .addHeader("Content-type", "application/json").build(); + try { + Response response = new OkHttpClient().newCall(request).execute(); + JsonParser parser = new JsonParser(); + JsonArray responses = parser.parse(response.body().string()).getAsJsonArray(); + //As far as I know, there's always exactly one item in the array. + JsonObject response0 = responses.get(0).getAsJsonObject(); + JsonArray translations = response0.getAsJsonArray("translations"); + JsonObject translation0 = translations.get(0).getAsJsonObject(); + testResultValueLabel.setText(translation0.get("text").getAsString()); + return true; + } catch (IOException | IllegalStateException | ClassCastException | NullPointerException | IndexOutOfBoundsException e) { + logger.log(Level.WARNING, "Test of Bing Translator failed due to exception", e); + return false; + } + } + + /** + * Get the currently set authentication key to be used for the Microsoft + * translation service + * + * @return the authentication key specified in the textarea + */ + String getAuthenticationKey() { + return authenticationKeyField.getText(); + } + + /** + * Get the currently selected target language code + * + * @return the target language code of the language selected in the combobox + */ + String getTargetLanguageCode() { + return targetLanguageCode; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties index 56782cdf33..0021118e14 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties @@ -1,4 +1,15 @@ GoogleTranslatorSettingsPanel.browseButton.text=Browse -GoogleTranslatorSettingsPanel.credentialsLabel.text=Credentials: +GoogleTranslatorSettingsPanel.credentialsLabel.text=Credentials Path: GoogleTranslatorSettingsPanel.warningLabel.text= GoogleTranslatorSettingsPanel.targetLanguageLabel.text=Target Language: +BingTranslatorSettingsPanel.testButton.text=Test +BingTranslatorSettingsPanel.testResultValueLabel.text= +BingTranslatorSettingsPanel.resultLabel.text=Result: +BingTranslatorSettingsPanel.untranslatedLabel.text=Untranslated: +BingTranslatorSettingsPanel.targetLanguageLabel.text=Target Language: +BingTranslatorSettingsPanel.authenticationKeyField.toolTipText=Enter the hash for the +GoogleTranslatorSettingsPanel.testButton.text=Test +GoogleTranslatorSettingsPanel.untranslatedLabel.text=Untranslated: +GoogleTranslatorSettingsPanel.resultLabel.text=Result: +GoogleTranslatorSettingsPanel.testResultValueLabel.text= +BingTranslatorSettingsPanel.authenticationKeyLabel.text=Authentication Key: diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties-MERGED index b0e30f632c..51cbb9a26f 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties-MERGED @@ -1,8 +1,12 @@ +BingTranslator.name.text=Bing Translator +BingTranslatorSettingsPanel.warning.invalidKey=Invalid translation authentication key +BingTranslatorSettingsPanel.warning.targetLanguageFailure=Unable to get list of target languages or parse the result that was received GoogleTranslator.name.text=Google Translate GoogleTranslatorSettingsPanel.browseButton.text=Browse -GoogleTranslatorSettingsPanel.credentialsLabel.text=Credentials: +GoogleTranslatorSettingsPanel.credentialsLabel.text=Credentials Path: GoogleTranslatorSettingsPanel.errorMessage.fileNotFound=Credentials file not found, please set the location to be a valid JSON credentials file. GoogleTranslatorSettingsPanel.errorMessage.noFileSelected=A JSON file must be selected to provide your credentials for Google Translate. +GoogleTranslatorSettingsPanel.errorMessage.translationFailure=Translation failure with specified credentials GoogleTranslatorSettingsPanel.errorMessage.unableToMakeCredentials=Unable to construct credentials object from credentials file, please set the location to be a valid JSON credentials file. GoogleTranslatorSettingsPanel.errorMessage.unableToReadCredentials=Unable to read credentials from credentials file, please set the location to be a valid JSON credentials file. GoogleTranslatorSettingsPanel.errorMessage.unknownFailureGetting=Failure getting list of supported languages with current credentials file. @@ -11,3 +15,14 @@ GoogleTranslatorSettingsPanel.fileChooser.confirmButton=Select GoogleTranslatorSettingsPanel.json.description=JSON Files GoogleTranslatorSettingsPanel.warningLabel.text= GoogleTranslatorSettingsPanel.targetLanguageLabel.text=Target Language: +BingTranslatorSettingsPanel.testButton.text=Test +BingTranslatorSettingsPanel.testResultValueLabel.text= +BingTranslatorSettingsPanel.resultLabel.text=Result: +BingTranslatorSettingsPanel.untranslatedLabel.text=Untranslated: +BingTranslatorSettingsPanel.targetLanguageLabel.text=Target Language: +BingTranslatorSettingsPanel.authenticationKeyField.toolTipText=Enter the hash for the +GoogleTranslatorSettingsPanel.testButton.text=Test +GoogleTranslatorSettingsPanel.untranslatedLabel.text=Untranslated: +GoogleTranslatorSettingsPanel.resultLabel.text=Result: +GoogleTranslatorSettingsPanel.testResultValueLabel.text= +BingTranslatorSettingsPanel.authenticationKeyLabel.text=Authentication Key: diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslator.java b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslator.java index 38506ef936..8289010828 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslator.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslator.java @@ -28,10 +28,14 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.commons.lang3.StringUtils; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.coreutils.EscapeUtil; import org.sleuthkit.autopsy.texttranslation.TextTranslator; import org.sleuthkit.autopsy.texttranslation.TranslationException; @@ -43,7 +47,8 @@ import org.sleuthkit.autopsy.texttranslation.TranslationException; public final class GoogleTranslator implements TextTranslator { private static final Logger logger = Logger.getLogger(GoogleTranslator.class.getName()); - private static final int MAX_STRING_LENGTH = 15000; + //See translate method for justification of this limit. + private static final int MAX_PAYLOAD_SIZE = 5000; private final GoogleTranslatorSettingsPanel settingsPanel; private final GoogleTranslatorSettings settings = new GoogleTranslatorSettings(); private Translate googleTranslate; @@ -57,26 +62,49 @@ public final class GoogleTranslator implements TextTranslator { loadTranslator(); } + /** + * Check if google is able to be reached + * + * @return true if it can be, false otherwise + */ + private static boolean googleIsReachable() { + String host = "www.google.com"; + InetAddress address; + try { + address = InetAddress.getByName(host); + return address.isReachable(1500); + } catch (UnknownHostException ex) { + logger.log(Level.WARNING, "Unable to reach google.com due to unknown host", ex); + return false; + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to reach google.com due IOException", ex); + return false; + } + } + @Override public String translate(String string) throws TranslationException { + if (!googleIsReachable()) { + throw new TranslationException("Failure translating using GoogleTranslator: Cannot connect to Google"); + } + if (googleTranslate != null) { try { // Translates some text into English, without specifying the source language. - - // HTML files were producing lots of white space at the end String substring = string.trim(); - // WE can't currently set parameters, so we are using the default behavior of - // asuming the input is HTML. We need to replace newlines with
for Google to preserve them + // We can't currently set parameters, so we are using the default behavior of + // assuming the input is HTML. We need to replace newlines with
for Google to preserve them substring = substring.replaceAll("(\r\n|\n)", "
"); - // The API complains if the "Payload" is over 204800 bytes. I'm assuming that - // deals with the full request. At some point, we get different errors about too - // much text. Officially, Google says they will googleTranslate only 5k chars, - // but we have seen more than that working. - // there could be a value betwen 15k and 25k that works. I (BC) didn't test further - if (substring.length() > MAX_STRING_LENGTH) { - substring = substring.substring(0, MAX_STRING_LENGTH); + // The API complains if the "Payload" is over 204800 bytes. Google references that + //their service is optimized for 2K code points and recommends keeping the requests that size. + //There is a hard limit of 30K code points per request. There is also a time-based quota that + //we are not enforcing, which may lead to 403 errors. We are currently configured for a max of 5K + //in each request, for two reasons. 1) To be more in line with Google's recommendation. 2) To + //minimize accidental exceedence of time based quotas. + if (substring.length() > MAX_PAYLOAD_SIZE) { + substring = substring.substring(0, MAX_PAYLOAD_SIZE); } Translation translation = googleTranslate.translate(substring); @@ -84,8 +112,12 @@ public final class GoogleTranslator implements TextTranslator { // put back the newlines translatedString = translatedString.replaceAll("
", "\n"); + + // With our current settings, Google Translate outputs HTML + // so we need to undo the escape characters. + translatedString = EscapeUtil.unEscapeHtml(translatedString); return translatedString; - } catch (Throwable ex) { + } catch (Throwable ex) { //Catching throwables because some of this Google Translate code throws throwables throw new TranslationException("Failure translating using GoogleTranslator", ex); } @@ -106,16 +138,20 @@ public final class GoogleTranslator implements TextTranslator { } /** - * Load the Google Cloud Translation service give the currently saved + * Load the Google Cloud Translation service given the currently saved * settings */ private void loadTranslator() { InputStream credentialStream = null; Credentials creds = null; - try { - credentialStream = new FileInputStream(settings.getCredentialPath()); - } catch (FileNotFoundException ex) { - logger.log(Level.WARNING, "JSON file for GoogleTranslator credentials not found", ex); + if (StringUtils.isBlank(settings.getCredentialPath())) { + logger.log(Level.INFO, "No credentials file has been provided for Google Translator"); + } else { + try { + credentialStream = new FileInputStream(settings.getCredentialPath()); + } catch (FileNotFoundException ex) { + logger.log(Level.WARNING, "JSON file for GoogleTranslator credentials not found", ex); + } } if (credentialStream != null) { try { @@ -142,4 +178,9 @@ public final class GoogleTranslator implements TextTranslator { settings.saveSettings(); loadTranslator(); } + + @Override + public int getMaxPayloadSize() { + return MAX_PAYLOAD_SIZE; + } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettings.java b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettings.java index 55771b8c7a..34ebfd67d4 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettings.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettings.java @@ -92,12 +92,14 @@ public final class GoogleTranslatorSettings { } if (ModuleSettings.settingExists(GOOGLE_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY)) { targetLanguageCode = ModuleSettings.getConfigSetting(GOOGLE_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY); - } else { + } + if (targetLanguageCode == null || StringUtils.isBlank(targetLanguageCode)) { targetLanguageCode = DEFAULT_TARGET_LANGUAGE; } if (ModuleSettings.settingExists(GOOGLE_TRANSLATE_NAME, CREDENTIAL_PATH_KEY)) { credentialPath = ModuleSettings.getConfigSetting(GOOGLE_TRANSLATE_NAME, CREDENTIAL_PATH_KEY); - } else { + } + if (credentialPath == null) { credentialPath = DEFAULT_CREDENTIAL_PATH; } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettingsPanel.form index 97a10b500f..b55a40b30f 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettingsPanel.form @@ -16,13 +16,9 @@ - + - - - - @@ -31,7 +27,7 @@ - + @@ -42,6 +38,18 @@ + + + + + + + + + + + + @@ -60,9 +68,16 @@ - + + + + + + + + + - @@ -98,7 +113,7 @@ - + @@ -118,5 +133,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettingsPanel.java index fbb957413b..84fad9e8e1 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslatorSettingsPanel.java @@ -21,7 +21,9 @@ package org.sleuthkit.autopsy.texttranslation.translators; import com.google.auth.Credentials; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.translate.Language; +import com.google.cloud.translate.Translate; import com.google.cloud.translate.TranslateOptions; +import com.google.cloud.translate.Translation; import java.awt.event.ItemListener; import java.io.File; import java.io.FileInputStream; @@ -44,6 +46,7 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { private static final Logger logger = Logger.getLogger(GoogleTranslatorSettingsPanel.class.getName()); private static final String JSON_EXTENSION = "json"; + private static final String DEFUALT_TEST_STRING = "traducción exitoso"; //spanish which should translate to something along the lines of "successful translation" private static final long serialVersionUID = 1L; private final ItemListener listener = new ComboBoxSelectionListener(); private String targetLanguageCode = ""; @@ -60,16 +63,15 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { /** * Private method to make a temporary translation service given the current - * settings and use it to retrieve the available target languages for - * population of combobox with target language with unsaved settings. + * settings with unsaved settings. * - * @return A list of Languages + * @return A Translate object which is the translation service */ @Messages({"GoogleTranslatorSettingsPanel.errorMessage.fileNotFound=Credentials file not found, please set the location to be a valid JSON credentials file.", "GoogleTranslatorSettingsPanel.errorMessage.unableToReadCredentials=Unable to read credentials from credentials file, please set the location to be a valid JSON credentials file.", "GoogleTranslatorSettingsPanel.errorMessage.unableToMakeCredentials=Unable to construct credentials object from credentials file, please set the location to be a valid JSON credentials file.", "GoogleTranslatorSettingsPanel.errorMessage.unknownFailureGetting=Failure getting list of supported languages with current credentials file.",}) - private List getListOfTargetLanguages() { + private Translate getTemporaryTranslationService() { //This method also has the side effect of more or less validating the JSON file which was selected as it is necessary to get the list of target languages try { InputStream credentialStream; @@ -77,31 +79,31 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { credentialStream = new FileInputStream(credentialsPathField.getText()); } catch (FileNotFoundException ignored) { warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_fileNotFound()); - return new ArrayList<>(); + return null; } Credentials creds; try { creds = ServiceAccountCredentials.fromStream(credentialStream); } catch (IOException ignored) { warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_unableToMakeCredentials()); - return new ArrayList<>(); + return null; } if (creds == null) { warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_unableToReadCredentials()); logger.log(Level.WARNING, "Credentials were not successfully made, no translations will be available from the GoogleTranslator"); - return new ArrayList<>(); + return null; } else { TranslateOptions.Builder builder = TranslateOptions.newBuilder(); builder.setCredentials(creds); builder.setTargetLanguage(targetLanguageCode); //localize the list to the currently selected target language warningLabel.setText(""); //clear any previous warning text - return builder.build().getService().listSupportedLanguages(); + return builder.build().getService(); } } catch (Throwable throwable) { //Catching throwables because some of this Google Translate code throws throwables warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_unknownFailureGetting()); logger.log(Level.WARNING, "Throwable caught while getting list of supported languages", throwable); - return new ArrayList<>(); + return null; } } @@ -114,29 +116,51 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { targetLanguageComboBox.removeItemListener(listener); try { if (!StringUtils.isBlank(credentialsPathField.getText())) { - List listSupportedLanguages = getListOfTargetLanguages(); + List listSupportedLanguages; + Translate tempService = getTemporaryTranslationService(); + if (tempService != null) { + listSupportedLanguages = tempService.listSupportedLanguages(); + } else { + listSupportedLanguages = new ArrayList<>(); + } targetLanguageComboBox.removeAllItems(); if (!listSupportedLanguages.isEmpty()) { listSupportedLanguages.forEach((lang) -> { - targetLanguageComboBox.addItem(new GoogleLanguageWrapper(lang)); + targetLanguageComboBox.addItem(new LanguageWrapper(lang)); }); selectLanguageByCode(targetLanguageCode); targetLanguageComboBox.addItemListener(listener); - targetLanguageComboBox.setEnabled(true); + enableControls(true); + } else { - targetLanguageComboBox.setEnabled(false); + enableControls(false); } } else { warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_noFileSelected()); - targetLanguageComboBox.setEnabled(false); + enableControls(false); } } catch (Throwable throwable) { warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_unknownFailurePopulating()); logger.log(Level.WARNING, "Throwable caught while populating list of supported languages", throwable); - targetLanguageComboBox.setEnabled(false); + enableControls(false); } } + /** + * Helper method to enable/disable all controls which are dependent on valid + * credentials having been provided + * + * @param enabled true to enable the controls, false to disable them + */ + private void enableControls(boolean enabled) { + targetLanguageComboBox.setEnabled(enabled); + testButton.setEnabled(enabled); + testResultValueLabel.setEnabled(enabled); + testUntranslatedTextField.setEnabled(enabled); + untranslatedLabel.setEnabled(enabled); + resultLabel.setEnabled(enabled); + } + /** * Given a language code select the corresponding language in the combo box * if it is present @@ -145,7 +169,7 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { */ private void selectLanguageByCode(String code) { for (int i = 0; i < targetLanguageComboBox.getModel().getSize(); i++) { - if (targetLanguageComboBox.getItemAt(i).getLanguage().getCode().equals(code)) { + if (targetLanguageComboBox.getItemAt(i).getLanguageCode().equals(code)) { targetLanguageComboBox.setSelectedIndex(i); return; } @@ -167,6 +191,11 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { targetLanguageComboBox = new javax.swing.JComboBox<>(); targetLanguageLabel = new javax.swing.JLabel(); warningLabel = new javax.swing.JLabel(); + testResultValueLabel = new javax.swing.JLabel(); + resultLabel = new javax.swing.JLabel(); + untranslatedLabel = new javax.swing.JLabel(); + testUntranslatedTextField = new javax.swing.JTextField(); + testButton = new javax.swing.JButton(); org.openide.awt.Mnemonics.setLocalizedText(credentialsLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.credentialsLabel.text")); // NOI18N @@ -186,6 +215,25 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { warningLabel.setForeground(new java.awt.Color(255, 0, 0)); org.openide.awt.Mnemonics.setLocalizedText(warningLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.warningLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(testResultValueLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.testResultValueLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(resultLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.resultLabel.text")); // NOI18N + resultLabel.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(untranslatedLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.untranslatedLabel.text")); // NOI18N + untranslatedLabel.setEnabled(false); + + testUntranslatedTextField.setText(DEFUALT_TEST_STRING); + testUntranslatedTextField.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(testButton, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.testButton.text")); // NOI18N + testButton.setEnabled(false); + testButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + testButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -193,9 +241,6 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 551, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, Short.MAX_VALUE)) .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) .addComponent(credentialsLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) @@ -203,13 +248,24 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addComponent(credentialsPathField, javax.swing.GroupLayout.DEFAULT_SIZE, 443, Short.MAX_VALUE) + .addComponent(credentialsPathField, javax.swing.GroupLayout.DEFAULT_SIZE, 451, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(browseButton) .addGap(14, 14, 14)) .addGroup(layout.createSequentialGroup() .addComponent(targetLanguageComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 317, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, Short.MAX_VALUE)))))) + .addGap(0, 0, Short.MAX_VALUE)))) + .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 551, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createSequentialGroup() + .addComponent(testButton, javax.swing.GroupLayout.PREFERRED_SIZE, 83, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(untranslatedLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(testUntranslatedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 140, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(resultLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(testResultValueLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -223,9 +279,15 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(targetLanguageLabel) .addComponent(targetLanguageComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(23, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 15, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(testButton) + .addComponent(testUntranslatedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(untranslatedLabel) + .addComponent(resultLabel) + .addComponent(testResultValueLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE)) ); }// //GEN-END:initComponents @@ -249,12 +311,33 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { } }//GEN-LAST:event_browseButtonActionPerformed + @Messages({"GoogleTranslatorSettingsPanel.errorMessage.translationFailure=Translation failure with specified credentials"}) + private void testButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_testButtonActionPerformed + testResultValueLabel.setText(""); + Translate tempTranslate = getTemporaryTranslationService(); + if (tempTranslate != null) { + try { + Translation translation = tempTranslate.translate(testUntranslatedTextField.getText()); + testResultValueLabel.setText(translation.getTranslatedText()); + warningLabel.setText(""); + } catch (Exception ex) { + warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_translationFailure()); + logger.log(Level.WARNING, Bundle.GoogleTranslatorSettingsPanel_errorMessage_translationFailure(), ex); + } + } + }//GEN-LAST:event_testButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton browseButton; private javax.swing.JLabel credentialsLabel; private javax.swing.JTextField credentialsPathField; - private javax.swing.JComboBox targetLanguageComboBox; + private javax.swing.JLabel resultLabel; + private javax.swing.JComboBox targetLanguageComboBox; private javax.swing.JLabel targetLanguageLabel; + private javax.swing.JButton testButton; + private javax.swing.JLabel testResultValueLabel; + private javax.swing.JTextField testUntranslatedTextField; + private javax.swing.JLabel untranslatedLabel; private javax.swing.JLabel warningLabel; // End of variables declaration//GEN-END:variables @@ -284,7 +367,7 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel { @Override public void itemStateChanged(java.awt.event.ItemEvent evt) { - String selectedCode = ((GoogleLanguageWrapper) targetLanguageComboBox.getSelectedItem()).getLanguage().getCode(); + String selectedCode = ((LanguageWrapper) targetLanguageComboBox.getSelectedItem()).getLanguageCode(); if (!StringUtils.isBlank(selectedCode) && !selectedCode.equals(targetLanguageCode)) { targetLanguageCode = selectedCode; populateTargetLanguageComboBox(); diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleLanguageWrapper.java b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/LanguageWrapper.java similarity index 51% rename from Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleLanguageWrapper.java rename to Core/src/org/sleuthkit/autopsy/texttranslation/translators/LanguageWrapper.java index c61ed1c948..2fa308291b 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleLanguageWrapper.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/LanguageWrapper.java @@ -21,19 +21,33 @@ package org.sleuthkit.autopsy.texttranslation.translators; import com.google.cloud.translate.Language; /** - * Wrapper for the Language class + * Wrapper for Language definitions used by translators */ -class GoogleLanguageWrapper { +class LanguageWrapper { - private final Language language; + private final String languageCode; + private final String languageDisplayName; /** - * Create a new GoogleLanguageWrapper + * Create a new LanguageWrapper to wrap the google language object * * @param lang the Language object to wrap */ - GoogleLanguageWrapper(Language lang) { - language = lang; + LanguageWrapper(Language language) { + languageCode = language.getCode(); + languageDisplayName = language.getName(); + } + + /** + * Create a new LanguageWrapper to wrap json elements that identify a + * language for microsofts translation service + * + * @param code the code which uniquely identifies a language + * @param name the name of the language + */ + LanguageWrapper(String code, String name) { + languageCode = code; + languageDisplayName = name; } /** @@ -41,14 +55,14 @@ class GoogleLanguageWrapper { * * @return the wrapped Language */ - Language getLanguage() { - return language; + String getLanguageCode() { + return languageCode; } @Override public String toString() { - //toString overridden so that the jComboBox in the GoogleTranslatorSettingsPanel will display the name of the language - return language.getName(); + //toString overridden so that the jComboBox in the TranslatorSettingsPanels will display the name of the language + return languageDisplayName; } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties index 08c2523b0f..0298ef77ae 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties @@ -1,8 +1,8 @@ OptionsCategory_Name_Machine_Translation=Machine Translation OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings TranslationContentPanel.ShowLabel.text=Show: -TranslationContentPanel.warningLabel2MB.text=Only the first 1MB of text will be displayed TranslationContentPanel.ocrLabel.text=OCR: TranslationOptionsPanel.translationServiceLabel.text=Text translator: TranslationOptionsPanelController.moduleErr=Module Error TranslationOptionsPanelController.moduleErr.msg=A module caused an error listening to TranslationSettingsPanelController updates. See log to determine which module. Some data could be incomplete. +TranslationContentPanel.warningLabel.text= diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED index 0304d7b1f3..dfaa8b9d0d 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED @@ -1,6 +1,6 @@ OptionsCategory_Name_Machine_Translation=Machine Translation OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings -TranslatedContentPanel.comboBoxOption.originalText=Original Text +TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB) TranslatedContentPanel.comboBoxOption.translatedText=Translated Text TranslatedContentViewer.emptyTranslation=The resulting translation was empty. TranslatedContentViewer.errorExtractingText=Could not extract text from file. @@ -12,11 +12,11 @@ TranslatedContentViewer.noServiceProvider=Machine Translation software was not f TranslatedContentViewer.textAlreadyIndexed=Please view the original text in the Indexed Text viewer. TranslatedContentViewer.translatingText=Translating text, please wait... TranslatedContentViewer.translationException=Error encountered while attempting translation. +TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated TranslatedTextViewer.title=Translation TranslatedTextViewer.toolTip=Displays translated file text. TranslationContentPanel.autoDetectOCR=Autodetect language TranslationContentPanel.ShowLabel.text=Show: -TranslationContentPanel.warningLabel2MB.text=Only the first 1MB of text will be displayed TranslationContentPanel.ocrLabel.text=OCR: TranslationOptionsPanel.noTextTranslators.text=No text translators exist, translation is disabled. TranslationOptionsPanel.noTextTranslatorSelected.text=No text translator selected, translation is disabled. @@ -25,3 +25,4 @@ TranslationOptionsPanel.translationDisabled.text=Translation disabled TranslationOptionsPanel.translationServiceLabel.text=Text translator: TranslationOptionsPanelController.moduleErr=Module Error TranslationOptionsPanelController.moduleErr.msg=A module caused an error listening to TranslationSettingsPanelController updates. See log to determine which module. Some data could be incomplete. +TranslationContentPanel.warningLabel.text= diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index e9656bb0a9..b111f431d9 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -67,7 +67,7 @@ public final class TranslatedTextViewer implements TextViewer { private static final boolean OCR_ENABLED = true; private static final boolean OCR_DISABLED = false; - private static final int MAX_SIZE_1MB = 1024000; + private static final int MAX_EXTRACT_SIZE_BYTES = 25600; private static final List INSTALLED_LANGUAGE_PACKS = PlatformUtil.getOcrLanguagePacks(); private final TranslationContentPanel panel = new TranslationContentPanel(); @@ -77,6 +77,9 @@ public final class TranslatedTextViewer implements TextViewer { = new ThreadFactoryBuilder().setNameFormat("translation-content-viewer-%d").build(); private final ExecutorService executorService = Executors.newSingleThreadExecutor(translationThreadFactory); + @NbBundle.Messages({ + "TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated" + }) @Override public void setNode(final Node node) { this.node = node; @@ -92,6 +95,9 @@ public final class TranslatedTextViewer implements TextViewer { panel.addLanguagePackNames(INSTALLED_LANGUAGE_PACKS); } } + + int payloadMaxInKB = TextTranslationService.getInstance().getMaxPayloadSize() / 1000; + panel.setWarningLabelMsg(String.format(Bundle.TranslatedTextViewer_maxPayloadSize(), payloadMaxInKB)); //Force a background task. displayDropDownListener.actionPerformed(null); @@ -296,8 +302,9 @@ public final class TranslatedTextViewer implements TextViewer { //Correct for UTF-8 byte[] resultInUTF8Bytes = result.getBytes("UTF8"); - byte[] trimTo1MB = Arrays.copyOfRange(resultInUTF8Bytes, 0, MAX_SIZE_1MB ); - return new String(trimTo1MB, "UTF-8"); + byte[] trimToArraySize = Arrays.copyOfRange(resultInUTF8Bytes, 0, + Math.min(resultInUTF8Bytes.length, MAX_EXTRACT_SIZE_BYTES) ); + return new String(trimToArraySize, "UTF-8"); } /** @@ -331,7 +338,7 @@ public final class TranslatedTextViewer implements TextViewer { //Short-circuit the read if its greater than our max //translatable size - int bytesLeft = MAX_SIZE_1MB - bytesRead; + int bytesLeft = MAX_EXTRACT_SIZE_BYTES - bytesRead; if (bytesLeft < read) { textBuilder.append(cbuf, 0, bytesLeft); @@ -341,9 +348,8 @@ public final class TranslatedTextViewer implements TextViewer { textBuilder.append(cbuf, 0, read); bytesRead += read; } - - // The trim is on here because HTML files were observed with nearly 1MB of white space at the end - return textBuilder.toString().trim(); + + return textBuilder.toString(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.form b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.form index e042876476..a46e1ee1e3 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.form +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.form @@ -16,7 +16,7 @@ - + @@ -48,12 +48,12 @@ - - + + - - - + + + @@ -72,18 +72,25 @@
- - - - - - - - + + + + + + + + + + + + + + + + + - - @@ -153,13 +160,13 @@ - + - + diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.java index 0e625f1baa..90125371f9 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.java @@ -87,6 +87,10 @@ public class TranslationContentPanel extends javax.swing.JPanel { dropDown.removeActionListener(listener); } } + + void setWarningLabelMsg(String msg) { + warningLabel.setText(msg); + } @NbBundle.Messages({"TranslationContentPanel.autoDetectOCR=Autodetect language"}) final void reset() { @@ -246,7 +250,7 @@ public class TranslationContentPanel extends javax.swing.JPanel { /** * Selection choices to be displayed in the combobox dropdown. */ - @Messages({"TranslatedContentPanel.comboBoxOption.originalText=Original Text", + @Messages({"TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB)", "TranslatedContentPanel.comboBoxOption.translatedText=Translated Text"}) static enum DisplayDropdownOptions { ORIGINAL_TEXT(Bundle.TranslatedContentPanel_comboBoxOption_originalText()), @@ -280,7 +284,7 @@ public class TranslationContentPanel extends javax.swing.JPanel { displayTextComboBox = new javax.swing.JComboBox<>(); ocrDropdown = new javax.swing.JComboBox<>(); ocrLabel = new javax.swing.JLabel(); - warningLabel2MB = new javax.swing.JLabel(); + warningLabel = new javax.swing.JLabel(); jScrollPane1 = new javax.swing.JScrollPane(); displayTextArea = new javax.swing.JTextArea(); @@ -302,8 +306,8 @@ public class TranslationContentPanel extends javax.swing.JPanel { org.openide.awt.Mnemonics.setLocalizedText(ocrLabel, org.openide.util.NbBundle.getMessage(TranslationContentPanel.class, "TranslationContentPanel.ocrLabel.text")); // NOI18N ocrLabel.setEnabled(false); - warningLabel2MB.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/warning16.png"))); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(warningLabel2MB, org.openide.util.NbBundle.getMessage(TranslationContentPanel.class, "TranslationContentPanel.warningLabel2MB.text")); // NOI18N + warningLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/warning16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(warningLabel, org.openide.util.NbBundle.getMessage(TranslationContentPanel.class, "TranslationContentPanel.warningLabel.text")); // NOI18N javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); jPanel1.setLayout(jPanel1Layout); @@ -311,12 +315,12 @@ public class TranslationContentPanel extends javax.swing.JPanel { jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() .addContainerGap() - .addComponent(warningLabel2MB) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(warningLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 422, Short.MAX_VALUE) .addComponent(ShowLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(displayTextComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 128, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(displayTextComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 150, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(ocrLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 26, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(ocrDropdown, javax.swing.GroupLayout.PREFERRED_SIZE, 180, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -331,16 +335,20 @@ public class TranslationContentPanel extends javax.swing.JPanel { ); jPanel1Layout.setVerticalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createSequentialGroup() .addGap(9, 9, 9) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(displayTextComboBox, javax.swing.GroupLayout.DEFAULT_SIZE, 26, Short.MAX_VALUE) - .addComponent(ShowLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(ocrLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(warningLabel2MB)) - .addComponent(ocrDropdown)) - .addGap(7, 7, 7)) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(warningLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addContainerGap()) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(displayTextComboBox, javax.swing.GroupLayout.DEFAULT_SIZE, 26, Short.MAX_VALUE) + .addComponent(ShowLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(ocrLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(ocrDropdown)) + .addGap(7, 7, 7)))) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() .addContainerGap() @@ -362,7 +370,7 @@ public class TranslationContentPanel extends javax.swing.JPanel { this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jPanel1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 628, Short.MAX_VALUE) + .addComponent(jPanel1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 872, Short.MAX_VALUE) .addComponent(jScrollPane1) ); layout.setVerticalGroup( @@ -384,6 +392,6 @@ public class TranslationContentPanel extends javax.swing.JPanel { private javax.swing.JSeparator jSeparator2; private javax.swing.JComboBox ocrDropdown; private javax.swing.JLabel ocrLabel; - private javax.swing.JLabel warningLabel2MB; + private javax.swing.JLabel warningLabel; // End of variables declaration//GEN-END:variables } diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorTest.java new file mode 100644 index 0000000000..25c78ba049 --- /dev/null +++ b/Core/test/unit/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslatorTest.java @@ -0,0 +1,92 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.texttranslation.translators; + +//import java.io.IOException; +//import org.junit.After; +//import org.junit.AfterClass; +//import org.junit.Before; +//import org.junit.BeforeClass; +import org.junit.Test; +//import static org.junit.Assert.*; + +/** + * Tests for the BingTranslator translation service, these tests have been + * commented out because they require credentials to perform + */ +public class BingTranslatorTest { + + @Test + public void testTranslate() { +// BingTranslator translator = new BingTranslator(); +// String input = "gato"; +// String expectedTranslation = "cat"; +// runTest(translator, input, expectedTranslation); + } + +// @Test +// public void testQuickStartSentence() throws Exception { +// BingTranslator translator = new BingTranslator(); +// String input = "Willkommen bei Microsoft Translator. Raten Sie mal, wie viele Sprachen ich spreche."; +// String expectedTranslation = "Welcome to Microsoft Translator. Guess how many languages I speak."; +// runTest(translator, input, expectedTranslation); +// } +// +// @Test +// public void testCharacterEscapes() throws Exception { +// BingTranslator translator = new BingTranslator(); +// String input = "\"gato\"";; +// String expectedTranslation = "Cat"; +// runTest(translator, input, expectedTranslation); +// } +// +// @Test +// public void testLineBreaks() throws Exception { +// BingTranslator translator = new BingTranslator(); +// String input = "gato\nperro";; +// String expectedTranslation = "cat\nDog"; +// runTest(translator, input, expectedTranslation); +// } +// +// /** +// * Test whether translator throws an error. This should not be part of our +// * regular testing, because we are limited to only 2MB of free translations +// * ever. +// * @param translator A BingTranslator +// * @param input Text to translate +// * @param expectedTranslation Not used unless you uncomment those lines. +// */ +// public void runTest(BingTranslator translator, String input, String expectedTranslation) { +// String translation; +// try { +// translation = translator.translate(input); +// } +// catch (Throwable e) { +// fail("Bing translation produced an exception: " + e.getMessage()); +// return; +// }; +// +// /* +// //It's unrealistic to expect the same answer every time, but sometimes +// //it's helpful to have this in your debug process. +// System.out.println(translation); +// assertEquals(expectedTranslation, translation); +// */ +// } +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index 52295254c8..0e88900aee 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -698,7 +698,7 @@ public final class ImageGalleryController { //grab files with supported mime-types + MIMETYPE_CLAUSE //NON-NLS //grab files with image or video mime-types even if we don't officially support them - + " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )" //NON-NLS + + " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )" //NON-NLS + " ORDER BY parent_path "; } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java index 1c68d6c49d..a320590822 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2018 Basis Technology Corp. + * Copyright 2013-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AdHocSearchFilterNode.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AdHocSearchFilterNode.java index 57b84b11d6..cff13cc16e 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AdHocSearchFilterNode.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AdHocSearchFilterNode.java @@ -32,6 +32,7 @@ import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.openide.util.lookup.ProxyLookup; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.actions.AddContentTagAction; @@ -163,6 +164,7 @@ class AdHocSearchFilterNode extends FilterNode { actionsList.add(new ExternalViewerAction(NbBundle.getMessage(this.getClass(), "KeywordSearchFilterNode.getFileActions.openExternViewActLbl"), getOriginal())); actionsList.add(null); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/docs/doxygen-user/images/InterestingFiles/bomb_png.png b/docs/doxygen-user/images/InterestingFiles/bomb_png.png new file mode 100644 index 0000000000..016ff5dea5 Binary files /dev/null and b/docs/doxygen-user/images/InterestingFiles/bomb_png.png differ diff --git a/docs/doxygen-user/images/InterestingFiles/download_archive.png b/docs/doxygen-user/images/InterestingFiles/download_archive.png new file mode 100644 index 0000000000..aa457e929c Binary files /dev/null and b/docs/doxygen-user/images/InterestingFiles/download_archive.png differ diff --git a/docs/doxygen-user/images/InterestingFiles/ingest.png b/docs/doxygen-user/images/InterestingFiles/ingest.png new file mode 100644 index 0000000000..aadf558768 Binary files /dev/null and b/docs/doxygen-user/images/InterestingFiles/ingest.png differ diff --git a/docs/doxygen-user/images/InterestingFiles/main.png b/docs/doxygen-user/images/InterestingFiles/main.png new file mode 100644 index 0000000000..67a27cdb7e Binary files /dev/null and b/docs/doxygen-user/images/InterestingFiles/main.png differ diff --git a/docs/doxygen-user/images/InterestingFiles/new_large_files.png b/docs/doxygen-user/images/InterestingFiles/new_large_files.png new file mode 100644 index 0000000000..a159f5af1f Binary files /dev/null and b/docs/doxygen-user/images/InterestingFiles/new_large_files.png differ diff --git a/docs/doxygen-user/images/InterestingFiles/new_rule.png b/docs/doxygen-user/images/InterestingFiles/new_rule.png new file mode 100644 index 0000000000..3385a402c0 Binary files /dev/null and b/docs/doxygen-user/images/InterestingFiles/new_rule.png differ diff --git a/docs/doxygen-user/images/InterestingFiles/new_rule_set.png b/docs/doxygen-user/images/InterestingFiles/new_rule_set.png new file mode 100644 index 0000000000..eb857b4734 Binary files /dev/null and b/docs/doxygen-user/images/InterestingFiles/new_rule_set.png differ diff --git a/docs/doxygen-user/images/InterestingFiles/private_folder.png b/docs/doxygen-user/images/InterestingFiles/private_folder.png new file mode 100644 index 0000000000..706bbeca57 Binary files /dev/null and b/docs/doxygen-user/images/InterestingFiles/private_folder.png differ diff --git a/docs/doxygen-user/images/InterestingFiles/results.png b/docs/doxygen-user/images/InterestingFiles/results.png new file mode 100644 index 0000000000..9ce29ceaea Binary files /dev/null and b/docs/doxygen-user/images/InterestingFiles/results.png differ diff --git a/docs/doxygen-user/images/interesting_files_configuration.PNG b/docs/doxygen-user/images/interesting_files_configuration.PNG deleted file mode 100644 index 2c37a4f27b..0000000000 Binary files a/docs/doxygen-user/images/interesting_files_configuration.PNG and /dev/null differ diff --git a/docs/doxygen-user/images/interesting_files_ingest_settings.PNG b/docs/doxygen-user/images/interesting_files_ingest_settings.PNG deleted file mode 100644 index 854a495862..0000000000 Binary files a/docs/doxygen-user/images/interesting_files_ingest_settings.PNG and /dev/null differ diff --git a/docs/doxygen-user/images/interesting_files_results.PNG b/docs/doxygen-user/images/interesting_files_results.PNG deleted file mode 100644 index 26dd994cd9..0000000000 Binary files a/docs/doxygen-user/images/interesting_files_results.PNG and /dev/null differ diff --git a/docs/doxygen-user/interesting_files.dox b/docs/doxygen-user/interesting_files.dox index 67041193ba..080b1072af 100644 --- a/docs/doxygen-user/interesting_files.dox +++ b/docs/doxygen-user/interesting_files.dox @@ -1,81 +1,114 @@ /*! \page interesting_files_identifier_page Interesting Files Identifier Module -What Does It Do -======== +\section interesting_files_overview Overview -The Interesting Files module allows you to search for files or directories in a data source and generate alerts when they are found. You configure rules for the files that you want to find. +The Interesting Files module allows you to automatically flag files and directories that match a set of rules. This can be useful if you always need to check whether files with a given name or path are in the data source, or if you are always interested in files with a certain type. -Use this to be notified when certain things are found. There are examples below that generate alerts when VMWare images are found or when iPhone backup files are found. This module is useful for file types that will frequently have a consistent name and that may not be part of the standard checklist that you look for, or if you simply want to automate your checklist. +This module allows you to make sets of rules that will be run against each file as it is processed. If a file matches any of the rules, you will see an entry for it in the \ref tree_viewer_page. You can share your rules with other users, and import sets made by others into your copy of Autopsy. -Configuration -======= +\section interesting_files_terminology Terminology -Add rules using "Tools", "Options", "Interesting Files". +
    +
  • A rule is a set of conditions that must be true about a file for it to match the rule. All conditions in the rule must be true. For example, if a rule has conditions "file size > 1 MB" and "file extension = .txt", only files that match both conditions will be considered a match. +
  • A rule set is a collection of rules. If a file matches any rule in the rule set it will be flagged as a match for this rule set. Rule sets can be enabled and disabled at ingest time. +
-All rules need to be part of a set. Select "New set" on the left side panel to create a new set. Sets need to have the following defined: +\section interesting_files_config Configuration -- Set Name (required) -- Set Description (optional) +To create and edit your rule sets, go to "Tools", "Options" and then select the "Interesting Files" tab. The area on the left side will show you a list of all the rule sets that are currently available. Selecting a rule set will display its description and information about each of its rules on the right side of the panel. -Sets can be renamed, edited, copied, and imported and exported from the left side panel. +\image html InterestingFiles/main.png -Rules specify what to look for in a data source. Each rule specifies: -- Type: If the rule should be applied to only files, only directories, or both files and directories. -- Name Pattern: String to match the file name against. Note that you can enter multiple extensions in a comma-separated list. -- Name Pattern Type: Should the pattern be matched against the full file type or just the extension. -- Path Pattern: A substring of the parent path that must be matched. This allows you to restrict generic names to a specific structure (such as an application name). A substring match is performed. -- Rule Name: Additional details that are displayed in the UI when that rule is matched. This allows you to determine which rule in the set matched. +The buttons on the bottom of the left side of the panel control the rule sets. -\image html interesting_files_configuration.PNG +
    +
  • New Set - Allows you to create a new rule set (rules will be added later). You will see a new window asking for the name of the new rule set, an optional description, and whether known files should be ignored (i.e., if a file is in the NSRL, then it won't show up on the list of matches even if it satisfies the conditions of one of the rules in the set). +\image html InterestingFiles/new_rule_set.png +
  • Edit Set - Brings up the same window as "New Set" and allows you to change any of the fields. +
  • Delete Set - Removes the selected rule set +
  • Copy Set - Makes a copy of the selected rule set. It will bring up the same window as "New Set". You must change the rule set name in order to save the copy. +
  • Import Set - Imports a previously exported rule set. Once imported, you will not need the original copy. +
  • Export Set - Exports the selected rule set in a format that can be shared with other Autopsy users. +
+Selecting a rule set will display its description, whether it ignores known files, and the rules contained in the set. Selecting a rule will display the conditions for that rule in the "Rule Details" section. -VMWare Example --------- -This set of rules is to detect VMWare Player or vmdk files. This would help to make sure you look into the virtual machines for additional evidence. +The buttons under the list of rules allow you to create new rules and edit or delete existing rules. Selecting "New Rule" will bring up a new window to create the rule. -NOTE: This is not extensive and is simply a minimal example: +\image html InterestingFiles/new_rule.png +The top line allows you to choose whether you want to match only files, only directories, or both. If you select directories or both, some of the condition types will be unavailable since they only apply to files. -- Set Name: VMWare -- Rule 1: - - Type: Files - - Full Name: vmplayer.exe - - Name: Program EXE -- Rule 2: - - Type: Files - - Extension: vmdk - - Name: VMDK File +Each rule must have at least one condition. To create conditions, check the box to the left of the condition you want to enable. The following is a description of each condition, with some full examples after. -iPhone Backups Example -------------- -This set of rules is to detect a folder for iPhone Backups. These are typically in a folder such as "%AppData%\Roaming\Apple Computer\MobileSync\Backup" on Windows. Here is a rule that you could use for that. +
    +
  • Name - Enter either the full file name or one or more extensions, and select whether this is an exact match or a substring/regex match. If substring/regex match is enabled, it will automatically add wildcards to the beginning and end of the text. If you're only matching directories, this will match the directory name. If you're using a comma-separated list of extensions, make sure the regex checkbox is disabled - the two features do not work together. The following table shows some examples of what the different combinations can be used for. -- Set Name: iPhone Backups -- Rule 1: - - Type: Directory - - Name: Backup - - Path: Apple Computer/MobileSync + + + + + + + + +
    TypeSubstring/RegexTextDescriptionSample match
    Full Namefalse\verbatim test.txt \endverbatimWill match files named "test.txt"text.txt
    Full Nametrue\verbatim bomb \endverbatimWill match files with "bomb" anywhere their namePipe bomb.png
    Full Nametrue\verbatim virus.*\.exe \endverbatimWill match files with "virus" followed by ".exe" anywhere their namebad_virus.exe
    Extension Onlyfalse\verbatim zip \endverbatimWill match .zip filesmyArchive.zip
    Extension Onlyfalse\verbatim zip,rar,7z \endverbatimWill match .zip, .rar, and .7z filesanotherArchive.rar
    Extension Onlytrue\verbatim jp \endverbatimWill match .jpg, .jpeg files, and any others with "jp" in the extensionmyImage.jpg
    +
  • Path Substring - Enter a folder name that must be part of file's path for it to be a match. If you only want to specify that a word appears somewhere in the path, use the regex option. + + + + + +
    RegexTextDescriptionSample match
    false\verbatim Documents \endverbatimMatch any file that has a folder named "Documents" in its path/folder1/Documents/fileA.doc
    true\verbatim bomb \endverbatimMatch any file with "bomb" in the path/folder1/bomb making/file2.doc
    true\verbatim Users/.*/Downloads \endverbatimMatch any file with "Users" and "Downloads" in the pathC:/Users/user1/Downloads/myFile.txt
    -Using the Module -====== +
  • MIME Type - Use the pull-down list to select a MIME type. Only a single MIME type can be selected. -When you enable the Interesting Files module, you can choose what rule sets to enable. To add rules, use the "Advanced" button from the ingest module panel. +
  • File Size - Select whether you want to match files equal to, smaller than, or larger than a given size. -When files are found, they will be in the Interesting Files area of the tree. You should see the set and rule names with the match. +
  • Modified Within - Select how recently a file must have been modified to match the rule. +
+Finally you can optionally enter a name for the rule. This will be displayed in the UI for each match. -Ingest Settings ------- +\subsection interesting_files_examples Examples +Here are a few examples of rules being created. -When running the ingest modules, the user can choose which interesting file rules to enable . -
-\image html interesting_files_ingest_settings.PNG +This is a rule that matches any file with "bomb" in the name that also has an "image/png" MIME type. -Seeing Results ------- -The results show up in the tree under "Results", "Interesting Items". +\image html InterestingFiles/bomb_png.png -\image html interesting_files_results.PNG +This is a rule that matches folders named "Private". + +\image html InterestingFiles/private_folder.png + +This rule is looking for archives in the user download directory. It requires "Users" and "Downloads" in the file's path, and an extension of .zip, .rar, or .7z. + +\image html InterestingFiles/download_archive.png + +This is a rule that matches files with size at least 50MB that have been modified in the last week. + +\image html InterestingFiles/new_large_files.png + +\section interesting_files_running Running the Module + +At runtime, you can select which rule sets you would like to run on your data source. + +\image html InterestingFiles/ingest.png + +\section interesting_files_results Viewing Results + +Files that match any of the rules in the enabled rule sets will be shown in the Results section of the \ref tree_viewer_page under "Interesting Items" and then the name of the rule set that matched. Note that other modules besides Interesting Files put results in this section of the tree, so there may be more than just what matched your rule sets. Selecting the "Interesting Files" node under one of your rule sets will display all matching files in the \ref result_viewer_page. + +\image html InterestingFiles/results.png + +You can see which rule matched in the "Category" column. You can export some or all of the files for further analysis. To do this, first use the standard Windows file +selection methods to highlight the files you want to export in the \ref result_viewer_page : +
    +
  • Hold down Ctrl and click on each file you want to export +
  • Hold down Shift to select a range of files +
  • Click on any file in the Result Viewer and then hit Ctrl+A to select all the files +
+Once you have your desired files selected, right click and select “Extract Files” to save copies of them. */ diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java index 15d52b536e..7e5a321313 100755 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java @@ -143,8 +143,9 @@ final class VcardParser { * @throws NoCurrentCaseException If there is no open case. */ void parse(File vcardFile, AbstractFile abstractFile) throws IOException, NoCurrentCaseException { - VCard vcard = Ezvcard.parse(vcardFile).first(); - addContactArtifact(vcard, abstractFile); + for (VCard vcard: Ezvcard.parse(vcardFile).all()) { + addContactArtifact(vcard, abstractFile); + } }