diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddLogicalImageTask.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddLogicalImageTask.java new file mode 100644 index 0000000000..e4d2b89b17 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddLogicalImageTask.java @@ -0,0 +1,145 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-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.casemodule; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import org.apache.commons.io.FileUtils; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imagewriter.ImageWriterSettings; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * A runnable that + * - copy the logical image folder to a destination folder + * - add alert.txt and users.txt files to report + * - add an image data source to the case database. + */ +public class AddLogicalImageTask extends AddImageTask { + + private final static Logger logger = Logger.getLogger(AddLogicalImageTask.class.getName()); + private final static String ALERT_TXT = "alert.txt"; //NON-NLS + private final static String USERS_TXT = "users.txt"; //NON-NLS + private final File src; + private final File dest; + private final DataSourceProcessorCallback callback; + private final DataSourceProcessorProgressMonitor progressMonitor; + + public AddLogicalImageTask(String deviceId, String imagePath, int sectorSize, + String timeZone, boolean ignoreFatOrphanFiles, + String md5, String sha1, String sha256, + ImageWriterSettings imageWriterSettings, + File src, File dest, + DataSourceProcessorProgressMonitor progressMonitor, + DataSourceProcessorCallback callback + ) { + super(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, + md5, sha1, sha256, imageWriterSettings, progressMonitor, callback); + this.src = src; + this.dest = dest; + this.progressMonitor = progressMonitor; + this.callback = callback; + } + + /** + * Copy the src directory to dest. + * Add alert.txt and users.txt to the case report + * Adds the image to the case database. + */ + @Messages({ + "# {0} - src", "# {1} - dest", "AddLogicalImageTask.copyingImageFromTo=Copying image from {0} to {1}", + "AddLogicalImageTask.doneCopying=Done copying", + "# {0} - src", "# {1} - dest", "AddLogicalImageTask.failedToCopyDirectory=Failed to copy directory {0} to {1}", + "# {0} - file", "AddLogicalImageTask.addingToReport=Adding {0} to report", + "# {0} - file", "AddLogicalImageTask.doneAddingToReport=Done adding {0} to report" + }) + @Override + public void run() { + List errorList = new ArrayList<>(); + List emptyDataSources = new ArrayList<>(); + + try { + progressMonitor.setProgressText(Bundle.AddLogicalImageTask_copyingImageFromTo(src.toString(), dest.toString())); + FileUtils.copyDirectory(src, dest); + progressMonitor.setProgressText(Bundle.AddLogicalImageTask_doneCopying()); + } catch (IOException ex) { + // Copy directory failed + String msg = Bundle.AddLogicalImageTask_failedToCopyDirectory(src.toString(), dest.toString()); + errorList.add(msg); + logger.log(Level.SEVERE, String.format("Failed to copy directory {0} to {1}", src.toString(), dest.toString())); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); + return; + } + + // Add the alert.txt and users.txt to the case report + progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingToReport(ALERT_TXT)); + String status = addReport(Paths.get(dest.toString(), ALERT_TXT), ALERT_TXT + " " + src.getName()); + if (status != null) { + errorList.add(status); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); + return; + } + progressMonitor.setProgressText(Bundle.AddLogicalImageTask_doneAddingToReport(ALERT_TXT)); + + progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingToReport(USERS_TXT)); + status = addReport(Paths.get(dest.toString(), USERS_TXT), USERS_TXT + " " + src.getName()); + if (status != null) { + errorList.add(status); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); + return; + } + progressMonitor.setProgressText(Bundle.AddLogicalImageTask_doneAddingToReport(USERS_TXT)); + + super.run(); + } + + /** + * Add a file specified by the reportPath to the case report. + * + * @param reportPath Path to the report to be added + * @param reportName Name associated the report + * @returns null if success, or exception message if failure + * + */ + @Messages({ + "# {0} - file", "# {1} - exception message", "AddLogicalImageTask.failedToAddReport=Failed to add report {0}. Reason= {1}" + }) + private String addReport(Path reportPath, String reportName) { + if (!reportPath.toFile().exists()) { + return null; // if the reportPath doesn't exist, just ignore it. + } + try { + Case.getCurrentCase().addReport(reportPath.toString(), "LogicalImager", reportName); //NON-NLS + return null; + } catch (TskCoreException ex) { + String msg = Bundle.AddLogicalImageTask_failedToAddReport(reportPath.toString(), ex.getMessage()); + logger.log(Level.SEVERE, String.format("Failed to add report {0}. Reason= {1}", reportPath.toString(), ex.getMessage())); + return msg; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index 4150e5729e..5ad9042e9f 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -1,5 +1,19 @@ AddImageWizardIngestConfigPanel.name.text=Configure Ingest Modules AddImageWizardSelectDspVisual.multiUserWarning.text=This type of Data Source Processor is not available in multi-user mode +# {0} - file +AddLogicalImageTask.addingToReport=Adding {0} to report +# {0} - src +# {1} - dest +AddLogicalImageTask.copyingImageFromTo=Copying image from {0} to {1} +# {0} - file +AddLogicalImageTask.doneAddingToReport=Done adding {0} to report +AddLogicalImageTask.doneCopying=Done copying +# {0} - file +# {1} - exception message +AddLogicalImageTask.failedToAddReport=Failed to add report {0}. Reason= {1} +# {0} - src +# {1} - dest +AddLogicalImageTask.failedToCopyDirectory=Failed to copy directory {0} to {1} # {0} - exception message Case.closeException.couldNotCloseCase=Error closing case: {0} Case.creationException.couldNotAcquireDirLock=Failed to get lock on case directory @@ -169,10 +183,19 @@ LogicalEvidenceFilePanel.validatePanel.nonL01Error.text=Only files with the .l01 LogicalFilesDspPanel.subTypeComboBox.l01FileOption.text=Logical evidence file (L01) LogicalFilesDspPanel.subTypeComboBox.localFilesOption.text=Local files and folders LogicalImagerDSProcessor.dataSourceType=Autopsy Imager +# {0} - directory +LogicalImagerDSProcessor.directoryAlreadyExists=Directory {0} already exists +# {0} - directory +LogicalImagerDSProcessor.failToCreateDirectory=Failed to create directory {0} +# {0} - imageDirPath +LogicalImagerDSProcessor.imageDirPathNotFound={0} not found.\nUSB drive has been ejected. LogicalImagerPanel.imageTable.columnModel.title0=Hostname LogicalImagerPanel.imageTable.columnModel.title1=Extracted Date LogicalImagerPanel.messageLabel.clickScanOrBrowse=Click SCAN or BROWSE button to find images +# {0} - sparseImageDirectory +# {1} - image LogicalImagerPanel.messageLabel.directoryDoesNotContainSparseImage=Directory {0} does not contain {1} +# {0} - invalidFormatDirectory LogicalImagerPanel.messageLabel.directoryFormatInvalid=Directory {0} does not match format Logical_Imager_HOSTNAME_yyyymmdd_HH_MM_SS LogicalImagerPanel.messageLabel.driveHasNoImages=Drive has no images LogicalImagerPanel.messageLabel.noExternalDriveFound=No drive found diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java index ea67c48290..ff01e1c540 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java @@ -18,7 +18,13 @@ */ package org.sleuthkit.autopsy.casemodule; +import java.io.File; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.UUID; import javax.swing.JPanel; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; @@ -26,6 +32,7 @@ import org.openide.util.lookup.ServiceProviders; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; +import org.sleuthkit.datamodel.Content; /** * A Logical Imager data source processor that implements the DataSourceProcessor service @@ -38,7 +45,10 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgress ) public class LogicalImagerDSProcessor implements DataSourceProcessor { + private static final String LOGICAL_IMAGER_DIR = "LogicalImager"; //NON-NLS + private static final String SPARSE_IMAGE_VHD = "sparse_image.vhd"; //NON-NLS private final LogicalImagerPanel configPanel; + private AddLogicalImageTask addLogicalImageTask; /* * Constructs a Logical Imager data source processor that implements the @@ -114,14 +124,57 @@ public class LogicalImagerDSProcessor implements DataSourceProcessor { * @param callback Callback that will be used by the background task * to return results. */ + @Messages({ + "# {0} - imageDirPath", "LogicalImagerDSProcessor.imageDirPathNotFound={0} not found.\nUSB drive has been ejected.", + "# {0} - directory", "LogicalImagerDSProcessor.failToCreateDirectory=Failed to create directory {0}", + "# {0} - directory", "LogicalImagerDSProcessor.directoryAlreadyExists=Directory {0} already exists", + }) @Override public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { configPanel.storeSettings(); - Path dirPath = configPanel.getImageDirPath(); - System.out.println("Choosen directory " + dirPath.toString()); - // TODO: process the data source in 5011 - } + + Path imageDirPath = configPanel.getImageDirPath(); + List errorList = new ArrayList<>(); + List emptyDataSources = new ArrayList<>(); + + if (!imageDirPath.toFile().exists()) { + // This can happen if the USB drive was selected in the panel, but + // was ejected before pressing the NEXT button + // TODO: Better ways to detect ejected USB drive? + String msg = Bundle.LogicalImagerDSProcessor_imageDirPathNotFound(imageDirPath.toString()); + errorList.add(msg); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); + return; + } + + // Create the LogicalImager directory under ModuleDirectory + String moduleDirectory = Case.getCurrentCase().getModuleDirectory(); + File logicalImagerDir = Paths.get(moduleDirectory, LOGICAL_IMAGER_DIR).toFile(); + if (!logicalImagerDir.exists() && !logicalImagerDir.mkdir()) { + // create failed + String msg = Bundle.LogicalImagerDSProcessor_failToCreateDirectory(logicalImagerDir); + errorList.add(msg); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); + return; + } + File dest = Paths.get(logicalImagerDir.toString(), imageDirPath.getFileName().toString()).toFile(); + if (dest.exists()) { + // Destination directory already exists + String msg = Bundle.LogicalImagerDSProcessor_directoryAlreadyExists(dest.toString()); + errorList.add(msg); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); + return; + } + File src = imageDirPath.toFile(); + String deviceId = UUID.randomUUID().toString(); + String timeZone = Calendar.getInstance().getTimeZone().getID(); + boolean ignoreFatOrphanFiles = false; + run(deviceId, Paths.get(src.toString(), SPARSE_IMAGE_VHD).toString(), 0, + timeZone, ignoreFatOrphanFiles, null, null, null, src, dest, + progressMonitor, callback); + } + /** * Adds a "Logical Imager" data source to the case database using a background task in * a separate thread and the given settings instead of those provided by the @@ -140,17 +193,28 @@ public class LogicalImagerDSProcessor implements DataSourceProcessor { * @param chunkSize The maximum size of each chunk of the raw * data source as it is divided up into virtual * unallocated space files. + * @param src The source directory of image. + * @param dest The destination directory to copy the source. * @param progressMonitor Progress monitor for reporting progress * during processing. * @param callback Callback to call when processing is done. */ - private void run(String deviceId, String imagePath, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - AddImageTask addImageTask = new AddImageTask(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, null, progressMonitor, callback); - new Thread(addImageTask).start(); + private void run(String deviceId, String imagePath, int sectorSize, String timeZone, + boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, + File src, File dest, + DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback + ) { + addLogicalImageTask = new AddLogicalImageTask(deviceId, imagePath, sectorSize, + timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, null, src, dest, + progressMonitor, callback); + new Thread(addLogicalImageTask).start(); } @Override public void cancel() { + if (addLogicalImageTask != null) { + addLogicalImageTask.cancelTask(); + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.form index d89ee47202..79e6241551 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.form @@ -1,6 +1,6 @@ -
+ @@ -213,6 +213,9 @@ + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.java index 44d50d0be5..cfc0f87eb1 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.java @@ -42,15 +42,14 @@ import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; /** - * Panel for adding an logical image file from drive letters. Allows the user - * to select a file. + * Panel for adding an logical image file from drive letters. Allows the user to + * select a file. */ @Messages({ "LogicalImagerPanel.messageLabel.selectedImage=Selected folder", "LogicalImagerPanel.messageLabel.noImageSelected=No image selected", "LogicalImagerPanel.messageLabel.driveHasNoImages=Drive has no images", - "LogicalImagerPanel.selectAcquisitionFromDriveLabel.text=Select acquisition from Drive", -}) + "LogicalImagerPanel.selectAcquisitionFromDriveLabel.text=Select acquisition from Drive",}) @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives public class LogicalImagerPanel extends JPanel implements DocumentListener { @@ -66,12 +65,12 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { private final String contextName; private Path choosenImageDirPath; private TableModel imageTableModel; - + /** * Creates new form LogicalImagerPanel * - * @param context A string context name used to read/store last - * used settings. + * @param context A string context name used to read/store last used + * settings. */ private LogicalImagerPanel(String context) { this.contextName = context; @@ -82,9 +81,9 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { /** * Creates and returns an instance of a LogicalImagerPanel. * - * @param context A string context name used to read/store last - * used settings. - * + * @param context A string context name used to read/store last used + * settings. + * * @return instance of the LogicalImagerPanel */ @Messages({ @@ -173,6 +172,7 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { } )); imageTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_OFF); + imageTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); imageTable.setShowHorizontalLines(false); imageTable.setShowVerticalLines(false); imageTable.getTableHeader().setReorderingAllowed(false); @@ -262,12 +262,12 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { int unit = si ? 1000 : 1024; if (bytes < unit) { return bytes + " B"; //NON-NLS - } + } int exp = (int) (Math.log(bytes) / Math.log(unit)); - String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i"); //NON-NLS + String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); //NON-NLS return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); //NON-NLS } - + @Messages({ "LogicalImagerPanel.messageLabel.scanningExternalDrives=Scanning external drives for sparse_image.vhd ...", "LogicalImagerPanel.messageLabel.noExternalDriveFound=No drive found" @@ -310,8 +310,11 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { }//GEN-LAST:event_scanButtonActionPerformed @Messages({ + "# {0} - sparseImageDirectory", + "# {1} - image", "LogicalImagerPanel.messageLabel.directoryDoesNotContainSparseImage=Directory {0} does not contain {1}", - "LogicalImagerPanel.messageLabel.directoryFormatInvalid=Directory {0} does not match format Logical_Imager_HOSTNAME_yyyymmdd_HH_MM_SS" + "# {0} - invalidFormatDirectory", + "LogicalImagerPanel.messageLabel.directoryFormatInvalid=Directory {0} does not match format Logical_Imager_HOSTNAME_yyyymmdd_HH_MM_SS" }) private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed imageTable.clearSelection(); @@ -325,13 +328,13 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { if (m.find()) { Path vhdPath = Paths.get(path, SPARSE_IMAGE_VHD); if (!vhdPath.toFile().exists()) { - setErrorMessage(Bundle.LogicalImagerPanel_messageLabel_directoryDoesNotContainSparseImage(path,SPARSE_IMAGE_VHD)); + setErrorMessage(Bundle.LogicalImagerPanel_messageLabel_directoryDoesNotContainSparseImage(path, SPARSE_IMAGE_VHD)); firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); return; } choosenImageDirPath = Paths.get(path); setNormalMessage(SELECTED_IMAGE + " " + path); - firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); + firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); } else { setErrorMessage(Bundle.LogicalImagerPanel_messageLabel_directoryFormatInvalid(path)); firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); @@ -351,7 +354,7 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { choosenImageDirPath = null; setErrorMessage(NO_IMAGE_SELECTED); firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); - } + } } private void imageTableMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_imageTableMouseClicked @@ -372,9 +375,9 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { int row = 0; // Find all directories with name like Logical_Imager_HOSTNAME_yyyymmdd_HH_MM_SS // and has a sparse_image.vhd file in it - for (File file : fList) { - if (file.isDirectory() - && Paths.get(driveLetter, file.getName(), SPARSE_IMAGE_VHD).toFile().exists()) { + for (File file : fList) { + if (file.isDirectory() + && Paths.get(driveLetter, file.getName(), SPARSE_IMAGE_VHD).toFile().exists()) { String dir = file.getName(); Matcher m = regex.matcher(dir); if (m.find()) { @@ -386,7 +389,7 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { String hour = m.group(5); String minute = m.group(6); String second = m.group(7); - String extractDate = year + "/" + month + "/" + day + String extractDate = year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second; imageTableModel.setValueAt(hostname, row, 0); imageTableModel.setValueAt(extractDate, row, 1); @@ -408,31 +411,31 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { choosenImageDirPath = null; setErrorMessage(DRIVE_HAS_NO_IMAGES); } - } + } } - + private void fixImageTableColumnWidth() { int width = imageScrollPane.getPreferredSize().width - 2; imageTable.getColumnModel().getColumn(0).setPreferredWidth((int) (.60 * width)); imageTable.getColumnModel().getColumn(1).setPreferredWidth((int) (.40 * width)); } - + private void setErrorMessage(String msg) { messageLabel.setForeground(Color.red); - messageLabel.setText(msg); + messageLabel.setText(msg); } - + private void setNormalMessage(String msg) { messageLabel.setForeground(Color.black); - messageLabel.setText(msg); + messageLabel.setText(msg); } private void clearImageTable() { imageTableModel = new ImageTableModel(); - imageTable.setModel(imageTableModel); + imageTable.setModel(imageTableModel); fixImageTableColumnWidth(); } - + private void driveListMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_driveListMouseClicked driveListSelect(); firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); @@ -480,10 +483,14 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { public boolean validatePanel() { return choosenImageDirPath != null && choosenImageDirPath.toFile().exists(); } - + Path getImageDirPath() { return choosenImageDirPath; } + + public void setMessageLabel(String message) { + messageLabel.setText(message); + } @Override public void insertUpdate(DocumentEvent e) { @@ -501,6 +508,7 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { } private class ImageTableModel extends AbstractTableModel { + private final List hostnames = new ArrayList<>(); private final List extractDates = new ArrayList<>(); private final List imageDirPaths = new ArrayList<>(); @@ -534,7 +542,7 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { } return colName; } - + @Override public Object getValueAt(int rowIndex, int columnIndex) { Object ret = null; @@ -553,7 +561,7 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { } return ret; } - + @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return false; diff --git a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java index ad46aec25a..5dd05ddae1 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java @@ -22,7 +22,11 @@ import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.Subscribe; import java.awt.event.ItemListener; import java.beans.PropertyChangeListener; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Collection; import java.util.EnumSet; @@ -51,6 +55,7 @@ import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.CaseDbAccessManager.CaseDbAccessQueryCallback; import org.sleuthkit.datamodel.CommunicationsFilter; import org.sleuthkit.datamodel.CommunicationsFilter.AccountTypeFilter; import org.sleuthkit.datamodel.CommunicationsFilter.DateRangeFilter; @@ -157,6 +162,29 @@ final public class FiltersPanel extends JPanel { applyFiltersButton.addActionListener(e -> applyFilters()); refreshButton.addActionListener(e -> applyFilters()); + + try { + String queryString = "max(date_time) as max, min(date_time) as min from account_relationships"; // NON-NLS + Case.getCurrentCaseThrows().getSleuthkitCase().getCaseDbAccessManager().select(queryString, new FilterPanelQueryCallback() { + @Override + public void process(ResultSet rs) { + try { + if (rs.next()) { + int startDate = rs.getInt("min"); // NON-NLS + int endData = rs.getInt("max"); // NON-NLS + + startDatePicker.setDate(LocalDateTime.ofInstant(Instant.ofEpochSecond(startDate), Utils.getUserPreferredZoneId()).toLocalDate()); + endDatePicker.setDate(LocalDateTime.ofInstant(Instant.ofEpochSecond(endData), Utils.getUserPreferredZoneId()).toLocalDate()); + } + } catch (SQLException ex) { + logger.log(Level.WARNING, "Unable to set filter date pickers due to SQL exception", ex); //NON-NLS + } + } + + }); + } catch (NoCurrentCaseException | TskCoreException ex) { + logger.log(Level.SEVERE, "Unable to set filter date pickers due to exception", ex); //NON-NLS + } } /** @@ -958,4 +986,17 @@ final public class FiltersPanel extends JPanel { checkbox.addItemListener(l); } } + + /** + * A simple class that implements CaseDbAccessQueryCallback. Can be used + * as an anonymous innerclass with the CaseDbAccessManager select function. + */ + class FilterPanelQueryCallback implements CaseDbAccessQueryCallback { + + @Override + public void process(ResultSet rs) { + // Subclasses can implement their own process function. + } + } + } \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form index 54e6d45006..97b3c67f72 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form @@ -16,11 +16,11 @@ - - + + @@ -28,29 +28,12 @@ - + - - - - - - - - - - - - - - - - - @@ -61,5 +44,9 @@ + + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java index 31731c006f..f915b8d5a6 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java @@ -18,9 +18,19 @@ */ package org.sleuthkit.autopsy.contentviewers; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +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.Document; +import org.jsoup.nodes.Node; import org.openide.util.NbBundle.Messages; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.w3c.dom.events.EventTarget; /** * A file content viewer for HTML files. @@ -29,47 +39,58 @@ import org.openide.util.NbBundle.Messages; final class HtmlPanel extends javax.swing.JPanel { private static final long serialVersionUID = 1L; - + private static final String TEXT_TYPE = "text/plain"; + private final JFXPanel jfxPanel = new JFXPanel(); + private WebView webView; private String htmlText; - + /** * Creates new form HtmlViewerPanel */ HtmlPanel() { initComponents(); - - Utilities.configureTextPaneAsHtml(htmlbodyTextPane); + Platform.runLater(() -> { + webView = new WebView(); + //disable the context menu so they can't open linked pages by right clicking + webView.setContextMenuEnabled(false); + //disable java script + webView.getEngine().setJavaScriptEnabled(false); + //disable clicking on links + webView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Worker.State oldValue, Worker.State newValue) { + if (newValue == Worker.State.SUCCEEDED) { + disableHyperLinks(); + } + } + }); + Scene scene = new Scene(webView); + jfxPanel.setScene(scene); + jfxPanel.setPreferredSize(htmlJPanel.getPreferredSize()); + htmlJPanel.add(jfxPanel); + }); } - + /** * Set the text pane's HTML text and refresh the view to display it. - * + * * @param htmlText The HTML text to be applied to the text pane. */ void setHtmlText(String htmlText) { this.htmlText = htmlText; refresh(); } - + /** * Clear the HTML in the text pane and disable the show/hide button. */ void reset() { - htmlbodyTextPane.setText(""); + Platform.runLater(() -> { + webView.getEngine().loadContent("", TEXT_TYPE); + }); showImagesToggleButton.setEnabled(false); } - /** - * Guarantee the HTML text has 'html' and 'body' tags. - * - * @param htmlText The HTML text - * - * @return The HTML text with the 'html' and 'body' tags applied. - */ - private String wrapInHtmlBody(String htmlText) { - return "" + htmlText + ""; - } - /** * Cleans out input HTML string * @@ -78,37 +99,40 @@ final class HtmlPanel extends javax.swing.JPanel { * @return The cleansed HTML String */ private String cleanseHTML(String htmlInString) { - - Document doc = Jsoup.parse(htmlInString); - - // Update all 'img' tags. - doc.select("img[src]").forEach(img -> img.attr("src", "")); - + 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(); } - + /** * Refresh the panel to reflect the current show/hide images setting. */ @Messages({ "HtmlPanel_showImagesToggleButton_show=Show Images", "HtmlPanel_showImagesToggleButton_hide=Hide Images", - "Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML.", - }) + "Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML.",}) private void refresh() { if (false == htmlText.isEmpty()) { try { if (showImagesToggleButton.isSelected()) { showImagesToggleButton.setText(Bundle.HtmlPanel_showImagesToggleButton_hide()); - this.htmlbodyTextPane.setText(wrapInHtmlBody(htmlText)); + Platform.runLater(() -> { + webView.getEngine().loadContent(htmlText); + }); } else { showImagesToggleButton.setText(Bundle.HtmlPanel_showImagesToggleButton_show()); - this.htmlbodyTextPane.setText(wrapInHtmlBody(cleanseHTML(htmlText))); + Platform.runLater(() -> { + webView.getEngine().loadContent(cleanseHTML(htmlText)); + }); } showImagesToggleButton.setEnabled(true); - htmlbodyTextPane.setCaretPosition(0); - } catch(Exception ex) { - this.htmlbodyTextPane.setText(wrapInHtmlBody(Bundle.Html_text_display_error())); + } catch (Exception ignored) { + Platform.runLater(() -> { + webView.getEngine().loadContent(Bundle.Html_text_display_error(), TEXT_TYPE); + }); } } } @@ -122,14 +146,8 @@ final class HtmlPanel extends javax.swing.JPanel { // //GEN-BEGIN:initComponents private void initComponents() { - htmlScrollPane = new javax.swing.JScrollPane(); - htmlbodyTextPane = new javax.swing.JTextPane(); showImagesToggleButton = new javax.swing.JToggleButton(); - - htmlScrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); - - htmlbodyTextPane.setEditable(false); - htmlScrollPane.setViewportView(htmlbodyTextPane); + htmlJPanel = new javax.swing.JPanel(); org.openide.awt.Mnemonics.setLocalizedText(showImagesToggleButton, org.openide.util.NbBundle.getMessage(HtmlPanel.class, "HtmlPanel.showImagesToggleButton.text")); // NOI18N showImagesToggleButton.addActionListener(new java.awt.event.ActionListener() { @@ -138,21 +156,23 @@ final class HtmlPanel extends javax.swing.JPanel { } }); + htmlJPanel.setLayout(new java.awt.BorderLayout()); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(htmlScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 300, Short.MAX_VALUE) .addGroup(layout.createSequentialGroup() .addComponent(showImagesToggleButton) - .addGap(0, 0, Short.MAX_VALUE)) + .addGap(0, 95, Short.MAX_VALUE)) + .addComponent(htmlJPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(showImagesToggleButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(htmlScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 71, Short.MAX_VALUE)) + .addComponent(htmlJPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 33, Short.MAX_VALUE)) ); }// //GEN-END:initComponents @@ -160,10 +180,27 @@ final class HtmlPanel extends javax.swing.JPanel { refresh(); }//GEN-LAST:event_showImagesToggleButtonActionPerformed + /** + * Disable the click events on hyper links so that new pages can not be + * opened. + */ + private void disableHyperLinks() { + Platform.runLater(() -> { + Document document = webView.getEngine().getDocument(); + if (document != null) { + NodeList nodeList = document.getElementsByTagName("a"); + for (int i = 0; i < nodeList.getLength(); i++) { + ((EventTarget) nodeList.item(i)).addEventListener("click", (evt) -> { + evt.preventDefault(); + evt.stopPropagation(); + }, true); + } + } + }); + } // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JScrollPane htmlScrollPane; - private javax.swing.JTextPane htmlbodyTextPane; + private javax.swing.JPanel htmlJPanel; private javax.swing.JToggleButton showImagesToggleButton; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java index e511517f7f..d6f68086c4 100644 --- a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java +++ b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java @@ -77,6 +77,7 @@ public final class UserPreferences { public static final String DISPLAY_TRANSLATED_NAMES = "DisplayTranslatedNames"; public static final String EXTERNAL_HEX_EDITOR_PATH = "ExternalHexEditorPath"; public static final String SOLR_MAX_JVM_SIZE = "SolrMaxJVMSize"; + public static final String RESULTS_TABLE_PAGE_SIZE = "ResultsTablePageSize"; // Prevent instantiation. private UserPreferences() { @@ -500,6 +501,24 @@ public final class UserPreferences { preferences.putInt(SOLR_MAX_JVM_SIZE, maxSize); } + /** + * Get the maximum number of results to display in a result table. + * + * @return Saved value or default (0) which indicates no max. + */ + public static int getResultsTablePageSize() { + return preferences.getInt(RESULTS_TABLE_PAGE_SIZE, 0); + } + + /** + * Set the maximum number of results to display in a result table. + * + * @param pageSize + */ + public static void setResultsTablePageSize(int pageSize) { + preferences.putInt(RESULTS_TABLE_PAGE_SIZE, pageSize); + } + /** * Set the HdX path. * diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties index 602b958900..ac5bccebc8 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -207,3 +207,13 @@ ExternalViewerGlobalSettingsPanel.jTable1.columnModel.title0_1=Mime type/Extensi AutopsyOptionsPanel.maxSolrMemoryLabel.text=Maximum Solr JVM Memory: AutopsyOptionsPanel.maxMemoryUnitsLabel2.text=MB AutopsyOptionsPanel.solrJVMHeapWarning.text=NOTE: Setting this too large may impact overall performance. +DataResultViewerTable.gotoPageTextField.text= +DataResultViewerTable.gotoPageLabel.AccessibleContext.accessibleName= +DataResultViewerTable.gotoPageLabel.text=Go to Page: +DataResultViewerTable.pageNextButton.text= +DataResultViewerTable.pagePrevButton.text= +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 diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED index afe2fb7724..2c0af06f50 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED @@ -33,6 +33,12 @@ DataResultViewerTable.commentRenderer.tagComment.toolTip=Comment exists on assoc DataResultViewerTable.countRender.name=O DataResultViewerTable.countRender.toolTip=O(ccurrences) indicates the number of data sources containing the item in the Central Repository DataResultViewerTable.firstColLbl=Name +DataResultViewerTable.goToPageTextField.err=Invalid page number +# {0} - totalPages +DataResultViewerTable.goToPageTextField.msgDlg=Please enter a valid page number between 1 and {0} +# {0} - currentPage +# {1} - totalPages +DataResultViewerTable.pageNumbers.curOfTotal={0} of {1} DataResultViewerTable.scoreRender.name=S DataResultViewerTable.scoreRender.toolTip=S(core) indicates whether the item is interesting or notable DataResultViewerTable.title=Table @@ -254,3 +260,13 @@ ExternalViewerGlobalSettingsPanel.jTable1.columnModel.title0_1=Mime type/Extensi AutopsyOptionsPanel.maxSolrMemoryLabel.text=Maximum Solr JVM Memory: AutopsyOptionsPanel.maxMemoryUnitsLabel2.text=MB AutopsyOptionsPanel.solrJVMHeapWarning.text=NOTE: Setting this too large may impact overall performance. +DataResultViewerTable.gotoPageTextField.text= +DataResultViewerTable.gotoPageLabel.AccessibleContext.accessibleName= +DataResultViewerTable.gotoPageLabel.text=Go to Page: +DataResultViewerTable.pageNextButton.text= +DataResultViewerTable.pagePrevButton.text= +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 diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form index d6c32623a4..6b17cfd6dd 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form @@ -16,16 +16,127 @@ - + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -33,5 +144,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index bf18c4b831..a5bbc461b8 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-2018 Basis Technology Corp. + * Copyright 2012-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.corecomponents; +import com.google.common.eventbus.Subscribe; import java.awt.Component; import java.awt.Cursor; import java.awt.FontMetrics; @@ -26,6 +27,7 @@ import java.awt.dnd.DnDConstants; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.FeatureDescriptor; +import java.beans.PropertyChangeEvent; import java.beans.PropertyVetoException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -35,9 +37,12 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; +import java.util.prefs.PreferenceChangeEvent; import java.util.prefs.Preferences; import javax.swing.ImageIcon; +import javax.swing.JOptionPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import static javax.swing.SwingConstants.CENTER; @@ -61,15 +66,24 @@ import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.nodes.Node.Property; +import org.openide.nodes.NodeEvent; +import org.openide.nodes.NodeListener; +import org.openide.nodes.NodeMemberEvent; +import org.openide.nodes.NodeReorderEvent; import org.openide.util.ImageUtilities; import org.openide.util.NbBundle; import org.openide.util.NbPreferences; 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.ThreadConfined; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.datamodel.NodeSelectionInfo; +import org.sleuthkit.autopsy.datamodel.BaseChildFactory; +import org.sleuthkit.autopsy.datamodel.BaseChildFactory.PageChangeEvent; +import org.sleuthkit.autopsy.datamodel.BaseChildFactory.PageCountChangeEvent; +import org.sleuthkit.autopsy.datamodel.BaseChildFactory.PageSizeChangeEvent; /** * A tabular result viewer that displays the children of the given root node @@ -104,6 +118,18 @@ public class DataResultViewerTable extends AbstractDataResultViewer { private final IconRendererTableListener iconRendererListener; private Node rootNode; + /** + * Multiple nodes may have been visited in the context of this + * DataResultViewerTable. We keep track of the page state for these nodes in + * the following map. + */ + private final Map nodeNameToPagingSupportMap = new ConcurrentHashMap<>(); + + /** + * The paging support instance for the current node. + */ + private PagingSupport pagingSupport = null; + /** * Constructs a tabular result viewer that displays the children of the * given root node using an OutlineView. The viewer should have an ancestor @@ -149,6 +175,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { */ initComponents(); + initializePagingSupport(); + /* * Configure the child OutlineView (explorer view) component. */ @@ -177,6 +205,32 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outline.getTableHeader().addMouseListener(outlineViewListener); } + private void initializePagingSupport() { + if (pagingSupport == null) { + pagingSupport = new PagingSupport(""); + } + + // Start out with paging controls invisible + pagingSupport.togglePageControls(false); + + /** + * Set up a change listener so we know when the user changes the page + * size + */ + UserPreferences.addChangeListener((PreferenceChangeEvent evt) -> { + if (evt.getKey().equals(UserPreferences.RESULTS_TABLE_PAGE_SIZE)) { + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + /** + * If multiple nodes have been viewed we have to notify all of + * them about the change in page size. + */ + nodeNameToPagingSupportMap.values().forEach((ps) -> { + ps.postPageSizeChangeEvent(); + }); + } + }); + } + /** * Creates a new instance of a tabular result viewer that displays the * children of a given root node using an OutlineView. This method exists to @@ -252,6 +306,56 @@ public class DataResultViewerTable extends AbstractDataResultViewer { */ if (rootNode != null && rootNode.getChildren().getNodesCount() > 0) { this.rootNode = rootNode; + + /** + * Check to see if we have previously created a paging support + * class for this node. + */ + String nodeName = rootNode.getName(); + pagingSupport = nodeNameToPagingSupportMap.get(nodeName); + if (pagingSupport == null) { + pagingSupport = new PagingSupport(nodeName); + nodeNameToPagingSupportMap.put(nodeName, pagingSupport); + } + pagingSupport.updateControls(); + + rootNode.addNodeListener(new NodeListener() { + @Override + public void childrenAdded(NodeMemberEvent nme) { + /** + * This is the only somewhat reliable way I could find + * to reset the cursor after a page change. When you + * change page the old children nodes will be removed + * and new ones added. + */ + SwingUtilities.invokeLater(() -> { + setCursor(null); + }); + } + + @Override + public void childrenRemoved(NodeMemberEvent nme) { + SwingUtilities.invokeLater(() -> { + setCursor(null); + }); + } + + @Override + public void childrenReordered(NodeReorderEvent nre) { + // No-op + } + + @Override + public void nodeDestroyed(NodeEvent ne) { + // No-op + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + // No-op + } + }); + this.getExplorerManager().setRootContext(this.rootNode); setupTable(); } else { @@ -665,6 +769,152 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } } + /** + * Maintains the current page state for a node and provides support for + * paging through results. Uses an EventBus to communicate with child + * factory implementations. + */ + private class PagingSupport { + + private int currentPage; + private int totalPages; + private final String nodeName; + + PagingSupport(String nodeName) { + currentPage = 1; + totalPages = 0; + this.nodeName = nodeName; + initialize(); + } + + private void initialize() { + if (!nodeName.isEmpty()) { + BaseChildFactory.register(nodeName, this); + } + updateControls(); + } + + void nextPage() { + currentPage++; + postPageChangeEvent(); + } + + void previousPage() { + currentPage--; + postPageChangeEvent(); + } + + @NbBundle.Messages({"# {0} - totalPages", + "DataResultViewerTable.goToPageTextField.msgDlg=Please enter a valid page number between 1 and {0}", + "DataResultViewerTable.goToPageTextField.err=Invalid page number"}) + void gotoPage() { + try { + currentPage = Integer.decode(gotoPageTextField.getText()); + } catch (NumberFormatException e) { + //ignore input + return; + } + + if (currentPage > totalPages || currentPage < 1) { + currentPage = 1; + JOptionPane.showMessageDialog(DataResultViewerTable.this, + Bundle.DataResultViewerTable_goToPageTextField_msgDlg(totalPages), + Bundle.DataResultViewerTable_goToPageTextField_err(), + JOptionPane.WARNING_MESSAGE); + return; + } + postPageChangeEvent(); + } + + /** + * Notify subscribers (i.e. child factories) that a page change has + * occurred. + */ + void postPageChangeEvent() { + try { + BaseChildFactory.post(nodeName, new PageChangeEvent(currentPage)); + } catch (BaseChildFactory.NoSuchEventBusException ex) { + LOGGER.log(Level.WARNING, "Failed to post page change event.", ex); //NON-NLS + } + DataResultViewerTable.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + updateControls(); + } + + /** + * Notify subscribers (i.e. child factories) that a page size change has + * occurred. + */ + void postPageSizeChangeEvent() { + // Reset page variables when page size changes + currentPage = 1; + totalPages = 0; + + if (this == pagingSupport) { + updateControls(); + } + try { + BaseChildFactory.post(nodeName, new PageSizeChangeEvent(UserPreferences.getResultsTablePageSize())); + } catch (BaseChildFactory.NoSuchEventBusException ex) { + LOGGER.log(Level.WARNING, "Failed to post page size change event.", ex); //NON-NLS + } + } + + /** + * Subscribe to notification that the number of pages has changed. + * + * @param event + */ + @Subscribe + public void subscribeToPageCountChange(PageCountChangeEvent event) { + if (event != null) { + totalPages = event.getPageCount(); + if (totalPages > 1) { + // Make paging controls visible if there is more than one page. + togglePageControls(true); + } + + updateControls(); + } + } + + /** + * Make paging controls visible or invisible based on flag. + * + * @param onOff + */ + private void togglePageControls(boolean onOff) { + pageLabel.setVisible(onOff); + pagesLabel.setVisible(onOff); + pagePrevButton.setVisible(onOff); + pageNextButton.setVisible(onOff); + pageNumLabel.setVisible(onOff); + gotoPageLabel.setVisible(onOff); + gotoPageTextField.setVisible(onOff); + gotoPageTextField.setVisible(onOff); + validate(); + repaint(); + } + + @NbBundle.Messages({"# {0} - currentPage", "# {1} - totalPages", + "DataResultViewerTable.pageNumbers.curOfTotal={0} of {1}"}) + private void updateControls() { + if (totalPages == 0) { + pagePrevButton.setEnabled(false); + pageNextButton.setEnabled(false); + pageNumLabel.setText(""); + gotoPageTextField.setText(""); + gotoPageTextField.setEnabled(false); + } else { + pageNumLabel.setText(Bundle.DataResultViewerTable_pageNumbers_curOfTotal(Integer.toString(currentPage), Integer.toString(totalPages))); + + pageNextButton.setEnabled(currentPage != totalPages); + pagePrevButton.setEnabled(currentPage != 1); + gotoPageTextField.setEnabled(totalPages > 1); + gotoPageTextField.setText(""); + } + } + } + /** * Listener which sets the custom icon renderer on columns which contain * icons instead of text when a column is added. @@ -1033,21 +1283,129 @@ public class DataResultViewerTable extends AbstractDataResultViewer { // //GEN-BEGIN:initComponents private void initComponents() { + pageLabel = new javax.swing.JLabel(); + pageNumLabel = new javax.swing.JLabel(); + pagesLabel = new javax.swing.JLabel(); + pagePrevButton = new javax.swing.JButton(); + pageNextButton = new javax.swing.JButton(); outlineView = new OutlineView(DataResultViewerTable.FIRST_COLUMN_LABEL); + gotoPageLabel = new javax.swing.JLabel(); + gotoPageTextField = new javax.swing.JTextField(); + + pageLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pageLabel.text")); // NOI18N + + pageNumLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pageNumLabel.text")); // NOI18N + + pagesLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pagesLabel.text")); // NOI18N + + pagePrevButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_back.png"))); // NOI18N + pagePrevButton.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pagePrevButton.text")); // NOI18N + pagePrevButton.setDisabledIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_back_disabled.png"))); // NOI18N + pagePrevButton.setFocusable(false); + pagePrevButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + pagePrevButton.setMargin(new java.awt.Insets(2, 0, 2, 0)); + pagePrevButton.setPreferredSize(new java.awt.Dimension(55, 23)); + pagePrevButton.setRolloverIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_back_hover.png"))); // NOI18N + pagePrevButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + pagePrevButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + pagePrevButtonActionPerformed(evt); + } + }); + + pageNextButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_forward.png"))); // NOI18N + pageNextButton.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pageNextButton.text")); // NOI18N + pageNextButton.setDisabledIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_forward_disabled.png"))); // NOI18N + pageNextButton.setFocusable(false); + pageNextButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + pageNextButton.setMargin(new java.awt.Insets(2, 0, 2, 0)); + pageNextButton.setMaximumSize(new java.awt.Dimension(27, 23)); + pageNextButton.setMinimumSize(new java.awt.Dimension(27, 23)); + pageNextButton.setRolloverIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_forward_hover.png"))); // NOI18N + pageNextButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + pageNextButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + pageNextButtonActionPerformed(evt); + } + }); + + gotoPageLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.gotoPageLabel.text")); // NOI18N + + gotoPageTextField.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.gotoPageTextField.text")); // NOI18N + gotoPageTextField.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + gotoPageTextFieldActionPerformed(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, 691, Short.MAX_VALUE) + .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(pageLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(pageNumLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 53, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(pagesLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pageNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(gotoPageLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) ); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {pageNextButton, pagePrevButton}); + layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 366, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(pageLabel) + .addComponent(pageNumLabel) + .addComponent(pagesLabel) + .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) + .addContainerGap()) ); + + layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {pageNextButton, pagePrevButton}); + + gotoPageLabel.getAccessibleContext().setAccessibleName(""); }// //GEN-END:initComponents + + private void pagePrevButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pagePrevButtonActionPerformed + pagingSupport.previousPage(); + }//GEN-LAST:event_pagePrevButtonActionPerformed + + private void pageNextButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pageNextButtonActionPerformed + pagingSupport.nextPage(); + }//GEN-LAST:event_pageNextButtonActionPerformed + + private void gotoPageTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_gotoPageTextFieldActionPerformed + pagingSupport.gotoPage(); + }//GEN-LAST:event_gotoPageTextFieldActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel gotoPageLabel; + private javax.swing.JTextField gotoPageTextField; private org.openide.explorer.view.OutlineView outlineView; + private javax.swing.JLabel pageLabel; + private javax.swing.JButton pageNextButton; + private javax.swing.JLabel pageNumLabel; + private javax.swing.JButton pagePrevButton; + private javax.swing.JLabel pagesLabel; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form index a348b49368..1e3c9cbf5e 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form @@ -68,7 +68,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -153,6 +153,11 @@ + + + + + @@ -206,6 +211,11 @@ + + + + + @@ -407,6 +417,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java index e65373ab76..f23caa3c61 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java @@ -35,6 +35,7 @@ import org.sleuthkit.autopsy.texttranslation.TextTranslationService; /** * Panel for configuring view preferences. */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives public class ViewPreferencesPanel extends JPanel implements OptionsPanel { private final boolean immediateUpdates; @@ -85,6 +86,8 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { TextTranslationService tts = TextTranslationService.getInstance(); fileNameTranslationColumnCheckbox.setEnabled(tts.hasProvider()); + maxResultsSpinner.setValue(UserPreferences.getResultsTablePageSize()); + // Current Case Settings boolean caseIsOpen = Case.isCaseOpen(); currentCaseSettingsPanel.setEnabled(caseIsOpen); @@ -114,6 +117,7 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { UserPreferences.setShowOnlyCurrentUserTags(hideOtherUsersTagsCheckbox.isSelected()); UserPreferences.setHideCentralRepoCommentsAndOccurrences(commentsOccurencesColumnsCheckbox.isSelected()); UserPreferences.setDisplayTranslatedFileNames(fileNameTranslationColumnCheckbox.isSelected()); + UserPreferences.setResultsTablePageSize((int)maxResultsSpinner.getValue()); storeGroupItemsInTreeByDataSource(); @@ -167,6 +171,8 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { translateTextLabel = new javax.swing.JLabel(); commentsOccurencesColumnWrapAroundText = new javax.swing.JLabel(); fileNameTranslationColumnCheckbox = new javax.swing.JCheckBox(); + maxResultsLabel = new javax.swing.JLabel(); + maxResultsSpinner = new javax.swing.JSpinner(); currentCaseSettingsPanel = new javax.swing.JPanel(); groupByDataSourceCheckbox = new javax.swing.JCheckBox(); currentSessionSettingsPanel = new javax.swing.JPanel(); @@ -284,6 +290,16 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { } }); + org.openide.awt.Mnemonics.setLocalizedText(maxResultsLabel, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.maxResultsLabel.text")); // NOI18N + maxResultsLabel.setToolTipText(org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.maxResultsLabel.toolTipText")); // NOI18N + + maxResultsSpinner.setModel(new javax.swing.SpinnerNumberModel(0, 0, 50000, 10000)); + maxResultsSpinner.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + maxResultsSpinnerStateChanged(evt); + } + }); + javax.swing.GroupLayout globalSettingsPanelLayout = new javax.swing.GroupLayout(globalSettingsPanel); globalSettingsPanel.setLayout(globalSettingsPanelLayout); globalSettingsPanelLayout.setHorizontalGroup( @@ -333,7 +349,11 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { .addComponent(keepCurrentViewerRadioButton) .addComponent(useBestViewerRadioButton) .addComponent(useLocalTimeRadioButton) - .addComponent(useAnotherTimeRadioButton)))))) + .addComponent(useAnotherTimeRadioButton))))) + .addGroup(globalSettingsPanelLayout.createSequentialGroup() + .addComponent(maxResultsLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(maxResultsSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, 74, javax.swing.GroupLayout.PREFERRED_SIZE))) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); globalSettingsPanelLayout.setVerticalGroup( @@ -381,6 +401,10 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { .addComponent(translateTextLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(fileNameTranslationColumnCheckbox))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(maxResultsLabel) + .addComponent(maxResultsSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); @@ -593,6 +617,14 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { } }//GEN-LAST:event_fileNameTranslationColumnCheckboxActionPerformed + private void maxResultsSpinnerStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_maxResultsSpinnerStateChanged + if (immediateUpdates) { + UserPreferences.setResultsTablePageSize((int)maxResultsSpinner.getValue()); + } else { + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + } + }//GEN-LAST:event_maxResultsSpinnerStateChanged + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JLabel centralRepoLabel; @@ -613,6 +645,8 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { private javax.swing.JLabel hideSlackFilesLabel; private javax.swing.JScrollPane jScrollPane1; private javax.swing.JRadioButton keepCurrentViewerRadioButton; + private javax.swing.JLabel maxResultsLabel; + private javax.swing.JSpinner maxResultsSpinner; private javax.swing.JLabel selectFileLabel; private javax.swing.JList timeZoneList; private javax.swing.JLabel translateTextLabel; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index 05f80dc5d3..fa4529e44b 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java @@ -34,7 +34,6 @@ import java.util.stream.Collectors; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; -import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.WeakListeners; @@ -55,6 +54,8 @@ import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.Score; import org.sleuthkit.autopsy.coreutils.Logger; import static org.sleuthkit.autopsy.datamodel.Bundle.*; import static org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType.*; +import org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException; +import org.sleuthkit.autopsy.datamodel.BaseChildFactory.RefreshKeysEvent; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.ModuleContentEvent; import org.sleuthkit.autopsy.texttranslation.NoServiceProviderException; @@ -164,17 +165,15 @@ public abstract class AbstractAbstractFileNode extends A if (getContent().getId() == newContent.getId()) { // If so, refresh our children. try { - Children parentsChildren = getParentNode().getChildren(); // We only want to refresh our parents children if we are in the // data sources branch of the tree. The parent nodes in other // branches of the tree (e.g. File Types and Deleted Files) do // not need to be refreshed. - if (parentsChildren instanceof ContentChildren) { - ((ContentChildren) parentsChildren).refreshChildren(); - parentsChildren.getNodesCount(); - } + BaseChildFactory.post(getParentNode().getName(), new RefreshKeysEvent()); } catch (NullPointerException ex) { // Skip + } catch (NoSuchEventBusException ex) { + logger.log(Level.WARNING, "Failed to post key refresh event", ex); //NON-NLS } } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentChildren.java index 71705d2725..82c7418a7f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentChildren.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2016 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,228 +18,28 @@ */ package org.sleuthkit.autopsy.datamodel; -import org.openide.nodes.AbstractNode; -import org.openide.nodes.Children.Keys; import org.openide.nodes.Node; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.datamodel.FileTypes.FileTypesNode; -import org.sleuthkit.autopsy.datamodel.accounts.Accounts; -import org.sleuthkit.autopsy.datamodel.accounts.Accounts.AccountsRootNode; -import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; -import org.sleuthkit.datamodel.DerivedFile; -import org.sleuthkit.datamodel.Directory; -import org.sleuthkit.datamodel.File; -import org.sleuthkit.datamodel.Image; -import org.sleuthkit.datamodel.LayoutFile; -import org.sleuthkit.datamodel.LocalFile; -import org.sleuthkit.datamodel.LocalDirectory; -import org.sleuthkit.datamodel.SlackFile; -import org.sleuthkit.datamodel.SleuthkitItemVisitor; import org.sleuthkit.datamodel.SleuthkitVisitableItem; -import org.sleuthkit.datamodel.VirtualDirectory; -import org.sleuthkit.datamodel.Volume; /** - * Abstract subclass for ContentChildren and RootContentChildren implementations + * Abstract subclass for ContentChildren implementation * that handles creating Nodes from Content objects. */ -abstract class AbstractContentChildren extends Keys { +abstract class AbstractContentChildren extends BaseChildFactory { private final CreateSleuthkitNodeVisitor createSleuthkitNodeVisitor = new CreateSleuthkitNodeVisitor(); - private final CreateAutopsyNodeVisitor createAutopsyNodeVisitor = new CreateAutopsyNodeVisitor(); - /** - * Uses lazy Content.Keys - */ - AbstractContentChildren() { - /* - * This was turned off because we were getting out of memory errors when - * the filter nodes were hiding nodes. Turning this off seemed to help - */ - super(false); //don't use lazy behavior + AbstractContentChildren(String nodeName) { + super(nodeName, new DataSourcesKnownAndSlackFilter<>()); } @Override - protected Node[] createNodes(T key) { + protected Node createNodeForKey(T key) { if (key instanceof SleuthkitVisitableItem) { - return new Node[]{((SleuthkitVisitableItem) key).accept(createSleuthkitNodeVisitor)}; + return ((SleuthkitVisitableItem) key).accept(createSleuthkitNodeVisitor); } else { - return new Node[]{((AutopsyVisitableItem) key).accept(createAutopsyNodeVisitor)}; - } - } - - /** - * Creates appropriate Node for each sub-class of Content - */ - public static class CreateSleuthkitNodeVisitor extends SleuthkitItemVisitor.Default> { - - @Override - public AbstractContentNode visit(Directory drctr) { - return new DirectoryNode(drctr); - } - - @Override - public AbstractContentNode visit(File file) { - return new FileNode(file); - } - - @Override - public AbstractContentNode visit(Image image) { - return new ImageNode(image); - } - - @Override - public AbstractContentNode visit(Volume volume) { - return new VolumeNode(volume); - } - - @Override - public AbstractContentNode visit(LayoutFile lf) { - return new LayoutFileNode(lf); - } - - @Override - public AbstractContentNode visit(DerivedFile df) { - return new LocalFileNode(df); - } - - @Override - public AbstractContentNode visit(LocalFile lf) { - return new LocalFileNode(lf); - } - - @Override - public AbstractContentNode visit(VirtualDirectory ld) { - return new VirtualDirectoryNode(ld); - } - - @Override - public AbstractContentNode visit(LocalDirectory ld) { - return new LocalDirectoryNode(ld); - } - - @Override - public AbstractContentNode visit(SlackFile sf) { - return new SlackFileNode(sf); - } - - @Override - public AbstractContentNode visit(BlackboardArtifact art) { - return new BlackboardArtifactNode(art); - } - - @Override - protected AbstractContentNode defaultVisit(SleuthkitVisitableItem di) { - throw new UnsupportedOperationException(NbBundle.getMessage(this.getClass(), - "AbstractContentChildren.CreateTSKNodeVisitor.exception.noNodeMsg")); - } - } - - /** - * Gets a DisplayableItemNode for use as a subtree root node for the Autopsy - * tree view from each type of AutopsyVisitableItem visited. There are - * AutopsyVisitableItems for the Data Sources, Views, Results, and Reports - * subtrees, and for the subtrees of Results (e.g., Extracted Content, Hash - * Set Hits, etc.). - */ - static class CreateAutopsyNodeVisitor extends AutopsyItemVisitor.Default { - - @Override - public ExtractedContent.RootNode visit(ExtractedContent ec) { - return ec.new RootNode(ec.getSleuthkitCase()); - } - - @Override - public AbstractNode visit(FileTypesByExtension sf) { - return sf.new FileTypesByExtNode(sf.getSleuthkitCase(), null); - } - - @Override - public AbstractNode visit(RecentFiles rf) { - return new RecentFilesNode(rf.getSleuthkitCase()); - } - - @Override - public AbstractNode visit(DeletedContent dc) { - return new DeletedContent.DeletedContentsNode(dc.getSleuthkitCase(), dc.filteringDataSourceObjId()); - } - - @Override - public AbstractNode visit(FileSize dc) { - return new FileSize.FileSizeRootNode(dc.getSleuthkitCase(), dc.filteringDataSourceObjId()); - } - - @Override - public AbstractNode visit(KeywordHits kh) { - return kh.new RootNode(); - } - - @Override - public AbstractNode visit(HashsetHits hh) { - return hh.new RootNode(); - } - - @Override - public AbstractNode visit(InterestingHits ih) { - return ih.new RootNode(); - } - - @Override - public AbstractNode visit(EmailExtracted ee) { - return ee.new RootNode(); - } - - @Override - public AbstractNode visit(Tags tagsNodeKey) { - return tagsNodeKey.new RootNode(tagsNodeKey.filteringDataSourceObjId()); - } - - @Override - public AbstractNode visit(DataSources i) { - return new DataSourcesNode(i.filteringDataSourceObjId()); - } - - @Override - public AbstractNode visit(DataSourceGrouping datasourceGrouping) { - return new DataSourceGroupingNode(datasourceGrouping.getDataSource()); - } - - @Override - public AbstractNode visit(Views v) { - return new ViewsNode(v.getSleuthkitCase(), v.filteringDataSourceObjId()); - } - - @Override - public AbstractNode visit(Results results) { - return new ResultsNode(results.getSleuthkitCase(), results.filteringDataSourceObjId() ); - } - - @Override - public AbstractNode visit(FileTypes ft) { - return ft.new FileTypesNode(); - } - - @Override - public AbstractNode visit(Reports reportsItem) { - return new Reports.ReportsListNode(); - } - - @Override - public AbstractNode visit(Accounts accountsItem) { - return accountsItem.new AccountsRootNode(); - } - - @Override - protected AbstractNode defaultVisit(AutopsyVisitableItem di) { - throw new UnsupportedOperationException( - NbBundle.getMessage(this.getClass(), - "AbstractContentChildren.createAutopsyNodeVisitor.exception.noNodeMsg")); - } - - @Override - public AbstractNode visit(FileTypesByMimeType ftByMimeTypeItem) { - return ftByMimeTypeItem.new ByMimeTypeNode(); + return null; } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java index 3fc3fa9f4e..4f6f2e5f47 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.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 java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.logging.Level; +import org.openide.nodes.Children; import org.openide.util.lookup.Lookups; import org.openide.util.Lookup; @@ -65,8 +66,7 @@ public abstract class AbstractContentNode extends ContentNode * @param lookup The Lookup object for the node. */ AbstractContentNode(T content, Lookup lookup) { - //TODO consider child factory for the content children - super(new ContentChildren(content), lookup); + super(Children.create(new ContentChildren(content), true), lookup); this.content = content; //super.setName(ContentUtils.getSystemName(content)); super.setName("content_" + Long.toString(content.getId())); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java index 69e3787e99..b8625b782e 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java @@ -135,7 +135,7 @@ public final class AutopsyTreeChildFactory extends ChildFactory.Detachable 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 com.google.common.collect.Lists; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.PreferenceChangeEvent; +import java.util.stream.Collectors; +import org.openide.nodes.ChildFactory; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.datamodel.Content; + +/** + * Abstract child factory that provides paging and filtering functionality to + * subclasses. + * + * @param + */ +public abstract class BaseChildFactory extends ChildFactory.Detachable { + + private static final Logger logger = Logger.getLogger(BaseChildFactory.class.getName()); + + private Predicate filter; + private boolean isPageChangeEvent; + private boolean isPageSizeChangeEvent; + + private final PagingSupport pagingSupport; + + /** + * This static map is used to facilitate communication between the UI and + * the child factory. + */ + private static Map nodeNameToEventBusMap = new ConcurrentHashMap<>(); + + @Messages({ + "# {0} - node name", "BaseChildFactory.NoSuchEventBusException.message=No event bus for node: {0}" + }) + public static class NoSuchEventBusException extends Exception { + + public NoSuchEventBusException(String nodeName) { + super(Bundle.BaseChildFactory_NoSuchEventBusException_message(nodeName)); + } + } + + /** + * Register the given subscriber for the given node name. Will create the + * event bus for the given node name if it does not exist. + * + * @param nodeName The name of the node. + * @param subscriber The subscriber to register. + */ + public static void register(String nodeName, Object subscriber) { + EventBus bus = nodeNameToEventBusMap.get(nodeName); + if (bus == null) { + bus = new EventBus(nodeName); + nodeNameToEventBusMap.put(nodeName, bus); + } + bus.register(subscriber); + } + + /** + * Post the given event for the given node name. + * + * @param nodeName The name of the node. + * @param event The event to post. + * + * @throws + * org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException + */ + public static void post(String nodeName, Object event) throws NoSuchEventBusException { + EventBus bus = nodeNameToEventBusMap.get(nodeName); + if (bus == null) { + throw new NoSuchEventBusException(nodeName); + } + bus.post(event); + } + + public BaseChildFactory(String nodeName) { + /** + * Initialize a no-op filter that always returns true. + */ + this(nodeName, x -> true); + } + + public BaseChildFactory(String nodeName, Predicate filter) { + pagingSupport = new PagingSupport(nodeName); + pagingSupport.initialize(); + isPageChangeEvent = false; + isPageSizeChangeEvent = false; + this.filter = filter; + } + + @Override + protected void addNotify() { + onAdd(); + } + + @Override + protected void removeNotify() { + onRemove(); + } + + /** + * Subclasses implement this to construct a collection of keys. + * + * @return + */ + protected abstract List makeKeys(); + + /** + * Subclasses implement this to initialize any required resources. + */ + protected abstract void onAdd(); + + /** + * Subclasses implement this to clean up any resources they acquired in + * onAdd() + */ + protected abstract void onRemove(); + + @Override + protected boolean createKeys(List toPopulate) { + /** + * For page change events and page size change events we simply return + * the previously calculated set of keys, otherwise we make a new set of + * keys. + */ + if (!isPageChangeEvent && !isPageSizeChangeEvent) { + List allKeys = makeKeys(); + + pagingSupport.splitKeysIntoPages(allKeys.stream().filter(filter).collect(Collectors.toList())); + } + + toPopulate.addAll(pagingSupport.getCurrentPage()); + + // Reset page change and page size change event flags + isPageChangeEvent = false; + isPageSizeChangeEvent = false; + + return true; + } + + /** + * Event used to trigger recreation of the keys. + */ + public static class RefreshKeysEvent { + } + + /** + * Event used to let subscribers know that the user has navigated to a + * different page. + */ + public static class PageChangeEvent { + + private final int pageNumber; + + public PageChangeEvent(int newPageNumber) { + pageNumber = newPageNumber; + } + + public int getPageNumber() { + return pageNumber; + } + } + + /** + * Event used to let subscribers know that the number of pages has changed. + */ + public static class PageCountChangeEvent { + + private final int pageCount; + + public PageCountChangeEvent(int newPageCount) { + pageCount = newPageCount; + } + + public int getPageCount() { + return pageCount; + } + } + + /** + * Event used to let subscribers know that the page size has changed. + */ + public static class PageSizeChangeEvent { + + private final int pageSize; + + public PageSizeChangeEvent(int newPageSize) { + pageSize = newPageSize; + } + + public int getPageSize() { + return pageSize; + } + } + + /** + * Class that supplies paging related functionality to the base child + * factory class. + */ + private class PagingSupport { + + private final String nodeName; + private int pageSize; + private int currentPage; + private List> pages; + + /** + * Construct PagingSupport instance for the given node name. + * + * @param nodeName Name of the node in the tree for which results are + * being displayed. The node name is used to allow + * communication between the UI and the ChildFactory via + * an EventBus. + */ + PagingSupport(String nodeName) { + currentPage = 1; + pageSize = UserPreferences.getResultsTablePageSize(); + pages = new ArrayList<>(); + this.nodeName = nodeName; + } + + void initialize() { + /** + * Set up a change listener so we know when the user changes the + * page size. + */ + UserPreferences.addChangeListener((PreferenceChangeEvent evt) -> { + if (evt.getKey().equals(UserPreferences.RESULTS_TABLE_PAGE_SIZE)) { + pageSize = UserPreferences.getResultsTablePageSize(); + } + }); + + register(nodeName, this); + } + + /** + * Get the list of keys at the current page. + * + * @return List of keys. + */ + List getCurrentPage() { + if (!pages.isEmpty()) { + return pages.get(currentPage - 1); + } + + return Collections.emptyList(); + } + + /** + * Split the given collection of keys into pages based on page size. + * + * @param keys + */ + void splitKeysIntoPages(List keys) { + int oldPageCount = pages.size(); + + /** + * 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 (pages.size() != oldPageCount) { + try { + // Number of pages has changed so we need to send out a notification. + post(nodeName, new PageCountChangeEvent(pages.size())); + } catch (NoSuchEventBusException ex) { + logger.log(Level.WARNING, "Failed to post page change event.", ex); + } + } + } + + /** + * Receives page change events from UI components and triggers a refresh + * in the child factory. + * + * @param event + */ + @Subscribe + private void subscribeToPageChange(PageChangeEvent event) { + if (event != null) { + currentPage = event.getPageNumber(); + isPageChangeEvent = true; + refresh(true); + } + } + + /** + * Receives page size change events from UI components and triggers a + * refresh in the child factory if necessary. + * + * @param event + */ + @Subscribe + private void subscribeToPageSizeChange(PageSizeChangeEvent event) { + if (event != null) { + int newPageSize = event.getPageSize(); + if (pageSize == newPageSize) { + // No change...nothing to do. + return; + } + + pageSize = newPageSize; + splitKeysIntoPages(pages.stream().flatMap(List::stream).collect(Collectors.toList())); + + currentPage = 1; + isPageSizeChangeEvent = true; + refresh(true); + } + } + + @Subscribe + private void subscribeToRefreshKeys(RefreshKeysEvent event) { + if (event != null) { + refresh(true); + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED index c11a681494..1d8fb7eb14 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED @@ -43,6 +43,8 @@ ArtifactStringContent.attrsTableHeader.type=Type ArtifactStringContent.attrsTableHeader.value=Value ArtifactStringContent.failedToGetAttributes.message=Failed to get some or all attributes from case database ArtifactStringContent.failedToGetSourcePath.message=Failed to get source file path from case database +# {0} - node name +BaseChildFactory.NoSuchEventBusException.message=No event bus for node: {0} BlackboardArtifactNode.createSheet.artifactDetails.displayName=Result Details BlackboardArtifactNode.createSheet.artifactDetails.name=Result Details BlackboardArtifactNode.createSheet.artifactMD5.displayName=MD5 Hash diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java index cc03bb4d46..44885e1ef8 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,17 +33,15 @@ import org.sleuthkit.datamodel.VolumeSystem; /** * Makes the children nodes / keys for a given content object. Has knowledge * about the structure of the directory tree and what levels should be ignored. - * TODO consider a ContentChildren child factory */ class ContentChildren extends AbstractContentChildren { private static final Logger logger = Logger.getLogger(ContentChildren.class.getName()); - //private static final int MAX_CHILD_COUNT = 1000000; private final Content parent; ContentChildren(Content parent) { - super(); //initialize lazy behavior + super("content_" + Long.toString(parent.getId())); this.parent = parent; } @@ -90,7 +88,7 @@ class ContentChildren extends AbstractContentChildren { children.add(c); } } else if (c instanceof LocalDirectory) { - LocalDirectory localDir = (LocalDirectory)c; + LocalDirectory localDir = (LocalDirectory) c; if (localDir.isRoot()) { children.addAll(getDisplayChildren(localDir)); } else { @@ -104,27 +102,13 @@ class ContentChildren extends AbstractContentChildren { } @Override - protected void addNotify() { - super.addNotify(); - - //TODO check global settings - //if above limit, query and return subrange - //StopWatch s2 = new StopWatch(); - //s2.start(); - //logger.log(Level.INFO, "GETTING CHILDREN CONTENT for parent: " + parent.getName()); - List children = getDisplayChildren(parent); - //s2.stop(); - //logger.log(Level.INFO, "GOT CHILDREN CONTENTS:" + children.size() + ", took: " + s2.getElapsedTime()); - - //limit number children - //setKeys(children.subList(0, Math.min(children.size(), MAX_CHILD_COUNT))); - setKeys(children); + protected List makeKeys() { + return getDisplayChildren(parent); } @Override - protected void removeNotify() { - super.removeNotify(); - setKeys(new ArrayList<>()); + protected void onAdd() { + // No-op } /** @@ -133,7 +117,11 @@ class ContentChildren extends AbstractContentChildren { * them). */ void refreshChildren() { - List children = getDisplayChildren(parent); - setKeys(children); + refresh(true); + } + + @Override + protected void onRemove() { + // No-op } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/CreateSleuthkitNodeVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/CreateSleuthkitNodeVisitor.java new file mode 100644 index 0000000000..d9ff5fc33b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/CreateSleuthkitNodeVisitor.java @@ -0,0 +1,102 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.openide.util.NbBundle; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.DerivedFile; +import org.sleuthkit.datamodel.Directory; +import org.sleuthkit.datamodel.File; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.LayoutFile; +import org.sleuthkit.datamodel.LocalDirectory; +import org.sleuthkit.datamodel.LocalFile; +import org.sleuthkit.datamodel.SlackFile; +import org.sleuthkit.datamodel.SleuthkitItemVisitor; +import org.sleuthkit.datamodel.SleuthkitVisitableItem; +import org.sleuthkit.datamodel.VirtualDirectory; +import org.sleuthkit.datamodel.Volume; + +/** + * Creates appropriate Node for each sub-class of Content + */ +public class CreateSleuthkitNodeVisitor extends SleuthkitItemVisitor.Default> { + + @Override + public AbstractContentNode visit(Directory drctr) { + return new DirectoryNode(drctr); + } + + @Override + public AbstractContentNode visit(File file) { + return new FileNode(file); + } + + @Override + public AbstractContentNode visit(Image image) { + return new ImageNode(image); + } + + @Override + public AbstractContentNode visit(Volume volume) { + return new VolumeNode(volume); + } + + @Override + public AbstractContentNode visit(LayoutFile lf) { + return new LayoutFileNode(lf); + } + + @Override + public AbstractContentNode visit(DerivedFile df) { + return new LocalFileNode(df); + } + + @Override + public AbstractContentNode visit(LocalFile lf) { + return new LocalFileNode(lf); + } + + @Override + public AbstractContentNode visit(VirtualDirectory ld) { + return new VirtualDirectoryNode(ld); + } + + @Override + public AbstractContentNode visit(LocalDirectory ld) { + return new LocalDirectoryNode(ld); + } + + @Override + public AbstractContentNode visit(SlackFile sf) { + return new SlackFileNode(sf); + } + + @Override + public AbstractContentNode visit(BlackboardArtifact art) { + return new BlackboardArtifactNode(art); + } + + @Override + protected AbstractContentNode defaultVisit(SleuthkitVisitableItem di) { + throw new UnsupportedOperationException(NbBundle.getMessage(this.getClass(), + "AbstractContentChildren.CreateTSKNodeVisitor.exception.noNodeMsg")); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesKnownAndSlackFilter.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesKnownAndSlackFilter.java new file mode 100644 index 0000000000..98eb9a0d1d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesKnownAndSlackFilter.java @@ -0,0 +1,52 @@ +/* + * 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.util.prefs.PreferenceChangeEvent; +import org.sleuthkit.autopsy.core.UserPreferences; +import static org.sleuthkit.autopsy.datamodel.KnownAndSlackFilterBase.filterKnown; +import static org.sleuthkit.autopsy.datamodel.KnownAndSlackFilterBase.filterSlack; +import org.sleuthkit.datamodel.Content; + +/** + * Known and Slack filter for Data Sources section of the tree. + * + * @param + */ +class DataSourcesKnownAndSlackFilter extends KnownAndSlackFilterBase { + + static { + /** + * Watch for user preference changes and update variables inherited from + * our parent. The actual filtering is provided by our parent class. + */ + UserPreferences.addChangeListener((PreferenceChangeEvent evt) -> { + if (evt.getKey().equals(UserPreferences.HIDE_KNOWN_FILES_IN_DATA_SRCS_TREE)) { + filterKnown = UserPreferences.hideKnownFilesInDataSourcesTree(); + } else if (evt.getKey().equals(UserPreferences.HIDE_SLACK_FILES_IN_DATA_SRCS_TREE)) { + filterSlack = UserPreferences.hideSlackFilesInDataSourcesTree(); + } + }); + } + + DataSourcesKnownAndSlackFilter() { + filterKnown = UserPreferences.hideKnownFilesInDataSourcesTree(); + filterSlack = UserPreferences.hideSlackFilesInDataSourcesTree(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java index 5b3f2fa32a..1265db5658 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java @@ -27,6 +27,7 @@ import java.util.Comparator; import java.util.EnumSet; import java.util.List; import java.util.logging.Level; +import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; @@ -56,7 +57,7 @@ public class DataSourcesNode extends DisplayableItemNode { } public DataSourcesNode(long dsObjId) { - super(new DataSourcesNodeChildren(dsObjId), Lookups.singleton(NAME)); + super(Children.create(new DataSourcesNodeChildren(dsObjId), true), Lookups.singleton(NAME)); displayName = (dsObjId > 0) ? NbBundle.getMessage(DataSourcesNode.class, "DataSourcesNode.group_by_datasource.name") : NAME; init(); } @@ -87,7 +88,7 @@ public class DataSourcesNode extends DisplayableItemNode { } public DataSourcesNodeChildren(long dsObjId) { - super(); + super("ds_" + Long.toString(dsObjId)); this.currentKeys = new ArrayList<>(); this.datasourceObjId = dsObjId; } @@ -97,25 +98,24 @@ public class DataSourcesNode extends DisplayableItemNode { public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); if (eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { - reloadKeys(); + refresh(true); } } }; @Override - protected void addNotify() { + protected void onAdd() { Case.addEventTypeSubscriber(EnumSet.of(Case.Events.DATA_SOURCE_ADDED), pcl); - reloadKeys(); } @Override - protected void removeNotify() { + protected void onRemove() { Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.DATA_SOURCE_ADDED), pcl); currentKeys.clear(); - setKeys(Collections.emptySet()); } - private void reloadKeys() { + @Override + protected List makeKeys() { try { if (datasourceObjId == 0) { currentKeys = Case.getCurrentCaseThrows().getDataSources(); @@ -135,20 +135,11 @@ public class DataSourcesNode extends DisplayableItemNode { }); - setKeys(currentKeys); } catch (TskCoreException | NoCurrentCaseException | TskDataException ex) { logger.log(Level.SEVERE, "Error getting data sources: {0}", ex.getMessage()); // NON-NLS - setKeys(Collections.emptySet()); - } - } - - /** - * Refresh all content keys This creates new nodes of keys have changed. - */ - public void refreshContentKeys() { - for (Content key : currentKeys) { - refreshKey(key); } + + return currentKeys; } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java index 4e81d4b428..25069d9564 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.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"); @@ -39,7 +39,6 @@ import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.datamodel.AbstractFile; @@ -358,7 +357,7 @@ public class DeletedContent implements AutopsyVisitableItem { } } - static class DeletedContentChildren extends ChildFactory.Detachable { + static class DeletedContentChildren extends BaseChildFactory { private final SleuthkitCase skCase; private final DeletedContent.DeletedContentFilter filter; @@ -368,6 +367,7 @@ public class DeletedContent implements AutopsyVisitableItem { private final long datasourceObjId; DeletedContentChildren(DeletedContent.DeletedContentFilter filter, SleuthkitCase skCase, Observable o, long datasourceObjId) { + super(filter.getName(), new ViewsKnownAndSlackFilter<>()); this.skCase = skCase; this.filter = filter; this.notifier = o; @@ -376,6 +376,11 @@ public class DeletedContent implements AutopsyVisitableItem { private final Observer observer = new DeletedContentChildrenObserver(); + @Override + protected List makeKeys() { + return runFsQuery(); + } + // Cause refresh of children if there are changes private class DeletedContentChildrenObserver implements Observer { @@ -386,25 +391,19 @@ public class DeletedContent implements AutopsyVisitableItem { } @Override - protected void addNotify() { + protected void onAdd() { if (notifier != null) { notifier.addObserver(observer); } } @Override - protected void removeNotify() { + protected void onRemove() { if (notifier != null) { notifier.deleteObserver(observer); } } - @Override - protected boolean createKeys(List list) { - list.addAll(runFsQuery()); - return true; - } - static private String makeQuery(DeletedContent.DeletedContentFilter filter, long filteringDSObjId) { String query = ""; switch (filter) { @@ -440,11 +439,6 @@ public class DeletedContent implements AutopsyVisitableItem { } - if (UserPreferences.hideKnownFilesInViewsTree()) { - query += " AND (known != " + TskData.FileKnown.KNOWN.getFileKnownValue() //NON-NLS - + " OR known IS NULL)"; //NON-NLS - } - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { query += " AND data_source_obj_id = " + filteringDSObjId; } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java index ff268f2de2..83fcee1b65 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-2018 Basis Technology Corp. + * Copyright 2012-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,7 +42,6 @@ import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; @@ -494,41 +493,52 @@ public class EmailExtracted implements AutopsyVisitableItem { /** * Node representing mail folder content (mail messages) */ - private class MessageFactory extends ChildFactory implements Observer { + private class MessageFactory extends BaseChildFactory implements Observer { private final String accountName; private final String folderName; private MessageFactory(String accountName, String folderName) { - super(); + super(accountName + "_" + folderName); this.accountName = accountName; this.folderName = folderName; emailResults.addObserver(this); } @Override - protected boolean createKeys(List list) { - list.addAll(emailResults.getArtifactIds(accountName, folderName)); - return true; - } - - @Override - protected Node createNodeForKey(Long artifactId) { - if (skCase == null) { - return null; - } - try { - BlackboardArtifact artifact = skCase.getBlackboardArtifact(artifactId); - return new BlackboardArtifactNode(artifact); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Error creating mail messages nodes", ex); //NON-NLS - } - return null; + protected Node createNodeForKey(BlackboardArtifact art) { + return new BlackboardArtifactNode(art); } @Override public void update(Observable o, Object arg) { refresh(true); } + + @Override + protected List makeKeys() { + List keys = new ArrayList<>(); + + if (skCase != null) { + emailResults.getArtifactIds(accountName, folderName).forEach((id) -> { + try { + keys.add(skCase.getBlackboardArtifact(id)); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Error getting mail messages keys", ex); //NON-NLS + } + }); + } + return keys; + } + + @Override + protected void onAdd() { + // No-op + } + + @Override + protected void onRemove() { + // No-op + } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java index 4b3694b35f..a21afff190 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java @@ -423,12 +423,12 @@ public class ExtractedContent implements AutopsyVisitableItem { /** * Creates children for a given artifact type */ - private class ArtifactFactory extends ChildFactory.Detachable { + private class ArtifactFactory extends BaseChildFactory { private BlackboardArtifact.Type type; public ArtifactFactory(BlackboardArtifact.Type type) { - super(); + super(type.getTypeName()); this.type = type; } @@ -481,36 +481,34 @@ public class ExtractedContent implements AutopsyVisitableItem { }; @Override - protected void addNotify() { + protected void onAdd() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); } @Override - protected void removeNotify() { + protected void onRemove() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); } - @Override - protected boolean createKeys(List list) { - if (skCase != null) { - try { - List arts = - Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) ? - blackboard.getArtifacts(type.getTypeID(), datasourceObjId) : - skCase.getBlackboardArtifacts(type.getTypeID()); - list.addAll(arts); - } catch (TskException ex) { - Logger.getLogger(ArtifactFactory.class.getName()).log(Level.SEVERE, "Couldn't get blackboard artifacts from database", ex); //NON-NLS - } - } - return true; - } - @Override protected Node createNodeForKey(BlackboardArtifact key) { return new BlackboardArtifactNode(key); } + + @Override + protected List makeKeys() { + if (skCase != null) { + try { + return Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) + ? blackboard.getArtifacts(type.getTypeID(), datasourceObjId) + : skCase.getBlackboardArtifacts(type.getTypeID()); + } catch (TskException ex) { + Logger.getLogger(ArtifactFactory.class.getName()).log(Level.SEVERE, "Couldn't get blackboard artifacts from database", ex); //NON-NLS + } + } + return Collections.emptyList(); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java index 53628326a2..e9c49c596e 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.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"); @@ -361,7 +361,7 @@ public class FileSize implements AutopsyVisitableItem { /* * Makes children, which are nodes for files of a given range */ - static class FileSizeChildren extends ChildFactory.Detachable { + static class FileSizeChildren extends BaseChildFactory { private final SleuthkitCase skCase; private final FileSizeFilter filter; @@ -377,6 +377,7 @@ public class FileSize implements AutopsyVisitableItem { * added to case */ FileSizeChildren(FileSizeFilter filter, SleuthkitCase skCase, Observable o, long dsObjId) { + super(filter.getName(), new ViewsKnownAndSlackFilter<>()); this.skCase = skCase; this.filter = filter; this.notifier = o; @@ -385,14 +386,14 @@ public class FileSize implements AutopsyVisitableItem { } @Override - protected void addNotify() { + protected void onAdd() { if (notifier != null) { notifier.addObserver(observer); } } @Override - protected void removeNotify() { + protected void onRemove() { if (notifier != null) { notifier.deleteObserver(observer); } @@ -400,6 +401,11 @@ public class FileSize implements AutopsyVisitableItem { private final Observer observer = new FileSizeChildrenObserver(); + @Override + protected List makeKeys() { + return runFsQuery(); + } + // Cause refresh of children if there are changes private class FileSizeChildrenObserver implements Observer { @@ -409,12 +415,6 @@ public class FileSize implements AutopsyVisitableItem { } } - @Override - protected boolean createKeys(List list) { - list.addAll(runFsQuery()); - return true; - } - private static String makeQuery(FileSizeFilter filter, long filteringDSObjId) { String query; switch (filter) { @@ -436,17 +436,6 @@ public class FileSize implements AutopsyVisitableItem { // Ignore unallocated block files. query = query + " AND (type != " + TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS.getFileType() + ")"; //NON-NLS - // Hide known files if indicated in the user preferences. - if(UserPreferences.hideKnownFilesInViewsTree()) { - query += " AND (known != " + TskData.FileKnown.KNOWN.getFileKnownValue() //NON-NLS - + " OR known IS NULL)"; //NON-NLS - } - - // Hide slack files if indicated in the user preferences. - if(UserPreferences.hideSlackFilesInViewsTree()) { - query += " AND (type != " + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.getFileType() + ")"; //NON-NLS - } - // filter by datasource if indicated in case preferences if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { query += " AND data_source_obj_id = " + filteringDSObjId; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java index 5f1f33607d..152e8a6bac 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.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"); @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.datamodel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -292,7 +293,7 @@ public final class FileTypesByExtension implements AutopsyVisitableItem { * should refresh */ FileExtensionNode(FileTypesByExtension.SearchFilterInterface filter, SleuthkitCase skCase, FileTypesByExtObservable o) { - super(typesRoot, Children.create(new FileExtensionNodeChildren(filter, skCase, o), true), + super(typesRoot, Children.create(new FileExtensionNodeChildren(filter, skCase, o, filter.getDisplayName()), true), Lookups.singleton(filter.getDisplayName())); this.filter = filter; super.setName(filter.getDisplayName()); @@ -377,7 +378,7 @@ public final class FileTypesByExtension implements AutopsyVisitableItem { /** * Child node factory for a specific file type - does the database query. */ - private class FileExtensionNodeChildren extends ChildFactory.Detachable implements Observer { + private class FileExtensionNodeChildren extends BaseChildFactory implements Observer { private final SleuthkitCase skCase; private final FileTypesByExtension.SearchFilterInterface filter; @@ -390,22 +391,22 @@ public final class FileTypesByExtension implements AutopsyVisitableItem { * @param o Observable that will notify when there could be new * data to display */ - private FileExtensionNodeChildren(FileTypesByExtension.SearchFilterInterface filter, SleuthkitCase skCase, Observable o) { - super(); + private FileExtensionNodeChildren(FileTypesByExtension.SearchFilterInterface filter, SleuthkitCase skCase, Observable o, String nodeName) { + super(nodeName, new ViewsKnownAndSlackFilter<>()); this.filter = filter; this.skCase = skCase; notifier = o; } @Override - protected void addNotify() { + protected void onAdd() { if (notifier != null) { notifier.addObserver(this); } } @Override - protected void removeNotify() { + protected void onRemove() { if (notifier != null) { notifier.deleteObserver(this); } @@ -417,19 +418,19 @@ public final class FileTypesByExtension implements AutopsyVisitableItem { } @Override - protected boolean createKeys(List list) { - try { - list.addAll(skCase.findAllFilesWhere(createQuery(filter)) - .stream().map(f -> new FileTypesKey(f)).collect(Collectors.toList())); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Couldn't get search results", ex); //NON-NLS - } - return true; + protected Node createNodeForKey(FileTypesKey key) { + return key.accept(new FileTypes.FileNodeCreationVisitor()); } @Override - protected Node createNodeForKey(FileTypesKey key) { - return key.accept(new FileTypes.FileNodeCreationVisitor()); + protected List makeKeys() { + try { + return skCase.findAllFilesWhere(createQuery(filter)) + .stream().map(f -> new FileTypesKey(f)).collect(Collectors.toList()); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Couldn't get search results", ex); //NON-NLS + } + return Collections.emptyList(); } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java index 54bb2e7285..1e4f61fc87 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.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"); @@ -445,27 +445,16 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi * files that match MimeType which is represented by this position in the * tree. */ - private class MediaSubTypeNodeChildren extends ChildFactory.Detachable implements Observer { + private class MediaSubTypeNodeChildren extends BaseChildFactory implements Observer { private final String mimeType; private MediaSubTypeNodeChildren(String mimeType) { - super(); + super(mimeType, new ViewsKnownAndSlackFilter<>()); addObserver(this); this.mimeType = mimeType; } - @Override - protected boolean createKeys(List list) { - try { - list.addAll(skCase.findAllFilesWhere(createBaseWhereExpr() + " AND mime_type = '" + mimeType + "'") - .stream().map(f -> new FileTypesKey(f)).collect(Collectors.toList())); //NON-NLS - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Couldn't get search results", ex); //NON-NLS - } - return true; - } - @Override public void update(Observable o, Object arg) { refresh(true); @@ -475,5 +464,26 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi protected Node createNodeForKey(FileTypesKey key) { return key.accept(new FileTypes.FileNodeCreationVisitor()); } + + @Override + protected List makeKeys() { + try { + return skCase.findAllFilesWhere(createBaseWhereExpr() + " AND mime_type = '" + mimeType + "'") + .stream().map(f -> new FileTypesKey(f)).collect(Collectors.toList()); //NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Couldn't get search results", ex); //NON-NLS + } + return Collections.emptyList(); + } + + @Override + protected void onAdd() { + // No-op + } + + @Override + protected void onRemove() { + // No-op + } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java index 3262087487..9d6ed53431 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.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"); @@ -44,7 +44,6 @@ import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; @@ -378,60 +377,53 @@ public class HashsetHits implements AutopsyVisitableItem { /** * Creates the nodes for the hits in a given set. */ - private class HitFactory extends ChildFactory.Detachable implements Observer { + private class HitFactory extends BaseChildFactory implements Observer { private String hashsetName; private Map artifactHits = new HashMap<>(); private HitFactory(String hashsetName) { - super(); + super(hashsetName); this.hashsetName = hashsetName; } @Override - protected void addNotify() { + protected void onAdd() { hashsetResults.addObserver(this); } @Override - protected void removeNotify() { + protected void onRemove() { hashsetResults.deleteObserver(this); } @Override - protected boolean createKeys(List list) { - - if (skCase == null) { - return true; - } - - hashsetResults.getArtifactIds(hashsetName).forEach((id) -> { - try { - if (!artifactHits.containsKey(id)) { - BlackboardArtifact art = skCase.getBlackboardArtifact(id); - artifactHits.put(id, art); - } - } catch (TskException ex) { - logger.log(Level.SEVERE, "TSK Exception occurred", ex); //NON-NLS - } - }); - - // Adding all keys at once is more efficient than adding one at a - // time because Netbeans triggers internal processing each time an - // element is added to the list. - list.addAll(artifactHits.keySet()); - return true; - } - - @Override - protected Node createNodeForKey(Long id) { - BlackboardArtifact art = artifactHits.get(id); - return (null == art) ? null : new BlackboardArtifactNode(art); + protected Node createNodeForKey(BlackboardArtifact key) { + return new BlackboardArtifactNode(key); } @Override public void update(Observable o, Object arg) { refresh(true); } + + @Override + protected List makeKeys() { + if (skCase != null) { + + hashsetResults.getArtifactIds(hashsetName).forEach((id) -> { + try { + if (!artifactHits.containsKey(id)) { + BlackboardArtifact art = skCase.getBlackboardArtifact(id); + artifactHits.put(id, art); + } + } catch (TskException ex) { + logger.log(Level.SEVERE, "TSK Exception occurred", ex); //NON-NLS + } + }); + return new ArrayList<>(artifactHits.values()); + } + return Collections.emptyList(); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java index 06560e6b13..584a405fcc 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.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"); @@ -28,7 +28,6 @@ import java.util.EnumSet; import java.util.List; import java.util.logging.Level; import javax.swing.Action; -import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; @@ -47,6 +46,7 @@ import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.SleuthkitCase.CaseDbQuery; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.VirtualDirectory; +import org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException; /** * This class is used to represent the "Node" for the image. The children of @@ -224,18 +224,23 @@ public class ImageNode extends AbstractContentNode { if (parent != null) { // Is this a new carved file? if (parent.getName().equals(VirtualDirectory.NAME_CARVED)) { - // Was this new carved file produced from this image? - if (parent.getParent().getId() == getContent().getId()) { - Children children = getChildren(); - if (children != null) { - ((ContentChildren) children).refreshChildren(); - children.getNodesCount(); + // Is this new carved file for this data source? + if (newContent.getDataSource().getId() == getContent().getDataSource().getId()) { + // Find the image (if any) associated with the new content and + // trigger a refresh if it matches the image wrapped by this node. + while ((parent = parent.getParent()) != null) { + if (parent.getId() == getContent().getId()) { + BaseChildFactory.post(getName(), new BaseChildFactory.RefreshKeysEvent()); + break; + } } } } } } catch (TskCoreException ex) { // Do nothing. + } catch (NoSuchEventBusException ex) { + logger.log(Level.WARNING, "Failed to post key refresh event.", ex); // NON-NLS } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { if (evt.getNewValue() == null) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java index 8782b0fb89..c36833992c 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.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"); @@ -449,51 +449,57 @@ public class InterestingHits implements AutopsyVisitableItem { } } - private class HitFactory extends ChildFactory implements Observer { + private class HitFactory extends BaseChildFactory implements Observer { private final String setName; private final String typeName; private final Map artifactHits = new HashMap<>(); private HitFactory(String setName, String typeName) { - super(); + super(typeName); this.setName = setName; this.typeName = typeName; interestingResults.addObserver(this); } @Override - protected boolean createKeys(List list) { + protected List makeKeys() { - if (skCase == null) { - return true; - } - - interestingResults.getArtifactIds(setName, typeName).forEach((id) -> { - try { - if (!artifactHits.containsKey(id)) { - BlackboardArtifact art = skCase.getBlackboardArtifact(id); - artifactHits.put(id, art); + if (skCase != null) { + interestingResults.getArtifactIds(setName, typeName).forEach((id) -> { + try { + if (!artifactHits.containsKey(id)) { + BlackboardArtifact art = skCase.getBlackboardArtifact(id); + artifactHits.put(id, art); + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "TSK Exception occurred", ex); //NON-NLS } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "TSK Exception occurred", ex); //NON-NLS - } - }); + }); - list.addAll(artifactHits.keySet()); - - return true; + return new ArrayList<>(artifactHits.values()); + } + return Collections.emptyList(); } @Override - protected Node createNodeForKey(Long l) { - BlackboardArtifact art = artifactHits.get(l); - return (null == art) ? null : new BlackboardArtifactNode(art); + protected Node createNodeForKey(BlackboardArtifact art) { + return new BlackboardArtifactNode(art); } @Override public void update(Observable o, Object arg) { refresh(true); } + + @Override + protected void onAdd() { + // No-op + } + + @Override + protected void onRemove() { + // No-op + } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java index c80069c7ef..3cea364bf5 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.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"); @@ -25,6 +25,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -34,7 +35,6 @@ import java.util.Observable; import java.util.Observer; import java.util.Set; import java.util.logging.Level; -import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; @@ -46,7 +46,6 @@ import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import static org.sleuthkit.autopsy.datamodel.Bundle.*; import org.sleuthkit.autopsy.ingest.IngestManager; @@ -85,7 +84,6 @@ public class KeywordHits implements AutopsyVisitableItem { */ private static final String DEFAULT_INSTANCE_NAME = "DEFAULT_INSTANCE_NAME"; - /** * query attributes table for the ones that we need for the tree */ @@ -108,20 +106,20 @@ public class KeywordHits implements AutopsyVisitableItem { /** * Constructor - * - * @param skCase Case DB - */ + * + * @param skCase Case DB + */ KeywordHits(SleuthkitCase skCase) { this(skCase, 0); } - + /** * Constructor - * - * @param skCase Case DB - * @param objId Object id of the data source - * - */ + * + * @param skCase Case DB + * @param objId Object id of the data source + * + */ public KeywordHits(SleuthkitCase skCase, long objId) { this.skCase = skCase; this.datasourceObjId = objId; @@ -324,9 +322,9 @@ public class KeywordHits implements AutopsyVisitableItem { String queryStr = KEYWORD_HIT_ATTRIBUTES_QUERY; if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { - queryStr += " AND blackboard_artifacts.data_source_obj_id = " + datasourceObjId; + queryStr += " AND blackboard_artifacts.data_source_obj_id = " + datasourceObjId; } - + try (CaseDbQuery dbQuery = skCase.executeQuery(queryStr)) { ResultSet resultSet = dbQuery.getResultSet(); while (resultSet.next()) { @@ -510,9 +508,11 @@ public class KeywordHits implements AutopsyVisitableItem { } private abstract class KWHitsNodeBase extends DisplayableItemNode implements Observer { - - private KWHitsNodeBase(Children children, Lookup lookup) { + private String displayName; + + private KWHitsNodeBase(Children children, Lookup lookup, String displayName) { super(children, lookup); + this.displayName = displayName; } private KWHitsNodeBase(Children children) { @@ -530,7 +530,7 @@ public class KeywordHits implements AutopsyVisitableItem { } final void updateDisplayName() { - super.setDisplayName(getName() + " (" + countTotalDescendants() + ")"); + super.setDisplayName(displayName + " (" + countTotalDescendants() + ")"); } abstract int countTotalDescendants(); @@ -545,7 +545,7 @@ public class KeywordHits implements AutopsyVisitableItem { private final String listName; private ListNode(String listName) { - super(Children.create(new TermFactory(listName), true), Lookups.singleton(listName)); + super(Children.create(new TermFactory(listName), true), Lookups.singleton(listName), listName); super.setName(listName); this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/keyword_hits.png"); //NON-NLS this.listName = listName; @@ -631,6 +631,24 @@ public class KeywordHits implements AutopsyVisitableItem { } } + /** + * Create a ChildFactory object for the given set name and keyword. + * + * The type of ChildFactory we create is based on whether the node + * represents a regular expression keyword search or not. For regular + * expression keyword searches there will be an extra layer in the tree that + * represents each of the individual terms found by the regular expression. + * E.g., for an email regular expression search there will be a node in the + * tree for every email address hit. + */ + ChildFactory createChildFactory(String setName, String keyword) { + if (isOnlyDefaultInstance(keywordResults.getKeywordInstances(setName, keyword))) { + return new HitsFactory(setName, keyword, DEFAULT_INSTANCE_NAME); + } else { + return new RegExpInstancesFactory(setName, keyword); + } + } + /** * Represents the search term or regexp that user searched for */ @@ -640,8 +658,16 @@ public class KeywordHits implements AutopsyVisitableItem { private final String keyword; private TermNode(String setName, String keyword) { - super(Children.create(new RegExpInstancesFactory(setName, keyword), true), Lookups.singleton(keyword)); - super.setName(keyword); + super(Children.create(createChildFactory(setName, keyword), true), Lookups.singleton(keyword), keyword); + + /** + * We differentiate between the programmatic name and the display + * name. The programmatic name is used to create an association with + * an event bus and must be the same as the node name passed by our + * ChildFactory to it's parent constructor. See the HitsFactory + * constructor for an example. + */ + super.setName(setName + "_" + keyword); this.setName = setName; this.keyword = keyword; this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/keyword_hits.png"); //NON-NLS @@ -694,45 +720,11 @@ public class KeywordHits implements AutopsyVisitableItem { } } - /** - * Allows us to pass in either longs or strings as they keys for different - * types of nodes at the same level. Probably a better way to do this, but - * it works. - */ - private class RegExpInstanceKey { - - private final boolean isRegExp; - private String strKey; - private Long longKey; - - RegExpInstanceKey(String key) { - isRegExp = true; - strKey = key; - } - - RegExpInstanceKey(Long key) { - isRegExp = false; - longKey = key; - } - - boolean isRegExp() { - return isRegExp; - } - - Long getIdKey() { - return longKey; - } - - String getRegExpKey() { - return strKey; - } - } - /** * Creates the nodes for a given regexp that represent the specific terms * that were found */ - private class RegExpInstancesFactory extends DetachableObserverChildFactory { + private class RegExpInstancesFactory extends DetachableObserverChildFactory { private final String keyword; private final String setName; @@ -744,33 +736,15 @@ public class KeywordHits implements AutopsyVisitableItem { } @Override - protected boolean createKeys(List list) { - List instances = keywordResults.getKeywordInstances(setName, keyword); - // The keys are different depending on what we are displaying. - // regexp get another layer to show instances. - // Exact/substring matches don't. - if (isOnlyDefaultInstance(instances)) { - list.addAll(keywordResults.getArtifactIds(setName, keyword, DEFAULT_INSTANCE_NAME).stream() - .map(RegExpInstanceKey::new) - .collect(Collectors.toList())); - } else { - list.addAll(instances.stream() - .map(RegExpInstanceKey::new) - .collect(Collectors.toList())); - } + protected boolean createKeys(List list) { + list.addAll(keywordResults.getKeywordInstances(setName, keyword)); return true; } @Override - protected Node createNodeForKey(RegExpInstanceKey key) { - if (key.isRegExp()) { - return new RegExpInstanceNode(setName, keyword, key.getRegExpKey()); - } else { - // if it isn't a regexp, then skip the 'instance' layer of the tree - return createBlackboardArtifactNode(key.getIdKey()); - } + protected Node createNodeForKey(String key) { + return new RegExpInstanceNode(setName, keyword, key); } - } /** @@ -783,8 +757,16 @@ public class KeywordHits implements AutopsyVisitableItem { private final String instance; private RegExpInstanceNode(String setName, String keyword, String instance) { - super(Children.create(new HitsFactory(setName, keyword, instance), true), Lookups.singleton(instance)); - super.setName(instance); //the instance represents the name of the keyword hit at this point as the keyword is the regex + super(Children.create(new HitsFactory(setName, keyword, instance), true), Lookups.singleton(instance), instance); + + /** + * We differentiate between the programmatic name and the display + * name. The programmatic name is used to create an association with + * an event bus and must be the same as the node name passed by our + * ChildFactory to it's parent constructor. See the HitsFactory + * constructor for an example. + */ + super.setName(setName + "_" + keyword + "_" + instance); this.setName = setName; this.keyword = keyword; this.instance = instance; @@ -837,7 +819,7 @@ public class KeywordHits implements AutopsyVisitableItem { /** * Create a blackboard node for the given Keyword Hit artifact * - * @param artifactId + * @param art * * @return Node or null on error */ @@ -850,81 +832,110 @@ public class KeywordHits implements AutopsyVisitableItem { "KeywordHits.createNodeForKey.chgTime.name=ChangeTime", "KeywordHits.createNodeForKey.chgTime.displayName=Change Time", "KeywordHits.createNodeForKey.chgTime.desc=Change Time"}) - private BlackboardArtifactNode createBlackboardArtifactNode(Long artifactId) { + private BlackboardArtifactNode createBlackboardArtifactNode(BlackboardArtifact art) { if (skCase == null) { return null; } - try { - BlackboardArtifact art = skCase.getBlackboardArtifact(artifactId); - BlackboardArtifactNode n = new BlackboardArtifactNode(art); - // The associated file should be available through the Lookup that - // gets created when the BlackboardArtifactNode is constructed. - AbstractFile file = n.getLookup().lookup(AbstractFile.class); - if (file == null) { - try { - file = skCase.getAbstractFileById(art.getObjectID()); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "TskCoreException while constructing BlackboardArtifact Node from KeywordHitsKeywordChildren", ex); //NON-NLS - return n; - } - } - /* - * It is possible to get a keyword hit on artifacts generated for - * the underlying image in which case MAC times are not - * available/applicable/useful. - */ - if (file == null) { + BlackboardArtifactNode n = new BlackboardArtifactNode(art); //NON-NLS + + // The associated file should be available through the Lookup that + // gets created when the BlackboardArtifactNode is constructed. + AbstractFile file = n.getLookup().lookup(AbstractFile.class); + if (file == null) { + try { + file = skCase.getAbstractFileById(art.getObjectID()); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "TskCoreException while constructing BlackboardArtifact Node from KeywordHitsKeywordChildren", ex); //NON-NLS return n; } - - n.addNodeProperty(new NodeProperty<>( - KeywordHits_createNodeForKey_modTime_name(), - KeywordHits_createNodeForKey_modTime_displayName(), - KeywordHits_createNodeForKey_modTime_desc(), - ContentUtils.getStringTime(file.getMtime(), file))); - n.addNodeProperty(new NodeProperty<>( - KeywordHits_createNodeForKey_accessTime_name(), - KeywordHits_createNodeForKey_accessTime_displayName(), - KeywordHits_createNodeForKey_accessTime_desc(), - ContentUtils.getStringTime(file.getAtime(), file))); - n.addNodeProperty(new NodeProperty<>( - KeywordHits_createNodeForKey_chgTime_name(), - KeywordHits_createNodeForKey_chgTime_displayName(), - KeywordHits_createNodeForKey_chgTime_desc(), - ContentUtils.getStringTime(file.getCtime(), file))); - return n; - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "TSK Exception occurred", ex); //NON-NLS } - return null; + /* + * It is possible to get a keyword hit on artifacts generated for the + * underlying image in which case MAC times are not + * available/applicable/useful. + */ + if (file == null) { + return n; + } + n.addNodeProperty(new NodeProperty<>( + KeywordHits_createNodeForKey_modTime_name(), + KeywordHits_createNodeForKey_modTime_displayName(), + KeywordHits_createNodeForKey_modTime_desc(), + ContentUtils.getStringTime(file.getMtime(), file))); + n.addNodeProperty(new NodeProperty<>( + KeywordHits_createNodeForKey_accessTime_name(), + KeywordHits_createNodeForKey_accessTime_displayName(), + KeywordHits_createNodeForKey_accessTime_desc(), + ContentUtils.getStringTime(file.getAtime(), file))); + n.addNodeProperty(new NodeProperty<>( + KeywordHits_createNodeForKey_chgTime_name(), + KeywordHits_createNodeForKey_chgTime_displayName(), + KeywordHits_createNodeForKey_chgTime_desc(), + ContentUtils.getStringTime(file.getCtime(), file))); + return n; } /** * Creates nodes for individual files that had hits */ - private class HitsFactory extends DetachableObserverChildFactory { + private class HitsFactory extends BaseChildFactory implements Observer { private final String keyword; private final String setName; private final String instance; + private final Map artifactHits = new HashMap<>(); private HitsFactory(String setName, String keyword, String instance) { - super(); + /** + * The node name passed to the parent constructor will consist of + * the set name, keyword and optionally the instance name (in the + * case of regular expression hits. This name must match the name + * set in the TermNode or RegExpInstanceNode constructors. + */ + super(setName + "_" + keyword + (DEFAULT_INSTANCE_NAME.equals(instance) ? "" : "_" + instance)); this.setName = setName; this.keyword = keyword; this.instance = instance; } @Override - protected boolean createKeys(List list) { - list.addAll(keywordResults.getArtifactIds(setName, keyword, instance)); - return true; + protected List makeKeys() { + if (skCase != null) { + keywordResults.getArtifactIds(setName, keyword, instance).forEach((id) -> { + try { + if (!artifactHits.containsKey(id)) { + BlackboardArtifact art = skCase.getBlackboardArtifact(id); + artifactHits.put(id, art); + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "TSK Exception occurred", ex); //NON-NLS + } + }); + + return new ArrayList<>(artifactHits.values()); + } + return Collections.emptyList(); } @Override - protected Node createNodeForKey(Long artifactId) { - return createBlackboardArtifactNode(artifactId); + protected Node createNodeForKey(BlackboardArtifact art) { + return createBlackboardArtifactNode(art); + } + + @Override + protected void onAdd() { + keywordResults.addObserver(this); + } + + @Override + protected void onRemove() { + keywordResults.deleteObserver(this); + } + + @Override + public void update(Observable o, Object arg) { + refresh(true); } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/KnownAndSlackFilterBase.java b/Core/src/org/sleuthkit/autopsy/datamodel/KnownAndSlackFilterBase.java new file mode 100644 index 0000000000..5f5838db42 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/KnownAndSlackFilterBase.java @@ -0,0 +1,53 @@ +/* + * 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.util.function.Predicate; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskData; + +/** + * Predicate that can be used to filter known and/or slack files from + * Content collections based on user preferences. + */ +abstract class KnownAndSlackFilterBase implements Predicate { + protected static boolean filterKnown; + protected static boolean filterSlack; + + @Override + public boolean test(T t) { + AbstractFile af = null; + + if (t instanceof AbstractFile) { + af = (AbstractFile) t; + } + + if (af != null) { + if (af.getKnown() == TskData.FileKnown.KNOWN && filterKnown) { + return false; + } + if (af.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK) && filterSlack) { + return false; + } + } + + return true; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java index f04495a706..66291955a8 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,14 +20,20 @@ package org.sleuthkit.autopsy.datamodel; import java.util.Collection; import java.util.Collections; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.datamodel.accounts.Accounts; /** * Children implementation for the root node of a ContentNode tree. Accepts a * list of root Content objects for the tree. */ -public class RootContentChildren extends AbstractContentChildren { +public class RootContentChildren extends Children.Keys { private final Collection contentKeys; + private final CreateAutopsyNodeVisitor createAutopsyNodeVisitor = new CreateAutopsyNodeVisitor(); /** * @param contentKeys root Content objects for the Node tree @@ -56,4 +62,120 @@ public class RootContentChildren extends AbstractContentChildren { public void refreshContentKeys() { contentKeys.forEach(this::refreshKey); } + + @Override + protected Node[] createNodes(Object key) { + if (key instanceof AutopsyVisitableItem) { + return new Node[] {((AutopsyVisitableItem)key).accept(createAutopsyNodeVisitor)}; + } else { + return null; + } + } + + /** + * Gets a DisplayableItemNode for use as a subtree root node for the Autopsy + * tree view from each type of AutopsyVisitableItem visited. There are + * AutopsyVisitableItems for the Data Sources, Views, Results, and Reports + * subtrees, and for the subtrees of Results (e.g., Extracted Content, Hash + * Set Hits, etc.). + */ + static class CreateAutopsyNodeVisitor extends AutopsyItemVisitor.Default { + + @Override + public ExtractedContent.RootNode visit(ExtractedContent ec) { + return ec.new RootNode(ec.getSleuthkitCase()); + } + + @Override + public AbstractNode visit(FileTypesByExtension sf) { + return sf.new FileTypesByExtNode(sf.getSleuthkitCase(), null); + } + + @Override + public AbstractNode visit(RecentFiles rf) { + return new RecentFilesNode(rf.getSleuthkitCase()); + } + + @Override + public AbstractNode visit(DeletedContent dc) { + return new DeletedContent.DeletedContentsNode(dc.getSleuthkitCase(), dc.filteringDataSourceObjId()); + } + + @Override + public AbstractNode visit(FileSize dc) { + return new FileSize.FileSizeRootNode(dc.getSleuthkitCase(), dc.filteringDataSourceObjId()); + } + + @Override + public AbstractNode visit(KeywordHits kh) { + return kh.new RootNode(); + } + + @Override + public AbstractNode visit(HashsetHits hh) { + return hh.new RootNode(); + } + + @Override + public AbstractNode visit(InterestingHits ih) { + return ih.new RootNode(); + } + + @Override + public AbstractNode visit(EmailExtracted ee) { + return ee.new RootNode(); + } + + @Override + public AbstractNode visit(Tags tagsNodeKey) { + return tagsNodeKey.new RootNode(tagsNodeKey.filteringDataSourceObjId()); + } + + @Override + public AbstractNode visit(DataSources i) { + return new DataSourcesNode(i.filteringDataSourceObjId()); + } + + @Override + public AbstractNode visit(DataSourceGrouping datasourceGrouping) { + return new DataSourceGroupingNode(datasourceGrouping.getDataSource()); + } + + @Override + public AbstractNode visit(Views v) { + return new ViewsNode(v.getSleuthkitCase(), v.filteringDataSourceObjId()); + } + + @Override + public AbstractNode visit(Results results) { + return new ResultsNode(results.getSleuthkitCase(), results.filteringDataSourceObjId() ); + } + + @Override + public AbstractNode visit(FileTypes ft) { + return ft.new FileTypesNode(); + } + + @Override + public AbstractNode visit(Reports reportsItem) { + return new Reports.ReportsListNode(); + } + + @Override + public AbstractNode visit(Accounts accountsItem) { + return accountsItem.new AccountsRootNode(); + } + + @Override + protected AbstractNode defaultVisit(AutopsyVisitableItem di) { + throw new UnsupportedOperationException( + NbBundle.getMessage(this.getClass(), + "AbstractContentChildren.createAutopsyNodeVisitor.exception.noNodeMsg")); + } + + @Override + public AbstractNode visit(FileTypesByMimeType ftByMimeTypeItem) { + return ftByMimeTypeItem.new ByMimeTypeNode(); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ViewsKnownAndSlackFilter.java b/Core/src/org/sleuthkit/autopsy/datamodel/ViewsKnownAndSlackFilter.java new file mode 100644 index 0000000000..f6b7874122 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ViewsKnownAndSlackFilter.java @@ -0,0 +1,52 @@ +/* + * 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.util.prefs.PreferenceChangeEvent; +import org.sleuthkit.autopsy.core.UserPreferences; +import static org.sleuthkit.autopsy.datamodel.KnownAndSlackFilterBase.filterKnown; +import static org.sleuthkit.autopsy.datamodel.KnownAndSlackFilterBase.filterSlack; +import org.sleuthkit.datamodel.Content; + +/** + * Known and Slack filter for Views section of the tree. + * + * @param + */ +class ViewsKnownAndSlackFilter extends KnownAndSlackFilterBase { + + static { + /** + * Watch for user preference changes and update variables inherited from + * our parent. The actual filtering is provided by our parent class. + */ + UserPreferences.addChangeListener((PreferenceChangeEvent evt) -> { + if (evt.getKey().equals(UserPreferences.HIDE_KNOWN_FILES_IN_VIEWS_TREE)) { + filterKnown = UserPreferences.hideKnownFilesInViewsTree(); + } else if (evt.getKey().equals(UserPreferences.HIDE_SLACK_FILES_IN_VIEWS_TREE)) { + filterSlack = UserPreferences.hideSlackFilesInViewsTree(); + } + }); + } + + ViewsKnownAndSlackFilter() { + filterKnown = UserPreferences.hideKnownFilesInViewsTree(); + filterSlack = UserPreferences.hideSlackFilesInViewsTree(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java index 9612441910..d25b50d59f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,11 +23,13 @@ import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import java.util.logging.Level; import javax.swing.Action; -import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException; import org.sleuthkit.autopsy.directorytree.ExplorerNodeActionVisitor; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.ingest.IngestManager; @@ -43,6 +45,7 @@ import org.sleuthkit.autopsy.directorytree.FileSystemDetailsAction; * root directory of a file system */ public class VolumeNode extends AbstractContentNode { + private static final Logger logger = Logger.getLogger(VolumeNode.class.getName()); /** * Helper so that the display name and the name used in building the path @@ -105,18 +108,23 @@ public class VolumeNode extends AbstractContentNode { if (parent != null) { // Is this a new carved file? if (parent.getName().equals(VirtualDirectory.NAME_CARVED)) { - // Was this new carved file produced from this volume? - if (parent.getParent().getId() == getContent().getId()) { - Children children = getChildren(); - if (children != null) { - ((ContentChildren) children).refreshChildren(); - children.getNodesCount(); + // Is this new carved file for this data source? + if (newContent.getDataSource().getId() == getContent().getDataSource().getId()) { + // Find the volume (if any) associated with the new content and + // trigger a refresh if it matches the volume wrapped by this node. + while ((parent = parent.getParent()) != null) { + if (parent.getId() == getContent().getId()) { + BaseChildFactory.post(getName(), new BaseChildFactory.RefreshKeysEvent()); + break; + } } } } } } catch (TskCoreException ex) { // Do nothing. + } catch (NoSuchEventBusException ex) { + logger.log(Level.WARNING, eventType, ex); } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { if (evt.getNewValue() == null) { diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java index d98e58fe70..0b3a6f698f 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java @@ -26,8 +26,6 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.logging.Level; -import java.util.prefs.PreferenceChangeEvent; -import java.util.prefs.PreferenceChangeListener; import javax.swing.AbstractAction; import javax.swing.Action; import org.openide.explorer.ExplorerManager; @@ -41,7 +39,6 @@ import org.sleuthkit.autopsy.actions.AddBlackboardArtifactTagAction; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileBlackboardArtifactTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; -import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType; @@ -80,7 +77,6 @@ import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; import org.sleuthkit.datamodel.LocalDirectory; import org.sleuthkit.datamodel.SlackFile; -import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskException; import org.sleuthkit.datamodel.VirtualDirectory; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; @@ -96,33 +92,6 @@ public class DataResultFilterNode extends FilterNode { private static final Logger LOGGER = Logger.getLogger(DataResultFilterNode.class.getName()); - private static boolean filterKnownFromDataSources = UserPreferences.hideKnownFilesInDataSourcesTree(); - private static boolean filterKnownFromViews = UserPreferences.hideKnownFilesInViewsTree(); - private static boolean filterSlackFromDataSources = UserPreferences.hideSlackFilesInDataSourcesTree(); - private static boolean filterSlackFromViews = UserPreferences.hideSlackFilesInViewsTree(); - - static { - UserPreferences.addChangeListener(new PreferenceChangeListener() { - @Override - public void preferenceChange(PreferenceChangeEvent evt) { - switch (evt.getKey()) { - case UserPreferences.HIDE_KNOWN_FILES_IN_DATA_SRCS_TREE: - filterKnownFromDataSources = UserPreferences.hideKnownFilesInDataSourcesTree(); - break; - case UserPreferences.HIDE_KNOWN_FILES_IN_VIEWS_TREE: - filterKnownFromViews = UserPreferences.hideKnownFilesInViewsTree(); - break; - case UserPreferences.HIDE_SLACK_FILES_IN_DATA_SRCS_TREE: - filterSlackFromDataSources = UserPreferences.hideSlackFilesInDataSourcesTree(); - break; - case UserPreferences.HIDE_SLACK_FILES_IN_VIEWS_TREE: - filterSlackFromViews = UserPreferences.hideSlackFilesInViewsTree(); - break; - } - } - }); - } - static private final DisplayableItemNodeVisitor> getActionsDIV = new GetPopupActionsDisplayableItemNodeVisitor(); private final DisplayableItemNodeVisitor getPreferredActionsDIV = new GetPreferredActionsDisplayableItemNodeVisitor(); @@ -155,24 +124,6 @@ public class DataResultFilterNode extends FilterNode { this.sourceEm = em; } - /** - * Constructs a node used to wrap another node before passing it to the - * result viewers. The wrapper node defines the actions associated with the - * wrapped node and may filter out some of its children. - * - * @param node The node to wrap. - * @param em The ExplorerManager for the component that is creating - * the node. - * @param filterKnown Whether or not to filter out children that represent - * known files. - * @param filterSlack Whether or not to filter out children that represent - * virtual slack space files. - */ - private DataResultFilterNode(Node node, ExplorerManager em, boolean filterKnown, boolean filterSlack) { - super(node, new DataResultFilterChildren(node, em, filterKnown, filterSlack)); - this.sourceEm = em; - } - /** * Right click action for the nodes that we want to pass to the directory * table and the output view. @@ -294,10 +245,7 @@ public class DataResultFilterNode extends FilterNode { private static class DataResultFilterChildren extends FilterNode.Children { private final ExplorerManager sourceEm; - - private boolean filterKnown; - private boolean filterSlack; - private boolean filterArtifacts; // display message artifacts in the DataSource subtree + private final boolean filterArtifacts; // display message artifacts in the DataSource subtree /** * the constructor @@ -305,46 +253,13 @@ public class DataResultFilterNode extends FilterNode { private DataResultFilterChildren(Node arg, ExplorerManager sourceEm) { super(arg); - this.filterArtifacts = false; - switch (SelectionContext.getSelectionContext(arg)) { - case DATA_SOURCES: - filterSlack = filterSlackFromDataSources; - filterKnown = filterKnownFromDataSources; - filterArtifacts = true; - break; - case VIEWS: - filterSlack = filterSlackFromViews; - filterKnown = filterKnownFromViews; - break; - default: - filterSlack = false; - filterKnown = false; - break; - } - this.sourceEm = sourceEm; - } + filterArtifacts = SelectionContext.getSelectionContext(arg).equals(SelectionContext.DATA_SOURCES); - private DataResultFilterChildren(Node arg, ExplorerManager sourceEm, boolean filterKnown, boolean filterSlack) { - super(arg); - this.filterKnown = filterKnown; - this.filterSlack = filterSlack; this.sourceEm = sourceEm; } @Override protected Node[] createNodes(Node key) { - AbstractFile file = key.getLookup().lookup(AbstractFile.class); - if (file != null) { - if (filterKnown && (file.getKnown() == TskData.FileKnown.KNOWN)) { - // Filter out child nodes that represent known files - return new Node[]{}; - } - if (filterSlack && file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK)) { - // Filter out child nodes that represent slack files - return new Node[]{}; - } - } - // filter out all non-message artifacts, if displaying the results from the Data Source tree BlackboardArtifact art = key.getLookup().lookup(BlackboardArtifact.class); if (art != null @@ -354,9 +269,8 @@ public class DataResultFilterNode extends FilterNode { return new Node[]{}; } - return new Node[]{new DataResultFilterNode(key, sourceEm, filterKnown, filterSlack)}; + return new Node[]{new DataResultFilterNode(key, sourceEm)}; } - } @NbBundle.Messages("DataResultFilterNode.viewSourceArtifact.text=View Source Result") diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ViewAssociatedContentAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ViewAssociatedContentAction.java index aefde11f20..b11cd9e603 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ViewAssociatedContentAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ViewAssociatedContentAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,7 +22,7 @@ import java.awt.event.ActionEvent; import javax.swing.AbstractAction; import org.sleuthkit.autopsy.corecomponents.DataContentTopComponent; import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; -import org.sleuthkit.autopsy.datamodel.RootContentChildren; +import org.sleuthkit.autopsy.datamodel.CreateSleuthkitNodeVisitor; import org.sleuthkit.datamodel.Content; /** @@ -30,7 +30,7 @@ import org.sleuthkit.datamodel.Content; */ class ViewAssociatedContentAction extends AbstractAction { - private Content content; + private final Content content; public ViewAssociatedContentAction(String title, BlackboardArtifactNode node) { super(title); @@ -39,6 +39,6 @@ class ViewAssociatedContentAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { - DataContentTopComponent.getDefault().setNode(content.accept(new RootContentChildren.CreateSleuthkitNodeVisitor())); + DataContentTopComponent.getDefault().setNode(content.accept(new CreateSleuthkitNodeVisitor())); } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/MSOfficeEmbeddedContentExtractor.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/DocumentEmbeddedContentExtractor.java similarity index 92% rename from Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/MSOfficeEmbeddedContentExtractor.java rename to Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/DocumentEmbeddedContentExtractor.java index d9c142563b..a362c5789f 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/MSOfficeEmbeddedContentExtractor.java +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/DocumentEmbeddedContentExtractor.java @@ -22,8 +22,10 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,13 +35,11 @@ import org.apache.commons.io.IOUtils; import org.apache.poi.hwpf.usermodel.Picture; import org.apache.poi.hslf.usermodel.HSLFPictureData; import org.apache.poi.hslf.usermodel.HSLFSlideShow; -import org.apache.poi.hssf.record.RecordInputStream.LeftoverDataException; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.model.PicturesTable; import org.apache.poi.sl.usermodel.PictureData.PictureType; import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.util.RecordFormatException; import org.apache.tika.config.TikaConfig; import org.apache.tika.detect.Detector; import org.apache.tika.exception.TikaException; @@ -72,13 +72,13 @@ import org.xml.sax.SAXException; /** * Extracts embedded content (e.g. images, audio, video) from Microsoft Office - * documents (both original and OOXML forms). + * documents (both original and OOXML forms) and PDF documents. */ -class MSOfficeEmbeddedContentExtractor { +class DocumentEmbeddedContentExtractor { private final FileManager fileManager; private final IngestServices services; - private static final Logger LOGGER = Logger.getLogger(MSOfficeEmbeddedContentExtractor.class.getName()); + private static final Logger LOGGER = Logger.getLogger(DocumentEmbeddedContentExtractor.class.getName()); private final IngestJobContext context; private String parentFileName; private final String UNKNOWN_IMAGE_NAME_PREFIX = "image_"; //NON-NLS @@ -101,7 +101,8 @@ class MSOfficeEmbeddedContentExtractor { PPT("application/vnd.ms-powerpoint"), //NON-NLS PPTX("application/vnd.openxmlformats-officedocument.presentationml.presentation"), //NON-NLS XLS("application/vnd.ms-excel"), //NON-NLS - XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); //NON-NLS + XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), //NON-NLS + PDF("application/pdf"); //NON-NLS private final String mimeType; @@ -116,7 +117,7 @@ class MSOfficeEmbeddedContentExtractor { } private SupportedExtractionFormats abstractFileExtractionFormat; - MSOfficeEmbeddedContentExtractor(IngestJobContext context, FileTypeDetector fileTypeDetector, String moduleDirRelative, String moduleDirAbsolute) throws NoCurrentCaseException { + DocumentEmbeddedContentExtractor(IngestJobContext context, FileTypeDetector fileTypeDetector, String moduleDirRelative, String moduleDirAbsolute) throws NoCurrentCaseException { this.fileManager = Case.getCurrentCaseThrows().getServices().getFileManager(); this.services = IngestServices.getInstance(); @@ -190,6 +191,9 @@ class MSOfficeEmbeddedContentExtractor { case XLS: listOfExtractedImages = extractImagesFromXls(abstractFile); break; + case PDF: + listOfExtractedImages = extractEmbeddedContentFromPDF(abstractFile); + break; default: break; } @@ -470,6 +474,38 @@ class MSOfficeEmbeddedContentExtractor { return listOfExtractedImages; } + + /** + * Extracts embedded attachments from PDF files. + * + * @param abstractFile Input PDF file + * @return List of extracted files to be made into derived file instances. + */ + private List extractEmbeddedContentFromPDF(AbstractFile abstractFile) { + PDFAttachmentExtractor pdfExtractor = new PDFAttachmentExtractor(parser); + try { + Path outputDirectory = Paths.get(getOutputFolderPath(parentFileName)); + //Get map of attachment name -> location disk. + Map extractedAttachments = pdfExtractor.extract( + new ReadContentInputStream(abstractFile), abstractFile.getId(), + outputDirectory); + + //Convert output to hook into the existing logic for creating derived files + List extractedFiles = new ArrayList<>(); + extractedAttachments.entrySet().forEach((pathEntry) -> { + String fileName = pathEntry.getKey(); + Path writeLocation = pathEntry.getValue(); + extractedFiles.add(new ExtractedFile(fileName, + getFileRelativePath(writeLocation.getFileName().toString()), + writeLocation.toFile().length())); + }); + + return extractedFiles; + } catch (IOException | SAXException | TikaException ex) { + LOGGER.log(Level.WARNING, "Error attempting to extract attachments from PDFs", ex); //NON-NLS + } + return Collections.emptyList(); + } /** * Writes image to the module output location. diff --git a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java index 66c7f7030d..fd833b59a7 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java @@ -50,7 +50,7 @@ public final class EmbeddedFileExtractorIngestModule extends FileIngestModuleAda //Outer concurrent hashmap with keys of JobID, inner concurrentHashmap with keys of objectID private static final ConcurrentHashMap> mapOfDepthTrees = new ConcurrentHashMap<>(); private static final IngestModuleReferenceCounter refCounter = new IngestModuleReferenceCounter(); - private MSOfficeEmbeddedContentExtractor officeExtractor; + private DocumentEmbeddedContentExtractor documentExtractor; private SevenZipExtractor archiveExtractor; private FileTypeDetector fileTypeDetector; private long jobId; @@ -115,10 +115,10 @@ public final class EmbeddedFileExtractorIngestModule extends FileIngestModuleAda } /* * Construct an embedded content extractor for processing Microsoft - * Office documents. + * Office documents and PDF documents. */ try { - this.officeExtractor = new MSOfficeEmbeddedContentExtractor(context, fileTypeDetector, moduleDirRelative, moduleDirAbsolute); + this.documentExtractor = new DocumentEmbeddedContentExtractor(context, fileTypeDetector, moduleDirRelative, moduleDirAbsolute); } catch (NoCurrentCaseException ex) { throw new IngestModuleException(Bundle.EmbeddedFileExtractorIngestModule_UnableToGetMSOfficeExtractor_errMsg(), ex); } @@ -155,8 +155,8 @@ public final class EmbeddedFileExtractorIngestModule extends FileIngestModuleAda */ if (archiveExtractor.isSevenZipExtractionSupported(abstractFile)) { archiveExtractor.unpack(abstractFile, mapOfDepthTrees.get(jobId)); - } else if (officeExtractor.isContentExtractionSupported(abstractFile)) { - officeExtractor.extractEmbeddedContent(abstractFile); + } else if (documentExtractor.isContentExtractionSupported(abstractFile)) { + documentExtractor.extractEmbeddedContent(abstractFile); } return ProcessResult.OK; } diff --git a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/PDFAttachmentExtractor.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/PDFAttachmentExtractor.java new file mode 100755 index 0000000000..a36b5c365d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/PDFAttachmentExtractor.java @@ -0,0 +1,183 @@ +/* + * 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.modules.embeddedfileextractor; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.tika.exception.TikaException; +import org.apache.tika.extractor.EmbeddedDocumentExtractor; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.parser.AutoDetectParser; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; +import org.apache.tika.sax.BodyContentHandler; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.EncodedFileOutputStream; +import org.sleuthkit.datamodel.TskData; + +/** + * Facility for extracting and storing attachments from PDF documents. + * Implementation specifics, however, are generic enough to be used on any + * document with embedded resources. The current name reflects the only known + * use case for this class. + */ +final class PDFAttachmentExtractor { + + private static final Logger logger = Logger.getLogger(PDFAttachmentExtractor.class.getName()); + private final AutoDetectParser parser; + + public PDFAttachmentExtractor() { + parser = new AutoDetectParser(); + } + + public PDFAttachmentExtractor(AutoDetectParser parser) { + this.parser = parser; + } + + /** + * Extracts PDF attachments from a given input and writes them to the supplied + * output directory. + * + * @param input Input PDF to extract attachments from + * @param parentID ID for unique extraction names + * @param outputDir Directory to write attachments + * @return Map containing file name -> location on disk + * @throws IOException + * @throws SAXException + * @throws TikaException + */ + public Map extract(InputStream input, long parentID, Path outputDir) throws IOException, SAXException, TikaException { + ExtractionPreconditions.checkArgument(Files.exists(outputDir), + String.format("Output directory: %s, does not exist.", outputDir.toString())); //NON-NLS + + ParseContext parseContext = new ParseContext(); + parseContext.set(Parser.class, parser); + + //Keep track of the attachment files as they are being extracted and written to disk. + NewResourceWatcher watcher = new NewResourceWatcher(); + parseContext.set(EmbeddedDocumentExtractor.class, new EmbeddedAttachmentHandler(outputDir, parentID, watcher)); + + //Parse input with default params, except for our ParseContext + parser.parse(input, new BodyContentHandler(-1), new Metadata(), parseContext); + + return watcher.getSnapshot(); + } + + /** + * Internal Tika class that is invoked upon encountering an embedded + * resource. + */ + static class EmbeddedAttachmentHandler implements EmbeddedDocumentExtractor { + + private final Path outputDirectory; + private final NewResourceWatcher watcher; + private final Long parentID; + private Integer attachmentCount; + + public EmbeddedAttachmentHandler(Path outputDirectory, long parentID, NewResourceWatcher watcher) { + this.outputDirectory = outputDirectory; + this.watcher = watcher; + this.parentID = parentID; + attachmentCount = 0; + } + + @Override + public boolean shouldParseEmbedded(Metadata mtdt) { + //Grab every available attachment + return true; + } + + @Override + public void parseEmbedded(InputStream in, ContentHandler ch, Metadata mtdt, boolean bln) throws SAXException, IOException { + //Resource naming scheme is used internally in autopsy, therefore we can guarentee uniqueness. + String uniqueExtractedName = parentID + "_attch_" + attachmentCount++; //NON-NLS + + String name = mtdt.get(Metadata.RESOURCE_NAME_KEY); + String ext = FilenameUtils.getExtension(name); + + //Append the extension if we can. + if(ext == null) { + name = uniqueExtractedName; + } else if(!ext.isEmpty()) { + uniqueExtractedName += "." + ext; + } + + Path outputFile = outputDirectory.resolve(uniqueExtractedName); + + try (EncodedFileOutputStream outputStream = new EncodedFileOutputStream( + new FileOutputStream(outputFile.toFile()), TskData.EncodingType.XOR1)){ + IOUtils.copy(in, outputStream); + watcher.notify(name, outputFile); + } catch (IOException ex) { + logger.log(Level.WARNING, String.format("Could not extract attachment %s into directory %s", //NON-NLS + uniqueExtractedName, outputFile), ex); + } + } + } + + /** + * Convenient wrapper for keeping track of new resource paths and the display + * name for each of these resources. + * + * It is necessary to maintain a snapshot of only our changes when the + * output directory is shared among other processes/threads. + */ + static class NewResourceWatcher { + + private final Map newResourcePaths; + + public NewResourceWatcher() { + newResourcePaths = new HashMap<>(); + } + + public void notify(String name, Path newResource) { + newResourcePaths.put(name, newResource); + } + + public Map getSnapshot() { + return newResourcePaths; + } + } + + /** + * Static convenience methods that ensure the PDF extractor is being invoked + * correctly. + */ + static class ExtractionPreconditions { + + public static void checkArgument(boolean expression, String msg) throws IOException { + if (!expression) { + throw new IOException(msg); + } + } + + private ExtractionPreconditions(){ + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java index dace794e85..0c885472de 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java @@ -58,7 +58,7 @@ public class FileTypeDetector { * @return A list of all detectable file types. * * @throws FileTypeDetectorInitException If an error occurs while assembling - * the list of types + * the list of types */ public static synchronized SortedSet getDetectedTypes() throws FileTypeDetectorInitException { TreeSet detectedTypes = new TreeSet<>((String string1, String string2) -> { @@ -109,7 +109,9 @@ public class FileTypeDetector { * Tika, and Autopsy file type definitions take precendence over Tika. * * @throws FileTypeDetectorInitException If an initialization error occurs, - * e.g., user-defined file type definitions exist but cannot be loaded. + * e.g., user-defined file type + * definitions exist but cannot be + * loaded. */ public FileTypeDetector() throws FileTypeDetectorInitException { try { @@ -139,7 +141,7 @@ public class FileTypeDetector { * user-defined MIME type by this detector. * * @param customTypes - * @param mimeType The MIME type name (e.g., "text/html"). + * @param mimeType The MIME type name (e.g., "text/html"). * * @return True or false. */ @@ -170,7 +172,7 @@ public class FileTypeDetector { * @param file The file to test. * * @return A MIME type name. If file type could not be detected, or results - * were uncertain, octet-stream is returned. + * were uncertain, octet-stream is returned. * * */ @@ -223,7 +225,7 @@ public class FileTypeDetector { ReadContentInputStream stream = new ReadContentInputStream(file); try (TikaInputStream tikaInputStream = TikaInputStream.get(stream)) { - String tikaType = tika.detect(tikaInputStream, file.getName()); + String tikaType = tika.detect(tikaInputStream); /* * Remove the Tika suffix from the MIME type name. @@ -233,6 +235,21 @@ public class FileTypeDetector { * Remove the optional parameter from the MIME type. */ mimeType = removeOptionalParameter(mimeType); + + /* + * If Tika recognizes the file signature, then use the file + * name to refine the type. In short, this is to exclude the + * mime types that are determined solely by file extension. + * More details in JIRA-4871. + */ + if (!mimeType.equals(MimeTypes.OCTET_STREAM)) { + ReadContentInputStream secondPassStream = new ReadContentInputStream(file); + try (TikaInputStream secondPassTikaStream = TikaInputStream.get(secondPassStream)) { + tikaType = tika.detect(secondPassTikaStream, file.getName()); + mimeType = tikaType.replace("tika-", ""); //NON-NLS + mimeType = removeOptionalParameter(mimeType); + } + } /** * We cannot trust Tika's audio/mpeg mimetype. Lets verify the @@ -275,6 +292,7 @@ public class FileTypeDetector { * first 4 bits. * * @param x byte + * * @return Flag indicating the byte if 0xFF */ private boolean byteIs0xFF(byte x) { @@ -284,9 +302,10 @@ public class FileTypeDetector { /** * Retrieves the first N bytes from a file. * - * @param file Abstract file to read + * @param file Abstract file to read * @param offset Offset to begin reading - * @param n Number of bytes to read + * @param n Number of bytes to read + * * @return Byte array of size n * * @throws TskCoreException @@ -371,7 +390,7 @@ public class FileTypeDetector { * Constructs an exception to throw if an initialization error occurs, * e.g., user-defined file type definitions exist but cannot be loaded. * - * @param message The exception message, + * @param message The exception message, * @param throwable The underlying cause of the exception. */ FileTypeDetectorInitException(String message, Throwable throwable) { @@ -409,7 +428,7 @@ public class FileTypeDetector { * @return A MIME type name. * * @throws TskCoreException if detection is required and there is a problem - * writing the result to the case database. + * writing the result to the case database. * @deprecated Use getMIMEType instead, and call AbstractFile.setMIMEType * and AbstractFile.save to save the result to the file object and the * database. @@ -429,10 +448,10 @@ public class FileTypeDetector { * @param file The file. * * @return A MIME type name. If file type could not be detected or results - * were uncertain, octet-stream is returned. + * were uncertain, octet-stream is returned. * * @throws TskCoreException if detection is required and there is a problem - * writing the result to the case database. + * writing the result to the case database. * * @deprecated Use getMIMEType instead, and call AbstractFile.setMIMEType * and AbstractFile.save to save the result to the file object and the @@ -453,7 +472,7 @@ public class FileTypeDetector { * @param file The file to test. * * @return A MIME type name. If file type could not be detected or results - * were uncertain, octet-stream is returned. + * were uncertain, octet-stream is returned. * * @throws TskCoreException * @deprecated Use getMIMEType instead. diff --git a/Core/src/org/sleuthkit/autopsy/textextractors/HtmlTextExtractor.java b/Core/src/org/sleuthkit/autopsy/textextractors/HtmlTextExtractor.java index d117e80537..80218d039f 100644 --- a/Core/src/org/sleuthkit/autopsy/textextractors/HtmlTextExtractor.java +++ b/Core/src/org/sleuthkit/autopsy/textextractors/HtmlTextExtractor.java @@ -198,6 +198,7 @@ final class HtmlTextExtractor implements TextExtractor { renderer.setIncludeHyperlinkURLs(false); renderer.setDecorateFontStyles(false); renderer.setIncludeAlternateText(false); + renderer.setMaxLineLength(0); // don't force wrapping return new StringReader(renderer.toString()); } catch (IOException ex) { logger.log(Level.WARNING, "Error extracting HTML from content.", ex); diff --git a/Core/src/org/sleuthkit/autopsy/textextractors/TikaTextExtractor.java b/Core/src/org/sleuthkit/autopsy/textextractors/TikaTextExtractor.java index b23ff86442..2d923cc719 100644 --- a/Core/src/org/sleuthkit/autopsy/textextractors/TikaTextExtractor.java +++ b/Core/src/org/sleuthkit/autopsy/textextractors/TikaTextExtractor.java @@ -46,6 +46,7 @@ import org.apache.tika.Tika; import org.apache.tika.exception.TikaException; import org.apache.tika.metadata.Metadata; import org.apache.tika.parser.AutoDetectParser; +import org.apache.tika.parser.EmptyParser; import org.apache.tika.parser.ParseContext; import org.apache.tika.parser.Parser; import org.apache.tika.parser.ParsingReader; @@ -125,6 +126,16 @@ final class TikaTextExtractor implements TextExtractor { "application/x-z", //NON-NLS "application/x-compress"); //NON-NLS + //Tika should ignore types with embedded files that can be handled by the unpacking modules + private static final List EMBEDDED_FILE_MIME_TYPES + = ImmutableList.of("application/msword", //NON-NLS + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", //NON-NLS + "application/vnd.ms-powerpoint", //NON-NLS + "application/vnd.openxmlformats-officedocument.presentationml.presentation", //NON-NLS + "application/vnd.ms-excel", //NON-NLS + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", //NON-NLS + "application/pdf"); //NON-NLS + private static final java.util.logging.Logger TIKA_LOGGER = java.util.logging.Logger.getLogger("Tika"); //NON-NLS private static final Logger AUTOPSY_LOGGER = Logger.getLogger(TikaTextExtractor.class.getName()); @@ -184,7 +195,14 @@ final class TikaTextExtractor implements TextExtractor { InputStream stream = null; ParseContext parseContext = new ParseContext(); - parseContext.set(Parser.class, parser); + + //Disable appending embedded file text to output for EFE supported types + //JIRA-4975 + if(content instanceof AbstractFile && EMBEDDED_FILE_MIME_TYPES.contains(((AbstractFile)content).getMIMEType())) { + parseContext.set(Parser.class, new EmptyParser()); + } else { + parseContext.set(Parser.class, parser); + } if (ocrEnabled() && content instanceof AbstractFile) { AbstractFile file = ((AbstractFile) content); @@ -516,11 +534,11 @@ final class TikaTextExtractor implements TextExtractor { if (context != null) { ImageConfig configInstance = context.lookup(ImageConfig.class); if (configInstance != null) { - if(Objects.nonNull(configInstance.getOCREnabled())) { + if (Objects.nonNull(configInstance.getOCREnabled())) { this.tesseractOCREnabled = configInstance.getOCREnabled(); } - - if(Objects.nonNull(configInstance.getOCRLanguages())) { + + if (Objects.nonNull(configInstance.getOCRLanguages())) { this.languagePacks = formatLanguagePacks(configInstance.getOCRLanguages()); } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index 4a57368a0b..e9656bb0a9 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -52,6 +52,8 @@ import org.sleuthkit.autopsy.texttranslation.NoServiceProviderException; import org.sleuthkit.autopsy.texttranslation.TranslationException; import org.sleuthkit.datamodel.Content; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.texttranslation.ui.TranslationContentPanel.DisplayDropdownOptions; @@ -61,6 +63,8 @@ import org.sleuthkit.autopsy.texttranslation.ui.TranslationContentPanel.DisplayD @ServiceProvider(service = TextViewer.class, position = 4) public final class TranslatedTextViewer implements TextViewer { + private static final Logger logger = Logger.getLogger(TranslatedTextViewer.class.getName()); + private static final boolean OCR_ENABLED = true; private static final boolean OCR_DISABLED = false; private static final int MAX_SIZE_1MB = 1024000; @@ -169,16 +173,20 @@ public final class TranslatedTextViewer implements TextViewer { try { return getFileText(node); } catch (IOException ex) { + logger.log(Level.WARNING, "Error getting text", ex); return Bundle.TranslatedContentViewer_errorMsg(); } catch (TextExtractor.InitReaderException ex) { + logger.log(Level.WARNING, "Error getting text", ex); return Bundle.TranslatedContentViewer_errorExtractingText(); } } else { try { return translate(getFileText(node)); } catch (IOException ex) { + logger.log(Level.WARNING, "Error translating text", ex); return Bundle.TranslatedContentViewer_errorMsg(); } catch (TextExtractor.InitReaderException ex) { + logger.log(Level.WARNING, "Error translating text", ex); return Bundle.TranslatedContentViewer_errorExtractingText(); } } @@ -247,7 +255,8 @@ public final class TranslatedTextViewer implements TextViewer { } catch (NoServiceProviderException ex) { return Bundle.TranslatedContentViewer_noServiceProvider(); } catch (TranslationException ex) { - return Bundle.TranslatedContentViewer_translationException(); + logger.log(Level.WARNING, "Error translating text", ex); + return Bundle.TranslatedContentViewer_translationException() + " (" + ex.getMessage() + ")"; } } @@ -287,7 +296,7 @@ public final class TranslatedTextViewer implements TextViewer { //Correct for UTF-8 byte[] resultInUTF8Bytes = result.getBytes("UTF8"); - byte[] trimTo1MB = Arrays.copyOfRange(resultInUTF8Bytes, 0, MAX_SIZE_1MB / 1000); + byte[] trimTo1MB = Arrays.copyOfRange(resultInUTF8Bytes, 0, MAX_SIZE_1MB ); return new String(trimTo1MB, "UTF-8"); } @@ -333,7 +342,8 @@ public final class TranslatedTextViewer implements TextViewer { bytesRead += read; } - return textBuilder.toString(); + // The trim is on here because HTML files were observed with nearly 1MB of white space at the end + return textBuilder.toString().trim(); } /**