Merge pull request #5375 from sleuthkit/develop

Merge develop branch into file-search branch
This commit is contained in:
Richard Cordovano 2019-10-28 10:02:05 -04:00 committed by GitHub
commit 41b9047a43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1516 additions and 550 deletions

View File

@ -137,7 +137,7 @@
</target> </target>
<target name="get-deps" depends="init-ivy,getTSKJars,get-thirdparty-dependencies,get-InternalPythonModules, download-binlist,getTestDataFiles"> <target name="get-deps" depends="init-ivy,getTSKJars,get-thirdparty-dependencies,get-InternalPythonModules, download-binlist">
<mkdir dir="${ext.dir}"/> <mkdir dir="${ext.dir}"/>
<copy file="${thirdparty.dir}/LICENSE-2.0.txt" todir="${ext.dir}" /> <copy file="${thirdparty.dir}/LICENSE-2.0.txt" todir="${ext.dir}" />
<!-- fetch all the dependencies from Ivy and stick them in the right places --> <!-- fetch all the dependencies from Ivy and stick them in the right places -->

View File

@ -162,6 +162,8 @@ class CaseInformationPanel extends javax.swing.JPanel {
editCasePropertiesDialog.setResizable(true); editCasePropertiesDialog.setResizable(true);
editCasePropertiesDialog.pack(); editCasePropertiesDialog.pack();
editCasePropertiesDialog.setLocationRelativeTo(this); editCasePropertiesDialog.setLocationRelativeTo(this);
// Workaround to ensure dialog is not hidden on macOS
editCasePropertiesDialog.setAlwaysOnTop(true);
editCasePropertiesDialog.setVisible(true); editCasePropertiesDialog.setVisible(true);
editCasePropertiesDialog.toFront(); editCasePropertiesDialog.toFront();
caseDetailsPanel.updateCaseInfo(); caseDetailsPanel.updateCaseInfo();

View File

@ -18,6 +18,7 @@
*/ */
package org.sleuthkit.autopsy.casemodule; package org.sleuthkit.autopsy.casemodule;
import java.awt.Component;
import java.awt.Cursor; import java.awt.Cursor;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
@ -84,10 +85,17 @@ public final class CaseOpenAction extends CallableSystemAction implements Action
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
fileChooser.setMultiSelectionEnabled(false); fileChooser.setMultiSelectionEnabled(false);
fileChooser.setFileFilter(caseMetadataFileFilter); fileChooser.setFileFilter(caseMetadataFileFilter);
if (null != ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE)) { if (null != ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE)) {
fileChooser.setCurrentDirectory(new File(ModuleSettings.getConfigSetting("Case", PROP_BASECASE))); //NON-NLS fileChooser.setCurrentDirectory(new File(ModuleSettings.getConfigSetting("Case", PROP_BASECASE))); //NON-NLS
} }
/**
* If the open multi user case dialog is open make sure it's not set
* to always be on top as this hides the file chooser on macOS.
*/
OpenMultiUserCaseDialog multiUserCaseDialog = OpenMultiUserCaseDialog.getInstance();
multiUserCaseDialog.setAlwaysOnTop(false);
String optionsDlgTitle = NbBundle.getMessage(Case.class, "CloseCaseWhileIngesting.Warning.title"); String optionsDlgTitle = NbBundle.getMessage(Case.class, "CloseCaseWhileIngesting.Warning.title");
String optionsDlgMessage = NbBundle.getMessage(Case.class, "CloseCaseWhileIngesting.Warning"); String optionsDlgMessage = NbBundle.getMessage(Case.class, "CloseCaseWhileIngesting.Warning");
if (IngestRunningCheck.checkAndConfirmProceed(optionsDlgTitle, optionsDlgMessage)) { if (IngestRunningCheck.checkAndConfirmProceed(optionsDlgTitle, optionsDlgMessage)) {
@ -95,7 +103,12 @@ public final class CaseOpenAction extends CallableSystemAction implements Action
* Pop up a file chooser to allow the user to select a case metadata * Pop up a file chooser to allow the user to select a case metadata
* file (.aut file). * file (.aut file).
*/ */
int retval = fileChooser.showOpenDialog(WindowManager.getDefault().getMainWindow()); /**
* The parent of the fileChooser will either be the multi user
* case dialog or the startup window.
*/
int retval = fileChooser.showOpenDialog(multiUserCaseDialog.isVisible()
? multiUserCaseDialog : (Component) StartupWindowProvider.getInstance().getStartupWindow());
if (retval == JFileChooser.APPROVE_OPTION) { if (retval == JFileChooser.APPROVE_OPTION) {
/* /*
* Close the startup window, if it is open. * Close the startup window, if it is open.
@ -105,7 +118,7 @@ public final class CaseOpenAction extends CallableSystemAction implements Action
/* /*
* Close the Open Multi-User Case window, if it is open. * Close the Open Multi-User Case window, if it is open.
*/ */
OpenMultiUserCaseDialog.getInstance().setVisible(false); multiUserCaseDialog.setVisible(false);
/* /*
* Try to open the case associated with the case metadata file * Try to open the case associated with the case metadata file
@ -159,6 +172,8 @@ public final class CaseOpenAction extends CallableSystemAction implements Action
OpenMultiUserCaseDialog multiUserCaseWindow = OpenMultiUserCaseDialog.getInstance(); OpenMultiUserCaseDialog multiUserCaseWindow = OpenMultiUserCaseDialog.getInstance();
multiUserCaseWindow.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); multiUserCaseWindow.setLocationRelativeTo(WindowManager.getDefault().getMainWindow());
// Workaround to ensure that dialog is not hidden on macOS.
multiUserCaseWindow.setAlwaysOnTop(true);
multiUserCaseWindow.setVisible(true); multiUserCaseWindow.setVisible(true);
WindowManager.getDefault().getMainWindow().setCursor(null); WindowManager.getDefault().getMainWindow().setCursor(null);

View File

@ -249,6 +249,8 @@ public class CueBannerPanel extends javax.swing.JPanel {
private void openRecentCaseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openRecentCaseButtonActionPerformed private void openRecentCaseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openRecentCaseButtonActionPerformed
recentCasesWindow.setLocationRelativeTo(this); recentCasesWindow.setLocationRelativeTo(this);
OpenRecentCasePanel.getInstance(); //refreshes the recent cases table OpenRecentCasePanel.getInstance(); //refreshes the recent cases table
// Workaround to ensure that dialog is not hidden on macOS.
recentCasesWindow.setAlwaysOnTop(true);
recentCasesWindow.setVisible(true); recentCasesWindow.setVisible(true);
}//GEN-LAST:event_openRecentCaseButtonActionPerformed }//GEN-LAST:event_openRecentCaseButtonActionPerformed

View File

@ -71,6 +71,8 @@ final class NewCaseWizardAction extends CallableSystemAction {
wizardDescriptor.setTitleFormat(new MessageFormat("{0}")); wizardDescriptor.setTitleFormat(new MessageFormat("{0}"));
wizardDescriptor.setTitle(NbBundle.getMessage(this.getClass(), "NewCaseWizardAction.newCase.windowTitle.text")); wizardDescriptor.setTitle(NbBundle.getMessage(this.getClass(), "NewCaseWizardAction.newCase.windowTitle.text"));
Dialog dialog = DialogDisplayer.getDefault().createDialog(wizardDescriptor); Dialog dialog = DialogDisplayer.getDefault().createDialog(wizardDescriptor);
// Workaround to ensure new case dialog is not hidden on macOS
dialog.setAlwaysOnTop(true);
dialog.setVisible(true); dialog.setVisible(true);
dialog.toFront(); dialog.toFront();
if (wizardDescriptor.getValue() == WizardDescriptor.FINISH_OPTION) { if (wizardDescriptor.getValue() == WizardDescriptor.FINISH_OPTION) {

View File

@ -144,4 +144,13 @@ public class StartupWindowProvider implements StartupWindowInterface {
startupWindowToUse.close(); startupWindowToUse.close();
} }
} }
/**
* Get the chosen startup window.
*
* @return The startup window.
*/
public StartupWindowInterface getStartupWindow() {
return startupWindowToUse;
}
} }

View File

@ -224,6 +224,7 @@ public class IngestEventsListener {
* in the central repository. * in the central repository.
* *
* @param originalArtifact the artifact to create the interesting item for * @param originalArtifact the artifact to create the interesting item for
* @param caseDisplayNames the case names the artifact was previously seen in
*/ */
@NbBundle.Messages({"IngestEventsListener.prevExists.text=Previously Seen Devices (Central Repository)", @NbBundle.Messages({"IngestEventsListener.prevExists.text=Previously Seen Devices (Central Repository)",
"# {0} - typeName", "# {0} - typeName",

View File

@ -84,9 +84,12 @@ MediaViewImagePanel.zoomTextField.text=
MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotationTextField.text=
MediaViewImagePanel.rotateLeftButton.toolTipText= MediaViewImagePanel.rotateLeftButton.toolTipText=
HtmlPanel.showImagesToggleButton.text=Download Images HtmlPanel.showImagesToggleButton.text=Download Images
MediaPlayerPanel.audioSlider.toolTipText= MediaViewImagePanel.tagsMenu.text_1=Tags Menu
MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume
MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00
MediaPlayerPanel.audioSlider.toolTipText=
MediaPlayerPanel.rewindButton.text=\u2bc7\u2bc7
MediaPlayerPanel.fastForwardButton.text=\u2bc8\u2bc8
MediaPlayerPanel.playButton.text=\u25ba MediaPlayerPanel.playButton.text=\u25ba
MediaPlayerPanel.infoLabel.text=No Errors MediaPlayerPanel.infoLabel.text=No Errors
MediaViewImagePanel.tagsMenu.text_1=Tags Menu MediaPlayerPanel.VolumeIcon.text=Volume
MediaPlayerPanel.playBackSpeedLabel.text=Speed:

View File

@ -154,12 +154,15 @@ MediaViewImagePanel.zoomTextField.text=
MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotationTextField.text=
MediaViewImagePanel.rotateLeftButton.toolTipText= MediaViewImagePanel.rotateLeftButton.toolTipText=
HtmlPanel.showImagesToggleButton.text=Download Images HtmlPanel.showImagesToggleButton.text=Download Images
MediaPlayerPanel.audioSlider.toolTipText= MediaViewImagePanel.tagsMenu.text_1=Tags Menu
MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume
MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00
MediaPlayerPanel.audioSlider.toolTipText=
MediaPlayerPanel.rewindButton.text=\u2bc7\u2bc7
MediaPlayerPanel.fastForwardButton.text=\u2bc8\u2bc8
MediaPlayerPanel.playButton.text=\u25ba MediaPlayerPanel.playButton.text=\u25ba
MediaPlayerPanel.infoLabel.text=No Errors MediaPlayerPanel.infoLabel.text=No Errors
MediaViewImagePanel.tagsMenu.text_1=Tags Menu MediaPlayerPanel.VolumeIcon.text=Volume
MediaPlayerPanel.playBackSpeedLabel.text=Speed:
# {0} - tableName # {0} - tableName
SQLiteViewer.readTable.errorText=Error getting rows for table: {0} SQLiteViewer.readTable.errorText=Error getting rows for table: {0}
# {0} - tableName # {0} - tableName

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> <Form version="1.8" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
<AuxValues> <AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
@ -16,8 +16,8 @@
<Layout> <Layout>
<DimensionLayout dim="0"> <DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0"> <Group type="103" groupAlignment="0" attributes="0">
<Component id="videoPanel" alignment="0" max="32767" attributes="0"/>
<Component id="controlPanel" alignment="0" max="32767" attributes="0"/> <Component id="controlPanel" alignment="0" max="32767" attributes="0"/>
<Component id="videoPanel" alignment="0" max="32767" attributes="0"/>
</Group> </Group>
</DimensionLayout> </DimensionLayout>
<DimensionLayout dim="1"> <DimensionLayout dim="1">
@ -41,7 +41,7 @@
</DimensionLayout> </DimensionLayout>
<DimensionLayout dim="1"> <DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0"> <Group type="103" groupAlignment="0" attributes="0">
<EmptySpace min="0" pref="259" max="32767" attributes="0"/> <EmptySpace min="0" pref="131" max="32767" attributes="0"/>
</Group> </Group>
</DimensionLayout> </DimensionLayout>
</Layout> </Layout>
@ -51,47 +51,42 @@
<Layout> <Layout>
<DimensionLayout dim="0"> <DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0"> <Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="1" attributes="0"> <Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/> <EmptySpace min="-2" max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0"> <Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0"> <Component id="infoLabel" max="32767" attributes="0"/>
<Component id="playButton" min="-2" pref="64" max="-2" attributes="0"/> <Group type="102" alignment="1" attributes="0">
<EmptySpace type="unrelated" min="-2" max="-2" attributes="0"/> <Group type="103" groupAlignment="1" attributes="0">
<Component id="progressSlider" pref="680" max="32767" attributes="0"/> <Component id="buttonPanel" alignment="0" max="32767" attributes="0"/>
<EmptySpace type="unrelated" max="-2" attributes="0"/> <Component id="progressSlider" pref="623" max="32767" attributes="0"/>
<Component id="progressLabel" min="-2" max="-2" attributes="0"/> </Group>
</Group> <EmptySpace max="-2" attributes="0"/>
<Group type="102" attributes="0"> <Group type="103" groupAlignment="0" max="-2" attributes="0">
<Component id="infoLabel" max="32767" attributes="0"/> <Component id="progressLabel" max="32767" attributes="0"/>
<EmptySpace type="separate" max="-2" attributes="0"/> <Component id="playBackPanel" pref="0" max="32767" attributes="0"/>
<Component id="VolumeIcon" min="-2" pref="64" max="-2" attributes="0"/> </Group>
<EmptySpace min="-2" pref="2" max="-2" attributes="0"/> <EmptySpace min="-2" pref="10" max="-2" attributes="0"/>
<Component id="audioSlider" min="-2" pref="229" max="-2" attributes="0"/>
</Group> </Group>
</Group> </Group>
<EmptySpace min="-2" max="-2" attributes="0"/> <EmptySpace min="0" pref="0" max="-2" attributes="0"/>
</Group> </Group>
</Group> </Group>
</DimensionLayout> </DimensionLayout>
<DimensionLayout dim="1"> <DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0"> <Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0"> <Group type="102" attributes="0">
<Group type="103" groupAlignment="0" attributes="0"> <EmptySpace min="0" pref="0" max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" max="-2" attributes="0"> <Group type="103" groupAlignment="0" max="-2" attributes="0">
<Component id="progressLabel" max="32767" attributes="0"/> <Component id="progressLabel" max="32767" attributes="0"/>
<Component id="progressSlider" max="32767" attributes="0"/> <Component id="progressSlider" max="32767" attributes="0"/>
</Group>
<Component id="playButton" min="-2" max="-2" attributes="0"/>
</Group> </Group>
<EmptySpace max="-2" attributes="0"/> <EmptySpace min="-2" pref="5" max="-2" attributes="0"/>
<Group type="103" groupAlignment="1" attributes="0"> <Group type="103" groupAlignment="0" max="-2" attributes="0">
<Component id="audioSlider" min="-2" max="-2" attributes="0"/> <Component id="buttonPanel" max="32767" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0"> <Component id="playBackPanel" pref="0" max="32767" attributes="0"/>
<Component id="VolumeIcon" alignment="3" min="-2" pref="23" max="-2" attributes="0"/>
<Component id="infoLabel" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
</Group> </Group>
<EmptySpace min="-2" pref="13" max="-2" attributes="0"/> <EmptySpace min="-2" pref="14" max="-2" attributes="0"/>
<Component id="infoLabel" min="-2" max="-2" attributes="0"/>
</Group> </Group>
</Group> </Group>
</DimensionLayout> </DimensionLayout>
@ -111,26 +106,9 @@
<Dimension value="[200, 21]"/> <Dimension value="[200, 21]"/>
</Property> </Property>
</Properties> </Properties>
</Component> <AuxValues>
<Component class="javax.swing.JLabel" name="infoLabel"> <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="progressSlider.setUI(new CircularJSliderUI(progressSlider, new CircularJSliderConfiguration(new Dimension(18,18))));"/>
<Properties> </AuxValues>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.infoLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
<Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor">
<Color id="Default Cursor"/>
</Property>
</Properties>
</Component>
<Component class="javax.swing.JButton" name="playButton">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.playButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="playButtonActionPerformed"/>
</Events>
</Component> </Component>
<Component class="javax.swing.JLabel" name="progressLabel"> <Component class="javax.swing.JLabel" name="progressLabel">
<Properties> <Properties>
@ -139,31 +117,174 @@
</Property> </Property>
</Properties> </Properties>
</Component> </Component>
<Component class="javax.swing.JLabel" name="VolumeIcon"> <Container class="javax.swing.JPanel" name="buttonPanel">
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
<SubComponents>
<Component class="javax.swing.JButton" name="playButton">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.playButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="playButtonActionPerformed"/>
</Events>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="1" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="21" ipadY="0" insetsTop="5" insetsLeft="6" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JButton" name="fastForwardButton">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.fastForwardButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="fastForwardButtonActionPerformed"/>
</Events>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="2" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="5" insetsLeft="6" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JButton" name="rewindButton">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.rewindButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="rewindButtonActionPerformed"/>
</Events>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="5" insetsLeft="0" insetsBottom="1" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JLabel" name="VolumeIcon">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.VolumeIcon.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
<Property name="horizontalTextPosition" type="int" value="2"/>
</Properties>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="3" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="8" ipadY="7" insetsTop="6" insetsLeft="14" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JSlider" name="audioSlider">
<Properties>
<Property name="majorTickSpacing" type="int" value="10"/>
<Property name="maximum" type="int" value="50"/>
<Property name="minorTickSpacing" type="int" value="5"/>
<Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.audioSlider.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
<Property name="value" type="int" value="25"/>
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[200, 21]"/>
</Property>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[200, 21]"/>
</Property>
</Properties>
<AuxValues>
<AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="audioSlider.setUI(new CircularJSliderUI(audioSlider, new CircularJSliderConfiguration(new Dimension(15,15))));"/>
</AuxValues>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="4" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="-116" ipadY="7" insetsTop="3" insetsLeft="1" insetsBottom="0" insetsRight="10" anchor="18" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
</SubComponents>
</Container>
<Component class="javax.swing.JLabel" name="infoLabel">
<Properties> <Properties>
<Property name="horizontalAlignment" type="int" value="2"/>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.VolumeIcon.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/> <ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.infoLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property> </Property>
</Properties> <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor">
</Component> <Color id="Default Cursor"/>
<Component class="javax.swing.JSlider" name="audioSlider">
<Properties>
<Property name="majorTickSpacing" type="int" value="10"/>
<Property name="maximum" type="int" value="50"/>
<Property name="minorTickSpacing" type="int" value="5"/>
<Property name="paintTicks" type="boolean" value="true"/>
<Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.audioSlider.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
<Property name="value" type="int" value="25"/>
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[200, 21]"/>
</Property>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[200, 21]"/>
</Property> </Property>
</Properties> </Properties>
</Component> </Component>
<Container class="javax.swing.JPanel" name="playBackPanel">
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<Component id="playBackSpeedLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="32767" attributes="0"/>
<Component id="playBackSpeedComboBox" min="-2" max="-2" attributes="0"/>
<EmptySpace min="-2" pref="13" max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace min="-2" pref="6" max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="playBackSpeedComboBox" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="playBackSpeedLabel" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="32767" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Component class="javax.swing.JComboBox" name="playBackSpeedComboBox">
<Properties>
<Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor">
<StringArray count="8">
<StringItem index="0" value="0.25x"/>
<StringItem index="1" value="0.50x"/>
<StringItem index="2" value="0.75x"/>
<StringItem index="3" value="1x"/>
<StringItem index="4" value="1.25x"/>
<StringItem index="5" value="1.50x"/>
<StringItem index="6" value="1.75x"/>
<StringItem index="7" value="2x"/>
</StringArray>
</Property>
<Property name="selectedIndex" type="int" value="3"/>
<Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[53, 23]"/>
</Property>
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[53, 23]"/>
</Property>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[53, 23]"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="playBackSpeedComboBoxActionPerformed"/>
</Events>
<AuxValues>
<AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="&lt;String&gt;"/>
</AuxValues>
</Component>
<Component class="javax.swing.JLabel" name="playBackSpeedLabel">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaPlayerPanel.playBackSpeedLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
</SubComponents>
</Container>
</SubComponents> </SubComponents>
</Container> </Container>
</SubComponents> </SubComponents>

View File

@ -19,16 +19,26 @@
package org.sleuthkit.autopsy.contentviewers; package org.sleuthkit.autopsy.contentviewers;
import com.google.common.io.Files; import com.google.common.io.Files;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Level; import java.util.logging.Level;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
@ -37,7 +47,6 @@ import javax.swing.SwingWorker;
import javax.swing.Timer; import javax.swing.Timer;
import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeEvent;
import org.freedesktop.gstreamer.Bus; import org.freedesktop.gstreamer.Bus;
import org.freedesktop.gstreamer.ClockTime;
import org.freedesktop.gstreamer.Gst; import org.freedesktop.gstreamer.Gst;
import org.freedesktop.gstreamer.GstObject; import org.freedesktop.gstreamer.GstObject;
import org.freedesktop.gstreamer.State; import org.freedesktop.gstreamer.State;
@ -52,8 +61,17 @@ import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskData;
import javafx.embed.swing.JFXPanel; import javafx.embed.swing.JFXPanel;
import javax.swing.JComponent;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeListener; import javax.swing.event.ChangeListener;
import javax.swing.plaf.basic.BasicSliderUI;
import javax.swing.plaf.basic.BasicSliderUI.TrackListener;
import org.freedesktop.gstreamer.ClockTime;
import org.freedesktop.gstreamer.Format;
import org.freedesktop.gstreamer.GstException; import org.freedesktop.gstreamer.GstException;
import org.freedesktop.gstreamer.event.SeekFlags;
import org.freedesktop.gstreamer.event.SeekType;
/** /**
* This is a video player that is part of the Media View layered pane. It uses * This is a video player that is part of the Media View layered pane. It uses
@ -177,17 +195,27 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
private Bus.EOS endOfStreamListener; private Bus.EOS endOfStreamListener;
//Update progress bar and time label during video playback //Update progress bar and time label during video playback
private final Timer timer = new Timer(75, new VideoPanelUpdater()); //Updating every 16 MS = 62.5 FPS.
private final Timer timer = new Timer(16, new VideoPanelUpdater());
private static final int PROGRESS_SLIDER_SIZE = 2000; private static final int PROGRESS_SLIDER_SIZE = 2000;
private static final int SKIP_IN_SECONDS = 30;
private ExtractMedia extractMediaWorker; private ExtractMedia extractMediaWorker;
//Serialize setting the value of the Video progress slider.
//The slider is a shared resource between the VideoPanelUpdater
//and the TrackListener of the JSliderUI.
private final Semaphore sliderLock;
/** /**
* Creates new form MediaViewVideoPanel * Creates new form MediaViewVideoPanel
*/ */
public MediaPlayerPanel() throws GstException, UnsatisfiedLinkError { public MediaPlayerPanel() throws GstException, UnsatisfiedLinkError {
initComponents(); initComponents();
customizeComponents(); customizeComponents();
//True for fairness. In other words,
//acquire() calls are processed in order of invocation.
sliderLock = new Semaphore(1, true);
} }
private void customizeComponents() { private void customizeComponents() {
@ -202,11 +230,20 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
if (progressSlider.getValueIsAdjusting()) { if (progressSlider.getValueIsAdjusting()) {
long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS);
double relativePosition = progressSlider.getValue() * 1.0 / PROGRESS_SLIDER_SIZE; double relativePosition = progressSlider.getValue() * 1.0 / PROGRESS_SLIDER_SIZE;
long newPos = (long) (relativePosition * duration); long newStartTime = (long) (relativePosition * duration);
gstPlayBin.seek(newPos, TimeUnit.NANOSECONDS); double playBackRate = getPlayBackRate();
gstPlayBin.seek(playBackRate,
Format.TIME,
//FLUSH - flushes the pipeline
//ACCURATE - video will seek exactly to the position requested
EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE),
//Set the start position to newTime
SeekType.SET, newStartTime,
//Do nothing for the end position
SeekType.NONE, -1);
//Keep constantly updating the time label so users have a sense of //Keep constantly updating the time label so users have a sense of
//where the slider they are dragging is in relation to the video time //where the slider they are dragging is in relation to the video time
updateTimeLabel(newPos, duration); updateTimeLabel(newStartTime, duration);
} }
} }
}); });
@ -220,10 +257,12 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
errorListener = new Bus.ERROR() { errorListener = new Bus.ERROR() {
@Override @Override
public void errorMessage(GstObject go, int i, String string) { public void errorMessage(GstObject go, int i, String string) {
enableComponents(false); SwingUtilities.invokeLater(() -> {
infoLabel.setText(String.format( enableComponents(false);
"<html><font color='red'>%s</font></html>", infoLabel.setText(String.format(
MEDIA_PLAYER_ERROR_STRING)); "<html><font color='red'>%s</font></html>",
MEDIA_PLAYER_ERROR_STRING));
});
timer.stop(); timer.stop();
} }
}; };
@ -231,9 +270,13 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
@Override @Override
public void stateChanged(GstObject go, State oldState, State currentState, State pendingState) { public void stateChanged(GstObject go, State oldState, State currentState, State pendingState) {
if (State.PLAYING.equals(currentState)) { if (State.PLAYING.equals(currentState)) {
playButton.setText("||"); SwingUtilities.invokeLater(() -> {
playButton.setText("||");
});
} else { } else {
playButton.setText(""); SwingUtilities.invokeLater(() -> {
playButton.setText("");
});
} }
} }
}; };
@ -241,7 +284,6 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
@Override @Override
public void endOfStream(GstObject go) { public void endOfStream(GstObject go) {
gstPlayBin.seek(ClockTime.ZERO); gstPlayBin.seek(ClockTime.ZERO);
progressSlider.setValue(0);
/** /**
* Keep the video from automatically playing * Keep the video from automatically playing
*/ */
@ -268,7 +310,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
try { try {
//Pushing off initialization to the background //Pushing off initialization to the background
extractMediaWorker = new ExtractMedia(file, VideoUtils.getVideoFileInTempDir(file)); extractMediaWorker = new ExtractMedia(file, VideoUtils.getVideoFileInTempDir(file));
extractMediaWorker.execute(); extractMediaWorker.execute();
} catch (NoCurrentCaseException ex) { } catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS
infoLabel.setText(String.format("<html><font color='red'>%s</font></html>", Bundle.GstVideoPanel_noOpenCase_errMsg())); infoLabel.setText(String.format("<html><font color='red'>%s</font></html>", Bundle.GstVideoPanel_noOpenCase_errMsg()));
@ -301,8 +343,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
if (gstPlayBin != null) { if (gstPlayBin != null) {
gstPlayBin.stop(); gstPlayBin.stop();
gstPlayBin.getBus().disconnect(endOfStreamListener); gstPlayBin.getBus().disconnect(endOfStreamListener);
gstPlayBin.getBus().disconnect(endOfStreamListener); gstPlayBin.getBus().disconnect(stateChangeListener);
gstPlayBin.getBus().disconnect(endOfStreamListener); gstPlayBin.getBus().disconnect(errorListener);
gstPlayBin.dispose(); gstPlayBin.dispose();
fxAppSink.clear(); fxAppSink.clear();
gstPlayBin = null; gstPlayBin = null;
@ -317,6 +359,9 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
progressSlider.setEnabled(isEnabled); progressSlider.setEnabled(isEnabled);
videoPanel.setEnabled(isEnabled); videoPanel.setEnabled(isEnabled);
audioSlider.setEnabled(isEnabled); audioSlider.setEnabled(isEnabled);
rewindButton.setEnabled(isEnabled);
fastForwardButton.setEnabled(isEnabled);
playBackSpeedComboBox.setEnabled(isEnabled);
} }
@Override @Override
@ -373,7 +418,18 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
* @param total * @param total
*/ */
private void updateTimeLabel(long start, long total) { private void updateTimeLabel(long start, long total) {
progressLabel.setText(formatTime(start, false) + "/" + formatTime(total, true)); progressLabel.setText(formatTime(start) + "/" + formatTime(total));
}
/**
* Reads the current selected playback rate from the speed combo box.
*
* @return The selected rate.
*/
private double getPlayBackRate() {
int selectIndex = playBackSpeedComboBox.getSelectedIndex();
String selectText = playBackSpeedComboBox.getItemAt(selectIndex);
return Double.valueOf(selectText.substring(0, selectText.length() - 1));
} }
/** /**
@ -383,24 +439,18 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
"MediaPlayerPanel.unknownTime=Unknown", "MediaPlayerPanel.unknownTime=Unknown",
"MediaPlayerPanel.timeFormat=%02d:%02d:%02d" "MediaPlayerPanel.timeFormat=%02d:%02d:%02d"
}) })
private String formatTime(long ns, boolean ceiling) { private String formatTime(long ns) {
if (ns == -1) { if (ns == -1) {
return Bundle.MediaPlayerPanel_unknownTime(); return Bundle.MediaPlayerPanel_unknownTime();
} }
double millis = ns / 1000000.0; long seconds = TimeUnit.SECONDS.convert(ns, TimeUnit.NANOSECONDS);
double seconds; long hours = TimeUnit.HOURS.convert(seconds, TimeUnit.SECONDS);
if (ceiling) { seconds -= TimeUnit.SECONDS.convert(hours, TimeUnit.HOURS);
seconds = Math.ceil(millis / 1000); long minutes = TimeUnit.MINUTES.convert(seconds, TimeUnit.SECONDS);
} else { seconds -= TimeUnit.SECONDS.convert(minutes, TimeUnit.MINUTES);
seconds = millis / 1000;
}
double hours = seconds / 3600;
seconds -= (int) hours * 3600;
double minutes = seconds / 60;
seconds -= (int) minutes * 60;
return String.format(Bundle.MediaPlayerPanel_timeFormat(), (int) hours, (int) minutes, (int) seconds); return String.format(Bundle.MediaPlayerPanel_timeFormat(), hours, minutes, seconds);
} }
/** /**
@ -422,7 +472,11 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
protected Void doInBackground() throws Exception { protected Void doInBackground() throws Exception {
if (!tempFile.exists() || tempFile.length() < sourceFile.getSize()) { if (!tempFile.exists() || tempFile.length() < sourceFile.getSize()) {
progress = ProgressHandle.createHandle(NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(true)); progress = ProgressHandle.createHandle(NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(true));
progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering"));
SwingUtilities.invokeLater(() -> {
progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering"));
});
progress.start(100); progress.start(100);
try { try {
Files.createParentDirs(tempFile); Files.createParentDirs(tempFile);
@ -443,8 +497,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
protected void done() { protected void done() {
try { try {
super.get(); super.get();
if(this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
@ -460,8 +514,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
playBinBus.connect(endOfStreamListener); playBinBus.connect(endOfStreamListener);
playBinBus.connect(stateChangeListener); playBinBus.connect(stateChangeListener);
playBinBus.connect(errorListener); playBinBus.connect(errorListener);
if(this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
@ -471,14 +525,14 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
videoPanel.add(fxPanel); videoPanel.add(fxPanel);
fxAppSink = new JavaFxAppSink("JavaFxAppSink", fxPanel); fxAppSink = new JavaFxAppSink("JavaFxAppSink", fxPanel);
gstPlayBin.setVideoSink(fxAppSink); gstPlayBin.setVideoSink(fxAppSink);
if(this.isCancelled()) { if (this.isCancelled()) {
return; return;
} }
gstPlayBin.setVolume((audioSlider.getValue() * 2.0) / 100.0); gstPlayBin.setVolume((audioSlider.getValue() * 2.0) / 100.0);
gstPlayBin.pause(); gstPlayBin.pause();
timer.start(); timer.start();
enableComponents(true); enableComponents(true);
} catch (CancellationException ex) { } catch (CancellationException ex) {
@ -499,6 +553,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
if (!progressSlider.getValueIsAdjusting()) { if (!progressSlider.getValueIsAdjusting()) {
sliderLock.acquireUninterruptibly();
long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS);
long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS);
/** /**
@ -506,12 +561,218 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
* pipeline. We start this updater when data-flow has just been * pipeline. We start this updater when data-flow has just been
* initiated so buffering may still be in progress. * initiated so buffering may still be in progress.
*/ */
if (duration != -1) { if (duration >= 0 && position >= 0) {
double relativePosition = (double) position / duration; double relativePosition = (double) position / duration;
progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE));
} }
updateTimeLabel(position, duration); SwingUtilities.invokeLater(() -> {
updateTimeLabel(position, duration);
});
sliderLock.release();
}
}
}
/**
* Represents the default configuration for the circular JSliderUI.
*/
private class CircularJSliderConfiguration {
//Thumb configurations
private final Color thumbColor;
private final Dimension thumbDimension;
//Track configurations
//Progress bar can be bisected into a seen group
//and an unseen group.
private final Color unseen;
private final Color seen;
/**
* Default configuration
*
* JSlider is light blue RGB(0,130,255). Seen track is light blue
* RGB(0,130,255). Unseen track is light grey RGB(192, 192, 192).
*
* @param thumbDimension Size of the oval thumb.
*/
public CircularJSliderConfiguration(Dimension thumbDimension) {
Color lightBlue = new Color(0, 130, 255);
seen = lightBlue;
unseen = Color.LIGHT_GRAY;
thumbColor = lightBlue;
this.thumbDimension = new Dimension(thumbDimension);
}
public Color getThumbColor() {
return thumbColor;
}
public Color getUnseenTrackColor() {
return unseen;
}
public Color getSeenTrackColor() {
return seen;
}
public Dimension getThumbDimension() {
return new Dimension(thumbDimension);
}
}
/**
* Custom view for the JSlider.
*/
private class CircularJSliderUI extends BasicSliderUI {
private final CircularJSliderConfiguration config;
/**
* Creates a custom view for the JSlider. This view draws a blue oval
* thumb at the given width and height. It also paints the track blue as
* the thumb progresses.
*
* @param slider JSlider component
* @param config Configuration object. Contains info about thumb
* dimensions and colors.
*/
public CircularJSliderUI(JSlider slider, CircularJSliderConfiguration config) {
super(slider);
this.config = config;
}
@Override
protected Dimension getThumbSize() {
return config.getThumbDimension();
}
/**
* Modifies the View to be an oval rather than the rectangle Controller.
*/
@Override
public void paintThumb(Graphics graphic) {
Rectangle thumb = this.thumbRect;
Color original = graphic.getColor();
//Change the thumb view from the rectangle
//controller to an oval.
graphic.setColor(config.getThumbColor());
Dimension thumbDimension = config.getThumbDimension();
graphic.fillOval(thumb.x, thumb.y, thumbDimension.width, thumbDimension.height);
//Preserve the graphics original color
graphic.setColor(original);
}
@Override
public void paintTrack(Graphics graphic) {
//This rectangle is the bounding box for the progress bar
//portion of the slider. The track is painted in the middle
//of this rectangle and the thumb laid overtop.
Rectangle track = this.trackRect;
//Get the location of the thumb, this point splits the
//progress bar into 2 line segments, seen and unseen.
Rectangle thumb = this.thumbRect;
int thumbX = thumb.x;
int thumbY = thumb.y;
Color original = graphic.getColor();
//Paint the seen side
graphic.setColor(config.getSeenTrackColor());
graphic.drawLine(track.x, track.y + track.height / 2,
thumbX, thumbY + track.height / 2);
//Paint the unseen side
graphic.setColor(config.getUnseenTrackColor());
graphic.drawLine(thumbX, thumbY + track.height / 2,
track.x + track.width, track.y + track.height / 2);
//Preserve the graphics color.
graphic.setColor(original);
}
@Override
protected TrackListener createTrackListener(JSlider slider) {
return new CustomTrackListener();
}
@Override
protected void scrollDueToClickInTrack(int direction) {
//Set the thumb position to the mouse press location, as opposed
//to the closest "block" which is the default behavior.
Point mousePosition = slider.getMousePosition();
if (mousePosition == null) {
return;
}
int value = this.valueForXPosition(mousePosition.x);
//Lock the slider down, which is a shared resource.
//The VideoPanelUpdater (dedicated thread) keeps the
//slider in sync with the video position, so without
//proper locking our change could be overwritten.
sliderLock.acquireUninterruptibly();
slider.setValueIsAdjusting(true);
slider.setValue(value);
slider.setValueIsAdjusting(false);
sliderLock.release();
}
/**
* Applies anti-aliasing if available.
*/
@Override
public void update(Graphics graphic, JComponent component) {
if (graphic instanceof Graphics2D) {
Graphics2D graphic2 = (Graphics2D) graphic;
graphic2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
super.update(graphic, component);
}
/**
* This track listener will force the thumb to be snapped to the mouse
* location. This makes grabbing and dragging the JSlider much easier.
* Using the default track listener, the user would have to click
* exactly on the slider thumb to drag it. Now the thumb positions
* itself under the mouse so that it can always be dragged.
*/
private class CustomTrackListener extends CircularJSliderUI.TrackListener {
@Override
public void mousePressed(MouseEvent e) {
if (!slider.isEnabled()) {
return;
}
//Snap the thumb to position of the mouse
scrollDueToClickInTrack(0);
//Pause the video for convenience
gstPlayBin.pause();
//Handle the event as normal.
super.mousePressed(e);
}
@Override
public void mouseReleased(MouseEvent e) {
if (!slider.isEnabled()) {
return;
}
super.mouseReleased(e);
//Unpause once the mouse has been released.
gstPlayBin.play();
} }
} }
} }
@ -524,15 +785,22 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() { private void initComponents() {
java.awt.GridBagConstraints gridBagConstraints;
videoPanel = new javax.swing.JPanel(); videoPanel = new javax.swing.JPanel();
controlPanel = new javax.swing.JPanel(); controlPanel = new javax.swing.JPanel();
progressSlider = new javax.swing.JSlider(); progressSlider = new javax.swing.JSlider();
infoLabel = new javax.swing.JLabel();
playButton = new javax.swing.JButton();
progressLabel = new javax.swing.JLabel(); progressLabel = new javax.swing.JLabel();
buttonPanel = new javax.swing.JPanel();
playButton = new javax.swing.JButton();
fastForwardButton = new javax.swing.JButton();
rewindButton = new javax.swing.JButton();
VolumeIcon = new javax.swing.JLabel(); VolumeIcon = new javax.swing.JLabel();
audioSlider = new javax.swing.JSlider(); audioSlider = new javax.swing.JSlider();
infoLabel = new javax.swing.JLabel();
playBackPanel = new javax.swing.JPanel();
playBackSpeedComboBox = new javax.swing.JComboBox<>();
playBackSpeedLabel = new javax.swing.JLabel();
javax.swing.GroupLayout videoPanelLayout = new javax.swing.GroupLayout(videoPanel); javax.swing.GroupLayout videoPanelLayout = new javax.swing.GroupLayout(videoPanel);
videoPanel.setLayout(videoPanelLayout); videoPanel.setLayout(videoPanelLayout);
@ -542,7 +810,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
); );
videoPanelLayout.setVerticalGroup( videoPanelLayout.setVerticalGroup(
videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 259, Short.MAX_VALUE) .addGap(0, 131, Short.MAX_VALUE)
); );
progressSlider.setValue(0); progressSlider.setValue(0);
@ -550,9 +818,11 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
progressSlider.setDoubleBuffered(true); progressSlider.setDoubleBuffered(true);
progressSlider.setMinimumSize(new java.awt.Dimension(36, 21)); progressSlider.setMinimumSize(new java.awt.Dimension(36, 21));
progressSlider.setPreferredSize(new java.awt.Dimension(200, 21)); progressSlider.setPreferredSize(new java.awt.Dimension(200, 21));
progressSlider.setUI(new CircularJSliderUI(progressSlider, new CircularJSliderConfiguration(new Dimension(18,18))));
org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.infoLabel.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.progressLabel.text")); // NOI18N
infoLabel.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
buttonPanel.setLayout(new java.awt.GridBagLayout());
org.openide.awt.Mnemonics.setLocalizedText(playButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.playButton.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(playButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.playButton.text")); // NOI18N
playButton.addActionListener(new java.awt.event.ActionListener() { playButton.addActionListener(new java.awt.event.ActionListener() {
@ -560,64 +830,145 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
playButtonActionPerformed(evt); playButtonActionPerformed(evt);
} }
}); });
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 0;
gridBagConstraints.ipadx = 21;
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new java.awt.Insets(5, 6, 0, 0);
buttonPanel.add(playButton, gridBagConstraints);
org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.progressLabel.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(fastForwardButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.fastForwardButton.text")); // NOI18N
fastForwardButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
fastForwardButtonActionPerformed(evt);
}
});
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 2;
gridBagConstraints.gridy = 0;
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new java.awt.Insets(5, 6, 0, 0);
buttonPanel.add(fastForwardButton, gridBagConstraints);
org.openide.awt.Mnemonics.setLocalizedText(rewindButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.rewindButton.text")); // NOI18N
rewindButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
rewindButtonActionPerformed(evt);
}
});
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 0;
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new java.awt.Insets(5, 0, 1, 0);
buttonPanel.add(rewindButton, gridBagConstraints);
org.openide.awt.Mnemonics.setLocalizedText(VolumeIcon, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.VolumeIcon.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(VolumeIcon, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.VolumeIcon.text")); // NOI18N
VolumeIcon.setHorizontalTextPosition(javax.swing.SwingConstants.LEFT);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 3;
gridBagConstraints.gridy = 0;
gridBagConstraints.ipadx = 8;
gridBagConstraints.ipady = 7;
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new java.awt.Insets(6, 14, 0, 0);
buttonPanel.add(VolumeIcon, gridBagConstraints);
audioSlider.setMajorTickSpacing(10); audioSlider.setMajorTickSpacing(10);
audioSlider.setMaximum(50); audioSlider.setMaximum(50);
audioSlider.setMinorTickSpacing(5); audioSlider.setMinorTickSpacing(5);
audioSlider.setPaintTicks(true);
audioSlider.setToolTipText(org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.audioSlider.toolTipText")); // NOI18N audioSlider.setToolTipText(org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.audioSlider.toolTipText")); // NOI18N
audioSlider.setValue(25); audioSlider.setValue(25);
audioSlider.setMinimumSize(new java.awt.Dimension(200, 21)); audioSlider.setMinimumSize(new java.awt.Dimension(200, 21));
audioSlider.setPreferredSize(new java.awt.Dimension(200, 21)); audioSlider.setPreferredSize(new java.awt.Dimension(200, 21));
audioSlider.setUI(new CircularJSliderUI(audioSlider, new CircularJSliderConfiguration(new Dimension(15,15))));
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 4;
gridBagConstraints.gridy = 0;
gridBagConstraints.ipadx = -116;
gridBagConstraints.ipady = 7;
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new java.awt.Insets(3, 1, 0, 10);
buttonPanel.add(audioSlider, gridBagConstraints);
infoLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.infoLabel.text")); // NOI18N
infoLabel.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
playBackSpeedComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "0.25x", "0.50x", "0.75x", "1x", "1.25x", "1.50x", "1.75x", "2x" }));
playBackSpeedComboBox.setSelectedIndex(3);
playBackSpeedComboBox.setMaximumSize(new java.awt.Dimension(53, 23));
playBackSpeedComboBox.setMinimumSize(new java.awt.Dimension(53, 23));
playBackSpeedComboBox.setPreferredSize(new java.awt.Dimension(53, 23));
playBackSpeedComboBox.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
playBackSpeedComboBoxActionPerformed(evt);
}
});
org.openide.awt.Mnemonics.setLocalizedText(playBackSpeedLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.playBackSpeedLabel.text")); // NOI18N
javax.swing.GroupLayout playBackPanelLayout = new javax.swing.GroupLayout(playBackPanel);
playBackPanel.setLayout(playBackPanelLayout);
playBackPanelLayout.setHorizontalGroup(
playBackPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(playBackPanelLayout.createSequentialGroup()
.addComponent(playBackSpeedLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(playBackSpeedComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(13, 13, 13))
);
playBackPanelLayout.setVerticalGroup(
playBackPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(playBackPanelLayout.createSequentialGroup()
.addGap(6, 6, 6)
.addGroup(playBackPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(playBackSpeedComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(playBackSpeedLabel))
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel); javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel);
controlPanel.setLayout(controlPanelLayout); controlPanel.setLayout(controlPanelLayout);
controlPanelLayout.setHorizontalGroup( controlPanelLayout.setHorizontalGroup(
controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlPanelLayout.createSequentialGroup() .addGroup(controlPanelLayout.createSequentialGroup()
.addContainerGap() .addContainerGap()
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(controlPanelLayout.createSequentialGroup() .addComponent(infoLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(playButton, javax.swing.GroupLayout.PREFERRED_SIZE, 64, javax.swing.GroupLayout.PREFERRED_SIZE) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlPanelLayout.createSequentialGroup()
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 680, Short.MAX_VALUE) .addComponent(buttonPanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 623, Short.MAX_VALUE))
.addComponent(progressLabel)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(controlPanelLayout.createSequentialGroup() .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addComponent(infoLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addGap(18, 18, 18) .addComponent(playBackPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE))
.addComponent(VolumeIcon, javax.swing.GroupLayout.PREFERRED_SIZE, 64, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(10, 10, 10)))
.addGap(2, 2, 2) .addGap(0, 0, 0))
.addComponent(audioSlider, javax.swing.GroupLayout.PREFERRED_SIZE, 229, javax.swing.GroupLayout.PREFERRED_SIZE)))
.addContainerGap())
); );
controlPanelLayout.setVerticalGroup( controlPanelLayout.setVerticalGroup(
controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(controlPanelLayout.createSequentialGroup() .addGroup(controlPanelLayout.createSequentialGroup()
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGap(0, 0, 0)
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
.addComponent(playButton)) .addGap(5, 5, 5)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) .addComponent(buttonPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(audioSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(playBackPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE))
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addGap(14, 14, 14)
.addComponent(VolumeIcon, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(infoLabel))
.addComponent(infoLabel)))
.addGap(13, 13, 13))
); );
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout); this.setLayout(layout);
layout.setHorizontalGroup( layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
); );
layout.setVerticalGroup( layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
@ -628,23 +979,96 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
); );
}// </editor-fold>//GEN-END:initComponents }// </editor-fold>//GEN-END:initComponents
private void rewindButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rewindButtonActionPerformed
long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS);
//Skip 30 seconds.
long rewindDelta = TimeUnit.NANOSECONDS.convert(SKIP_IN_SECONDS, TimeUnit.SECONDS);
//Ensure new video position is within bounds
long newTime = Math.max(currentTime - rewindDelta, 0);
double playBackRate = getPlayBackRate();
gstPlayBin.seek(playBackRate,
Format.TIME,
//FLUSH - flushes the pipeline
//ACCURATE - video will seek exactly to the position requested
EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE),
//Set the start position to newTime
SeekType.SET, newTime,
//Do nothing for the end position
SeekType.NONE, -1);
}//GEN-LAST:event_rewindButtonActionPerformed
private void fastForwardButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fastForwardButtonActionPerformed
long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS);
long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS);
//Skip 30 seconds.
long fastForwardDelta = TimeUnit.NANOSECONDS.convert(SKIP_IN_SECONDS, TimeUnit.SECONDS);
//Ignore fast forward requests if there are less than 30 seconds left.
if (currentTime + fastForwardDelta >= duration) {
return;
}
long newTime = currentTime + fastForwardDelta;
double playBackRate = getPlayBackRate();
gstPlayBin.seek(playBackRate,
Format.TIME,
//FLUSH - flushes the pipeline
//ACCURATE - video will seek exactly to the position requested
EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE),
//Set the start position to newTime
SeekType.SET, newTime,
//Do nothing for the end position
SeekType.NONE, -1);
}//GEN-LAST:event_fastForwardButtonActionPerformed
private void playButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playButtonActionPerformed private void playButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playButtonActionPerformed
if (gstPlayBin.isPlaying()) { if (gstPlayBin.isPlaying()) {
gstPlayBin.pause(); gstPlayBin.pause();
} else { } else {
double playBackRate = getPlayBackRate();
long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS);
//Set playback rate before play.
gstPlayBin.seek(playBackRate,
Format.TIME,
//FLUSH - flushes the pipeline
//ACCURATE - video will seek exactly to the position requested
EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE),
//Set the start position to newTime
SeekType.SET, currentTime,
//Do nothing for the end position
SeekType.NONE, -1);
gstPlayBin.play(); gstPlayBin.play();
} }
}//GEN-LAST:event_playButtonActionPerformed }//GEN-LAST:event_playButtonActionPerformed
private void playBackSpeedComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playBackSpeedComboBoxActionPerformed
double playBackRate = getPlayBackRate();
long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS);
gstPlayBin.seek(playBackRate,
Format.TIME,
//FLUSH - flushes the pipeline
//ACCURATE - video will seek exactly to the position requested
EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE),
//Set the position to the currentTime, we are only adjusting the
//playback rate.
SeekType.SET, currentTime,
SeekType.NONE, 0);
}//GEN-LAST:event_playBackSpeedComboBoxActionPerformed
// Variables declaration - do not modify//GEN-BEGIN:variables // Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JLabel VolumeIcon; private javax.swing.JLabel VolumeIcon;
private javax.swing.JSlider audioSlider; private javax.swing.JSlider audioSlider;
private javax.swing.JPanel buttonPanel;
private javax.swing.JPanel controlPanel; private javax.swing.JPanel controlPanel;
private javax.swing.JButton fastForwardButton;
private javax.swing.JLabel infoLabel; private javax.swing.JLabel infoLabel;
private javax.swing.JPanel playBackPanel;
private javax.swing.JComboBox<String> playBackSpeedComboBox;
private javax.swing.JLabel playBackSpeedLabel;
private javax.swing.JButton playButton; private javax.swing.JButton playButton;
private javax.swing.JLabel progressLabel; private javax.swing.JLabel progressLabel;
private javax.swing.JSlider progressSlider; private javax.swing.JSlider progressSlider;
private javax.swing.JButton rewindButton;
private javax.swing.JPanel videoPanel; private javax.swing.JPanel videoPanel;
// End of variables declaration//GEN-END:variables // End of variables declaration//GEN-END:variables
} }

