diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle.properties index 3625d53e68..2e4e6187f6 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle.properties @@ -29,8 +29,8 @@ FileTypeIdGlobalSettingsPanel.JOptionPane.invalidInterestingFilesSetName.title=M FileTypeIdGlobalSettingsPanel.JOptionPane.storeFailed.title=Save Failed FileTypeIdGlobalSettingsPanel.JOptionPane.loadFailed.title=Load Failed FileTypeIdGlobalSettingsPanel.ingestRunningWarningLabel.text=Cannot make changes to file type definitions when ingest is running! -UserDefinedFileTypesManager.loadFileTypes.errorMessage=Failed to load existing file type definitions. -UserDefinedFileTypesManager.saveFileTypes.errorMessage=Failed to save file type definitions. +FileTypeIdGlobalSettingsPanel.loadFileTypes.errorMessage=Failed to load existing file type definitions. +FileTypeIdGlobalSettingsPanel.saveFileTypes.errorMessage=Failed to save file type definitions. FileTypeIdGlobalSettingsPanel.newTypeButton.text=New Type FileTypeIdGlobalSettingsPanel.jLabel2.text=Custom MIME Types: FileTypeIdGlobalSettingsPanel.jLabel3.text=Autopsy can automatically detect many file types. Add your custom file types here. diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle_ja.properties index f8eaeea272..ef48c700dc 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle_ja.properties @@ -26,8 +26,8 @@ FileTypeIdGlobalSettingsPanel.signatureComboBox.asciiItem=\u30b9\u30c8\u30ea\u30 FileTypeIdGlobalSettingsPanel.signatureComboBox.rawItem=\u30d0\u30a4\u30c8\uff08HEX\uff09 OptionsCategory_Keywords_FileTypeId=\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7ID OptionsCategory_Name_FileTypeId=\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7 -UserDefinedFileTypesManager.loadFileTypes.errorMessage=\u65e2\u5b58\u306e\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7\u5b9a\u7fa9\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f -UserDefinedFileTypesManager.saveFileTypes.errorMessage=\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7\u5b9a\u7fa9\u306e\u4fdd\u5b58\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002 +FileTypeIdGlobalSettingsPanel.loadFileTypes.errorMessage=\u65e2\u5b58\u306e\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7\u5b9a\u7fa9\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f +FileTypeIdGlobalSettingsPanel.saveFileTypes.errorMessage=\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7\u5b9a\u7fa9\u306e\u4fdd\u5b58\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002 FileTypeIdGlobalSettingsPanel.JOptionPane.invalidInterestingFilesSetName.message=\u30a2\u30e9\u30fc\u30c8\u3092\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u7591\u308f\u3057\u3044\u30d5\u30a1\u30a4\u30eb\u30bb\u30c3\u30c8\u540d\u304c\u5fc5\u8981\u3067\u3059\u3002 FileTypeIdGlobalSettingsPanel.offsetComboBox.startItem=\u958b\u59cb FileTypeIdGlobalSettingsPanel.offsetComboBox.endItem=\u505c\u6b62 diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/CustomFileTypesManager.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/CustomFileTypesManager.java new file mode 100644 index 0000000000..784dea47b9 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/CustomFileTypesManager.java @@ -0,0 +1,503 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2016 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.filetypeid; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.DatatypeConverter; +import javax.xml.parsers.ParserConfigurationException; +import org.openide.util.io.NbObjectInputStream; +import org.openide.util.io.NbObjectOutputStream; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.coreutils.XMLUtil; +import org.sleuthkit.autopsy.modules.filetypeid.FileType.Signature; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * A singleton manager for the custom file types defined by Autopsy and by + * users. + */ +final class CustomFileTypesManager { + + private static final String SERIALIZED_SETTINGS_FILE = "UserFileTypeDefinitions.settings"; //NON-NLS + private static final String XML_SETTINGS_FILE = "UserFileTypeDefinitions.xml"; //NON-NLS + private static final String FILE_TYPES_TAG_NAME = "FileTypes"; //NON-NLS + private static final String FILE_TYPE_TAG_NAME = "FileType"; //NON-NLS + private static final String MIME_TYPE_TAG_NAME = "MimeType"; //NON-NLS + private static final String SIGNATURE_TAG_NAME = "Signature"; //NON-NLS + private static final String SIGNATURE_TYPE_ATTRIBUTE = "type"; //NON-NLS + private static final String BYTES_TAG_NAME = "Bytes"; //NON-NLS + private static final String OFFSET_TAG_NAME = "Offset"; //NON-NLS + private static final String RELATIVE_ATTRIBUTE = "RelativeToStart"; //NON-NLS + private static CustomFileTypesManager instance; + private final List autopsyDefinedFileTypes = new ArrayList<>(); + private List userDefinedFileTypes = new ArrayList<>(); + + /** + * Gets the singleton manager of the custom file types defined by Autopsy + * and by users. + * + * @return The custom file types manager singleton. + * + * @throws CustomFileTypesException if there is a problem loading the custom + * file types. + */ + synchronized static CustomFileTypesManager getInstance() throws CustomFileTypesException { + if (null == instance) { + instance = new CustomFileTypesManager(); + try { + instance.loadUserDefinedFileTypes(); + instance.createAutopsyDefinedFileTypes(); + } catch (CustomFileTypesException ex) { + instance = null; + throw ex; + } + } + return instance; + } + + /** + * Constructs a manager for the custom file types defined by Autopsy and by + * users. + */ + private CustomFileTypesManager() { + } + + /** + * Gets the custom file types defined by Autopsy and by users. + * + * @return A list of custom file types, possibly empty. + */ + synchronized List getFileTypes() { + /** + * It is safe to return references instead of copies in this snapshot + * because FileType objects are immutable. + */ + List customTypes = new ArrayList<>(userDefinedFileTypes); + customTypes.addAll(autopsyDefinedFileTypes); + return customTypes; + } + + /** + * Gets the user-defined custom file types. + * + * @return A list of file types, possibly empty. + */ + synchronized List getUserDefinedFileTypes() { + /** + * It is safe to return references instead of copies in this snapshot + * because FileType objects are immutable. + */ + return new ArrayList<>(userDefinedFileTypes); + } + + /** + * Sets the user-defined custom file types. + * + * @param newFileTypes A list of user-defined file types. + * + * @throws CustomFileTypesException if there is a problem setting the file + * types. + */ + synchronized void setUserDefinedFileTypes(List newFileTypes) throws CustomFileTypesException { + String filePath = getFileTypeDefinitionsFilePath(SERIALIZED_SETTINGS_FILE); + writeSerializedFileTypes(newFileTypes, filePath); + userDefinedFileTypes = newFileTypes; + } + + /** + * Creates the custom file types defined by Autopsy. + * + * @throws CustomFileTypesException if there is a problem creating the file + * types. + */ + private void createAutopsyDefinedFileTypes() throws CustomFileTypesException { + byte[] byteArray; + FileType fileType; + try { + /* + * Add type for xml. + */ + List signatureList; + signatureList = new ArrayList<>(); + signatureList.add(new Signature(" fileTypes, String filePath) throws CustomFileTypesException { + try (NbObjectOutputStream out = new NbObjectOutputStream(new FileOutputStream(filePath))) { + UserDefinedFileTypesSettings settings = new UserDefinedFileTypesSettings(fileTypes); + out.writeObject(settings); + } catch (IOException ex) { + throw new CustomFileTypesException(String.format("Failed to write settings to %s", filePath), ex); //NON-NLS + } + } + + /** + * Reads serialized custom file types from a file. + * + * @param filePath The path to the file. + * + * @return The custom file types. + * + * @throws CustomFileTypesException if there is a problem reading the file + * types. + */ + private static List readSerializedFileTypes(String filePath) throws CustomFileTypesException { + File serializedDefs = new File(filePath); + try { + try (NbObjectInputStream in = new NbObjectInputStream(new FileInputStream(serializedDefs))) { + UserDefinedFileTypesSettings filesSetsSettings = (UserDefinedFileTypesSettings) in.readObject(); + return filesSetsSettings.getUserDefinedFileTypes(); + } + } catch (IOException | ClassNotFoundException ex) { + throw new CustomFileTypesException(String.format("Failed to read ssettings from %s", filePath), ex); //NON-NLS + } + } + + /** + * Reads custom file type definitions from an XML file. + * + * @param filePath The path to the file. + * + * @return A collection of custom file types read from the XML file. + * + * @throws IOException if there is problem reading the XML + * file. + * @throws SAXException if there is a problem parsing the + * XML file. + * @throws ParserConfigurationException if there is a problem parsing the + * XML file. + */ + private static List readFileTypesXML(String filePath) throws CustomFileTypesException { + try { + List fileTypes = new ArrayList<>(); + Document doc = XMLUtil.loadDocument(filePath); + if (doc != null) { + Element fileTypesElem = doc.getDocumentElement(); + if (fileTypesElem != null && fileTypesElem.getNodeName().equals(FILE_TYPES_TAG_NAME)) { + NodeList fileTypeElems = fileTypesElem.getElementsByTagName(FILE_TYPE_TAG_NAME); + for (int i = 0; i < fileTypeElems.getLength(); ++i) { + Element fileTypeElem = (Element) fileTypeElems.item(i); + FileType fileType = parseFileType(fileTypeElem); + fileTypes.add(fileType); + } + } + } + return fileTypes; + } catch (IOException | ParserConfigurationException | SAXException ex) { + throw new CustomFileTypesException(String.format("Failed to read ssettings from %s", filePath), ex); //NON-NLS + } + } + + /** + * Gets a custom file type definition from a file type XML element. + * + * @param fileTypeElem The XML element. + * + * @return A file type object. + * + * @throws IllegalArgumentException if there is a problem parsing the file + * type. + * @throws NumberFormatException if there is a problem parsing the file + * type. + */ + private static FileType parseFileType(Element fileTypeElem) throws IllegalArgumentException, NumberFormatException { + String mimeType = parseMimeType(fileTypeElem); + Signature signature = parseSignature(fileTypeElem); + // File type definitions in the XML file were written prior to the + // implementation of multiple signatures per type. + List sigList = new ArrayList<>(); + sigList.add(signature); + return new FileType(mimeType, sigList); + } + + /** + * Gets the MIME type from a file type XML element. + * + * @param fileTypeElem The element + * + * @return A MIME type string. + */ + private static String parseMimeType(Element fileTypeElem) { + return getChildElementTextContent(fileTypeElem, MIME_TYPE_TAG_NAME); + } + + /** + * Gets the signature from a file type XML element. + * + * @param fileTypeElem The XML element. + * + * @return The signature. + */ + private static Signature parseSignature(Element fileTypeElem) throws IllegalArgumentException, NumberFormatException { + NodeList signatureElems = fileTypeElem.getElementsByTagName(SIGNATURE_TAG_NAME); + Element signatureElem = (Element) signatureElems.item(0); + + String sigTypeAttribute = signatureElem.getAttribute(SIGNATURE_TYPE_ATTRIBUTE); + Signature.Type signatureType = Signature.Type.valueOf(sigTypeAttribute); + + String sigBytesString = getChildElementTextContent(signatureElem, BYTES_TAG_NAME); + byte[] signatureBytes = DatatypeConverter.parseHexBinary(sigBytesString); + + Element offsetElem = (Element) signatureElem.getElementsByTagName(OFFSET_TAG_NAME).item(0); + String offsetString = offsetElem.getTextContent(); + long offset = DatatypeConverter.parseLong(offsetString); + + boolean isRelativeToStart; + String relativeString = offsetElem.getAttribute(RELATIVE_ATTRIBUTE); + if (null == relativeString || relativeString.equals("")) { + isRelativeToStart = true; + } else { + isRelativeToStart = DatatypeConverter.parseBoolean(relativeString); + } + + return new Signature(signatureBytes, offset, signatureType, isRelativeToStart); + } + + /** + * Gets the text content of a single child element. + * + * @param elem The parent element. + * @param tagName The tag name of the child element. + * + * @return The text content or null if the tag doesn't exist. + */ + private static String getChildElementTextContent(Element elem, String tagName) { + NodeList childElems = elem.getElementsByTagName(tagName); + Node childNode = childElems.item(0); + if (childNode == null) { + return null; + } + Element childElem = (Element) childNode; + return childElem.getTextContent(); + } + + /** + * Gets the absolute path of a file type definitions file. + * + * @param fileName The name of the file. + * + * @return The absolute path to the file. + */ + private static String getFileTypeDefinitionsFilePath(String fileName) { + Path filePath = Paths.get(PlatformUtil.getUserConfigDirectory(), fileName); + return filePath.toAbsolutePath().toString(); + } + + /** + * .An exception thrown by the custom file types manager. + */ + static class CustomFileTypesException extends Exception { + + private static final long serialVersionUID = 1L; + + CustomFileTypesException(String message) { + super(message); + } + + CustomFileTypesException(String message, Throwable throwable) { + super(message, throwable); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java index e33740805f..44a7cda7da 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java @@ -24,9 +24,7 @@ import java.util.SortedSet; import org.apache.tika.Tika; import org.apache.tika.mime.MediaType; import org.apache.tika.mime.MimeTypes; -import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; @@ -34,24 +32,22 @@ import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; /** - * Detects the MIME type of a file by an inspection of its contents, using both - * user-defined type definitions and Tika. + * Detects the MIME type of a file by an inspection of its contents, using + * custom file type definitions by users, custom file type definitions by + * Autopsy, and Tika. */ -@NbBundle.Messages({ - "CouldNotInitializeFileTypeDetector=Error loading user-defined file types." -}) public class FileTypeDetector { private static final Tika tika = new Tika(); private static final int BUFFER_SIZE = 64 * 1024; private final byte buffer[] = new byte[BUFFER_SIZE]; private final List userDefinedFileTypes; - private static final Logger logger = Logger.getLogger(FileTypeDetector.class.getName()); + private final List autopsyDefinedFileTypes; /** * Constructs an object that detects the MIME type of a file by an - * inspection of its contents, using both user-defined type definitions and - * Tika. + * inspection of its contents, using custom file type definitions by users, + * custom file type definitions by Autopsy, and Tika. * * @throws FileTypeDetectorInitException if an initialization error occurs, * e.g., user-defined file type @@ -60,25 +56,28 @@ public class FileTypeDetector { */ public FileTypeDetector() throws FileTypeDetectorInitException { try { - userDefinedFileTypes = UserDefinedFileTypesManager.getInstance().getFileTypes(); - } catch (UserDefinedFileTypesManager.UserDefinedFileTypesException ex) { - throw new FileTypeDetectorInitException(Bundle.CouldNotInitializeFileTypeDetector(), ex); + userDefinedFileTypes = CustomFileTypesManager.getInstance().getFileTypes(); + autopsyDefinedFileTypes = CustomFileTypesManager.getInstance().getFileTypes(); + } catch (CustomFileTypesManager.CustomFileTypesException ex) { + throw new FileTypeDetectorInitException("Error loading custom file types", ex); //NON-NLS } } /** - * Gets the names of the user-defined MIME types. + * Gets the names of the custom file types defined by the user or by + * Autopsy. * * @return A list of the user-defined MIME types. */ public List getUserDefinedTypes() { - List list = new ArrayList<>(); - if (userDefinedFileTypes != null) { - for (FileType fileType : userDefinedFileTypes) { - list.add(fileType.getMimeType()); - } + List customFileTypes = new ArrayList<>(); + for (FileType fileType : userDefinedFileTypes) { + customFileTypes.add(fileType.getMimeType()); } - return list; + for (FileType fileType : autopsyDefinedFileTypes) { + customFileTypes.add(fileType.getMimeType()); + } + return customFileTypes; } /** @@ -90,7 +89,9 @@ public class FileTypeDetector { * @return True or false. */ public boolean isDetectable(String mimeType) { - return isDetectableAsUserDefinedType(mimeType) || isDetectableByTika(mimeType); + return isDetectableAsCustomType(userDefinedFileTypes, mimeType) + || isDetectableAsCustomType(autopsyDefinedFileTypes, mimeType) + || isDetectableByTika(mimeType); } /** @@ -101,8 +102,8 @@ public class FileTypeDetector { * * @return True or false. */ - private boolean isDetectableAsUserDefinedType(String mimeType) { - for (FileType fileType : userDefinedFileTypes) { + private boolean isDetectableAsCustomType(List customTypes, String mimeType) { + for (FileType fileType : customTypes) { if (fileType.getMimeType().equals(mimeType)) { return true; } @@ -204,12 +205,21 @@ public class FileTypeDetector { } /* - * If the file is a regular file, give precedence to user-defined types. + * If the file is a regular file, give precedence to user-defined custom + * file types. */ if (null == mimeType) { mimeType = detectUserDefinedType(file); } + /* + * If the file does not match a user-defined type, give precedence to + * custom file types defined by Autopsy. + */ + if (null == mimeType) { + mimeType = detectAutopsyDefinedType(file); + } + /* * If the file does not match a user-defined type, send the initial * bytes to Tika. @@ -282,8 +292,8 @@ public class FileTypeDetector { } /** - * Determines whether or not the a file matches a user-defined or Autopsy - * predefined file type. + * Determines whether or not the a file matches a user-defined custom file + * type. * * @param file The file to test. * @@ -300,6 +310,25 @@ public class FileTypeDetector { return null; } + /** + * Determines whether or not the a file matches a custom file type defined + * by Autopsy. + * + * @param file The file to test. + * + * @return The file type name string or null, if no match is detected. + * + * @throws TskCoreException + */ + private String detectAutopsyDefinedType(AbstractFile file) throws TskCoreException { + for (FileType fileType : autopsyDefinedFileTypes) { + if (fileType.matches(file)) { + return fileType.getMimeType(); + } + } + return null; + } + /* * Exception thrown when a file type detector experiences an error * condition. @@ -345,7 +374,6 @@ public class FileTypeDetector { * instead of querying the blackboard. */ @Deprecated - @SuppressWarnings("deprecation") public String detectAndPostToBlackboard(AbstractFile file) throws TskCoreException { return getFileType(file); } diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.java index 1361169c45..5bb72154e5 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.java @@ -24,6 +24,7 @@ import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.Collections; import java.util.List; +import java.util.logging.Level; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListModel; import javax.swing.JOptionPane; @@ -32,10 +33,11 @@ import javax.swing.event.ListSelectionListener; import org.netbeans.spi.options.OptionsPanelController; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestModuleGlobalSettingsPanel; import org.sleuthkit.autopsy.modules.filetypeid.FileType.Signature; -import org.sleuthkit.autopsy.modules.filetypeid.UserDefinedFileTypesManager.UserDefinedFileTypesException; +import org.sleuthkit.autopsy.modules.filetypeid.CustomFileTypesManager.CustomFileTypesException; /** * A panel to allow a user to make custom file type definitions. In addition to @@ -43,12 +45,15 @@ import org.sleuthkit.autopsy.modules.filetypeid.UserDefinedFileTypesManager.User * appears in the NetBeans options dialog as an options panel. */ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPanel implements OptionsPanel { + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(FileTypeIdGlobalSettingsPanel.class.getName()); private static final String RAW_SIGNATURE_TYPE_COMBO_BOX_ITEM = NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.signatureComboBox.rawItem"); private static final String ASCII_SIGNATURE_TYPE_COMBO_BOX_ITEM = NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.signatureComboBox.asciiItem"); private static final String START_OFFSET_RELATIVE_COMBO_BOX_ITEM = NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.offsetComboBox.startItem"); private static final String END_OFFSET_RELATIVE_COMBO_BOX_ITEM = NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.offsetComboBox.endItem"); + /** * The list model for the file types list component of this panel is the set * of MIME types associated with the user-defined file types. A mapping of @@ -207,14 +212,15 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane @Override public void load() { try { - fileTypes = UserDefinedFileTypesManager.getInstance().getUserDefinedFileTypes(); + fileTypes = CustomFileTypesManager.getInstance().getUserDefinedFileTypes(); updateFileTypesListModel(); if (!typesListModel.isEmpty()) { typesList.setSelectedIndex(0); } - } catch (UserDefinedFileTypesException ex) { + } catch (CustomFileTypesException ex) { + logger.log(Level.SEVERE, "Failed to get custom file types", ex); JOptionPane.showMessageDialog(null, - ex.getLocalizedMessage(), + NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.loadFileTypes.errorMessage"), NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.JOptionPane.loadFailed.title"), JOptionPane.ERROR_MESSAGE); fileTypes = Collections.emptyList(); @@ -266,10 +272,11 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane @Override public void store() { try { - UserDefinedFileTypesManager.getInstance().setUserDefinedFileTypes(fileTypes); - } catch (UserDefinedFileTypesManager.UserDefinedFileTypesException ex) { + CustomFileTypesManager.getInstance().setUserDefinedFileTypes(fileTypes); + } catch (CustomFileTypesManager.CustomFileTypesException ex) { + logger.log(Level.SEVERE, "Failed to set custom file types", ex); JOptionPane.showMessageDialog(null, - ex.getLocalizedMessage(), + NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.saveFileTypes.errorMessage"), NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.JOptionPane.storeFailed.title"), JOptionPane.ERROR_MESSAGE); } diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java deleted file mode 100644 index d22114bfbb..0000000000 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java +++ /dev/null @@ -1,616 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011-2016 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.filetypeid; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import javax.xml.bind.DatatypeConverter; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.TransformerException; -import org.openide.util.NbBundle; -import org.openide.util.io.NbObjectInputStream; -import org.openide.util.io.NbObjectOutputStream; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.PlatformUtil; -import org.sleuthkit.autopsy.coreutils.XMLUtil; -import org.sleuthkit.autopsy.modules.filetypeid.FileType.Signature; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -/** - * Manages user-defined file types characterized by MIME type, signature, and - * optional membership in an interesting files set. - *

- * Note that this class exposes a very simple get/set API that operates on the - * user-defined file types as a complete set - there is no concept of adding, - * editing or deleting file types singly. This works because this class is not - * exposed outside of this ingest module package and is ONLY used in a very - * specific paradigm. In this paradigm, there is a single modal writer of file - * types in the form of a global settings panel that disables itself when ingest - * is running so that multiple readers in the form of file ingest modules get a - * consistent set of file type definitions. - *

- * Thread-safe. - */ -final class UserDefinedFileTypesManager { - - private static final Logger logger = Logger.getLogger(UserDefinedFileTypesManager.class.getName()); - private static final String USER_DEFINED_TYPES_XML_FILE = "UserFileTypeDefinitions.xml"; //NON-NLS - private static final String USER_DEFINED_TYPES_SERIALIZATION_FILE = "UserFileTypeDefinitions.settings"; - private static final String FILE_TYPES_TAG_NAME = "FileTypes"; //NON-NLS - private static final String FILE_TYPE_TAG_NAME = "FileType"; //NON-NLS - private static final String MIME_TYPE_TAG_NAME = "MimeType"; //NON-NLS - private static final String SIGNATURE_TAG_NAME = "Signature"; //NON-NLS - private static final String SIGNATURE_TYPE_ATTRIBUTE = "type"; //NON-NLS - private static final String BYTES_TAG_NAME = "Bytes"; //NON-NLS - private static final String OFFSET_TAG_NAME = "Offset"; //NON-NLS - private static final String RELATIVE_ATTRIBUTE = "RelativeToStart"; //NON-NLS - private static final String INTERESTING_FILES_SET_TAG_NAME = "InterestingFileSset"; //NON-NLS - private static final String ALERT_ATTRIBUTE = "alert"; //NON-NLS - private static final String ENCODING_FOR_XML_FILE = "UTF-8"; //NON-NLS - private static UserDefinedFileTypesManager instance; - - /** - * File types to be persisted to the user-defined file type definitions file - * are stored in this mapping of MIME types to file types. Access to this - * map is guarded by the intrinsic lock of the user-defined file types - * manager for thread-safety. - */ - private final List userDefinedFileTypes = new ArrayList<>(); - - /** - * The combined set of user-defined file types and file types predefined by - * Autopsy are stored in this mapping of MIME types to file types. This is - * the current working set of file types. Access to this map is guarded by - * the intrinsic lock of the user-defined file types manager for - * thread-safety. - */ - private final List fileTypes = new ArrayList<>(); - - /** - * Gets the singleton manager of user-defined file types characterized by - * MIME type, signature, and optional membership in an interesting files - * set. - * - * @return The user-defined file types manager singleton. - */ - synchronized static UserDefinedFileTypesManager getInstance() { - if (instance == null) { - instance = new UserDefinedFileTypesManager(); - } - return instance; - } - - /** - * Creates a manager of user-defined file types characterized by MIME type, - * signature, and optional membership in an interesting files set. - */ - private UserDefinedFileTypesManager() { - } - - /** - * Gets both the predefined and the user-defined file types. - * - * @return A mapping of file type names to file types, possibly empty. - * - * @throws - * org.sleuthkit.autopsy.modules.filetypeid.UserDefinedFileTypesManager.UserDefinedFileTypesException - */ - synchronized List getFileTypes() throws UserDefinedFileTypesException { - loadFileTypes(); - - /** - * It is safe to return references to the internal file type objects - * because they are immutable. Note that - * Collections.unmodifiableCollection() is not used here because this - * view of the file types is a snapshot. - */ - return new ArrayList<>(fileTypes); - } - - /** - * Gets the user-defined file types. - * - * @return A mapping of file type names to file types, possibly empty. - * - * @throws - * org.sleuthkit.autopsy.modules.filetypeid.UserDefinedFileTypesManager.UserDefinedFileTypesException - */ - synchronized List getUserDefinedFileTypes() throws UserDefinedFileTypesException { - loadFileTypes(); - - /** - * It is safe to return references to the internal file type objects - * because they are immutable. Note that - * Collections.unmodifiableCollection() is not used here because this - * view of the file types is a snapshot. - */ - return new ArrayList<>(userDefinedFileTypes); - } - - /** - * Loads the MIME type to file type mappings with predefined and - * user-defined types. - * - * @throws - * org.sleuthkit.autopsy.modules.filetypeid.UserDefinedFileTypesManager.UserDefinedFileTypesException - */ - private void loadFileTypes() throws UserDefinedFileTypesException { - fileTypes.clear(); - userDefinedFileTypes.clear(); - /** - * Load the predefined types first so that they can be overwritten by - * any user-defined types with the same names. - */ - loadPredefinedFileTypes(); - loadUserDefinedFileTypes(); - } - - /** - * Adds the predefined file types to the in-memory mappings of MIME types to - * file types. - * - * @throws - * org.sleuthkit.autopsy.modules.filetypeid.UserDefinedFileTypesManager.UserDefinedFileTypesException - */ - private void loadPredefinedFileTypes() throws UserDefinedFileTypesException { - byte[] byteArray; - FileType fileType; - - try { - List signatureList; - signatureList = new ArrayList<>(); - signatureList.add(new Signature(" newFileTypes) throws UserDefinedFileTypesException { - String filePath = getFileTypeDefinitionsFilePath(USER_DEFINED_TYPES_SERIALIZATION_FILE); - writeFileTypes(newFileTypes, filePath); - } - - /** - * Gets the absolute path of a file type definitions file. - * - * @param fileName The name of the file. - * - * @return The absolute path to the file. - */ - private static String getFileTypeDefinitionsFilePath(String fileName) { - Path filePath = Paths.get(PlatformUtil.getUserConfigDirectory(), fileName); - return filePath.toAbsolutePath().toString(); - } - - /** - * Writes a set of file types to a file. - * - * @param fileTypes A collection of file types. - * @param filePath The path to the destination file. - * - * @throws ParserConfigurationException - * @throws IOException - * @throws FileNotFoundException - * @throws UnsupportedEncodingException - * @throws TransformerException - */ - private static void writeFileTypes(List fileTypes, String filePath) throws UserDefinedFileTypesException { - try (NbObjectOutputStream out = new NbObjectOutputStream(new FileOutputStream(filePath))) { - UserDefinedFileTypesSettings settings = new UserDefinedFileTypesSettings(fileTypes); - out.writeObject(settings); - } catch (IOException ex) { - throw new UserDefinedFileTypesException(String.format("Failed to write settings to %s", filePath), ex); - } - } - - /** - * Reads the file types - * - * @param filePath the file path where the file types are to be read - * - * @return the file types - * - * @throws ParserConfigurationException If the file cannot be read - */ - private static List readFileTypesSerialized() throws UserDefinedFileTypesException { - File serializedDefs = new File(getFileTypeDefinitionsFilePath(USER_DEFINED_TYPES_SERIALIZATION_FILE)); - try { - try (NbObjectInputStream in = new NbObjectInputStream(new FileInputStream(serializedDefs))) { - UserDefinedFileTypesSettings filesSetsSettings = (UserDefinedFileTypesSettings) in.readObject(); - return filesSetsSettings.getUserDefinedFileTypes(); - } - } catch (IOException | ClassNotFoundException ex) { - throw new UserDefinedFileTypesException("Couldn't read serialized settings.", ex); - } - } - - /** - * Provides a mechanism for reading a set of file type definitions from an - * XML file. - */ - private static class XMLDefinitionsReader { - - /** - * Reads a set of file type definitions from an XML file. - * - * @param filePath The path to the XML file. - * - * @return A collection of file types read from the XML file. - */ - private static List readFileTypes(String filePath) throws IOException, SAXException, ParserConfigurationException { - List fileTypes = new ArrayList<>(); - /* - * RC: Commenting out the loadDocument overload that validates - * against the XSD is a temp fix for a failure to provide an upgrade - * path when the RelativeToStart attribute was added to the - * Signature element. The upgrade path can be supplied, but the plan - * is to replace the use of XML with object serialization for the - * settings, so it may not be worth the effort. - */ - // private static final String FILE_TYPE_DEFINITIONS_SCHEMA_FILE = "FileTypes.xsd"; //NON-NLS - // Document doc = XMLUtil.loadDocument(filePath, UserDefinedFileTypesManager.class, FILE_TYPE_DEFINITIONS_SCHEMA_FILE); - Document doc = XMLUtil.loadDocument(filePath); - if (doc != null) { - Element fileTypesElem = doc.getDocumentElement(); - if (fileTypesElem != null && fileTypesElem.getNodeName().equals(FILE_TYPES_TAG_NAME)) { - NodeList fileTypeElems = fileTypesElem.getElementsByTagName(FILE_TYPE_TAG_NAME); - for (int i = 0; i < fileTypeElems.getLength(); ++i) { - Element fileTypeElem = (Element) fileTypeElems.item(i); - FileType fileType = XMLDefinitionsReader.parseFileType(fileTypeElem); - fileTypes.add(fileType); - } - } - } - return fileTypes; - } - - /** - * Gets a file type definition from a file type XML element. - * - * @param fileTypeElem The XML element. - * - * @return A file type object. - * - * @throws IllegalArgumentException - * @throws NumberFormatException - */ - private static FileType parseFileType(Element fileTypeElem) throws IllegalArgumentException, NumberFormatException { - String mimeType = XMLDefinitionsReader.parseMimeType(fileTypeElem); - Signature signature = XMLDefinitionsReader.parseSignature(fileTypeElem); - // File type definitions in the XML file were written prior to the - // implementation of multiple signatures per type. - List sigList = new ArrayList<>(); - sigList.add(signature); - return new FileType(mimeType, sigList); - } - - /** - * Gets the MIME type from a file type XML element. - * - * @param fileTypeElem The element - * - * @return A MIME type string. - */ - private static String parseMimeType(Element fileTypeElem) { - return getChildElementTextContent(fileTypeElem, MIME_TYPE_TAG_NAME); - } - - /** - * Gets the signature from a file type XML element. - * - * @param fileTypeElem The XML element. - * - * @return The signature. - */ - private static Signature parseSignature(Element fileTypeElem) throws IllegalArgumentException, NumberFormatException { - NodeList signatureElems = fileTypeElem.getElementsByTagName(SIGNATURE_TAG_NAME); - Element signatureElem = (Element) signatureElems.item(0); - - String sigTypeAttribute = signatureElem.getAttribute(SIGNATURE_TYPE_ATTRIBUTE); - Signature.Type signatureType = Signature.Type.valueOf(sigTypeAttribute); - - String sigBytesString = getChildElementTextContent(signatureElem, BYTES_TAG_NAME); - byte[] signatureBytes = DatatypeConverter.parseHexBinary(sigBytesString); - - Element offsetElem = (Element) signatureElem.getElementsByTagName(OFFSET_TAG_NAME).item(0); - String offsetString = offsetElem.getTextContent(); - long offset = DatatypeConverter.parseLong(offsetString); - - boolean isRelativeToStart; - String relativeString = offsetElem.getAttribute(RELATIVE_ATTRIBUTE); - if (null == relativeString || relativeString.equals("")) { - isRelativeToStart = true; - } else { - isRelativeToStart = DatatypeConverter.parseBoolean(relativeString); - } - - return new Signature(signatureBytes, offset, signatureType, isRelativeToStart); - } - - /** - * Gets the interesting files set name from a file type XML element. - * - * @param fileTypeElem The XML element. - * - * @return The files set name, possibly empty. - */ - private static String parseInterestingFilesSet(Element fileTypeElem) { - String filesSetName = ""; - NodeList filesSetElems = fileTypeElem.getElementsByTagName(INTERESTING_FILES_SET_TAG_NAME); - if (filesSetElems.getLength() > 0) { - Element filesSetElem = (Element) filesSetElems.item(0); - filesSetName = filesSetElem.getTextContent(); - } - return filesSetName; - } - - /** - * Gets the alert attribute from a file type XML element. - * - * @param fileTypeElem The XML element. - * - * @return True or false; - */ - private static boolean parseAlert(Element fileTypeElem) { - String alertAttribute = fileTypeElem.getAttribute(ALERT_ATTRIBUTE); - return Boolean.parseBoolean(alertAttribute); - } - - /** - * Gets the text content of a single child element. - * - * @param elem The parent element. - * @param tagName The tag name of the child element. - * - * @return The text content or null if the tag doesn't exist. - */ - private static String getChildElementTextContent(Element elem, String tagName) { - NodeList childElems = elem.getElementsByTagName(tagName); - Node childNode = childElems.item(0); - if (childNode == null) { - return null; - } - Element childElem = (Element) childNode; - return childElem.getTextContent(); - } - - /** - * Private constructor suppresses creation of instanmces of this utility - * class. - */ - private XMLDefinitionsReader() { - } - - } - - /** - * Logs an exception, bundles the exception with a simple message in a - * uniform exception type, and throws the wrapper exception. - * - * @param ex The exception to wrap. - * @param messageKey A key into the bundle file that maps to the desired - * message. - * - * @throws - * org.sleuthkit.autopsy.modules.filetypeid.UserDefinedFileTypesManager.UserDefinedFileTypesException - */ - private void throwUserDefinedFileTypesException(Exception ex, String messageKey) throws UserDefinedFileTypesException { - String message = NbBundle.getMessage(UserDefinedFileTypesManager.class, messageKey); - logger.log(Level.SEVERE, message, ex); - throw new UserDefinedFileTypesException(message, ex); - } - - /** - * Used to translate more implementation-details-specific exceptions (which - * are logged by this class) into more generic exceptions for propagation to - * clients of the user-defined file types manager. - */ - static class UserDefinedFileTypesException extends Exception { - - private static final long serialVersionUID = 1L; - - UserDefinedFileTypesException(String message) { - super(message); - } - - UserDefinedFileTypesException(String message, Throwable throwable) { - super(message, throwable); - } - } - -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java b/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java index 45c22ac047..0e1996456e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java @@ -20,7 +20,6 @@ package org.sleuthkit.autopsy.timeline; import java.io.IOException; import java.net.URL; -import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import javafx.collections.FXCollections; @@ -34,8 +33,6 @@ import javafx.scene.control.ListView; import javafx.scene.image.Image; import javafx.stage.Modality; import javafx.stage.Stage; -import javax.annotation.Nullable; -import org.apache.commons.lang3.StringUtils; import org.controlsfx.dialog.ProgressDialog; import org.controlsfx.tools.Borders; import org.openide.util.NbBundle; @@ -50,13 +47,13 @@ class PromptDialogManager { private static final Logger LOGGER = Logger.getLogger(PromptDialogManager.class.getName()); - @NbBundle.Messages("PrompDialogManager.buttonType.showTimeline=Show Timeline") - private static final ButtonType SHOW_TIMELINE = new ButtonType(Bundle.PrompDialogManager_buttonType_showTimeline(), ButtonBar.ButtonData.OK_DONE); + @NbBundle.Messages("PrompDialogManager.buttonType.showTimeline=Continue") + private static final ButtonType CONTINUE = new ButtonType(Bundle.PrompDialogManager_buttonType_showTimeline(), ButtonBar.ButtonData.OK_DONE); @NbBundle.Messages("PrompDialogManager.buttonType.continueNoUpdate=Continue Without Updating") private static final ButtonType CONTINUE_NO_UPDATE = new ButtonType(Bundle.PrompDialogManager_buttonType_continueNoUpdate(), ButtonBar.ButtonData.CANCEL_CLOSE); - @NbBundle.Messages("PrompDialogManager.buttonType.update=Update") + @NbBundle.Messages("PrompDialogManager.buttonType.update=Update DB") private static final ButtonType UPDATE = new ButtonType(Bundle.PrompDialogManager_buttonType_update(), ButtonBar.ButtonData.OK_DONE); /** @@ -89,7 +86,7 @@ class PromptDialogManager { } /** - * Bring the currently managed dialog (if there is one) to the front + * Bring the currently managed dialog (if there is one) to the front. * * @return True if a dialog was brought to the front, or false of there is * no currently managed open dialog @@ -151,60 +148,45 @@ class PromptDialogManager { } /** - * Prompt the user that ingest is running and the db may not end up + * Prompt the user that ingest is running and the DB may not end up * complete. * - * @return True if they want to continue anyways + * @return True if they want to continue anyways. */ @NbBundle.Messages({ - "PromptDialogManager.confirmDuringIngest.headerText=You are trying to show a timeline before ingest has been completed.\nThe timeline may be incomplete.", + "PromptDialogManager.confirmDuringIngest.headerText=You are trying to update the Timeline DB before ingest has been completed. The Timeline DB may be incomplete.", "PromptDialogManager.confirmDuringIngest.contentText=Do you want to continue?"}) @ThreadConfined(type = ThreadConfined.ThreadType.JFX) boolean confirmDuringIngest() { - currentDialog = new Alert(Alert.AlertType.CONFIRMATION, Bundle.PromptDialogManager_confirmDuringIngest_contentText(), SHOW_TIMELINE, ButtonType.CANCEL); + currentDialog = new Alert(Alert.AlertType.CONFIRMATION, Bundle.PromptDialogManager_confirmDuringIngest_contentText(), CONTINUE, ButtonType.CANCEL); currentDialog.initModality(Modality.APPLICATION_MODAL); currentDialog.setTitle(Bundle.Timeline_dialogs_title()); setDialogIcons(currentDialog); currentDialog.setHeaderText(Bundle.PromptDialogManager_confirmDuringIngest_headerText()); - //show dialog and map all results except "show timeline" to false. - return currentDialog.showAndWait().map(SHOW_TIMELINE::equals).orElse(false); + //show dialog and map all results except "continue" to false. + return currentDialog.showAndWait().map(CONTINUE::equals).orElse(false); } /** * Prompt the user to confirm rebuilding the database for the given list of - * reasons, adding that "ingest has finished" for the datasource with the - * given name, if not blank, as a reason and as extra header text. + * reasons. * - * @param finishedDataSourceName The name of the datasource that has - * finished be analyzed. Will be ignored if it - * is null or empty. - * @param rebuildReasons A List of reasons why the database is out - * of date. + * @param rebuildReasons A List of reasons why the database is out of date. * * @return True if the user a confirms rebuilding the database. */ @NbBundle.Messages({ - "PromptDialogManager.rebuildPrompt.headerText=The Timeline database is incomplete and/or out of date." - + " Some events may be missing or inaccurate and some features may be unavailable.", - "# {0} - data source name", - "PromptDialogManager.rebuildPrompt.ingestDone=Ingest has finished for {0}.", + "PromptDialogManager.rebuildPrompt.headerText=The Timeline DB is incomplete and/or out of date. Some events may be missing or inaccurate and some features may be unavailable.", "PromptDialogManager.rebuildPrompt.details=Details"}) @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - boolean confirmRebuild(@Nullable String finishedDataSourceName, List rebuildReasons) { + boolean confirmRebuild(List rebuildReasons) { currentDialog = new Alert(Alert.AlertType.CONFIRMATION, Bundle.TimeLinecontroller_updateNowQuestion(), UPDATE, CONTINUE_NO_UPDATE); currentDialog.initModality(Modality.APPLICATION_MODAL); currentDialog.setTitle(Bundle.Timeline_dialogs_title()); setDialogIcons(currentDialog); - //configure header text depending on presence of finishedDataSourceName - String headerText = Bundle.PromptDialogManager_rebuildPrompt_headerText(); - if (StringUtils.isNotBlank(finishedDataSourceName)) { - String datasourceMessage = Bundle.PromptDialogManager_rebuildPrompt_ingestDone(finishedDataSourceName); - rebuildReasons.add(0, datasourceMessage); - headerText = datasourceMessage + "\n\n" + headerText; - } - currentDialog.setHeaderText(headerText); + currentDialog.setHeaderText(Bundle.PromptDialogManager_rebuildPrompt_headerText()); //set up listview of reasons to rebuild ListView listView = new ListView<>(FXCollections.observableArrayList(rebuildReasons)); @@ -224,17 +206,4 @@ class PromptDialogManager { //show dialog and map all results except "update" to false. return currentDialog.showAndWait().map(UPDATE::equals).orElse(false); } - - /** - * Prompt the user to confirm rebuilding the database for the given list of - * reasons. - * - * @param rebuildReasons S List of reasons why the database is out of date. - * - * @return True if the user a confirms rebuilding the database. - */ - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - boolean confirmRebuild(ArrayList rebuildReasons) { - return confirmRebuild(null, rebuildReasons); - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index 369bd11910..a02917d75f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -51,7 +51,6 @@ import javafx.concurrent.Task; import javafx.concurrent.Worker; import static javafx.concurrent.Worker.State.FAILED; import static javafx.concurrent.Worker.State.SUCCEEDED; -import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.Immutable; import javax.swing.SwingUtilities; @@ -74,10 +73,9 @@ import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.ingest.IngestManager; import static org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent.CANCELLED; -import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisEvent; -import static org.sleuthkit.autopsy.timeline.Bundle.*; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -90,7 +88,6 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.TimeUnits; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; -import org.sleuthkit.datamodel.Content; /** * Controller in the MVC design along with FilteredEventsModel TimeLineView. @@ -142,7 +139,7 @@ public class TimeLineController { private final ReadOnlyStringWrapper taskTitle = new ReadOnlyStringWrapper(); - private final ReadOnlyStringWrapper status = new ReadOnlyStringWrapper(); + private final ReadOnlyStringWrapper statusMessage = new ReadOnlyStringWrapper(); /** * Status is a string that will be displayed in the status bar as a kind of @@ -150,12 +147,12 @@ public class TimeLineController { * * @return The status property */ - public ReadOnlyStringProperty getStatusProperty() { - return status.getReadOnlyProperty(); + public ReadOnlyStringProperty statusMessageProperty() { + return statusMessage.getReadOnlyProperty(); } - public void setStatus(String string) { - status.set(string); + public void setStatusMessage(String string) { + statusMessage.set(string); } private final Case autoCase; private final PerCaseTimelineProperties perCaseTimelineProperties; @@ -202,10 +199,10 @@ public class TimeLineController { private final PropertyChangeListener ingestModuleListener = new AutopsyIngestModuleListener(); @GuardedBy("this") - private final ReadOnlyObjectWrapper viewMode = new ReadOnlyObjectWrapper<>(VisualizationMode.COUNTS); + private final ReadOnlyObjectWrapper visualizationMode = new ReadOnlyObjectWrapper<>(VisualizationMode.COUNTS); synchronized public ReadOnlyObjectProperty visualizationModeProperty() { - return viewMode.getReadOnlyProperty(); + return visualizationMode.getReadOnlyProperty(); } @GuardedBy("filteredEvents") @@ -264,6 +261,7 @@ public class TimeLineController { @NbBundle.Messages({ "TimeLineController.setEventsDBStale.errMsgStale=Failed to mark the timeline db as stale. Some results may be out of date or missing.", "TimeLineController.setEventsDBStale.errMsgNotStale=Failed to mark the timeline db as not stale. Some results may be out of date or missing."}) + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void setEventsDBStale(final Boolean stale) { eventsDBStale.set(stale); try { @@ -389,12 +387,13 @@ public class TimeLineController { perCaseTimelineProperties.setIngestRunning(ingestRunning); } catch (IOException ex) { MessageNotifyUtil.Notify.error(Bundle.Timeline_dialogs_title(), - ingestRunning ? TimeLineController_setIngestRunning_errMsgRunning() - : TimeLinecontroller_setIngestRunning_errMsgNotRunning()); + ingestRunning ? Bundle.TimeLineController_setIngestRunning_errMsgRunning() + : Bundle.TimeLinecontroller_setIngestRunning_errMsgNotRunning()); LOGGER.log(Level.SEVERE, "Error marking the ingest state while the timeline db was populated.", ex); //NON-NLS } if (markDBNotStale) { setEventsDBStale(false); + filteredEvents.postDBUpdated(); } SwingUtilities.invokeLater(this::showWindow); break; @@ -454,6 +453,7 @@ public class TimeLineController { mainFrame.close(); mainFrame = null; } + OpenTimelineAction.invalidateController(); } /** @@ -470,21 +470,17 @@ public class TimeLineController { listeningToAutopsy = true; } - Platform.runLater(() -> promptForRebuild(null)); + Platform.runLater(this::promptForRebuild); } /** - * Prompt the user to confirm rebuilding the db because ingest has finished - * on the datasource with the given name. Checks if a database rebuild is - * necessary for any other reasons and includes those in the prompt. If the - * user confirms, rebuilds the database. Shows the timeline window when the - * rebuild is done, or immediately if the rebuild is not confirmed. - * - * @param dataSourceName The name of the datasource that ingest has finished - * processing. Will be ignored if it is null or empty. + * Prompt the user to confirm rebuilding the db. Checks if a database + * rebuild is necessary and includes the reasons in the prompt. If the user + * confirms, rebuilds the database. Shows the timeline window when the + * rebuild is done, or immediately if the rebuild is not confirmed. F */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private void promptForRebuild(@Nullable String dataSourceName) { + private void promptForRebuild() { //if there is an existing prompt or progressdialog, just show that if (promptDialogManager.bringCurrentDialogToFront()) { @@ -500,7 +496,7 @@ public class TimeLineController { //if necessary prompt user with reasons to rebuild List rebuildReasons = getRebuildReasons(); if (false == rebuildReasons.isEmpty()) { - if (promptDialogManager.confirmRebuild(dataSourceName, rebuildReasons)) { + if (promptDialogManager.confirmRebuild(rebuildReasons)) { rebuildRepo(); return; } @@ -560,7 +556,7 @@ public class TimeLineController { * Request a time range the same length as the given period and centered * around the middle of the currently viewed time range. * - * @param period The period of time to shw around the current center of the + * @param period The period of time to show around the current center of the * view. */ synchronized public void pushPeriod(ReadablePeriod period) { @@ -586,9 +582,14 @@ public class TimeLineController { pushTimeRange(new Interval(start, end)); } - synchronized public void setViewMode(VisualizationMode visualizationMode) { - if (viewMode.get() != visualizationMode) { - viewMode.set(visualizationMode); + /** + * Set a new Visualization mode as the active one. + * + * @param visualizationMode The new VisaualizationMode to set. + */ + synchronized public void setVisualizationMode(VisualizationMode visualizationMode) { + if (this.visualizationMode.get() != visualizationMode) { + this.visualizationMode.set(visualizationMode); } } @@ -812,47 +813,6 @@ public class TimeLineController { TimeLineController.timeZone.set(timeZone); } - Interval getSpanningInterval(Collection eventIDs) { - return filteredEvents.getSpanningInterval(eventIDs); - - } - - /** - * Is the timeline window open? - * - * @return True if the timeline is open. - */ - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - private boolean isWindowOpen() { - return mainFrame != null && mainFrame.isOpened() && mainFrame.isVisible(); - } - - /** - * Rebuild the db ONLY IF THE TIMELINE WINDOW IS OPEN. The user will be - * prompted with reasons why the database needs to be rebuilt and can still - * cancel the rebuild. The prompt will include that ingest has finished for - * the given datasource name, if not blank. - * - * @param dataSourceName The name of the datasource that has finished - * ingest. Will be ignored if it is null or empty. - */ - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - private void rebuildIfWindowOpen(@Nullable String dataSourceName) { - if (isWindowOpen()) { - Platform.runLater(() -> this.promptForRebuild(dataSourceName)); - } - } - - /** - * Rebuild the db ONLY IF THE TIMELINE WINDOW IS OPEN. The user will be - * prompted with reasons why the database needs to be rebuilt and can still - * cancel the rebuild. - */ - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - public void rebuildIfWindowOpen() { - rebuildIfWindowOpen(null); - } - /** * Listener for IngestManager.IngestModuleEvents. */ @@ -870,16 +830,14 @@ public class TimeLineController { try { Case.getCurrentCase(); } catch (IllegalStateException notUsed) { - /** - * Case is closed, do nothing. - */ + // Case is closed, do nothing. return; } switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) { case CONTENT_CHANGED: case DATA_ADDED: - //since black board artifacts or new derived content have been added, the db is stale. + //since black board artifacts or new derived content have been added, the DB is stale. Platform.runLater(() -> setEventsDBStale(true)); break; case FILE_DONE: @@ -902,9 +860,9 @@ public class TimeLineController { public void propertyChange(PropertyChangeEvent evt) { switch (IngestManager.IngestJobEvent.valueOf(evt.getPropertyName())) { case DATA_SOURCE_ANALYSIS_COMPLETED: - // include data source name in rebuild prompt on ingest completed - final Content dataSource = ((DataSourceAnalysisEvent) evt).getDataSource(); - SwingUtilities.invokeLater(() -> rebuildIfWindowOpen(dataSource.getName())); + //mark db stale, and prompt to rebuild + Platform.runLater(() -> setEventsDBStale(true)); + filteredEvents.postAutopsyEventLocally((AutopsyEvent) evt); break; case DATA_SOURCE_ANALYSIS_STARTED: case CANCELLED: @@ -939,10 +897,10 @@ public class TimeLineController { case DATA_SOURCE_ADDED: //mark db stale, and prompt to rebuild Platform.runLater(() -> setEventsDBStale(true)); + filteredEvents.postAutopsyEventLocally((AutopsyEvent) evt); break; case CURRENT_CASE: //close timeline on case changes. - OpenTimelineAction.invalidateController(); SwingUtilities.invokeLater(TimeLineController.this::shutDownTimeLine); break; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/RebuildDataBase.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/UpdateDB.java similarity index 80% rename from Core/src/org/sleuthkit/autopsy/timeline/actions/RebuildDataBase.java rename to Core/src/org/sleuthkit/autopsy/timeline/actions/UpdateDB.java index 63669522df..599f029844 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/RebuildDataBase.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/UpdateDB.java @@ -25,20 +25,26 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.TimeLineController; /** - * An action that rebuilds the events database to include any new results from + * An action that rebuilds the timeline database to include any new results from * ingest. */ -public class RebuildDataBase extends Action { +public class UpdateDB extends Action { private static final Image DB_REFRESH = new Image("org/sleuthkit/autopsy/timeline/images/database_refresh.png"); + /** + * Constructor + * + * @param controller The TimeLineController for this action. + */ @NbBundle.Messages({ "RebuildDataBase.text=Update DB", "RebuildDataBase.longText=Update the DB to include new events."}) - public RebuildDataBase(TimeLineController controller) { + public UpdateDB(TimeLineController controller) { super(Bundle.RebuildDataBase_text()); setLongText(Bundle.RebuildDataBase_longText()); setGraphic(new ImageView(DB_REFRESH)); setEventHandler(actionEvent -> controller.rebuildRepo()); + disabledProperty().bind(controller.eventsDBStaleProperty().not()); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index b2b9136136..8917c0cb84 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -40,9 +40,11 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent.DeletedContentTagInfo; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; import org.sleuthkit.autopsy.timeline.db.EventsRepository; +import org.sleuthkit.autopsy.timeline.events.DBUpdatedEvent; import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; import org.sleuthkit.autopsy.timeline.events.TagsAddedEvent; import org.sleuthkit.autopsy.timeline.events.TagsDeletedEvent; @@ -416,6 +418,15 @@ public final class FilteredEventsModel { return false; } + /** + * Post a TagsAddedEvent to all registered subscribers, if the given set of + * updated event IDs is not empty. + * + * @param updatedEventIDs The set of event ids to be included in the + * TagsAddedEvent. + * + * @return True if an event was posted. + */ private boolean postTagsAdded(Set updatedEventIDs) { boolean tagsUpdated = !updatedEventIDs.isEmpty(); if (tagsUpdated) { @@ -424,6 +435,15 @@ public final class FilteredEventsModel { return tagsUpdated; } + /** + * Post a TagsDeletedEvent to all registered subscribers, if the given set + * of updated event IDs is not empty. + * + * @param updatedEventIDs The set of event ids to be included in the + * TagsDeletedEvent. + * + * @return True if an event was posted. + */ private boolean postTagsDeleted(Set updatedEventIDs) { boolean tagsUpdated = !updatedEventIDs.isEmpty(); if (tagsUpdated) { @@ -432,16 +452,45 @@ public final class FilteredEventsModel { return tagsUpdated; } + /** + * Register the given object to receive events. + * + * @param o The object to register. Must implement public methods annotated + * with Subscribe. + */ synchronized public void registerForEvents(Object o) { eventbus.register(o); } + /** + * Un-register the given object, so it no longer receives events. + * + * @param o The object to un-register. + */ synchronized public void unRegisterForEvents(Object o) { eventbus.unregister(0); } - public void refresh() { + /** + * Post a DBUpdatedEvent to all registered subscribers. + */ + public void postDBUpdated() { + eventbus.post(new DBUpdatedEvent()); + } + + /** + * Post a RefreshRequestedEvent to all registered subscribers. + */ + public void postRefreshRequest() { eventbus.post(new RefreshRequestedEvent()); } + /** + * (Re)Post an AutopsyEvent received from another event distribution system + * locally to all registered subscribers. + */ + public void postAutopsyEventLocally(AutopsyEvent event) { + eventbus.post(event); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/DBUpdatedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/DBUpdatedEvent.java new file mode 100644 index 0000000000..75d72d913a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/events/DBUpdatedEvent.java @@ -0,0 +1,29 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2016 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.timeline.events; + +/** + * A "local" event published by filteredEventsModel to indicate that DB has been + * updated. + * + * This event is not intended for use out side of the Timeline module. + */ +public class DBUpdatedEvent { + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java index f8cdfe23e7..47b04eb479 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java @@ -22,8 +22,8 @@ package org.sleuthkit.autopsy.timeline.events; * A "local" event published by filteredEventsModel to indicate that the user * requested that the current visualization be refreshed with out changing any * of the parameters ( to include more up to date tag data for example.) - *

- * This event is not intended for use out side of the timeline module. + * + * This event is not intended for use out side of the Timeline module. */ public class RefreshRequestedEvent { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/warning.png b/Core/src/org/sleuthkit/autopsy/timeline/images/warning.png new file mode 100644 index 0000000000..b6281848a7 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/warning.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/warning_triangle.png b/Core/src/org/sleuthkit/autopsy/timeline/images/warning_triangle.png new file mode 100644 index 0000000000..b7024717af Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/warning_triangle.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/warning_triangle_small.png b/Core/src/org/sleuthkit/autopsy/timeline/images/warning_triangle_small.png new file mode 100644 index 0000000000..06da735537 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/warning_triangle_small.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 3bcde800cf..f685b8262e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.timeline.ui; -import com.google.common.eventbus.Subscribe; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -69,7 +68,6 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; /** * Abstract base class for TimeLineChart based visualizations. @@ -95,18 +93,29 @@ public abstract class AbstractVisualizationPane> dataSeries = FXCollections.>observableArrayList(); protected final Map> eventTypeToSeriesMap = new HashMap<>(); @@ -128,7 +137,41 @@ public abstract class AbstractVisualizationPane selectedNodes = FXCollections.observableArrayList(); - private InvalidationListener updateListener = any -> update(); + /** + * Listener that is attached to various properties that should trigger a vis + * update when they change. + */ + private InvalidationListener updateListener = any -> refresh(); + + /** + * Does the visualization represent an out-of-date state of the DB. It might + * if, for example, tags have been updated but the vis. was not refreshed. + * + * @return True if the visualization does not represent the curent state of + * the DB. + */ + public boolean isOutOfDate() { + return outOfDate.get(); + } + + /** + * Set this visualization out of date because, for example, tags have been + * updated but the vis. was not refreshed. + */ + void setOutOfDate() { + outOfDate.set(true); + } + + /** + * Get a ReadOnlyBooleanProperty that holds true if this visualization does + * not represent the current state of the DB> + * + * @return A ReadOnlyBooleanProperty that holds the out-of-date state for + * this visualization. + */ + public ReadOnlyBooleanProperty outOfDateProperty() { + return outOfDate.getReadOnlyProperty(); + } public Pane getSpecificLabelPane() { return specificLabelPane; @@ -335,13 +378,13 @@ public abstract class AbstractVisualizationPane controller.setStatus(isHover() ? DEFAULT_TOOLTIP.getText() : "")); + hoverProperty().addListener(hoverProp -> controller.setStatusMessage(isHover() ? DEFAULT_TOOLTIP.getText() : "")); } - /** - * Handle a RefreshRequestedEvent from the events model by updating the - * visualization. - * - * @param event The RefreshRequestedEvent to handle. - */ - @Subscribe - public void handleRefreshRequested(RefreshRequestedEvent event) { - update(); - } - /** * Iterate through the list of tick-marks building a two level structure of * replacement tick mark labels. (Visually) upper level has most @@ -646,13 +678,13 @@ public abstract class AbstractVisualizationPane The type of a single object that can represent * the range of data displayed along the X-Axis. */ - abstract protected class VisualizationUpdateTask extends LoggedTask { + abstract protected class VisualizationRefreshTask extends LoggedTask { private final Node center; @@ -660,10 +692,10 @@ public abstract class AbstractVisualizationPane - - - + - - - - - - - - - - + + + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java index bf5a37a0e7..49460d1ad8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java @@ -29,7 +29,6 @@ import java.util.function.Supplier; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; @@ -68,18 +67,22 @@ import org.controlsfx.control.action.ActionUtils; import org.joda.time.DateTime; import org.joda.time.Interval; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.VisualizationMode; import org.sleuthkit.autopsy.timeline.actions.Back; import org.sleuthkit.autopsy.timeline.actions.ResetFilters; import org.sleuthkit.autopsy.timeline.actions.SaveSnapshotAsReport; +import org.sleuthkit.autopsy.timeline.actions.UpdateDB; import org.sleuthkit.autopsy.timeline.actions.ZoomIn; import org.sleuthkit.autopsy.timeline.actions.ZoomOut; import org.sleuthkit.autopsy.timeline.actions.ZoomToEvents; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.events.DBUpdatedEvent; import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; import org.sleuthkit.autopsy.timeline.events.TagsUpdatedEvent; import org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane; @@ -100,6 +103,7 @@ final public class VisualizationPanel extends BorderPane { private static final Logger LOGGER = Logger.getLogger(VisualizationPanel.class.getName()); private static final Image INFORMATION = new Image("org/sleuthkit/autopsy/timeline/images/information.png", 16, 16, true, true); // NON-NLS + private static final Image WARNING = new Image("org/sleuthkit/autopsy/timeline/images/warning_triangle.png", 16, 16, true, true); // NON-NLS private static final Image REFRESH = new Image("org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png"); // NON-NLS private static final Background GRAY_BACKGROUND = new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)); @@ -167,19 +171,14 @@ final public class VisualizationPanel extends BorderPane { private Button snapShotButton; @FXML private Button refreshButton; + @FXML + private Button updateDBButton; /* * Wraps contained visualization so that we can show notifications over it. */ private final NotificationPane notificationPane = new NotificationPane(); - /* - * Boolean property that holds true if the visualziation may not represent - * the current state of the DB, because, for example, tags have been updated - * but the vis. was not refreshed. - */ - private final ReadOnlyBooleanWrapper needsRefresh = new ReadOnlyBooleanWrapper(false); - private final TimeLineController controller; private final FilteredEventsModel filteredEvents; @@ -205,7 +204,7 @@ final public class VisualizationPanel extends BorderPane { /** * hides the notification pane on any event */ - private final InvalidationListener zoomListener = any -> setNeedsRefresh(false); + private final InvalidationListener zoomListener = any -> handleRefreshRequested(null); /** * listen to change in end time picker and push to controller @@ -263,7 +262,8 @@ final public class VisualizationPanel extends BorderPane { "VisualizationPanel.countsToggle.text=Counts", "VisualizationPanel.detailsToggle.text=Details", "VisualizationPanel.zoomMenuButton.text=Zoom in/out to", - "VisualizationPanel.tagsAddedOrDeleted=Tags have been created and/or deleted. The visualization may not be up to date."}) + "VisualizationPanel.tagsAddedOrDeleted=Tags have been created and/or deleted. The visualization may not be up to date." + }) void initialize() { assert endPicker != null : "fx:id=\"endPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'."; // NON-NLS assert histogramBox != null : "fx:id=\"histogramBox\" was not injected: check your FXML file 'ViewWrapper.fxml'."; // NON-NLS @@ -274,15 +274,7 @@ final public class VisualizationPanel extends BorderPane { //configure notification pane notificationPane.getStyleClass().add(NotificationPane.STYLE_CLASS_DARK); - notificationPane.getActions().setAll(new Refresh()); setCenter(notificationPane); - needsRefresh.addListener(observable -> { - if (needsRefresh.get()) { - notificationPane.show(Bundle.VisualizationPanel_tagsAddedOrDeleted(), new ImageView(INFORMATION)); - } else { - notificationPane.hide(); - } - }); //configure visualization mode toggle visualizationModeLabel.setText(Bundle.VisualizationPanel_visualizationModeLabel_text()); @@ -292,9 +284,9 @@ final public class VisualizationPanel extends BorderPane { if (newValue == null) { countsToggle.getToggleGroup().selectToggle(oldValue != null ? oldValue : countsToggle); } else if (newValue == countsToggle && oldValue != null) { - controller.setViewMode(VisualizationMode.COUNTS); + controller.setVisualizationMode(VisualizationMode.COUNTS); } else if (newValue == detailsToggle && oldValue != null) { - controller.setViewMode(VisualizationMode.DETAIL); + controller.setVisualizationMode(VisualizationMode.DETAIL); } else if (newValue == listToggle && oldValue != null) { controller.setViewMode(VisualizationMode.LIST); } @@ -303,7 +295,7 @@ final public class VisualizationPanel extends BorderPane { if (countsToggle.getToggleGroup() != null) { countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener); } else { - countsToggle.toggleGroupProperty().addListener((Observable observable) -> { + countsToggle.toggleGroupProperty().addListener((Observable toggleGroup) -> { countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener); }); } @@ -311,9 +303,8 @@ final public class VisualizationPanel extends BorderPane { controller.visualizationModeProperty().addListener(visualizationMode -> syncVisualizationMode()); syncVisualizationMode(); - //configure snapshor button / action ActionUtils.configureButton(new SaveSnapshotAsReport(controller, notificationPane::getContent), snapShotButton); - ActionUtils.configureButton(new Refresh(), refreshButton); + ActionUtils.configureButton(new UpdateDB(controller), updateDBButton); /////configure start and end pickers startLabel.setText(Bundle.VisualizationPanel_startLabel_text()); @@ -371,20 +362,13 @@ final public class VisualizationPanel extends BorderPane { filteredEvents.zoomParametersProperty().addListener(zoomListener); refreshTimeUI(); //populate the viz - //this should use an event(EventBus) , not this weird observable pattern - controller.eventsDBStaleProperty().addListener(staleProperty -> { - if (controller.isEventsDBStale()) { - Platform.runLater(VisualizationPanel.this::refreshHistorgram); - } - }); refreshHistorgram(); } /** - * Handle TagsUpdatedEvents. - * - * Mark that the visualization needs to be refreshed. + * Handle TagsUpdatedEvents by marking that the visualization needs to be + * refreshed. * * NOTE: This VisualizationPanel must be registered with the * filteredEventsModel's EventBus in order for this handler to be invoked. @@ -392,14 +376,19 @@ final public class VisualizationPanel extends BorderPane { * @param event The TagsUpdatedEvent to handle. */ @Subscribe - public void handleTimeLineTagEvent(TagsUpdatedEvent event) { - setNeedsRefresh(true); + public void handleTimeLineTagUpdate(TagsUpdatedEvent event) { + visualization.setOutOfDate(); + Platform.runLater(() -> { + if (notificationPane.isShowing() == false) { + notificationPane.getActions().setAll(new Refresh()); + notificationPane.show(Bundle.VisualizationPanel_tagsAddedOrDeleted(), new ImageView(INFORMATION)); + } + }); } /** - * Handle RefreshRequestedEvent. - * - * Mark that the visualization has been refreshed. + * Handle a RefreshRequestedEvent from the events model by refreshing the + * visualization. * * NOTE: This VisualizationPanel must be registered with the * filteredEventsModel's EventBus in order for this handler to be invoked. @@ -407,19 +396,69 @@ final public class VisualizationPanel extends BorderPane { * @param event The RefreshRequestedEvent to handle. */ @Subscribe - public void handleRefreshRequestedEvent(RefreshRequestedEvent event) { - setNeedsRefresh(false); + public void handleRefreshRequested(RefreshRequestedEvent event) { + visualization.refresh(); + Platform.runLater(() -> { + if (Bundle.VisualizationPanel_tagsAddedOrDeleted().equals(notificationPane.getText())) { + notificationPane.hide(); + } + }); } /** - * Set whether the visualziation may not represent the current state of the - * DB, because, for example, tags have been updated. + * Handle a DBUpdatedEvent from the events model by refreshing the + * visualization. * - * @param needsRefresh True if the visualization may not represent the - * current state of the DB. + * NOTE: This VisualizationPanel must be registered with the + * filteredEventsModel's EventBus in order for this handler to be invoked. + * + * @param event The DBUpdatedEvent to handle. */ - private void setNeedsRefresh(Boolean needsRefresh) { - Platform.runLater(() -> VisualizationPanel.this.needsRefresh.set(needsRefresh)); + @Subscribe + public void handleDBUpdated(DBUpdatedEvent event) { + visualization.refresh(); + refreshHistorgram(); + Platform.runLater(notificationPane::hide); + } + + /** + * Handle a DataSourceAddedEvent from the events model by showing a + * notification. + * + * NOTE: This VisualizationPanel must be registered with the + * filteredEventsModel's EventBus in order for this handler to be invoked. + * + * @param event The DataSourceAddedEvent to handle. + */ + @Subscribe + @NbBundle.Messages({ + "# {0} - datasource name", + "VisualizationPanel.notification.newDataSource={0} has been added as a new datasource. The Timeline DB may be out of date."}) + public void handlDataSourceAdded(DataSourceAddedEvent event) { + Platform.runLater(() -> { + notificationPane.getActions().setAll(new UpdateDB(controller)); + notificationPane.show(Bundle.VisualizationPanel_notification_newDataSource(event.getDataSource().getName()), new ImageView(WARNING)); + }); + } + + /** + * Handle a DataSourceAnalysisCompletedEvent from the events modelby showing + * a notification. + * + * NOTE: This VisualizationPanel must be registered with the + * filteredEventsModel's EventBus in order for this handler to be invoked. + * + * @param event The DataSourceAnalysisCompletedEvent to handle. + */ + @Subscribe + @NbBundle.Messages({ + "# {0} - datasource name", + "VisualizationPanel.notification.analysisComplete=Analysis has finished for {0}. The Timeline DB may be out of date."}) + public void handleAnalysisCompleted(DataSourceAnalysisCompletedEvent event) { + Platform.runLater(() -> { + notificationPane.getActions().setAll(new UpdateDB(controller)); + notificationPane.show(Bundle.VisualizationPanel_notification_analysisComplete(event.getDataSource().getName()), new ImageView(WARNING)); + }); } /** @@ -584,13 +623,15 @@ final public class VisualizationPanel extends BorderPane { toolBar.getItems().removeAll(visualization.getSettingsNodes()); visualization.dispose(); } - //setup new vis. + visualization = vizPane; - visualization.update(); - toolBar.getItems().addAll(vizPane.getSettingsNodes()); + //setup new vis. + ActionUtils.configureButton(new Refresh(), refreshButton);//configure new refresh action for new visualization + visualization.refresh(); + toolBar.getItems().addAll(2, vizPane.getSettingsNodes()); notificationPane.setContent(visualization); - //listen to has event sproperty and show "dialog" if it is false. + //listen to has events property and show "dialog" if it is false. visualization.hasVisibleEventsProperty().addListener(hasEvents -> { notificationPane.setContent(visualization.hasVisibleEvents() ? visualization @@ -601,7 +642,6 @@ final public class VisualizationPanel extends BorderPane { ); }); }); - setNeedsRefresh(false); } @NbBundle.Messages("NoEventsDialog.titledPane.text=No Visible Events") @@ -736,14 +776,14 @@ final public class VisualizationPanel extends BorderPane { private class Refresh extends Action { @NbBundle.Messages({ - "VisualizationPanel.refresh.text=Refresh", - "VisualizationPanel.refresh.longText=Refresh the visualization to include information that is in the database but not visualized, such as newly updated tags."}) + "VisualizationPanel.refresh.text=Refresh Vis.", + "VisualizationPanel.refresh.longText=Refresh the visualization to include information that is in the DB but not visualized, such as newly updated tags."}) Refresh() { super(Bundle.VisualizationPanel_refresh_text()); setLongText(Bundle.VisualizationPanel_refresh_longText()); setGraphic(new ImageView(REFRESH)); - setEventHandler(actionEvent -> filteredEvents.refresh()); - disabledProperty().bind(needsRefresh.not()); + setEventHandler(actionEvent -> filteredEvents.postRefreshRequest()); + disabledProperty().bind(visualization.outOfDateProperty().not()); } } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java index 75374caf23..ead57e88bb 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java @@ -146,7 +146,7 @@ public class CountsViewPane extends AbstractVisualizationPane { - update(); + refresh(); syncAxisScaleLabel(); }); syncAxisScaleLabel(); @@ -320,7 +320,7 @@ public class CountsViewPane extends AbstractVisualizationPane> { + private class CountsUpdateTask extends VisualizationRefreshTask> { CountsUpdateTask() { super(Bundle.CountsViewPane_loggedTask_name(), true); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java index 477ef7eb64..031101ab1e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java @@ -403,7 +403,7 @@ final class EventCountsChart extends StackedBarChart implements Bundle.CountsViewPane_detailSwitchMessage(), Bundle.CountsViewPane_detailSwitchTitle(), JOptionPane.YES_NO_OPTION); if (showConfirmDialog == JOptionPane.YES_OPTION) { - controller.setViewMode(VisualizationMode.DETAIL); + controller.setVisualizationMode(VisualizationMode.DETAIL); } /* diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index b6c50a8ee3..4f90a06bcd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -358,7 +358,7 @@ public class DetailViewPane extends AbstractVisualizationPane { + private class DetailsUpdateTask extends VisualizationRefreshTask { DetailsUpdateTask() { super(Bundle.DetailViewPane_loggedTask_name(), true); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListChart.java index 6f95bb2efe..2dd951a961 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListChart.java @@ -15,10 +15,13 @@ import javafx.scene.control.ContextMenu; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.ui.IntervalSelector; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import org.sleuthkit.datamodel.TskData; /** * @@ -28,28 +31,26 @@ class ListChart extends TableView implements TimeLineChart { private final TimeLineController controller; private final TableColumn idColumn = new TableColumn<>(); private final TableColumn millisColumn = new TableColumn<>(); + private final TableColumn iconColumn = new TableColumn<>(); + private final TableColumn descriptionColumn = new TableColumn<>(); + private final TableColumn baseTypeColumn = new TableColumn<>(); + private final TableColumn subTypeColumn = new TableColumn<>(); + private final TableColumn knownColumn = new TableColumn<>(); ListChart(TimeLineController controller) { this.controller = controller; - getColumns().addAll(Arrays.asList(idColumn, millisColumn)); + getColumns().addAll(Arrays.asList(idColumn, iconColumn, millisColumn)); idColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue())); + millisColumn.setCellValueFactory(param -> { return new SimpleObjectProperty<>(controller.getEventsModel().getEventById(param.getValue()).getStartMillis()); }); - millisColumn.setCellFactory(col -> new TableCell() { - @Override - protected void updateItem(Long item, boolean empty) { - super.updateItem(item, empty); - - if (empty || item == null) { - setText(""); - } else { - setText(TimeLineController.getZonedFormatter().print(item)); - - } - } + millisColumn.setCellFactory(col -> new EpochMillisCell()); + iconColumn.setCellValueFactory(param -> { + return new SimpleObjectProperty<>(controller.getEventsModel().getEventById(param.getValue()).getEventType().getFXImage()); }); + iconColumn.setCellFactory(col -> new ImageCell()); millisColumn.setSortType(TableColumn.SortType.DESCENDING); millisColumn.setSortable(true); @@ -103,4 +104,31 @@ class ListChart extends TableView implements TimeLineChart { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } + private static class ImageCell extends TableCell { + + @Override + protected void updateItem(Image item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setGraphic(null); + } else { + setGraphic(new ImageView(item)); + } + } + } + + private class EpochMillisCell extends TableCell { + + @Override + protected void updateItem(Long item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(""); + } else { + setText(TimeLineController.getZonedFormatter().print(item)); + } + } + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java index 60468f4952..68b08506df 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java @@ -97,7 +97,7 @@ public class ListViewPane extends AbstractVisualizationPane { + private class ListUpdateTask extends VisualizationRefreshTask { ListUpdateTask() { super("List update task", true);