diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index 229ffe5cdb..b58268dce3 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties @@ -158,6 +158,7 @@ CueBannerPanel.title.text=Open Recent Case GeneralFilter.rawImageDesc.text=Raw Images (*.img, *.dd, *.001, *.aa, *.raw, *.bin) GeneralFilter.encaseImageDesc.text=Encase Images (*.e01) GeneralFilter.virtualMachineImageDesc.text=Virtual Machines (*.vmdk, *.vhd) +GeneralFilter.executableDesc.text=Executables (*.exe) ImageDSProcessor.dsType.text=Image or VM File ImageDSProcessor.allDesc.text=All Supported Types ImageFilePanel.moduleErr=Module Error diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/GeneralFilter.java b/Core/src/org/sleuthkit/autopsy/casemodule/GeneralFilter.java index ffe6f26b72..0f90bc9fae 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/GeneralFilter.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/GeneralFilter.java @@ -42,6 +42,9 @@ public class GeneralFilter extends FileFilter { public static final String VIRTUAL_MACHINE_DESC = NbBundle.getMessage(GeneralFilter.class, "GeneralFilter.virtualMachineImageDesc.text"); + public static final List EXECUTABLE_EXTS = Arrays.asList(new String[]{".exe"}); //NON-NLS + public static final String EXECUTABLE_DESC = NbBundle.getMessage(GeneralFilter.class, "GeneralFilter.executableDesc.text"); + private List extensions; private String desc; diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/AddRuleDialog.java b/Core/src/org/sleuthkit/autopsy/directorytree/AddRuleDialog.java index a624d860ae..037860f2df 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/AddRuleDialog.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/AddRuleDialog.java @@ -5,15 +5,173 @@ */ package org.sleuthkit.autopsy.directorytree; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.beans.PropertyChangeEvent; +import javax.swing.BoxLayout; +import javax.swing.JButton; import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; /** - * - * @author smori + * A dialog for adding or editing an external viewer rule */ public class AddRuleDialog extends JDialog { - + + private ExternalViewerRule rule; + private final AddRulePanel addRulePanel; + private BUTTON_PRESSED result; + private JButton saveButton; + private JButton closeButton; + enum BUTTON_PRESSED { OK, CANCEL; } + + /** + * Creates a dialog for creating an external viewer rule + */ + @Messages({"AddRuleDialog.title=External Viewer Rule"}) + AddRuleDialog() { + super(new JFrame(Bundle.AddRuleDialog_title()), Bundle.AddRuleDialog_title(), true); + addRulePanel = new AddRulePanel(); + this.display(); + } + + /** + * Creates a dialog for editing an external viewer rule + * + * @param rule ExternalViewerRule to be edited + */ + AddRuleDialog(ExternalViewerRule rule) { + super(new JFrame(Bundle.AddRuleDialog_title()), Bundle.AddRuleDialog_title(), true); + addRulePanel = new AddRulePanel(rule); + this.display(); + } + + /** + * Displays the add external viewer rule dialog. + */ + @NbBundle.Messages({ + "AddRuleDialog.addButton.title=Save", + "AddRuleDialog.cancelButton.title=Cancel"}) + private void display() { + setLayout(new BorderLayout()); + + /** + * Center the dialog. + */ + Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize(); + int width = this.getSize().width; + int height = this.getSize().height; + setLocation((screenDimension.width - width) / 2, (screenDimension.height - height) / 2); + + add(this.addRulePanel, BorderLayout.PAGE_START); + + // Add a save button. + saveButton = new JButton(Bundle.AddRuleDialog_addButton_title()); + saveButton.addActionListener((ActionEvent e) -> { + doButtonAction(true); + }); + + // Add a close button. + closeButton = new JButton(Bundle.AddRuleDialog_cancelButton_title()); + closeButton.addActionListener((ActionEvent e) -> { + doButtonAction(false); + }); + + // Put the buttons in their own panel, under the settings panel. + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS)); + buttonPanel.add(new javax.swing.Box.Filler(new Dimension(10, 10), new Dimension(10, 10), new Dimension(10, 10))); + buttonPanel.add(saveButton); + buttonPanel.add(new javax.swing.Box.Filler(new Dimension(10, 10), new Dimension(10, 10), new Dimension(10, 10))); + buttonPanel.add(closeButton); + add(buttonPanel, BorderLayout.LINE_START); + + /** + * Add a handler for when the dialog window is closed directly, + * bypassing the buttons. + */ + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + doButtonAction(false); + } + }); + + /** + * Add a listener to enable the save button when a text field in the + * AddRulePanel is changed or modified. + */ + this.addRulePanel.addPropertyChangeListener((PropertyChangeEvent evt) -> { + if (evt.getPropertyName().equals(AddRulePanel.EVENT.CHANGED.toString())) { + enableSaveButton(); + } + }); + + enableSaveButton(); + + /** + * Show the dialog. + */ + pack(); + setResizable(false); + setVisible(true); + getRootPane().setDefaultButton(saveButton); + } + + /** + * Performs actions based on whether the save button was pressed or not. + * + * @param savePressed Whether save was pressed. + */ + private void doButtonAction(boolean savePressed) { + if (savePressed) { + ExternalViewerRule ruleFromPanel = addRulePanel.getRule(); + if (null != ruleFromPanel) { + this.rule = ruleFromPanel; + this.result = BUTTON_PRESSED.OK; + setVisible(false); + } + } else { + this.rule = null; + this.result = BUTTON_PRESSED.CANCEL; + setVisible(false); + } + } + + /** + * Gets the external viewer rule of this dialog + * + * @return The external viewer rule + */ + ExternalViewerRule getRule() { + return rule; + } + + /** + * Gets the button pressed on this dialog + * + * @return The button pressed to close the dialog + */ + BUTTON_PRESSED getResult() { + return result; + } + + /** + * Enables save button once addRulePanel's fields have text in them. Maps + * enter key to the save button. + */ + private void enableSaveButton() { + this.saveButton.setEnabled(addRulePanel.hasFields()); + getRootPane().setDefaultButton(saveButton); + } } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/AddRulePanel.form b/Core/src/org/sleuthkit/autopsy/directorytree/AddRulePanel.form index 4f9abb50dc..1c3f57eb3a 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/AddRulePanel.form +++ b/Core/src/org/sleuthkit/autopsy/directorytree/AddRulePanel.form @@ -1,6 +1,6 @@ -
+ @@ -16,13 +16,84 @@ - + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/AddRulePanel.java b/Core/src/org/sleuthkit/autopsy/directorytree/AddRulePanel.java index 5cae4d2e81..85bb82a6d0 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/AddRulePanel.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/AddRulePanel.java @@ -5,17 +5,137 @@ */ package org.sleuthkit.autopsy.directorytree; +import java.util.logging.Level; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.GeneralFilter; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; + /** - * - * @author smori + * Panel found in an AddRuleDialog */ -public class AddRulePanel extends javax.swing.JPanel { +class AddRulePanel extends javax.swing.JPanel { + + private static final Logger logger = Logger.getLogger(AddRulePanel.class.getName()); + private final JFileChooser fc = new JFileChooser(); + private static final GeneralFilter exeFilter = new GeneralFilter(GeneralFilter.EXECUTABLE_EXTS, GeneralFilter.EXECUTABLE_DESC); + + enum EVENT { + CHANGED + } /** * Creates new form AddRulePanel */ - public AddRulePanel() { + AddRulePanel() { initComponents(); + fc.setDragEnabled(false); + fc.setFileSelectionMode(JFileChooser.FILES_ONLY); + fc.setMultiSelectionEnabled(false); + fc.setFileFilter(exeFilter); + addListener(); + } + + /** + * Creates new form AddRulePanel if the user is editing a rule. Loads + * information of the rule being edited. + * + * @param rule to be edited + */ + AddRulePanel(ExternalViewerRule rule) { + this(); + nameTextField.setText(rule.getName()); + exePathTextField.setText(rule.getExePath()); + addListener(); + } + + /** + * Allows listeners for when the name or exePath text fields are modified. + */ + private void addListener() { + nameTextField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + fire(); + } + @Override + public void removeUpdate(DocumentEvent e) { + fire(); + } + @Override + public void insertUpdate(DocumentEvent e) { + fire(); + } + private void fire() { + firePropertyChange(EVENT.CHANGED.toString(), null, null); + } + }); + exePathTextField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + fire(); + } + @Override + public void removeUpdate(DocumentEvent e) { + fire(); + } + @Override + public void insertUpdate(DocumentEvent e) { + fire(); + } + private void fire() { + firePropertyChange(EVENT.CHANGED.toString(), null, null); + } + }); + } + + /** + * Check if the text fields are empty or not. + * + * @return true if neither of the text fields are empty + */ + boolean hasFields() { + return !exePathTextField.getText().isEmpty() && !nameTextField.getText().isEmpty(); + } + + /** + * Returns the ExternalViewerRule created from input text. Returns null if + * the name is not a valid MIME type (as defined by both autopsy and the + * user, checked through FileTypeDetector) or in the form of a valid + * extension. + * + * @return ExternalViewerRule or null + */ + ExternalViewerRule getRule() { + String name = nameTextField.getText(); + FileTypeDetector detector; + try { + detector = new FileTypeDetector(); + } catch (FileTypeDetector.FileTypeDetectorInitException ex) { + logger.log(Level.WARNING, "Couldn't create file type detector for file ext mismatch settings.", ex); + return null; + } + // Regex for MIME: ^[-\\w]+/[-\\w]+$ + if (name.isEmpty() || (!detector.isDetectable(name) && !name.matches("^\\.\\w+$"))) { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.JOptionPane.invalidExtOrMIME.message"), + NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.JOptionPane.invalidExtOrMIME.title"), + JOptionPane.ERROR_MESSAGE); + return null; + } + String exePath = exePathTextField.getText(); + if (exePath.isEmpty()) { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.JOptionPane.invalidExePath.message"), + NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.JOptionPane.invalidExePath.title"), + JOptionPane.ERROR_MESSAGE); + return null; + } + return new ExternalViewerRule(name, exePath); } /** @@ -27,19 +147,77 @@ public class AddRulePanel extends javax.swing.JPanel { // //GEN-BEGIN:initComponents private void initComponents() { + nameLabel = new javax.swing.JLabel(); + nameTextField = new javax.swing.JTextField(); + exePathLabel = new javax.swing.JLabel(); + exePathTextField = new javax.swing.JTextField(); + browseButton = new javax.swing.JButton(); + + org.openide.awt.Mnemonics.setLocalizedText(nameLabel, org.openide.util.NbBundle.getMessage(AddRulePanel.class, "AddRulePanel.nameLabel.text")); // NOI18N + + nameTextField.setText(org.openide.util.NbBundle.getMessage(AddRulePanel.class, "AddRulePanel.nameTextField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(exePathLabel, org.openide.util.NbBundle.getMessage(AddRulePanel.class, "AddRulePanel.exePathLabel.text")); // NOI18N + + exePathTextField.setText(org.openide.util.NbBundle.getMessage(AddRulePanel.class, "AddRulePanel.exePathTextField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(AddRulePanel.class, "AddRulePanel.browseButton.text")); // NOI18N + browseButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + browseButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 400, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(nameTextField) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(nameLabel) + .addComponent(exePathLabel)) + .addGap(0, 80, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addComponent(exePathTextField) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(browseButton))) + .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 300, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(nameLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(nameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(exePathLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(exePathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(browseButton)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); }// //GEN-END:initComponents + private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed + int returnState = fc.showOpenDialog(this); + if (returnState == JFileChooser.APPROVE_OPTION) { + String path = fc.getSelectedFile().getPath(); + exePathTextField.setText(path); + } + }//GEN-LAST:event_browseButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton browseButton; + private javax.swing.JLabel exePathLabel; + private javax.swing.JTextField exePathTextField; + private javax.swing.JLabel nameLabel; + private javax.swing.JTextField nameTextField; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties index 26b41f7cfd..c5b7452ba9 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties @@ -99,6 +99,18 @@ ExternalViewerGlobalSettingsPanel.newRuleButton.text=New Rule ExternalViewerGlobalSettingsPanel.editRuleButton.text=Edit Rule ExternalViewerGlobalSettingsPanel.deleteRuleButton.toolTipText= ExternalViewerGlobalSettingsPanel.deleteRuleButton.text=Delete Rule -ExternalViewerGlobalSettingsPanel.externalViewerTitleLabel.text=jLabel1 -ExternalViewerGlobalSettingsPanel.ruleListLabel.text=jLabel2 -ExternalViewerGlobalSettingsPanel.exePathLabel.text=jLabel1 +ExternalViewerGlobalSettingsPanel.externalViewerTitleLabel.text=Add your custom rules for external viewers +ExternalViewerGlobalSettingsPanel.ruleListLabel.text=MIME type and extensions +ExternalViewerGlobalSettingsPanel.exePathLabel.text=Program associated with this MIME type or extension +AddRulePanel.nameLabel.text=MIME type or extension +AddRulePanel.nameTextField.text= +AddRulePanel.browseButton.text=Browse +AddRulePanel.exePathLabel.text=Path of the program to use for files with this type or extension +AddRulePanel.exePathTextField.text= +ExternalViewerGlobalSettingsPanel.JOptionPane.invalidExtOrMIME.message=Extension or MIME type is invalid. Extensions should start with a "." character. +ExternalViewerGlobalSettingsPanel.JOptionPane.invalidExtOrMIME.title=Invalid Name +ExternalViewerGlobalSettingsPanel.JOptionPane.invalidExePath.message=The path to the program executable is invalid +ExternalViewerGlobalSettingsPanel.JOptionPane.invalidExePath.title=Invalid Path +ExternalViewerGlobalSettingsPanel.exePathNameLabel.text=No MIME type or extension currently selected +ExternalViewerGlobalSettingsPanel.JOptionPane.ruleAlreadyExists.message=A rule already exists with this MIME type or extension. Please edit that one. +ExternalViewerGlobalSettingsPanel.JOptionPane.ruleAlreadyExists.title=Rule not added \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java index 4a904042c7..256d212969 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java @@ -33,14 +33,15 @@ import org.sleuthkit.autopsy.datamodel.ContentUtils; /** * Extracts a File object to a temporary file in the case directory, and then - * tries to open it in the user's system with the default associated - * application. + * tries to open it in the user's system with the default or user specified + * associated application. */ public class ExternalViewerAction extends AbstractAction { private final static Logger logger = Logger.getLogger(ExternalViewerAction.class.getName()); private org.sleuthkit.datamodel.AbstractFile fileObject; final static String[] EXECUTABLE_EXT = {".exe", ".dll", ".com", ".bat", ".msi", ".reg", ".scr"}; //NON-NLS + private String fileObjectExt; public ExternalViewerAction(String title, Node fileNode) { super(title); @@ -53,12 +54,15 @@ public class ExternalViewerAction extends AbstractAction { boolean isExecutable = false; if (extPos != -1) { String extension = fileName.substring(extPos, fileName.length()).toLowerCase(); + fileObjectExt = extension; for (int i = 0; i < EXECUTABLE_EXT.length; ++i) { if (EXECUTABLE_EXT[i].equals(extension)) { isExecutable = true; break; } } + } else { + fileObjectExt = ""; } // no point opening a file if it's empty, and java doesn't know how to @@ -89,13 +93,31 @@ public class ExternalViewerAction extends AbstractAction { logger.log(Level.WARNING, "Can't save to temporary file.", ex); //NON-NLS } - try { - Desktop.getDesktop().open(tempFile); - } catch (IOException ex) { - logger.log(Level.WARNING, "Could not find a viewer for the given file: " + tempFile.getName(), ex); //NON-NLS - JOptionPane.showMessageDialog(null, Bundle.ExternalViewerAction_actionPerformed_failure_message(), Bundle.ExternalViewerAction_actionPerformed_failure_title(), JOptionPane.ERROR_MESSAGE); + /** + * Check if the file MIME type or extension exists in the user defined + * settings. Otherwise open with the default associated application. + */ + String exePath = ExternalViewerRulesManager.getInstance().getExePathForName(fileObject.getMIMEType()); + if (exePath.equals("")) { + exePath = ExternalViewerRulesManager.getInstance().getExePathForName(fileObjectExt); + } + if (!exePath.equals("")) { + Runtime runtime = Runtime.getRuntime(); + String[] s = new String[]{exePath, tempFile.getAbsolutePath()}; + try { + runtime.exec(s); + } catch (IOException ex) { + logger.log(Level.WARNING, "Could not open the specified viewer for the given file: " + tempFile.getName(), ex); //NON-NLS + JOptionPane.showMessageDialog(null, Bundle.ExternalViewerAction_actionPerformed_failure_message(), Bundle.ExternalViewerAction_actionPerformed_failure_title(), JOptionPane.ERROR_MESSAGE); + } + } else { + try { + Desktop.getDesktop().open(tempFile); + } catch (IOException ex) { + logger.log(Level.WARNING, "Could not find a viewer for the given file: " + tempFile.getName(), ex); //NON-NLS + JOptionPane.showMessageDialog(null, Bundle.ExternalViewerAction_actionPerformed_failure_message(), Bundle.ExternalViewerAction_actionPerformed_failure_title(), JOptionPane.ERROR_MESSAGE); + } } - // delete the file on exit tempFile.deleteOnExit(); } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerGlobalSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerGlobalSettingsPanel.form index 45c5575f53..94a6e5ef8f 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerGlobalSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerGlobalSettingsPanel.form @@ -37,7 +37,7 @@ - + @@ -53,7 +53,7 @@ - + @@ -69,10 +69,13 @@ - + - - + + + + + @@ -81,7 +84,9 @@ - + + + @@ -94,6 +99,13 @@ + + + + + + + @@ -112,7 +124,7 @@ - + @@ -121,7 +133,7 @@ - + @@ -134,7 +146,7 @@ - + @@ -154,7 +166,7 @@ - + @@ -164,13 +176,7 @@ - - - - - - - + diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerGlobalSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerGlobalSettingsPanel.java index f189d0f9e7..1abf9c18f9 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerGlobalSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerGlobalSettingsPanel.java @@ -1,11 +1,12 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. +* To change this license header, choose License Headers in Project Properties. +* To change this template file, choose Tools | Templates +* and open the template in the editor. */ package org.sleuthkit.autopsy.directorytree; -import java.util.logging.Level; +import java.util.ArrayList; +import java.util.Collections; import javax.swing.DefaultListModel; import javax.swing.JOptionPane; import javax.swing.event.ListSelectionEvent; @@ -16,8 +17,9 @@ import org.sleuthkit.autopsy.corecomponents.OptionsPanel; import org.sleuthkit.autopsy.coreutils.Logger; /** - * - * @author smori + * An options panel for the user to create, edit, and delete associations for + * opening files in external viewers. Users can associate a file by either MIME + * type or by extension to an executable file. */ public class ExternalViewerGlobalSettingsPanel extends javax.swing.JPanel implements OptionsPanel { @@ -33,23 +35,25 @@ public class ExternalViewerGlobalSettingsPanel extends javax.swing.JPanel implem customizeComponents(); } + /** + * Initializes field variables. Adds a listener to the list of rules. + */ private void customizeComponents() { rulesListModel = new DefaultListModel<>(); + rules = new ArrayList<>(); rulesList.setModel(rulesListModel); - rulesList.addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting() == false) { if (rulesList.getSelectedIndex() == -1) { - clearRuleDetailsComponents(); + clearExePath(); } else { - populateRuleDetailsComponents(); + populateExePath(); } } } }); - } /** @@ -65,9 +69,10 @@ public class ExternalViewerGlobalSettingsPanel extends javax.swing.JPanel implem jSplitPane1 = new javax.swing.JSplitPane(); exePanel = new javax.swing.JPanel(); exePathLabel = new javax.swing.JLabel(); + exePathNameLabel = new javax.swing.JLabel(); rulesPanel = new javax.swing.JPanel(); ruleListLabel = new javax.swing.JLabel(); - jScrollPane1 = new javax.swing.JScrollPane(); + rulesScrollPane = new javax.swing.JScrollPane(); rulesList = new javax.swing.JList<>(); newRuleButton = new javax.swing.JButton(); editRuleButton = new javax.swing.JButton(); @@ -77,38 +82,39 @@ public class ExternalViewerGlobalSettingsPanel extends javax.swing.JPanel implem org.openide.awt.Mnemonics.setLocalizedText(externalViewerTitleLabel, org.openide.util.NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.externalViewerTitleLabel.text")); // NOI18N - jSplitPane1.setDividerLocation(400); + jSplitPane1.setDividerLocation(350); jSplitPane1.setDividerSize(1); org.openide.awt.Mnemonics.setLocalizedText(exePathLabel, org.openide.util.NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.exePathLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(exePathNameLabel, org.openide.util.NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.exePathNameLabel.text")); // NOI18N + javax.swing.GroupLayout exePanelLayout = new javax.swing.GroupLayout(exePanel); exePanel.setLayout(exePanelLayout); exePanelLayout.setHorizontalGroup( exePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(exePanelLayout.createSequentialGroup() .addContainerGap() - .addComponent(exePathLabel) - .addContainerGap(284, Short.MAX_VALUE)) + .addGroup(exePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(exePathLabel) + .addComponent(exePathNameLabel)) + .addContainerGap(114, Short.MAX_VALUE)) ); exePanelLayout.setVerticalGroup( exePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(exePanelLayout.createSequentialGroup() .addContainerGap() .addComponent(exePathLabel) - .addContainerGap(435, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(exePathNameLabel) + .addContainerGap(411, Short.MAX_VALUE)) ); jSplitPane1.setRightComponent(exePanel); org.openide.awt.Mnemonics.setLocalizedText(ruleListLabel, org.openide.util.NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.ruleListLabel.text")); // NOI18N - rulesList.setModel(new javax.swing.AbstractListModel() { - String[] strings = { "Item 1", "Item 2", "Item 3", "Item 4", "Item 5" }; - public int getSize() { return strings.length; } - public Object getElementAt(int i) { return strings[i]; } - }); - jScrollPane1.setViewportView(rulesList); + rulesScrollPane.setViewportView(rulesList); newRuleButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/add16.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(newRuleButton, org.openide.util.NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.newRuleButton.text")); // NOI18N @@ -145,14 +151,14 @@ public class ExternalViewerGlobalSettingsPanel extends javax.swing.JPanel implem .addComponent(ruleListLabel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(rulesPanelLayout.createSequentialGroup() .addGroup(rulesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) - .addComponent(jScrollPane1, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(rulesScrollPane, javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.LEADING, rulesPanelLayout.createSequentialGroup() .addComponent(newRuleButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(editRuleButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(deleteRuleButton))) - .addGap(0, 68, Short.MAX_VALUE))) + .addGap(0, 18, Short.MAX_VALUE))) .addContainerGap()) ); rulesPanelLayout.setVerticalGroup( @@ -161,7 +167,7 @@ public class ExternalViewerGlobalSettingsPanel extends javax.swing.JPanel implem .addContainerGap() .addComponent(ruleListLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 387, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(rulesScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 387, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(rulesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(newRuleButton) @@ -189,7 +195,7 @@ public class ExternalViewerGlobalSettingsPanel extends javax.swing.JPanel implem .addContainerGap() .addComponent(externalViewerTitleLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jSplitPane1) + .addComponent(jSplitPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 458, Short.MAX_VALUE) .addContainerGap()) ); }// //GEN-END:initComponents @@ -198,9 +204,18 @@ public class ExternalViewerGlobalSettingsPanel extends javax.swing.JPanel implem AddRuleDialog dialog = new AddRuleDialog(); AddRuleDialog.BUTTON_PRESSED result = dialog.getResult(); if (result == AddRuleDialog.BUTTON_PRESSED.OK) { - rules.add(dialog.getRule()); - updateRulesListModel(); - firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + ExternalViewerRule newRule = dialog.getRule(); + // Only allow one association for each MIME type or extension. + if (rules.contains(newRule)) { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.JOptionPane.ruleAlreadyExists.message"), + NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.JOptionPane.ruleAlreadyExists.title"), + JOptionPane.ERROR_MESSAGE); + } else { + rules.add(dialog.getRule()); + updateRulesListModel(); + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + } } }//GEN-LAST:event_newRuleButtonActionPerformed @@ -210,73 +225,101 @@ public class ExternalViewerGlobalSettingsPanel extends javax.swing.JPanel implem AddRuleDialog.BUTTON_PRESSED result = dialog.getResult(); if (result == AddRuleDialog.BUTTON_PRESSED.OK) { rules.remove(selected); - rules.add(selected, dialog.getRule()); - updateRulesListModel(); - firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + ExternalViewerRule newRule = dialog.getRule(); + // Only allow one association for each MIME type or extension. + if (rules.contains(newRule)) { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.JOptionPane.ruleAlreadyExists.message"), + NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.JOptionPane.ruleAlreadyExists.title"), + JOptionPane.ERROR_MESSAGE); + } else { + rules.add(selected, dialog.getRule()); + updateRulesListModel(); + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + } } }//GEN-LAST:event_editRuleButtonActionPerformed - + private void deleteRuleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteRuleButtonActionPerformed ExternalViewerRule rule = rulesList.getSelectedValue(); rules.remove(rule); updateRulesListModel(); - if (!rulesListModel.isEmpty()) { - rulesList.setSelectedIndex(0); - } firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); }//GEN-LAST:event_deleteRuleButtonActionPerformed - @Override public void store() { - try { - ExternalViewerRulesManager.getInstance().setUserRules(rules); - } catch (ExternalViewerRulesManager.CustomRulesException ex) { - logger.log(Level.SEVERE, "Failed to set custom rules", ex); - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.saveFileTypes.errorMessage"), - NbBundle.getMessage(ExternalViewerGlobalSettingsPanel.class, "ExternalViewerGlobalSettingsPanel.JOptionPane.storeFailed.title"), - JOptionPane.ERROR_MESSAGE); - } + ExternalViewerRulesManager.getInstance().setUserRules(rules); } @Override public void load() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + rules = ExternalViewerRulesManager.getInstance().getUserRules(); + updateRulesListModel(); + if (!rulesListModel.isEmpty()) { + rulesList.setSelectedIndex(0); + } + enableButtons(); } /** - * Sets the list model for the file types list component. + * Enable edit and delete buttons if there is a rule selected. + */ + private void enableButtons() { + boolean ruleIsSelected = rulesList.getSelectedIndex() != -1; + editRuleButton.setEnabled(ruleIsSelected); + deleteRuleButton.setEnabled(ruleIsSelected); + } + + /** + * Sets the list model for the rules list component, sorted by the MIME + * type or extension alphabetically. */ private void updateRulesListModel() { rulesListModel.clear(); + Collections.sort(rules, (ExternalViewerRule rule1, ExternalViewerRule rule2) -> { + return rule1.getName().compareTo(rule2.getName()); + }); for (ExternalViewerRule rule : rules) { rulesListModel.addElement(rule); } + if (!rulesListModel.isEmpty()) { + rulesList.setSelectedIndex(0); + } } - private void populateRuleDetailsComponents() { + /** + * Fills in the .exe file path label if a rule is selected. + */ + private void populateExePath() { ExternalViewerRule rule = rulesList.getSelectedValue(); - exePathLabel.setText(rule.getExePath()); + if (rule != null) { + exePathNameLabel.setText(rule.getExePath()); + } + enableButtons(); } - private void clearRuleDetailsComponents() { + /** + * Clears the .exe file path label. + */ + private void clearExePath() { rulesList.clearSelection(); - exePathLabel.setText(""); + exePathNameLabel.setText(""); + enableButtons(); } - // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton deleteRuleButton; private javax.swing.JButton editRuleButton; private javax.swing.JPanel exePanel; private javax.swing.JLabel exePathLabel; + private javax.swing.JLabel exePathNameLabel; private javax.swing.JLabel externalViewerTitleLabel; - private javax.swing.JScrollPane jScrollPane1; private javax.swing.JSplitPane jSplitPane1; private javax.swing.JButton newRuleButton; private javax.swing.JLabel ruleListLabel; private javax.swing.JList rulesList; private javax.swing.JPanel rulesPanel; + private javax.swing.JScrollPane rulesScrollPane; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerOptionsPanelController.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerOptionsPanelController.java index 6f76231c8a..0bcb0e7772 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerOptionsPanelController.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerOptionsPanelController.java @@ -2,7 +2,7 @@ * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. -*/ + */ package org.sleuthkit.autopsy.directorytree; import java.beans.PropertyChangeEvent; @@ -13,6 +13,9 @@ import org.netbeans.spi.options.OptionsPanelController; import org.openide.util.HelpCtx; import org.openide.util.Lookup; +/** + * Controller for the ExternalViewerGlobalSettingsPanel + */ @OptionsPanelController.TopLevelRegistration( categoryName = "#OptionsCategory_Name_ExternalViewer", iconBase = "org/sleuthkit/autopsy/modules/filetypeid/user-defined-file-types-settings.png", @@ -21,17 +24,17 @@ import org.openide.util.Lookup; position = 9 ) public final class ExternalViewerOptionsPanelController extends OptionsPanelController { - + private ExternalViewerGlobalSettingsPanel panel; private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); private boolean changed; - + @Override public void update() { getPanel().load(); changed = false; } - + @Override public void applyChanges() { if (changed) { @@ -39,56 +42,53 @@ public final class ExternalViewerOptionsPanelController extends OptionsPanelCont changed = false; } } - + @Override public void cancel() { } - + @Override public boolean isValid() { return true; } - + @Override public boolean isChanged() { return changed; } - + @Override public JComponent getComponent(Lookup lkp) { return getPanel(); } - + @Override public HelpCtx getHelpCtx() { return null; } - + @Override public void addPropertyChangeListener(PropertyChangeListener l) { pcs.addPropertyChangeListener(l); } - + @Override public void removePropertyChangeListener(PropertyChangeListener l) { pcs.removePropertyChangeListener(l); } - + private ExternalViewerGlobalSettingsPanel getPanel() { if (panel == null) { panel = new ExternalViewerGlobalSettingsPanel(); - panel.addPropertyChangeListener(new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals(OptionsPanelController.PROP_CHANGED)) { - changed(); - } + panel.addPropertyChangeListener((PropertyChangeEvent evt) -> { + if (evt.getPropertyName().equals(OptionsPanelController.PROP_CHANGED)) { + changed(); } }); } return panel; } - + void changed() { if (!changed) { changed = true; diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerRule.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerRule.java index 12e789a863..7934c79274 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerRule.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerRule.java @@ -5,33 +5,60 @@ */ package org.sleuthkit.autopsy.directorytree; +import java.util.Objects; + /** - * - * @author smori + * An object that represents an association between a MIME type or extension + * name to a user defined executable. */ class ExternalViewerRule { - private String name; - private String exePath; - + + private final String name; + private final String exePath; + + /** + * Creates a new ExternalViewerRule + * + * @param name MIME type or extension + * @param exePath Absolute path of the exe file + */ ExternalViewerRule(String name, String exePath) { this.name = name; this.exePath = exePath; } - + String getName() { return name; } - + String getExePath() { return exePath; } - - void editName(String newName) { - name = newName; + + @Override + public String toString() { + return name; } - - void editExePath(String newExePath) { - exePath = newExePath; + + /** + * Only one association is allowed per MIME type or extension, so rules are + * equal if the names are the same. + */ + @Override + public boolean equals(Object other) { + if (other != null && other instanceof ExternalViewerRule) { + ExternalViewerRule that = (ExternalViewerRule) other; + if (this.getName().equals(that.getName())) { + return true; + } + } + return false; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 67 * hash + Objects.hashCode(this.name); + return hash; } - } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerRulesManager.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerRulesManager.java index 156b6c7c24..60e66665a1 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerRulesManager.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerRulesManager.java @@ -5,10 +5,91 @@ */ package org.sleuthkit.autopsy.directorytree; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; + /** - * - * @author smori + * Manager for user's external viewer rules, used by the options panel and the + * ExternalViewerAction. Reads from and writes to a preferences file. */ public class ExternalViewerRulesManager { - + + private static final String RULES_SETTINGS_NAME = "ExternalViewerRules"; //NON-NLS + private static final String RULES_SETTINGS_KEY = "Rules"; //NON-NLS + private static ExternalViewerRulesManager instance; + private List userRules = new ArrayList<>(); + + /** + * Gets the singleton manager of the external viewer rules defined by users. + * + * @return The external viewer rules manager singleton. + */ + synchronized static ExternalViewerRulesManager getInstance() { + if (instance == null) { + instance = new ExternalViewerRulesManager(); + instance.loadUserDefinedRules(); + } + return instance; + } + + private ExternalViewerRulesManager() { + } + + /** + * Loads user defined rules from the configuration settings file. + */ + private void loadUserDefinedRules() { + String setting = ModuleSettings.getConfigSetting(RULES_SETTINGS_NAME, RULES_SETTINGS_KEY); + if (setting != null && !setting.isEmpty()) { + List ruleTuples = Arrays.asList(setting.split("\\|")); + for (String ruleTuple : ruleTuples) { + String[] ruleParts = ruleTuple.split(">"); + userRules.add(new ExternalViewerRule(ruleParts[0], ruleParts[1])); + } + } + } + + /** + * Writes a list of ExternalViewerRule objects to a configuration settings + * file. + * + * @param rules to be written and saved. + */ + synchronized void setUserRules(List rules) { + StringBuilder setting = new StringBuilder(); + for (ExternalViewerRule rule : rules) { + if (setting.length() != 0) { + setting.append("|"); + } + setting.append(rule.getName()).append(">"); + setting.append(rule.getExePath()); + } + ModuleSettings.setConfigSetting(RULES_SETTINGS_NAME, RULES_SETTINGS_KEY, setting.toString()); + userRules = new ArrayList<>(rules); + } + + /** + * @return a list of the user's rules as ExternalViewerRule objects + */ + synchronized List getUserRules() { + // ExternalViewerRule objects are immutable + return new ArrayList<>(userRules); + } + + /** + * Finds the executable path associated with a rule name (MIME type or + * extension). Returns an empty string if the rule name is not found. + * @param name of MIME type or extension. + * @return the associated executable absolute path. + */ + synchronized String getExePathForName(String name) { + for (ExternalViewerRule rule : userRules) { + if (rule.getName().equals(name)) { + return rule.getExePath(); + } + } + return ""; + } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/CustomFileTypesManager.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/CustomFileTypesManager.java index 4e06ee7d74..22b064dca6 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/CustomFileTypesManager.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/CustomFileTypesManager.java @@ -372,7 +372,7 @@ final class CustomFileTypesManager { return filesSetsSettings.getUserDefinedFileTypes(); } } catch (IOException | ClassNotFoundException ex) { - throw new CustomFileTypesException(String.format("Failed to read ssettings from %s", filePath), ex); //NON-NLS + throw new CustomFileTypesException(String.format("Failed to read settings from %s", filePath), ex); //NON-NLS } }