View File

@ -1,15 +1,15 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2018 Basis Technology Corp. * Copyright 2018-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org * Contact: carrier <at> sleuthkit <dot> org
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -23,32 +23,42 @@ import org.openide.nodes.Node;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
/** /**
* Utility classes for content viewers. * Utility methods for content viewers.
* In theory, this would live in the contentviewer package,
* but the initial method was needed only be viewers in
* corecomponents and therefore can stay out of public API.
*/ */
public class DataContentViewerUtility { public class DataContentViewerUtility {
/** /**
* Returns the first non-Blackboard Artifact from a Node. * Gets a Content object from the Lookup of a display Node object,
* Needed for (at least) Hex and Strings that want to view * preferring to return any Content object other than a BlackboardArtifact
* all types of content (not just AbstractFile), but don't want * object.
* to display an artifact unless that's the only thing there. *
* Scenario is hash hit or interesting item hit. * This method was written with the needs of the hex and strings content
* * viewers in mind - the algorithm is exactly what those viewers require.
* @param node Node passed into content viewer *
* @return highest priority content or null if there is no content * @param node A display Node object.
*
* @return If there are multiple Content objects associated with the Node,
* the first Content object that is not a BlackboardArtifact object
* is returned. If no Content objects other than artifacts are found,
* the first BlackboardArtifact object found is returned. If no
* Content objects are found, null is returned.
*/ */
public static Content getDefaultContent(Node node) { public static Content getDefaultContent(Node node) {
Content bbContentSeen = null; Content artifact = null;
for (Content content : (node).getLookup().lookupAll(Content.class)) { for (Content content : node.getLookup().lookupAll(Content.class)) {
if (content instanceof BlackboardArtifact) { if (content instanceof BlackboardArtifact && artifact == null) {
bbContentSeen = content; artifact = content;
} } else {
else {
return content; return content;
} }
} }
return bbContentSeen; return artifact;
} }
/*
* Private constructor to prevent instantiation of utility class.
*/
private DataContentViewerUtility() {
}
} }

View File

@ -2,16 +2,15 @@ OptionsCategory_Name_Machine_Translation=Machine Translation
OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings
TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB) TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB)
TranslatedContentPanel.comboBoxOption.translatedText=Translated Text TranslatedContentPanel.comboBoxOption.translatedText=Translated Text
TranslatedContentViewer.emptyTranslation=The resulting translation was empty. TranslatedContentViewer.emptyTranslation=The machine translation software did not return any text.
TranslatedContentViewer.errorExtractingText=Could not extract text from file. # {0} - exception message
TranslatedContentViewer.errorMsg=Error encountered while getting file text. TranslatedContentViewer.errorExtractingText=An error occurred while extracting the text ({0}).
TranslatedContentViewer.extractingFileText=Extracting text from file, please wait... TranslatedContentViewer.extractingText=Extracting text, please wait...
TranslatedContentViewer.extractingImageText=Extracting text from image, please wait... TranslatedContentViewer.fileHasNoText=File has no text.
TranslatedContentViewer.noIndexedTextMsg=Run the Keyword Search Ingest Module to get text for translation. TranslatedContentViewer.noServiceProvider=The machine translation software was not found.
TranslatedContentViewer.noServiceProvider=Machine Translation software was not found.
TranslatedContentViewer.textAlreadyIndexed=Please view the original text in the Indexed Text viewer.
TranslatedContentViewer.translatingText=Translating text, please wait... TranslatedContentViewer.translatingText=Translating text, please wait...
TranslatedContentViewer.translationException=Error encountered while attempting translation. # {0} - exception message
TranslatedContentViewer.translationException=An error occurred while translating the text ({0}).
TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated
TranslatedTextViewer.title=Translation TranslatedTextViewer.title=Translation
TranslatedTextViewer.toolTip=Displays translated file text. TranslatedTextViewer.toolTip=Displays translated file text.

View File

@ -53,6 +53,7 @@ import org.sleuthkit.autopsy.texttranslation.TranslationException;
import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Content;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import javax.swing.SwingUtilities;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.coreutils.PlatformUtil;
import org.sleuthkit.autopsy.texttranslation.ui.TranslationContentPanel.DisplayDropdownOptions; import org.sleuthkit.autopsy.texttranslation.ui.TranslationContentPanel.DisplayDropdownOptions;
@ -63,7 +64,7 @@ import org.sleuthkit.autopsy.texttranslation.ui.TranslationContentPanel.DisplayD
@ServiceProvider(service = TextViewer.class, position = 4) @ServiceProvider(service = TextViewer.class, position = 4)
public final class TranslatedTextViewer implements TextViewer { public final class TranslatedTextViewer implements TextViewer {
private static final Logger logger = Logger.getLogger(TranslatedTextViewer.class.getName()); private static final Logger logger = Logger.getLogger(TranslatedTextViewer.class.getName());
private static final boolean OCR_ENABLED = true; private static final boolean OCR_ENABLED = true;
private static final boolean OCR_DISABLED = false; private static final boolean OCR_DISABLED = false;
@ -72,7 +73,7 @@ public final class TranslatedTextViewer implements TextViewer {
private final TranslationContentPanel panel = new TranslationContentPanel(); private final TranslationContentPanel panel = new TranslationContentPanel();
private volatile Node node; private volatile Node node;
private volatile BackgroundTranslationTask updateTask; private volatile ExtractAndTranslateTextTask backgroundTask;
private final ThreadFactory translationThreadFactory private final ThreadFactory translationThreadFactory
= new ThreadFactoryBuilder().setNameFormat("translation-content-viewer-%d").build(); = new ThreadFactoryBuilder().setNameFormat("translation-content-viewer-%d").build();
private final ExecutorService executorService = Executors.newSingleThreadExecutor(translationThreadFactory); private final ExecutorService executorService = Executors.newSingleThreadExecutor(translationThreadFactory);
@ -95,7 +96,7 @@ public final class TranslatedTextViewer implements TextViewer {
panel.addLanguagePackNames(INSTALLED_LANGUAGE_PACKS); panel.addLanguagePackNames(INSTALLED_LANGUAGE_PACKS);
} }
} }
int payloadMaxInKB = TextTranslationService.getInstance().getMaxTextChars() / 1000; int payloadMaxInKB = TextTranslationService.getInstance().getMaxTextChars() / 1000;
panel.setWarningLabelMsg(String.format(Bundle.TranslatedTextViewer_maxPayloadSize(), payloadMaxInKB)); panel.setWarningLabelMsg(String.format(Bundle.TranslatedTextViewer_maxPayloadSize(), payloadMaxInKB));
@ -129,10 +130,10 @@ public final class TranslatedTextViewer implements TextViewer {
public void resetComponent() { public void resetComponent() {
panel.reset(); panel.reset();
this.node = null; this.node = null;
if (updateTask != null) { if (backgroundTask != null) {
updateTask.cancel(true); backgroundTask.cancel(true);
} }
updateTask = null; backgroundTask = null;
} }
@Override @Override
@ -157,62 +158,74 @@ public final class TranslatedTextViewer implements TextViewer {
} }
/** /**
* Fetches file text and performs translation. * Extracts text from a file and optionally translates it.
*/ */
private class BackgroundTranslationTask extends SwingWorker<String, Void> { private class ExtractAndTranslateTextTask extends SwingWorker<String, Void> {
private final AbstractFile file;
private final boolean translateText;
private ExtractAndTranslateTextTask(AbstractFile file, boolean translateText) {
this.file = file;
this.translateText = translateText;
}
@NbBundle.Messages({ @NbBundle.Messages({
"TranslatedContentViewer.noIndexedTextMsg=Run the Keyword Search Ingest Module to get text for translation.", "TranslatedContentViewer.extractingText=Extracting text, please wait...",
"TranslatedContentViewer.textAlreadyIndexed=Please view the original text in the Indexed Text viewer.", "TranslatedContentViewer.translatingText=Translating text, please wait...",
"TranslatedContentViewer.errorMsg=Error encountered while getting file text.", "# {0} - exception message", "TranslatedContentViewer.errorExtractingText=An error occurred while extracting the text ({0}).",
"TranslatedContentViewer.errorExtractingText=Could not extract text from file.", "TranslatedContentViewer.fileHasNoText=File has no text.",
"TranslatedContentViewer.translatingText=Translating text, please wait..." "TranslatedContentViewer.noServiceProvider=The machine translation software was not found.",
"# {0} - exception message", "TranslatedContentViewer.translationException=An error occurred while translating the text ({0})."
}) })
@Override @Override
public String doInBackground() throws InterruptedException { public String doInBackground() throws InterruptedException {
if (this.isCancelled()) { if (this.isCancelled()) {
throw new InterruptedException(); throw new InterruptedException();
} }
String dropdownSelection = panel.getDisplayDropDownSelection();
if (dropdownSelection.equals(DisplayDropdownOptions.ORIGINAL_TEXT.toString())) { SwingUtilities.invokeLater(() -> {
try { panel.display(Bundle.TranslatedContentViewer_extractingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC);
return getFileText(node); });
} catch (IOException ex) { String fileText;
logger.log(Level.WARNING, "Error getting text", ex); try {
return Bundle.TranslatedContentViewer_errorMsg(); fileText = getFileText(file);
} catch (TextExtractor.InitReaderException ex) { } catch (IOException | TextExtractor.InitReaderException ex) {
logger.log(Level.WARNING, "Error getting text", ex); logger.log(Level.WARNING, String.format("Error extracting text for file %s (objId=%d)", file.getName(), file.getId()), ex);
return Bundle.TranslatedContentViewer_errorExtractingText(); return Bundle.TranslatedContentViewer_errorExtractingText(ex.getMessage());
}
} else {
try {
return translate(getFileText(node));
} catch (IOException ex) {
logger.log(Level.WARNING, "Error translating text", ex);
return Bundle.TranslatedContentViewer_errorMsg();
} catch (TextExtractor.InitReaderException ex) {
logger.log(Level.WARNING, "Error translating text", ex);
return Bundle.TranslatedContentViewer_errorExtractingText();
}
} }
}
/** if (this.isCancelled()) {
* Update the extraction loading message depending on the file type. throw new InterruptedException();
*
* @param isImage Boolean indicating if the selecting node is an image
*/
@NbBundle.Messages({"TranslatedContentViewer.extractingImageText=Extracting text from image, please wait...",
"TranslatedContentViewer.extractingFileText=Extracting text from file, please wait...",})
private void updateExtractionLoadingMessage(boolean isImage) {
if (isImage) {
panel.display(Bundle.TranslatedContentViewer_extractingImageText(),
ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC);
} else {
panel.display(Bundle.TranslatedContentViewer_extractingFileText(),
ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC);
} }
if (fileText == null || fileText.isEmpty()) {
return Bundle.TranslatedContentViewer_fileHasNoText();
}
if (!this.translateText) {
return fileText;
}
SwingUtilities.invokeLater(() -> {
panel.display(Bundle.TranslatedContentViewer_translatingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC);
});
String translation;
try {
translation = translate(fileText);
} catch (NoServiceProviderException ex) {
logger.log(Level.WARNING, String.format("Error translating text for file %s (objId=%d)", file.getName(), file.getId()), ex);
translation = Bundle.TranslatedContentViewer_noServiceProvider();
} catch (TranslationException ex) {
logger.log(Level.WARNING, String.format("Error translating text for file %s (objId=%d)", file.getName(), file.getId()), ex);
translation = Bundle.TranslatedContentViewer_translationException(ex.getMessage());
}
if (this.isCancelled()) {
throw new InterruptedException();
}
return translation;
} }
@Override @Override
@ -227,8 +240,12 @@ public final class TranslatedTextViewer implements TextViewer {
String orientDetectSubstring = result.substring(0, maxOrientChars); String orientDetectSubstring = result.substring(0, maxOrientChars);
ComponentOrientation orientation = TextUtil.getTextDirection(orientDetectSubstring); ComponentOrientation orientation = TextUtil.getTextDirection(orientDetectSubstring);
panel.display(result, orientation, Font.PLAIN); panel.display(result, orientation, Font.PLAIN);
} catch (InterruptedException | ExecutionException | CancellationException ignored) {
//InterruptedException & CancellationException - User cancelled, no error. } catch (InterruptedException | CancellationException ignored) {
// Task cancelled, no error.
} catch (ExecutionException ex) {
logger.log(Level.WARNING, String.format("Error occurred during background task execution for file %s (objId=%d)", file.getName(), file.getId()), ex);
panel.display(Bundle.TranslatedContentViewer_translationException(ex.getMessage()), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC);
} }
} }
@ -240,36 +257,21 @@ public final class TranslatedTextViewer implements TextViewer {
* @return Translated text or error message * @return Translated text or error message
*/ */
@NbBundle.Messages({ @NbBundle.Messages({
"TranslatedContentViewer.emptyTranslation=The resulting translation was empty.", "TranslatedContentViewer.emptyTranslation=The machine translation software did not return any text."
"TranslatedContentViewer.noServiceProvider=Machine Translation software was not found.", })
"TranslatedContentViewer.translationException=Error encountered while attempting translation."}) private String translate(String input) throws NoServiceProviderException, TranslationException {
private String translate(String input) throws InterruptedException { TextTranslationService translatorInstance = TextTranslationService.getInstance();
if (this.isCancelled()) { String translatedResult = translatorInstance.translate(input);
throw new InterruptedException(); if (translatedResult.isEmpty()) {
} return Bundle.TranslatedContentViewer_emptyTranslation();
panel.display(Bundle.TranslatedContentViewer_translatingText(),
ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC);
try {
TextTranslationService translatorInstance = TextTranslationService.getInstance();
String translatedResult = translatorInstance.translate(input);
if (translatedResult.isEmpty()) {
return Bundle.TranslatedContentViewer_emptyTranslation();
}
return translatedResult;
} catch (NoServiceProviderException ex) {
return Bundle.TranslatedContentViewer_noServiceProvider();
} catch (TranslationException ex) {
logger.log(Level.WARNING, "Error translating text", ex);
return Bundle.TranslatedContentViewer_translationException() + " (" + ex.getMessage() + ")";
} }
return translatedResult;
} }
/** /**
* Extracts text from the given node * Extracts text from the given node
* *
* @param node Selected node in UI * @param file Selected node in UI
* *
* @return Extracted text * @return Extracted text
* *
@ -277,33 +279,22 @@ public final class TranslatedTextViewer implements TextViewer {
* @throws InterruptedException * @throws InterruptedException
* @throws * @throws
* org.sleuthkit.autopsy.textextractors.TextExtractor.InitReaderException * org.sleuthkit.autopsy.textextractors.TextExtractor.InitReaderException
* @throws NoOpenCoreException
* @throws KeywordSearchModuleException
*/ */
private String getFileText(Node node) throws IOException, private String getFileText(AbstractFile file) throws IOException,
InterruptedException, TextExtractor.InitReaderException { InterruptedException, TextExtractor.InitReaderException {
AbstractFile source = (AbstractFile) DataContentViewerUtility.getDefaultContent(node); final boolean isImage = file.getMIMEType().toLowerCase().startsWith("image/"); // NON-NLS
boolean isImage = false;
if (source != null) {
isImage = source.getMIMEType().toLowerCase().startsWith("image/");
}
updateExtractionLoadingMessage(isImage);
String result; String result;
if (isImage) { if (isImage) {
result = extractText(source, OCR_ENABLED); result = extractText(file, OCR_ENABLED);
} else { } else {
result = extractText(source, OCR_DISABLED); result = extractText(file, OCR_DISABLED);
} }
//Correct for UTF-8 //Correct for UTF-8
byte[] resultInUTF8Bytes = result.getBytes("UTF8"); byte[] resultInUTF8Bytes = result.getBytes("UTF8");
byte[] trimToArraySize = Arrays.copyOfRange(resultInUTF8Bytes, 0, byte[] trimToArraySize = Arrays.copyOfRange(resultInUTF8Bytes, 0,
Math.min(resultInUTF8Bytes.length, MAX_EXTRACT_SIZE_BYTES) ); Math.min(resultInUTF8Bytes.length, MAX_EXTRACT_SIZE_BYTES));
return new String(trimToArraySize, "UTF-8"); return new String(trimToArraySize, "UTF-8");
} }
@ -348,7 +339,7 @@ public final class TranslatedTextViewer implements TextViewer {
textBuilder.append(cbuf, 0, read); textBuilder.append(cbuf, 0, read);
bytesRead += read; bytesRead += read;
} }
return textBuilder.toString(); return textBuilder.toString();
} }
@ -399,23 +390,28 @@ public final class TranslatedTextViewer implements TextViewer {
*/ */
private abstract class SelectionChangeListener implements ActionListener { private abstract class SelectionChangeListener implements ActionListener {
public String currentSelection = null; private String currentSelection;
public abstract String getSelection(); abstract String getSelection();
@Override @Override
public final void actionPerformed(ActionEvent e) { public final void actionPerformed(ActionEvent e) {
String selection = getSelection(); String selection = getSelection();
if (!selection.equals(currentSelection)) { if (!selection.equals(currentSelection)) {
currentSelection = selection; currentSelection = selection;
if (updateTask != null && !updateTask.isDone()) {
updateTask.cancel(true); if (backgroundTask != null && !backgroundTask.isDone()) {
backgroundTask.cancel(true);
} }
updateTask = new BackgroundTranslationTask();
AbstractFile file = node.getLookup().lookup(AbstractFile.class);
String textDisplaySelection = panel.getDisplayDropDownSelection();
boolean translateText = !textDisplaySelection.equals(DisplayDropdownOptions.ORIGINAL_TEXT.toString());
backgroundTask = new ExtractAndTranslateTextTask(file, translateText);
//Pass the background task to a single threaded pool to keep //Pass the background task to a single threaded pool to keep
//the number of jobs running to one. //the number of jobs running to one.
executorService.execute(updateTask); executorService.execute(backgroundTask);
} }
} }
} }
@ -426,7 +422,7 @@ public final class TranslatedTextViewer implements TextViewer {
private class DisplayDropDownChangeListener extends SelectionChangeListener { private class DisplayDropDownChangeListener extends SelectionChangeListener {
@Override @Override
public String getSelection() { String getSelection() {
return panel.getDisplayDropDownSelection(); return panel.getDisplayDropDownSelection();
} }
} }
@ -437,7 +433,7 @@ public final class TranslatedTextViewer implements TextViewer {
private class OCRDropdownChangeListener extends SelectionChangeListener { private class OCRDropdownChangeListener extends SelectionChangeListener {
@Override @Override
public String getSelection() { String getSelection() {
return panel.getSelectedOcrLanguagePack(); return panel.getSelectedOcrLanguagePack();
} }
} }

View File

@ -236,28 +236,23 @@ class LineCallLogsParser(TskCallLogsParser):
def __init__(self, calllog_db): def __init__(self, calllog_db):
super(LineCallLogsParser, self).__init__(calllog_db.runQuery( super(LineCallLogsParser, self).__init__(calllog_db.runQuery(
""" """
SELECT Substr(CH.call_type, -1) AS direction, SELECT Substr(calls.call_type, -1) AS direction,
CH.start_time AS start_time, calls.start_time AS start_time,
CH.end_time AS end_time, calls.end_time AS end_time,
contacts_list_with_groups.members AS group_members, contact_book_w_groups.members AS group_members,
contacts_list_with_groups.member_names AS names, calls.caller_mid,
CH.caller_mid, calls.voip_type AS call_type,
CH.voip_type AS call_type, calls.voip_gc_media_type AS group_call_type
CH.voip_gc_media_type AS group_call_type
FROM (SELECT id, FROM (SELECT id,
Group_concat(M.m_id) AS members, Group_concat(M.m_id) AS members
Group_concat(Replace(C.server_name, ",", "")) AS member_names
FROM membership AS M FROM membership AS M
JOIN naver.contacts AS C
ON M.m_id = C.m_id
GROUP BY id GROUP BY id
UNION UNION
SELECT m_id, SELECT m_id,
NULL, NULL
server_name FROM naver.contacts) AS contact_book_w_groups
FROM naver.contacts) AS contacts_list_with_groups JOIN call_history AS calls
JOIN call_history AS CH ON calls.caller_mid = contact_book_w_groups.id
ON CH.caller_mid = contacts_list_with_groups.id
""" """
) )
) )
@ -355,43 +350,25 @@ class LineMessagesParser(TskMessagesParser):
def __init__(self, message_db): def __init__(self, message_db):
super(LineMessagesParser, self).__init__(message_db.runQuery( super(LineMessagesParser, self).__init__(message_db.runQuery(
""" """
SELECT contact_list_with_groups.name, SELECT contact_book_w_groups.id,
contact_list_with_groups.id, contact_book_w_groups.members,
contact_list_with_groups.members, messages.from_mid,
contact_list_with_groups.member_names, messages.content,
CH.from_mid, messages.created_time,
C.server_name AS from_name, messages.attachement_type,
CH.content, messages.attachement_local_uri,
CH.created_time, messages.status
CH.attachement_type, FROM (SELECT id,
CH.attachement_local_uri, Group_concat(M.m_id) AS members
CH.status FROM membership AS M
FROM (SELECT G.name, GROUP BY id
group_members.id, UNION
group_members.members, SELECT m_id,
group_members.member_names NULL
FROM (SELECT id, FROM contacts) AS contact_book_w_groups
group_concat(M.m_id) AS members, JOIN chat_history AS messages
group_concat(replace(C.server_name, ON messages.chat_id = contact_book_w_groups.id
",", WHERE attachement_type != 6
"")) as member_names
FROM membership AS M
JOIN contacts as C
ON M.m_id = C.m_id
GROUP BY id) AS group_members
JOIN groups AS G
ON G.id = group_members.id
UNION
SELECT server_name,
m_id,
NULL,
NULL
FROM contacts) AS contact_list_with_groups
JOIN chat_history AS CH
ON CH.chat_id = contact_list_with_groups.id
LEFT JOIN contacts as C
ON C.m_id = CH.from_mid
WHERE attachement_type != 6
""" """
) )
) )

View File

@ -76,11 +76,8 @@ class SkypeAnalyzer(general.AndroidComponentAnalyzer):
as they would be excluded in the join. Since the chatItem table stores both the as they would be excluded in the join. Since the chatItem table stores both the
group id or skype_id in one column, an implementation decision was made to union group id or skype_id in one column, an implementation decision was made to union
the person and particiapnt table together so that all rows are matched in one join the person and particiapnt table together so that all rows are matched in one join
with chatItem. This result is consistently labeled contact_list_with_groups in the with chatItem. This result is consistently labeled contact_book_w_groups in the
following queries. following queries.
- In order to keep the formatting of the name consistent throughout each query,
a _format_user_name() function was created to encapsulate the CASE statement
that was being shared across them. Refer to the method for more details.
""" """
def __init__(self): def __init__(self):
@ -93,7 +90,12 @@ class SkypeAnalyzer(general.AndroidComponentAnalyzer):
account_query_result = skype_db.runQuery( account_query_result = skype_db.runQuery(
""" """
SELECT entry_id, SELECT entry_id,
"""+_format_user_name()+""" AS name CASE
WHEN Ifnull(first_name, "") == "" AND Ifnull(last_name, "") == "" THEN entry_id
WHEN first_name is NULL THEN replace(last_name, ",", "")
WHEN last_name is NULL THEN replace(first_name, ",", "")
ELSE replace(first_name, ",", "") || " " || replace(last_name, ",", "")
END AS name
FROM user FROM user
""" """
) )
@ -251,14 +253,6 @@ class SkypeCallLogsParser(TskCallLogsParser):
def __init__(self, calllog_db): def __init__(self, calllog_db):
""" """
Big picture:
The query below creates a contacts_list_with_groups table, which
represents the recipient info. A chatItem record holds ids for
both the recipient and sender. The first join onto chatItem fills
in the blanks for the recipients. The second join back onto person
handles the sender info. The result is a table with all of the
communication details.
Implementation details: Implementation details:
- message_type w/ value 3 appeared to be the call type, regardless - message_type w/ value 3 appeared to be the call type, regardless
of if it was audio or video. of if it was audio or video.
@ -266,37 +260,23 @@ class SkypeCallLogsParser(TskCallLogsParser):
""" """
super(SkypeCallLogsParser, self).__init__(calllog_db.runQuery( super(SkypeCallLogsParser, self).__init__(calllog_db.runQuery(
""" """
SELECT contacts_list_with_groups.conversation_id, SELECT contact_book_w_groups.conversation_id,
contacts_list_with_groups.participant_ids, contact_book_w_groups.participant_ids,
contacts_list_with_groups.participants, messages.time,
time, messages.duration,
duration, messages.is_sender_me,
is_sender_me, messages.person_id AS sender_id
person_id as sender_id,
sender_name.name as sender_name
FROM (SELECT conversation_id, FROM (SELECT conversation_id,
Group_concat(person_id) AS participant_ids, Group_concat(person_id) AS participant_ids
Group_concat("""+_format_user_name()+""") AS participants FROM particiapnt
FROM particiapnt AS PART
JOIN person AS P
ON PART.person_id = P.entry_id
GROUP BY conversation_id GROUP BY conversation_id
UNION UNION
SELECT entry_id, SELECT entry_id AS conversation_id,
NULL, NULL
"""+_format_user_name()+""" AS participant FROM person) AS contact_book_w_groups
FROM person) AS contacts_list_with_groups join chatitem AS messages
JOIN chatitem AS C ON messages.conversation_link = contact_book_w_groups.conversation_id
ON C.conversation_link = contacts_list_with_groups.conversation_id WHERE message_type == 3
JOIN (SELECT entry_id as id,
"""+_format_user_name()+""" AS name
FROM person
UNION
SELECT entry_id as id,
"""+_format_user_name()+""" AS name
FROM user) AS sender_name
ON sender_name.id = C.person_id
WHERE message_type == 3
""" """
) )
) )
@ -347,7 +327,12 @@ class SkypeContactsParser(TskContactsParser):
super(SkypeContactsParser, self).__init__(contact_db.runQuery( super(SkypeContactsParser, self).__init__(contact_db.runQuery(
""" """
SELECT entry_id, SELECT entry_id,
"""+_format_user_name()+""" AS name CASE
WHEN Ifnull(first_name, "") == "" AND Ifnull(last_name, "") == "" THEN entry_id
WHEN first_name is NULL THEN replace(last_name, ",", "")
WHEN last_name is NULL THEN replace(first_name, ",", "")
ELSE replace(first_name, ",", "") || " " || replace(last_name, ",", "")
END AS name
FROM person FROM person
""" """
) )
@ -379,39 +364,25 @@ class SkypeMessagesParser(TskMessagesParser):
""" """
super(SkypeMessagesParser, self).__init__(message_db.runQuery( super(SkypeMessagesParser, self).__init__(message_db.runQuery(
""" """
SELECT contacts_list_with_groups.conversation_id, SELECT contact_book_w_groups.conversation_id,
contacts_list_with_groups.participant_ids, contact_book_w_groups.participant_ids,
contacts_list_with_groups.participants, messages.time,
time, messages.content,
content, messages.device_gallery_path,
device_gallery_path, messages.is_sender_me,
is_sender_me, messages.person_id as sender_id
person_id as sender_id, FROM (SELECT conversation_id,
sender_name.name AS sender_name Group_concat(person_id) AS participant_ids
FROM (SELECT conversation_id, FROM particiapnt
Group_concat(person_id) AS participant_ids, GROUP BY conversation_id
Group_concat("""+_format_user_name()+""") AS participants UNION
FROM particiapnt AS PART SELECT entry_id as conversation_id,
JOIN person AS P NULL
ON PART.person_id = P.entry_id FROM person) AS contact_book_w_groups
GROUP BY conversation_id JOIN chatitem AS messages
UNION ON messages.conversation_link = contact_book_w_groups.conversation_id
SELECT entry_id as conversation_id,
NULL,
"""+_format_user_name()+""" AS participant
FROM person) AS contacts_list_with_groups
JOIN chatitem AS C
ON C.conversation_link = contacts_list_with_groups.conversation_id
JOIN (SELECT entry_id as id,
"""+_format_user_name()+""" AS name
FROM person
UNION
SELECT entry_id as id,
"""+_format_user_name()+""" AS name
FROM user) AS sender_name
ON sender_name.id = C.person_id
WHERE message_type != 3 WHERE message_type != 3
""" """
) )
) )
self._SKYPE_MESSAGE_TYPE = "Skype Message" self._SKYPE_MESSAGE_TYPE = "Skype Message"
@ -469,25 +440,3 @@ class SkypeMessagesParser(TskMessagesParser):
if group_ids is not None: if group_ids is not None:
return self.result_set.getString("conversation_id") return self.result_set.getString("conversation_id")
return super(SkypeMessagesParser, self).get_thread_id() return super(SkypeMessagesParser, self).get_thread_id()
def _format_user_name():
"""
This CASE SQL statement is used in many queries to
format the names of users. For a user, there is a first_name
column and a last_name column. Some of these columns can be null
and our goal is to produce the cleanest data possible. In the event
that both the first and last name columns are null, we return the skype_id
which is stored in the database as 'entry_id'. Commas are removed from the name
so that we can concatenate names into a comma seperate list for group chats.
"""
return """
CASE
WHEN Ifnull(first_name, "") == "" AND Ifnull(last_name, "") == "" THEN entry_id
WHEN first_name is NULL THEN replace(last_name, ",", "")
WHEN last_name is NULL THEN replace(first_name, ",", "")
ELSE replace(first_name, ",", "") || " " || replace(last_name, ",", "")
END
"""

View File

@ -290,53 +290,50 @@ class TextNowMessagesParser(TskMessagesParser):
""" """
super(TextNowMessagesParser, self).__init__(message_db.runQuery( super(TextNowMessagesParser, self).__init__(message_db.runQuery(
""" """
SELECT CASE
SELECT CASE WHEN messages.message_direction == 2 THEN NULL
WHEN message_direction == 2 THEN "" WHEN contact_book_w_groups.to_addresses IS NULL THEN
WHEN to_addresses IS NULL THEN M.contact_value messages.contact_value
ELSE contact_name END from_address,
end from_address, CASE
CASE WHEN messages.message_direction == 1 THEN NULL
WHEN message_direction == 1 THEN "" WHEN contact_book_w_groups.to_addresses IS NULL THEN
WHEN to_addresses IS NULL THEN M.contact_value messages.contact_value
ELSE to_addresses ELSE contact_book_w_groups.to_addresses
end to_address, END to_address,
message_direction, messages.message_direction,
message_text, messages.message_text,
M.READ, messages.READ,
M.date, messages.DATE,
M.attach, messages.attach,
thread_id thread_id
FROM (SELECT group_info.contact_value, FROM (SELECT GM.contact_value,
group_info.to_addresses, Group_concat(GM.member_contact_value) AS to_addresses,
G.contact_value AS thread_id G.contact_value AS thread_id
FROM (SELECT GM.contact_value, FROM group_members AS GM
Group_concat(GM.member_contact_value) AS to_addresses join GROUPS AS G
FROM group_members AS GM ON G.contact_value = GM.contact_value
GROUP BY GM.contact_value) AS group_info GROUP BY GM.contact_value
JOIN groups AS G UNION
ON G.contact_value = group_info.contact_value SELECT contact_value,
UNION NULL,
SELECT c.contact_value, NULL
NULL, FROM contacts) AS contact_book_w_groups
"-1" join messages
FROM contacts AS c) AS to_from_map ON messages.contact_value = contact_book_w_groups.contact_value
JOIN messages AS M WHERE message_type NOT IN ( 102, 100 )
ON M.contact_value = to_from_map.contact_value
WHERE message_type NOT IN ( 102, 100 )
""" """
) )
) )
self._TEXTNOW_MESSAGE_TYPE = "TextNow Message" self._TEXTNOW_MESSAGE_TYPE = "TextNow Message"
self._INCOMING_MESSAGE_TYPE = 1 self._INCOMING_MESSAGE_TYPE = 1
self._OUTGOING_MESSAGE_TYPE = 2 self._OUTGOING_MESSAGE_TYPE = 2
self._UNKNOWN_THREAD_ID = "-1"
def get_message_type(self): def get_message_type(self):
return self._TEXTNOW_MESSAGE_TYPE return self._TEXTNOW_MESSAGE_TYPE
def get_phone_number_from(self): def get_phone_number_from(self):
if self.result_set.getString("from_address") == "": if self.result_set.getString("from_address") is None:
return super(TextNowMessagesParser, self).get_phone_number_from() return super(TextNowMessagesParser, self).get_phone_number_from()
return self.result_set.getString("from_address") return self.result_set.getString("from_address")
@ -347,10 +344,9 @@ class TextNowMessagesParser(TskMessagesParser):
return self.OUTGOING return self.OUTGOING
def get_phone_number_to(self): def get_phone_number_to(self):
if self.result_set.getString("to_address") == "": if self.result_set.getString("to_address") is None:
return super(TextNowMessagesParser, self).get_phone_number_to() return super(TextNowMessagesParser, self).get_phone_number_to()
recipients = self.result_set.getString("to_address").split(",") return self.result_set.getString("to_address").split(",")
return recipients
def get_message_date_time(self): def get_message_date_time(self):
#convert ms to s #convert ms to s
@ -359,7 +355,7 @@ class TextNowMessagesParser(TskMessagesParser):
def get_message_read_status(self): def get_message_read_status(self):
read = self.result_set.getBoolean("read") read = self.result_set.getBoolean("read")
if self.get_message_direction() == self.INCOMING: if self.get_message_direction() == self.INCOMING:
if read == True: if read:
return self.READ return self.READ
return self.UNREAD return self.UNREAD
@ -375,6 +371,6 @@ class TextNowMessagesParser(TskMessagesParser):
def get_thread_id(self): def get_thread_id(self):
thread_id = self.result_set.getString("thread_id") thread_id = self.result_set.getString("thread_id")
if thread_id == self._UNKNOWN_THREAD_ID: if thread_id is None:
return super(TextNowMessagesParser, self).get_thread_id() return super(TextNowMessagesParser, self).get_thread_id()
return thread_id return thread_id

View File

@ -433,31 +433,28 @@ class WhatsAppMessagesParser(TskMessagesParser):
def __init__(self, message_db): def __init__(self, message_db):
super(WhatsAppMessagesParser, self).__init__(message_db.runQuery( super(WhatsAppMessagesParser, self).__init__(message_db.runQuery(
""" """
SELECT M.key_remote_jid AS id, SELECT messages.key_remote_jid AS id,
contact_info.recipients, contact_book_w_groups.recipients,
key_from_me AS direction, key_from_me AS direction,
CASE messages.data AS content,
WHEN M.data IS NULL THEN "" messages.timestamp AS send_timestamp,
ELSE M.data messages.received_timestamp,
END AS content, messages.remote_resource AS group_sender,
M.timestamp AS send_timestamp, messages.media_url AS attachment
M.received_timestamp, FROM (SELECT jid,
M.remote_resource AS group_sender, recipients
M.media_url As attachment FROM wadb.wa_contacts AS contacts
FROM (SELECT jid, left join (SELECT gjid,
recipients Group_concat(CASE
FROM wadb.wa_contacts AS WC WHEN jid == "" THEN NULL
LEFT JOIN (SELECT gjid, ELSE jid
group_concat(CASE END) AS recipients
WHEN jid == "" THEN NULL FROM group_participants
ELSE jid GROUP BY gjid) AS groups
END) AS recipients ON contacts.jid = groups.gjid
FROM group_participants GROUP BY jid) AS contact_book_w_groups
GROUP BY gjid) AS group_map join messages
ON WC.jid = group_map.gjid ON messages.key_remote_jid = contact_book_w_groups.jid
GROUP BY jid) AS contact_info
JOIN messages AS M
ON M.key_remote_jid = contact_info.jid
""" """
) )
) )
@ -503,6 +500,8 @@ class WhatsAppMessagesParser(TskMessagesParser):
def get_message_text(self): def get_message_text(self):
message = self.result_set.getString("content") message = self.result_set.getString("content")
if message is None:
message = super(WhatsAppMessagesParser, self).get_message_text()
attachment = self.result_set.getString("attachment") attachment = self.result_set.getString("attachment")
if attachment is not None: if attachment is not None:
return general.appendAttachmentList(message, [attachment]) return general.appendAttachmentList(message, [attachment])

View File

@ -6,6 +6,8 @@ Overview
This is the User's Guide for the <a href="http://www.sleuthkit.org/autopsy/">open source Autopsy platform</a>. Autopsy allows you to examine a hard drive or mobile device and recover evidence from it. This guide should help you with using Autopsy. The <a href="http://www.sleuthkit.org/autopsy/docs/api-docs/"> developer's guide</a> will help you develop your own Autopsy modules. This is the User's Guide for the <a href="http://www.sleuthkit.org/autopsy/">open source Autopsy platform</a>. Autopsy allows you to examine a hard drive or mobile device and recover evidence from it. This guide should help you with using Autopsy. The <a href="http://www.sleuthkit.org/autopsy/docs/api-docs/"> developer's guide</a> will help you develop your own Autopsy modules.
Note: For those users running Autopsy on Mac devices, the functionality available through the "Tools" -> "Options" dialog as described in this documentation can be accessed through the system menu bar under "Preferences" or through the Cmd + , (command-comma) shortcut.
Help Topics Help Topics
------- -------
The following topics are available here: The following topics are available here:

View File

@ -772,6 +772,9 @@ INPUT = main.dox \
regressionTesting.dox \ regressionTesting.dox \
native_libs.dox \ native_libs.dox \
modDevPython.dox \ modDevPython.dox \
modFileIngestTutorial.dox \
modDSIngestTutorial.dox \
modReportModuleTutorial.dox \
debugTsk.dox \ debugTsk.dox \
../../Core/src \ ../../Core/src \
../../CoreLibs/src \ ../../CoreLibs/src \
@ -867,7 +870,7 @@ EXAMPLE_RECURSIVE = NO
# that contain images that are to be included in the documentation (see the # that contain images that are to be included in the documentation (see the
# \image command). # \image command).
IMAGE_PATH = . IMAGE_PATH = images/
# The INPUT_FILTER tag can be used to specify a program that doxygen should # The INPUT_FILTER tag can be used to specify a program that doxygen should
# invoke to filter for each input file. Doxygen will invoke the filter program # invoke to filter for each input file. Doxygen will invoke the filter program

View File

@ -1,5 +1,5 @@
<hr/> <hr/>
<p><i>Copyright &#169; 2012-2018 Basis Technology. Generated on: $date<br/> <p><i>Copyright &#169; 2012-2019 Basis Technology. Generated on: $date<br/>
This work is licensed under a This work is licensed under a
<a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/us/">Creative Commons Attribution-Share Alike 3.0 United States License</a>. <a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/us/">Creative Commons Attribution-Share Alike 3.0 United States License</a>.
</i></p> </i></p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 233 KiB

View File

@ -9,9 +9,9 @@ If these pages don't answer your question, then send the question to the <a href
<h3>Writing Python or Java Modules</h3> <h3>Writing Python or Java Modules</h3>
If you want to write Java or Python modules, then there are some tutorials and detailed pages in this document. The Python tutorials include: If you want to write Java or Python modules, then there are some tutorials and detailed pages in this document. The Python tutorials include:
- File Ingest Modules: http://www.basistech.com/python-autopsy-module-tutorial-1-the-file-ingest-module/ - File Ingest Modules: \subpage mod_python_file_ingest_tutorial_page
- Data Source Ingest Modules: http://www.basistech.com/python-autopsy-module-tutorial-2-the-data-source-ingest-module/ - Data Source Ingest Modules: \subpage mod_python_ds_ingest_tutorial_page
- Report Modules: http://www.basistech.com/python-autopsy-module-tutorial-3-the-report-module/ - Report Modules: \subpage mod_python_report_tutorial_page
This document contains the following pages: This document contains the following pages:
- \subpage platform_page - \subpage platform_page

View File

@ -0,0 +1,171 @@
/*! \page mod_python_ds_ingest_tutorial_page Python Tutorial #2: Writing a Data Source Ingest Module
In the \ref mod_python_file_ingest_tutorial_page "first tutorial" we built a basic Python Autopsy module that looked for big and round files. In this tutorial we're going to make two data source ingest modules. The first focuses on finding SQLite databases and parsing them, and the second focuses on running a command line tool on a disk image.
The main difference from the first tutorial, which focused on file ingest modules, is that these are data source ingest modules. Data source-ingest modules are given a reference to a data source and the module needs to find the files to analyze, whereas file-level ingest modules are given a reference to each file in the data source.
\section python_tutorial2_assumptions Assumptions
This post assumes you've read the \ref mod_python_file_ingest_tutorial_page "first tutorial". That means that you should know why it is better to write an Autopsy module versus a stand-alone tool, and what you need to set up (Autopsy installed, text editor, etc.). You may also recall the limitations (and benefits) of data source ingest modules. The most notable difference between them is that data source-ingest modules may not have access to carved files or files that are inside of ZIP files. For our example in this post, we are looking for a SQLite database with a specific name, and it will not be inside of a ZIP file, so data source ingest modules are the most efficient and will get us results faster.
The other assumption is that you know something about SQL queries. We have some example queries below and we don't go into detail about how they work.
\section python_tutorial2_getting_started Getting Started
\subsection python_tutorial2_folder Making Your Module Folder
We'll start by making our module folder. As we learned in the \ref mod_python_file_ingest_tutorial_page "first tutorial", every Python module in Autopsy gets its own folder. To find out where you should put your Python module, launch Autopsy and choose the Tools->Python Plugins menu item. That will open a subfolder in your AppData folder, such as "C:\Users\JDoe\AppData\Roaming\Autopsy\python_modules".
Make a folder inside of there to store your module. Call it "DemoScript2". Copy the <a href="https://github.com/sleuthkit/autopsy/blob/develop/pythonExamples/dataSourceIngestModule.py" target="_blank" rel="noopener noreferrer">dataSourcengestModule.py</a> sample file from github into the this new folder and rename it to FindContactsDb.py.
\subsection python_tutorial2_script Writing The Script
We are going to write a script that:
<ul>
<li>Queries the backend database for files of a given name</li>
<li>Opens the database</li>
<li>Queries data from the database and makes an artifact for each row</li>
</ul>
Open the FindContactsDb.py script in your favorite text editor. The sample Autopsy Python modules all have TODO entries in them to let you know what you should change. The below steps jump from one TODO to the next.
<ol>
<li><strong>Factory Class Name</strong>: The first thing to do is rename the sample class name from "SampleJythonDataSourceIngestModuleFactory" to "ContactsDbIngestModuleFactory". In the sample module, there are several uses of this class name, so you should search and replace for these strings.</li>
<li><strong>Name and Description</strong>: The next TODO entries are for names and descriptions. These are shown to users. For this example, we'll name it "Contacts Db Analyzer". The description can be anything you want. Note that Autopsy requires that modules have unique names, so don't make it too generic.</li>
<li><strong>Ingest Module Class Name</strong>: The next thing to do is rename the ingest module class from "SampleJythonDataSourceIngestModule" to "ContactsDbIngestModule". Our usual naming convention is that this class is the same as the factory class with "Factory" removed from the end. There are a couple of places where this name is used, so do a search and replace in your code.</li>
<li><strong>startUp() method</strong>: The startUp() method is where each module initializes. For our example, we don't need to do anything special in here except save a reference to the passed in context object. This is used later on to see if the module has been cancelled.</li>
<li><strong>process() method</strong>: This is where we do our analysis and we'll focus on this more in the next section.</li>
</ol>
That's it. In the file-level ingest module, we had a shutdown() method, but we do not need that with data source-level ingest modules. When their process method is finished, it can shut itself down. The process() method will be called only once.
\subsection python_tutorial2_process The process() Method
The process method in a data source-level ingest module is passed in reference to the data source as a <a href="https://www.sleuthkit.org/sleuthkit/docs/jni-docs/interfaceorg_1_1sleuthkit_1_1datamodel_1_1_content.html" target="_blank" rel="noopener noreferrer">Content</a> object and a <a href="https://sleuthkit.org/autopsy/docs/api-docs/3.1/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_data_source_ingest_module_progress.html" target="_blank" rel="noopener noreferrer">Progress Bar</a> class to update our progress.</p>
<p>For this tutorial, you can start by deleting the contents of the existing process() method in the sample module. The full source code is linked to at the end of this blog and shows more detail about a fully fledged module. We'll just cover the analytics in the blog.</p>
\subsubsection python_tutorial2_getting_files Getting Files
Because data source-level ingest modules are not passed in specific files to analyze, nearly all of these types of modules will need to use the org.sleuthkit.autopsy.casemodule.services.FileManager service to find relevant files. Check out the methods on that class to see the different ways that you can find files.
NOTE: See the \ref python_tutorial2_running_exes section for an example of when you simply want to run a command line tool on a disk image instead of querying for files to analyze.
For our example, we want to find all files named "contacts.db". The org.sleuthkit.autopsy.casemodule.services.FileManager class contains several findFiles() methods to help. You can search for all files with a given name or files with a given name in a particular folder. You can also use SQL syntax to match file patterns, such as "%.jpg" to find all files with a JPEG extension.
Our example needs these two lines to get the FileManager for the current case and to find the files.
\verbatim
fileManager = Case.getCurrentCase().getServices().getFileManager()
files = fileManager.findFiles(dataSource, "contacts.db")\endverbatim
findFiles() returns a list of <a href="https://sleuthkit.org/sleuthkit/docs/jni-docs/classorg_1_1sleuthkit_1_1datamodel_1_1_abstract_file.html">AbstractFile</a> objects. This gives you access to the file's metadata and content.
For our example, we are going to open these SQLite files. That means that we need to save them to disk. This is less than ideal because it wastes time writing the data to disk and then reading it back in, but it is the only option with many libraries. If you are doing some other type analysis on the content, then you do not need to write it to disk. You can read directly from the AbstractFile (see the sample modules for specific code to do this).
The org.sleuthkit.autopsy.datamodel.ContentUtils class provides a utility to save file content to disk. We'll make a path in the temp folder of our case directory. To prevent naming collisions, we'll name the file based on its unique ID. The following two lines save the file to lclDbPath.
\verbatim
lclDbPath = os.path.join(Case.getCurrentCase().getTempDirectory(), str(file.getId()) + ".db")
ContentUtils.writeToFile(file, File(lclDbPath))\endverbatim
\subsubsection python_tutorial2_analyzing_sqlite Analyzing SQLite
Next, we need to open the SQLite database. We are going to use the Java JDBC infrastructure for this. JDBC is Java's generic way of dealing with different types of databases. To open the database, we do this:
\verbatim
Class.forName("org.sqlite.JDBC").newInstance()
dbConn = DriverManager.getConnection("jdbc:sqlite:%s" % lclDbPath)\endverbatim
With our connection in hand, we can do some queries. In our sample database, we have a single table named "contacts", which has columns for name, email, and phone. We first start by querying for all rows in our simple table:
\verbatim
stmt = dbConn.createStatement()
resultSet = stmt.executeQuery("SELECT * FROM contacts")\endverbatim
For each row, we are going to get the values for the name, e-mail, and phone number and make a TSK_CONTACT artifact. Recall from the first tutorial that posting artifacts to the blackboard allows modules to communicate with each other and also allows you to easily display data to the user. The TSK_CONTACT artifact is for storing contact information.
The basic approach in our example is to make an artifact of a given type (TSK_CONTACT) and have it be associated with the database it came from. We then make attributes for the name, email, and phone. The following code does this for each row in the database:
\verbatim
while resultSet.next():
# Make an artifact on the blackboard and give it attributes
art = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT)
name = resultSet.getString("name")
art.addAttribute(BlackboardAttribute(
BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME_PERSON.getTypeID(),
ContactsDbIngestModuleFactory.moduleName, name))
email = resultSet.getString("email")
art.addAttribute(BlackboardAttribute(
BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL.getTypeID(),
ContactsDbIngestModuleFactory.moduleName, email))
phone = resultSet.getString("phone")
art.addAttribute(BlackboardAttribute(
BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER.getTypeID(),
ContactsDbIngestModuleFactory.moduleName, phone))\endverbatim
That's it. We've just found the databases, queried them, and made artifacts for the user to see. There are some final things though. First, we should fire off an event so that the UI updates and refreshes with the new artifacts. We can fire just one event after each database is parsed (or you could fire one for each artifact - it's up to you).
\verbatim
IngestServices.getInstance().fireModuleDataEvent(
ModuleDataEvent(ContactsDbIngestModuleFactory.moduleName,
BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT, None))\endverbatim
And the final thing is to clean up. We should close the database connections and delete our temporary file.
\verbatim
stmt.close()
dbConn.close()
os.remove(lclDbPath)\endverbatim
\subsection python_tutorial2_niceties Niceties
Data source-level ingest modules can run for quite some time. Therefore, data source-level ingest modules should do some additional things that file-level ingest modules do not need to.
<ul>
<li>Progress bars: Each data source-level ingest module will have its own progress bar in the lower right. A reference to it is passed into the process() method. You should update it to provide user feedback.</li>
<li>Cancellation: A user could cancel ingest while your module is running. You should periodically check if that occurred so that you can bail out as soon as possible. You can do that with a check of:
\verbatim if self.context.isJobCancelled():\endverbatim </li>
</ul>
\subsection python_tutorial2_tips Debugging and Development Tips
You can find the full file along with a small sample database on <a href="https://github.com/sleuthkit/autopsy/tree/develop/pythonExamples/Aug2015DataSourceTutorial">github</a>. To use the database, add it as a logical file and run your module on it.
Whenever you have syntax errors or other errors in your script, you will get some form of dialog from Autopsy when you try to run ingest modules. If that happens, fix the problem and run ingest modules again. You don't need to restart Autopsy each time!
The sample module has some log statements in there to help debug what is going on since we don't know of better ways to debug the scripts while running in Autopsy.
\section python_tutorial2_running_exes Running Executables
While the above example outlined using the FileManager to find files to analyze, the other common use of data source-level ingest modules is to wrap a command line tool that takes a disk image as input. A sample program (RunExe.py) that does that can be found on <a href="https://github.com/sleuthkit/autopsy/tree/develop/pythonExamples/Aug2015DataSourceTutorial">github</a>. I'll cover the big topics of that program in this section. There are more details in the script about error checking and such.
\subsection python_tutorial2_finding_exe Finding The Executable
To write this kind of data source-level ingest module, put the executable in your module's folder (the DemoScript2 folder we previously made). Use "__file__" to get the path to where your script is and then use some os.path methods to get to the executable in the same folder.
\verbatim
path_to_exe = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img_stat.exe")\endverbatim
In our sample program, we do this and verify we can find it in the startup() method so that if we don't, then ingest never starts.
\subsection python_tutorial2_running_the_exe Running The Executable
Data sources can be disk images, but they can also be a folder of files. We only want to run our executable on a disk image. So, verify that:
\verbatim
if not isinstance(dataSource, Image):
self.log(Level.INFO, "Ignoring data source. Not an image")
return IngestModule.ProcessResult.OK \endverbatim
You can get the path to the disk image using dataSource.getPaths().
Once you have the EXE and the disk image, you can use the various <a href="https://pymotw.com/2/subprocess/">subprocess</a> methods to run them.
\subsection python_tutorial2_showing_results Showing the User Results
After the command line tool runs, you have the option of either showing the user the raw output of the tool or parsing it into individual artifacts. Refer to previous sections of this tutorial and the previous tutorial for making artifacts. If you want to simply show the user the output of the tool, then save the output to the Reports folder in the Case directory:
\verbatim
reportPath = os.path.join(Case.getCurrentCase().getCaseDirectory(),
"Reports", "img_stat-" + str(dataSource.getId()) + ".txt") \endverbatim
Then you can add the report to the case so that it shows up in the tree in the main UI panel.
\verbatim Case.getCurrentCase().addReport(reportPath, "Run EXE", "img_stat output")\endverbatim
\section python_tutorial2_conclusion Conclusion
Data source-level ingest modules allow you to query for a subset of files by name or to run on an entire disk image. This tutorial has shown an example of both use cases and shown how to use SQLite in Jython.
*/

View File

@ -15,11 +15,10 @@ Using it is very easy though in Autopsy and it allows you to access all of the J
To develop a module, you should follow this section to get your environment setup and then read the later sections on the different types of modules. To develop a module, you should follow this section to get your environment setup and then read the later sections on the different types of modules.
There are also a set of tutorials that Basis Technology published on their blog. While not as thorough as this documentation, they are an easy introduction to the general ideas. There are also a set of tutorials that provide an easy introduction to the general ideas.
- File Ingest Modules: \subpage mod_python_file_ingest_tutorial_page
- File Ingest Modules: http://www.basistech.com/python-autopsy-module-tutorial-1-the-file-ingest-module/ - Data Source Ingest Modules: \subpage mod_python_ds_ingest_tutorial_page
- Data Source Ingest Modules: http://www.basistech.com/python-autopsy-module-tutorial-2-the-data-source-ingest-module/ - Report Modules: \subpage mod_python_report_tutorial_page
- Report Modules: http://www.basistech.com/python-autopsy-module-tutorial-3-the-report-module/
\section mod_dev_py_setup Basic Setup \section mod_dev_py_setup Basic Setup

View File

@ -0,0 +1,154 @@
/*! \page mod_python_file_ingest_tutorial_page Python Tutorial #1: Writing a File Ingest Module
\section python_tutorial1_why Why Write a File Ingest Module?
<ul>
<li>Autopsy hides the fact that a file is coming from a file system, was carved, was from inside of a ZIP file, or was part of a local file. So, you don't need to spend time supporting all of the ways that your user may want to get data to you. You just need to worry about analyzing the content.</li>
<li>Autopsy displays files automatically and can include them in reports if you use standard blackboard artifacts (described later). That means you don't need to worry about UIs and reports.</li>
<li>Autopsy gives you access to results from other modules. So, you can build on top of their results instead of duplicating them.</li>
</ul>
\section python_tutorial1_ingest_modules Ingest Modules
For our first example, we're going to write an ingest module. Ingest modules in Autopsy run on the data sources that are added to a case. When you add a disk image (or local drive or logical folder) in Autopsy, you'll be presented with a list of modules to run (such as hash lookup and keyword search).
\image html ingest-modules.PNG
Those are all ingest modules. We're going to write one of those. There are two types of ingest modules that we can build:
<ul>
<li>File Ingest Modules are the easiest to write. During their lifetime, they will get passed in each file in the data source. This includes files that are found via carving or inside of ZIP files (if those modules are also enabled).</li>
<li>Data Source Ingest Modules require slightly more work because you have to query the database for the files of interest. If you only care about a small number of files, know their name, and know they won't be inside of ZIP files, then these are your best bet.</li>
</ul>
For this first tutorial, we're going to write a file ingest module. The \ref mod_python_ds_ingest_tutorial_page "second tutorial" will focus on data source ingest modules. Regardless of the type of ingest module you are writing, you will need to work with two classes:
<ul>
<li>The factory class provides Autopsy with module information such as display name and version. It also creates instances of ingest modules as needed.</li>
<li>The ingest module class will do the actual analysis. One of these will be created per thread. For file ingest modules, Autopsy will typically create two or more of these at a time so that it can analyze files in parallel. If you keep things simple, and don't use static variables, then you don't have to think about anything multithreaded.</li>
</ul>
\section python_tutorial1_getting_started Getting Started
To write your first file ingest module, you'll need:
<ul>
<li>An installed copy of Autopsy available from <a href="https://www.sleuthkit.org/autopsy/download.php" target="_blank" rel="noopener noreferrer">SleuthKit</a></li>
<li>A text editor.</li>
<li>A copy of the sample file ingest module from <a href="https://github.com/sleuthkit/autopsy/blob/autopsy-4.12.0/pythonExamples/fileIngestModule.py" target="_blank" rel="noopener noreferrer">Github</a></li>
</ul>
Some other general notes are that you will be writing in Jython, which converts Python-looking code into Java. It has some limitations, including:
<ul>
<li>You can't use Python 3 (you are limited to Python 2.7)</li>
<li>You can't use libraries that use native code</li>
</ul>
But, Jython will give you access to all of the Java classes and services that Autopsy provides. So, if you want to stray from this example, then refer to the Developer docs on what classes and methods you have access to. The comments in the sample file will identify what type of object is being passed in along with a URL to its documentation.
\subsection python_tutorial1_folder Making Your Module Folder
Every Python module in Autopsy gets its own folder. This reduces naming collisions between modules. To find out where you should put your Python module, launch Autopsy and choose the Tools -&gt; Python Plugins menu item. That will open a folder in your AppData folder, such as "C:\Users\JDoe\AppData\Roaming\Autopsy\python_modules".
<p>Make a folder inside of there to store your module. Call it "DemoScript". Copy the fileIngestModule.py sample file listed above into the this new folder and rename it to FindBigRoundFiles.py. Your folder should look like this:
\image html demoScript_folder.png
\subsection python_tutorial1_writing Writing the Script
We are going to write a script that flags any file that is larger than 10MB and whose size is a multiple of 4096. We'll call these big and round files. This kind of technique could be useful for finding encrypted files. An additional check would be for entropy of the file, but we'll keep the example simple.
Open the FindBigRoundFiles.py file in your favorite python text editor. The sample Autopsy Python modules all have TODO entries in them to let you know what you should change. The below steps jump from one TODO to the next.
<ol>
<li><b>Factory Class Name</b>: The first thing to do is rename the sample class name from "SampleJythonFileIngestModuleFactory" to "FindBigRoundFilesIngestModuleFactory". In the sample module, there are several uses of this class name, so you should search and replace for these strings.</li>
<li><b>Name and Description</b>: The next TODO entries are for names and descriptions. These are shown to users. For this example, we'll name it "Big and Round File Finder". The description can be anything you want. Note that Autopsy requires that modules have unique names, so don't make it too generic.</li>
<li><b>Ingest Module Class Name</b>: The next thing to do is rename the ingest module class from "SampleJythonFileIngestModule" to "FindBigRoundFilesIngestModule". Our usual naming convention is that this class is the same as the factory class with "Factory" removed from the end.</li>
<li><b>startUp() method</b>: The startUp() method is where each module initializes. For our example, we don't need to do anything special in here. Typically though, this is where you want to do stuff that could fail because throwing an exception here causes the entire ingest to stop.</li>
<li><b>process() method</b>: This is where we do our analysis. The sample module is well documented with what it does. It ignores non-files, looks at the file name, and makes a blackboard artifact for ".txt" files. There are also a bunch of other things that it does to show examples for easy copy and pasting, but we don't need them in our module. We'll cover what goes into this method in the next section.</li>
<li><b>shutdown() method</b>: The shutDown() method either frees resources that were allocated or sends summary messages. For our module, it will do nothing.</li>
</ol>
\subsection python_tutorial1_process The process() Method
The process() method is passed in a reference to an AbstractFile Object. With this, you have access to all of a file's contents and metadata. We want to flag files that are larger than 10MB and that are a multiple of 4096 bytes. The following code does that:
\verbatim if ((file.getSize() > 10485760) and ((file.getSize() % 4096) == 0)):
\endverbatim
Now that we have found the files, we want to do something with them. In our situation, we just want to alert the user to them. We do this by making an "Interesting Item" blackboard artifact. The <a href="https://sleuthkit.org/sleuthkit/docs/jni-docs/mod_bbpage.html" target="_blank" rel="noopener noreferrer">Blackboard</a> is where ingest modules can communicate with each other and with the Autopsy GUI. The blackboard has a set of artifacts on it and each artifact:</p>
<ul>
<li>Has a type</li>
<li>Is associated with a file</li>
<li>Has one or more attributes. Attributes are simply name and value pairs.</li>
</ul>
For our example, we are going to make an artifact of type "TSK_INTERESTING_FILE" whenever we find a big and round file. These are one of the most generic artifact types and are simply a way of alerting the user that a file is interesting for some reason. Once you make the artifact, it will be shown in the UI. The below code makes an artifact for the file and puts it into the set of "Big and Round Files". You can create whatever set names you want. The Autopsy GUI organizes Interesting Files by their set name.
\verbatim
art = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT)
att = BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID(),
FindBigRoundFilesIngestModuleFactory.moduleName, "Big and Round Files")
art.addAttribute(att)\endverbatim
The above code adds the artifact and a single attribute to the blackboard in the embedded database, but it does not notify other modules or the UI. The UI will eventually refresh, but it is faster to fire an event with this:
\verbatim
IngestServices.getInstance().fireModuleDataEvent(
ModuleDataEvent(FindBigRoundFilesIngestModuleFactory.moduleName,
BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT, None))\endverbatim
That's it. Your process() method should look something like this:
\verbatim
def process(self, file):
# Skip non-files
if ((file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) or
(file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS) or
(file.isFile() == False)):
return IngestModule.ProcessResult.OK
# Look for files bigger than 10MB that are a multiple of 4096
if ((file.getSize() &gt; 10485760) and ((file.getSize() % 4096) == 0)):
# Make an artifact on the blackboard. TSK_INTERESTING_FILE_HIT is a generic type of
# artifact. Refer to the developer docs for other examples.
art = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT)
att = BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID(),
FindBigRoundFilesIngestModuleFactory.moduleName, "Big and Round Files")
art.addAttribute(att)
# Fire an event to notify the UI and others that there is a new artifact
IngestServices.getInstance().fireModuleDataEvent(
ModuleDataEvent(FindBigRoundFilesIngestModuleFactory.moduleName,
BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT, None))
return IngestModule.ProcessResult.OK\endverbatim
Save this file and run the module on some of your data. If you have any big and round files, you should see an entry under the "Interesting Items" node in the tree.
\image html bigAndRoundFiles.png
\subsection python_tutorial1_debug Debugging and Development Tips
Whenever you have syntax errors or other errors in your script, you will get some form of dialog from Autopsy when you try to run ingest modules. If that happens, fix the problem and run ingest modules again. You don't need to restart Autopsy each time!
The sample module has some log statements in there to help debug what is going on since we don't know of better ways to debug the scripts while running in Autopsy.
*/

View File

@ -0,0 +1,123 @@
/*! \page mod_python_report_tutorial_page Python Tutorial #3: Writing a Report Module
In our last two tutorials, we built a Python Autopsy \ref mod_python_file_ingest_tutorial_page "file ingest modules" and \ref mod_python_ds_ingest_tutorial_page "data source ingest modules" that analyzed the data sources as they were added to cases. In our third post, we're going to make an entirely different kind of module, a report module.
Report modules are typically run after the user has completed their analysis. Autopsy comes with report modules to generate HTML, Excel, KML, and other types of reports. We're going to make a report module that outputs data in CSV.
Like in the second tutorial, we are going to assume that you've read at least the \ref mod_python_file_ingest_tutorial_page "first tutorial" to know how to get your environment set up. As a reminder, Python modules in Autopsy are written in Jython and have access to all of the Java classes (which is why we have links to Java documentation below).
\section python_tutorial3_report_modules Report Modules
Autopsy report modules are often run after the user has run some ingest modules, reviewed the results, and tagged some files of interest. The user will be given a list of report modules to choose from.
\image html reports_select.png
The main reasons for writing an Autopsy report module are:
<ul>
<li>You need the results in a custom output format, such as XML or JSON.</li>
<li>You want to upload results to a central location.</li>
<li>You want to perform additional analysis after all ingest modules have run. While the modules have the word "report" in them, there is no actual requirement that they produce a report or export data. The module can simply perform data analysis and post artifacts to the blackboard like ingest modules do.</li>
</ul>
As we dive into the details, you will notice that the report module API is fairly generic. This is because reports are created at a case level, not a data source level. So, when a user chooses to run a report module, all Autopsy does is tell it to run and gives it a path to a directory to store its results in. The report module can store whatever it wants in the directory.
Note that if you look at the \ref mod_report_page "full developer docs", there are other report module types that are supported in Java. These are not supported though in Python.
\subsection python_tutorial3_getting_content Getting Content
With report modules, it is up to you to find the content that you want to include in your report or analysis. Generally, you will want to access some or all of the files, tagged files, or blackboard artifacts. As you may recall from the previous tutorials, blackboard artifacts are how ingest modules in Autopsy store their results so that they can be shown in the UI, used by other modules, and included in the final report. In this tutorial, we will introduce the <a href="https://sleuthkit.org/sleuthkit/docs/jni-docs/classorg_1_1sleuthkit_1_1datamodel_1_1_sleuthkit_case.html">SleuthkitCase</a> class, which we generally don't introduce to module writers because it has lots of methods, many of which are low-level, and there are other classes, such as FileManager, that are more focused and easier to use.
\subsubsection python_tutorial3_getting_files Getting Files
You have three choices for getting files to report on. You can use the FileManager, which we used in \ref mod_python_ds_ingest_tutorial_page "the last Data Source-level Ingest Module tutorial". The only change is that you will need to call it multiple times, one for each data source in the case. You will have code that looks something like this:
\verbatim
dataSources = Case.getCurrentCase().getDataSources()
fileManager = Case.getCurrentCase().getServices().getFileManager()
for dataSource in dataSources:
files = fileManager.findFiles(dataSource, "%.txt")\endverbatim
Another approach is to use the <a href="https://sleuthkit.org/sleuthkit/docs/jni-docs/classorg_1_1sleuthkit_1_1datamodel_1_1_sleuthkit_case.html#a6b14c6b82bbc1cf71aa108f9e5c5ccc1">SleuthkitCase.findAllFilesWhere()</a> method that allows you to specify a SQL query. To use this method, you must know the schema of the database (which makes this a bit more challenging, but more powerful). The schema is defined on the <a href="https://wiki.sleuthkit.org/index.php?title=SQLite_Database_v3_Schema">wiki</a>.
Usually, you just need to focus on the <a href="https://wiki.sleuthkit.org/index.php?title=SQLite_Database_v3_Schema#tsk_files">tsk_files</a> table. You may run into memory problems and you can also use <a href="https://sleuthkit.org/sleuthkit/docs/jni-docs/classorg_1_1sleuthkit_1_1datamodel_1_1_sleuthkit_case.html#a2faec4e68be17f67db298a4ed3933bc3">SleuthkitCase.findAllFileIdsWhere()</a> to get just the IDs and then call <a href="https://sleuthkit.org/sleuthkit/docs/jni-docs/classorg_1_1sleuthkit_1_1datamodel_1_1_sleuthkit_case.html#a8cdd6582b18e9bfa814cffed8302e4b9">SleuthkitCase.getAbstractFileById()</a> to get files as needed.
A third approach is to call org.sleuthkit.autopsy.casemodule.Case.getDataSources(), and then recursively call getChildren() on each Content object. This will traverse all of the folders and files in the case. This is the most memory efficient, but also more complex to code.
\subsubsection python_tutorial3_getting_artifacts Getting Blackboard Artifacts
The blackboard is where modules store their analysis results. If you want to include them in your report, then there are several methods that you could use. If you want all artifacts of a given type, then you can use <a href="https://sleuthkit.org/sleuthkit/docs/jni-docs/classorg_1_1sleuthkit_1_1datamodel_1_1_sleuthkit_case.html#a0b8396fac6c40d8291cc48732dd15d74">SleuthkitCase.getBlackboardArtifacts()</a>. There are many variations of this method that take different arguments. Look at them to find the one that is most convenient for you.
\subsubsection python_tutorial3_getting_tags Getting Tagged Files or Artifacts
If you want to find files or artifacts that are tagged, then you can use the org.sleuthkit.autopsy.casemodule.services.TagsManager. It has methods to get all tags of a given name, such as org.sleuthkit.autopsy.casemodule.services.TagsManager.getContentTagsByTagName().
\section python_tutorial3_getting_started Getting Started
\subsection python_tutorial3_making_the_folder Making the Folder
We'll start by making our module folder. As we learned in \ref mod_python_file_ingest_tutorial_page "the first tutorial", every Python module in Autopsy gets its own folder. To find out where you should put your Python module, launch Autopsy and choose the Tools->Python Plugins menu item. That will open a subfolder in your AppData folder, such as "C:\Users\JDoe\AppData\Roaming\Autopsy\python_modules".
Make a folder inside of there to store your module. Call it "DemoScript3". Copy the <a href="https://github.com/sleuthkit/autopsy/blob/develop/pythonExamples/reportmodule.py">reportmodule.py</a> sample file into the this new folder and rename it to CSVReport.py.
\subsection python_tutorial3_writing_script Writing the Script
We are going to write a script that makes some basic CSV output: file name and MD5 hash. Open the CSVReport.py file in your favorite Python text editor. The sample Autopsy Python modules all have TODO entries in them to let you know what you should change. The below steps jump from one TODO to the next.
<ol>
<li>Factory Class Name: The first thing to do is rename the sample class name from "SampleGeneralReportModule" to "CSVReportModule". In the sample module, there are several uses of this class name, so you should search and replace for these strings.</li>
<li>Name and Description: The next TODO entries are for names and descriptions. These are shown to users. For this example, we'll name it "CSV Hash Report Module". The description can be anything you want. Note that Autopsy requires that modules have unique names, so don't make it too generic.</li>
<li>Relative File Path: The next step is to specify the filename that your module is going to use for the report. Autopsy will later provide you with a folder name to save your report in. If you have multiple file names, then pick the main one. This path will be shown to the user after the report has been generated so that they can open it. For this example, we'll call it "hashes.csv" in the getRelativeFilePath() method.</li>
<li>generateReport() Method: This method is what is called when the user wants to run the module. It gets passed in the base directory to store the results in and a progress bar. It is responsible for making the report and calling Case.addReport() so that it will be shown in the tree. We'll cover the details of this method in a later section.</li>
</ol>
\subsection python_tutorial3_generate_report The generateReport() method
The generateReport() method is where the work is done. The baseReportDir argument is a string for the base directory to store results in. The progressBar argument is a org.sleuthkit.autopsy.report.ReportProgressPanel
that shows the user progress while making long reports and to make the progress bar red if an error occurs.
We'll use one of the basic ideas from the sample, so you can copy and paste from that as you see fit to make this method. Our general approach is going to be this:
<ol>
<li>Open the CSV file.</li>
<li>Query for all files.</li>
<li>Cycle through each of the files and print a line of text.</li>
<li>Add the report to the Case database.</li>
</ol>
To focus on the essential code, we'll skip the progress bar details. However, the final solution that we'll link to at the end contains the progress bar code.
To open the report file in the right folder, we'll need a line such as this:
\verbatim
fileName = os.path.join(baseReportDir, self.getRelativeFilePath())
report = open(fileName, 'w')\endverbatim
Next we need to query for the files. In our case, we want all of the files, but can skip the directories. We'll use lines such as this to get the current case and then call the SleuthkitCase.findAllFilesWhere() method.
\verbatim
sleuthkitCase = Case.getCurrentCase().getSleuthkitCase()
files = sleuthkitCase.findAllFilesWhere("NOT meta_type = " +
str(TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue()))\endverbatim
Now, we want to print a line for each file. To do this, you'll need something like:
\verbatim
for file in files:
md5 = file.getMd5Hash()
if md5 is None:
md5 = ""
report.write(file.getParentPath() + file.getName() + "," + md5 + "n")\endverbatim
Note that the file will only have an MD5 value if the Hash Lookup ingest module was run on the data source.
Lastly, we want to add the report to the case database so that the user can later find it from the tree and we want to report that we completed successfully.
\verbatim
Case.getCurrentCase().addReport(fileName, self.moduleName, "Hashes CSV")
progressBar.complete(ReportStatus.COMPLETE)\endverbatim
That's it. The final code can be found <a href="https://github.com/sleuthkit/autopsy/tree/develop/pythonExamples/Sept2015ReportTutorial_CSV">on github</a>.
\subsection python_tutorial3_conclusions Conclusions
In this tutorial, we made a basic report module that creates a custom CSV file. The most challenging part of writing a report module is knowing how to get all of the data that you need. Hopefully, the \ref python_tutorial3_getting_content section above covered what you need, but if not, then go on the <a href="https://sleuthkit.discourse.group/">Sleuthkit forum</a> and we'll try to point you in the right direction.</p>
*/

View File

@ -83,7 +83,7 @@ class EmailMessage {
void setSubject(String subject) { void setSubject(String subject) {
if (subject != null) { if (subject != null) {
this.subject = subject; this.subject = subject;
if(subject.matches("^[R|r][E|e].*?:.*")) { if (subject.matches("^[R|r][E|e].*?:.*")) {
this.simplifiedSubject = subject.replaceAll("[R|r][E|e].*?:", "").trim(); this.simplifiedSubject = subject.replaceAll("[R|r][E|e].*?:", "").trim();
replySubject = true; replySubject = true;
} else { } else {
@ -93,19 +93,19 @@ class EmailMessage {
this.simplifiedSubject = ""; this.simplifiedSubject = "";
} }
} }
/** /**
* Returns the orginal subject with the "RE:" stripped off". * Returns the orginal subject with the "RE:" stripped off".
* *
* @return Message subject with the "RE" stripped off * @return Message subject with the "RE" stripped off
*/ */
String getSimplifiedSubject() { String getSimplifiedSubject() {
return simplifiedSubject; return simplifiedSubject;
} }
/** /**
* Returns whether or not the message subject started with "RE:" * Returns whether or not the message subject started with "RE:"
* *
* @return true if the original subject started with RE otherwise false. * @return true if the original subject started with RE otherwise false.
*/ */
boolean isReplySubject() { boolean isReplySubject() {
@ -121,6 +121,7 @@ class EmailMessage {
this.headers = headers; this.headers = headers;
} }
} }
String getTextBody() { String getTextBody() {
return textBody; return textBody;
} }
@ -211,75 +212,80 @@ class EmailMessage {
this.localPath = localPath; this.localPath = localPath;
} }
} }
/** /**
* Returns the value of the Message-ID header field of this message or * Returns the value of the Message-ID header field of this message or empty
* empty string if it is not present. * string if it is not present.
* *
* @return the identifier of this message. * @return the identifier of this message.
*/ */
String getMessageID() { String getMessageID() {
return messageID; return messageID;
} }
/** /**
* Sets the identifier of this message. * Sets the identifier of this message.
* *
* @param messageID identifer of this message * @param messageID identifer of this message
*/ */
void setMessageID(String messageID) { void setMessageID(String messageID) {
this.messageID = messageID; if (messageID != null) {
this.messageID = messageID;
} else {
this.messageID = "";
}
} }
/** /**
* Returns the messageID of the parent message or empty String if not present. * Returns the messageID of the parent message or empty String if not
* * present.
*
* @return the idenifier of the message parent * @return the idenifier of the message parent
*/ */
String getInReplyToID() { String getInReplyToID() {
return inReplyToID; return inReplyToID;
} }
/** /**
* Sets the messageID of the parent message. * Sets the messageID of the parent message.
* *
* @param inReplyToID messageID of the parent message. * @param inReplyToID messageID of the parent message.
*/ */
void setInReplyToID(String inReplyToID) { void setInReplyToID(String inReplyToID) {
this.inReplyToID = inReplyToID; this.inReplyToID = inReplyToID;
} }
/** /**
* Returns a list of Message-IDs listing the parent, grandparent, * Returns a list of Message-IDs listing the parent, grandparent,
* great-grandparent, and so on, of this message. * great-grandparent, and so on, of this message.
* *
* @return The reference list or empty string if none is available. * @return The reference list or empty string if none is available.
*/ */
List<String> getReferences() { List<String> getReferences() {
return references; return references;
} }
/** /**
* Set the list of reference message-IDs from the email message header. * Set the list of reference message-IDs from the email message header.
* *
* @param references * @param references
*/ */
void setReferences(List<String> references) { void setReferences(List<String> references) {
this.references = references; this.references = references;
} }
/** /**
* Sets the ThreadID of this message. * Sets the ThreadID of this message.
* *
* @param threadID - the thread ID to set * @param threadID - the thread ID to set
*/ */
void setMessageThreadID(String threadID) { void setMessageThreadID(String threadID) {
this.messageThreadID = threadID; this.messageThreadID = threadID;
} }
/** /**
* Returns the ThreadID for this message. * Returns the ThreadID for this message.
* *
* @return - the message thread ID or "" is non is available * @return - the message thread ID or "" is non is available
*/ */
String getMessageThreadID() { String getMessageThreadID() {
@ -308,7 +314,7 @@ class EmailMessage {
private long aTime = 0L; private long aTime = 0L;
private long mTime = 0L; private long mTime = 0L;
private TskData.EncodingType encodingType = TskData.EncodingType.NONE; private TskData.EncodingType encodingType = TskData.EncodingType.NONE;
String getName() { String getName() {
@ -394,14 +400,14 @@ class EmailMessage {
this.mTime = mTime.getTime() / 1000; this.mTime = mTime.getTime() / 1000;
} }
} }
void setEncodingType(TskData.EncodingType encodingType){ void setEncodingType(TskData.EncodingType encodingType) {
this.encodingType = encodingType; this.encodingType = encodingType;
} }
TskData.EncodingType getEncodingType(){ TskData.EncodingType getEncodingType() {
return encodingType; return encodingType;
} }
} }
} }