diff --git a/Core/build.xml b/Core/build.xml index 28e64b83e5..0e5c90ef04 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -137,7 +137,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java index a6494fe22b..76b56138d7 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java @@ -162,6 +162,8 @@ class CaseInformationPanel extends javax.swing.JPanel { editCasePropertiesDialog.setResizable(true); editCasePropertiesDialog.pack(); editCasePropertiesDialog.setLocationRelativeTo(this); + // Workaround to ensure dialog is not hidden on macOS + editCasePropertiesDialog.setAlwaysOnTop(true); editCasePropertiesDialog.setVisible(true); editCasePropertiesDialog.toFront(); caseDetailsPanel.updateCaseInfo(); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java index cc07148ba0..0ba92c7bce 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.casemodule; +import java.awt.Component; import java.awt.Cursor; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -84,10 +85,17 @@ public final class CaseOpenAction extends CallableSystemAction implements Action fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); fileChooser.setMultiSelectionEnabled(false); fileChooser.setFileFilter(caseMetadataFileFilter); + if (null != ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE)) { 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 optionsDlgMessage = NbBundle.getMessage(Case.class, "CloseCaseWhileIngesting.Warning"); 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 * 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) { /* * 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. */ - OpenMultiUserCaseDialog.getInstance().setVisible(false); + multiUserCaseDialog.setVisible(false); /* * 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(); multiUserCaseWindow.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); + // Workaround to ensure that dialog is not hidden on macOS. + multiUserCaseWindow.setAlwaysOnTop(true); multiUserCaseWindow.setVisible(true); WindowManager.getDefault().getMainWindow().setCursor(null); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java index 3ddb97fcfd..37c6c7c8b8 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java @@ -249,6 +249,8 @@ public class CueBannerPanel extends javax.swing.JPanel { private void openRecentCaseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openRecentCaseButtonActionPerformed recentCasesWindow.setLocationRelativeTo(this); OpenRecentCasePanel.getInstance(); //refreshes the recent cases table + // Workaround to ensure that dialog is not hidden on macOS. + recentCasesWindow.setAlwaysOnTop(true); recentCasesWindow.setVisible(true); }//GEN-LAST:event_openRecentCaseButtonActionPerformed diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java index 50688b1ac1..c5e6ece78d 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java @@ -71,6 +71,8 @@ final class NewCaseWizardAction extends CallableSystemAction { wizardDescriptor.setTitleFormat(new MessageFormat("{0}")); wizardDescriptor.setTitle(NbBundle.getMessage(this.getClass(), "NewCaseWizardAction.newCase.windowTitle.text")); Dialog dialog = DialogDisplayer.getDefault().createDialog(wizardDescriptor); + // Workaround to ensure new case dialog is not hidden on macOS + dialog.setAlwaysOnTop(true); dialog.setVisible(true); dialog.toFront(); if (wizardDescriptor.getValue() == WizardDescriptor.FINISH_OPTION) { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java index 13aae1c0de..8bec55f53c 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java @@ -144,4 +144,13 @@ public class StartupWindowProvider implements StartupWindowInterface { startupWindowToUse.close(); } } + + /** + * Get the chosen startup window. + * + * @return The startup window. + */ + public StartupWindowInterface getStartupWindow() { + return startupWindowToUse; + } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form index 605ed93697..d8433a907b 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form @@ -16,8 +16,8 @@ - + @@ -106,6 +106,9 @@ + + + @@ -172,7 +175,7 @@ - + @@ -192,6 +195,9 @@ + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java index dec8bf55b4..1f02708f64 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -19,8 +19,16 @@ package org.sleuthkit.autopsy.contentviewers; 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.ActionListener; +import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -30,6 +38,7 @@ import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import javax.swing.BoxLayout; @@ -52,8 +61,12 @@ import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskData; 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.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; @@ -189,12 +202,20 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie 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 */ public MediaPlayerPanel() throws GstException, UnsatisfiedLinkError { initComponents(); customizeComponents(); + //True for fairness. In other words, + //acquire() calls are processed in order of invocation. + sliderLock = new Semaphore(1, true); } private void customizeComponents() { @@ -532,6 +553,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override public void actionPerformed(ActionEvent e) { if (!progressSlider.getValueIsAdjusting()) { + sliderLock.acquireUninterruptibly(); long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); /** @@ -547,6 +569,210 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie 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 b 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(); } } } @@ -592,6 +818,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie progressSlider.setDoubleBuffered(true); progressSlider.setMinimumSize(new java.awt.Dimension(36, 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(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.progressLabel.text")); // NOI18N @@ -645,7 +872,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie gridBagConstraints.ipadx = 8; gridBagConstraints.ipady = 7; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; - gridBagConstraints.insets = new java.awt.Insets(6, 6, 0, 0); + gridBagConstraints.insets = new java.awt.Insets(6, 14, 0, 0); buttonPanel.add(VolumeIcon, gridBagConstraints); audioSlider.setMajorTickSpacing(10); @@ -655,6 +882,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie audioSlider.setValue(25); audioSlider.setMinimumSize(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; @@ -739,8 +967,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie this.setLayout(layout); layout.setHorizontalGroup( 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(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java index 24f54fa7ac..ea1e7b58a0 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2018 Basis Technology Corp. + * + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,32 +23,32 @@ import org.openide.nodes.Node; import org.sleuthkit.datamodel.BlackboardArtifact; /** - * Utility classes for content viewers. - * In theory, this would live in the contentviewer package, - * but the initial method was needed only be viewers in + * Utility classes 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 { + /** - * Returns the first non-Blackboard Artifact from a Node. - * Needed for (at least) Hex and Strings that want to view - * all types of content (not just AbstractFile), but don't want - * to display an artifact unless that's the only thing there. - * Scenario is hash hit or interesting item hit. - * + * Returns the first non-Blackboard Artifact from a Node. Needed for (at + * least) Hex and Strings that want to view all types of content (not just + * AbstractFile), but don't want to display an artifact unless that's the + * only thing there. Scenario is hash hit or interesting item hit. + * * @param node Node passed into content viewer + * * @return highest priority content or null if there is no content */ public static Content getDefaultContent(Node node) { Content bbContentSeen = null; - for (Content content : (node).getLookup().lookupAll(Content.class)) { + for (Content content : (node).getLookup().lookupAll(Content.class)) { if (content instanceof BlackboardArtifact) { bbContentSeen = content; - } - else { + } else { return content; } } return bbContentSeen; } + } diff --git a/InternalPythonModules/android/line.py b/InternalPythonModules/android/line.py index ab8e24c9f9..6edcf158fa 100644 --- a/InternalPythonModules/android/line.py +++ b/InternalPythonModules/android/line.py @@ -236,28 +236,23 @@ class LineCallLogsParser(TskCallLogsParser): def __init__(self, calllog_db): super(LineCallLogsParser, self).__init__(calllog_db.runQuery( """ - SELECT Substr(CH.call_type, -1) AS direction, - CH.start_time AS start_time, - CH.end_time AS end_time, - contacts_list_with_groups.members AS group_members, - contacts_list_with_groups.member_names AS names, - CH.caller_mid, - CH.voip_type AS call_type, - CH.voip_gc_media_type AS group_call_type + SELECT Substr(calls.call_type, -1) AS direction, + calls.start_time AS start_time, + calls.end_time AS end_time, + contact_book_w_groups.members AS group_members, + calls.caller_mid, + calls.voip_type AS call_type, + calls.voip_gc_media_type AS group_call_type FROM (SELECT id, - Group_concat(M.m_id) AS members, - Group_concat(Replace(C.server_name, ",", "")) AS member_names + Group_concat(M.m_id) AS members FROM membership AS M - JOIN naver.contacts AS C - ON M.m_id = C.m_id GROUP BY id UNION SELECT m_id, - NULL, - server_name - FROM naver.contacts) AS contacts_list_with_groups - JOIN call_history AS CH - ON CH.caller_mid = contacts_list_with_groups.id + NULL + FROM naver.contacts) AS contact_book_w_groups + JOIN call_history AS calls + ON calls.caller_mid = contact_book_w_groups.id """ ) ) @@ -355,43 +350,25 @@ class LineMessagesParser(TskMessagesParser): def __init__(self, message_db): super(LineMessagesParser, self).__init__(message_db.runQuery( """ - SELECT contact_list_with_groups.name, - contact_list_with_groups.id, - contact_list_with_groups.members, - contact_list_with_groups.member_names, - CH.from_mid, - C.server_name AS from_name, - CH.content, - CH.created_time, - CH.attachement_type, - CH.attachement_local_uri, - CH.status - FROM (SELECT G.name, - group_members.id, - group_members.members, - group_members.member_names - FROM (SELECT id, - group_concat(M.m_id) AS members, - group_concat(replace(C.server_name, - ",", - "")) 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 + SELECT contact_book_w_groups.id, + contact_book_w_groups.members, + messages.from_mid, + messages.content, + messages.created_time, + messages.attachement_type, + messages.attachement_local_uri, + messages.status + FROM (SELECT id, + Group_concat(M.m_id) AS members + FROM membership AS M + GROUP BY id + UNION + SELECT m_id, + NULL + FROM contacts) AS contact_book_w_groups + JOIN chat_history AS messages + ON messages.chat_id = contact_book_w_groups.id + WHERE attachement_type != 6 """ ) ) diff --git a/InternalPythonModules/android/skype.py b/InternalPythonModules/android/skype.py index 5044898d5f..d8b79ac7fe 100644 --- a/InternalPythonModules/android/skype.py +++ b/InternalPythonModules/android/skype.py @@ -76,11 +76,8 @@ class SkypeAnalyzer(general.AndroidComponentAnalyzer): 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 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. - - 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): @@ -93,7 +90,12 @@ class SkypeAnalyzer(general.AndroidComponentAnalyzer): account_query_result = skype_db.runQuery( """ 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 """ ) @@ -251,14 +253,6 @@ class SkypeCallLogsParser(TskCallLogsParser): 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: - message_type w/ value 3 appeared to be the call type, regardless of if it was audio or video. @@ -266,37 +260,23 @@ class SkypeCallLogsParser(TskCallLogsParser): """ super(SkypeCallLogsParser, self).__init__(calllog_db.runQuery( """ - SELECT contacts_list_with_groups.conversation_id, - contacts_list_with_groups.participant_ids, - contacts_list_with_groups.participants, - time, - duration, - is_sender_me, - person_id as sender_id, - sender_name.name as sender_name + SELECT contact_book_w_groups.conversation_id, + contact_book_w_groups.participant_ids, + messages.time, + messages.duration, + messages.is_sender_me, + messages.person_id AS sender_id FROM (SELECT conversation_id, - Group_concat(person_id) AS participant_ids, - Group_concat("""+_format_user_name()+""") AS participants - FROM particiapnt AS PART - JOIN person AS P - ON PART.person_id = P.entry_id + Group_concat(person_id) AS participant_ids + FROM particiapnt GROUP BY conversation_id UNION - SELECT entry_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 + SELECT entry_id AS conversation_id, + NULL + FROM person) AS contact_book_w_groups + join chatitem AS messages + ON messages.conversation_link = contact_book_w_groups.conversation_id + WHERE message_type == 3 """ ) ) @@ -347,7 +327,12 @@ class SkypeContactsParser(TskContactsParser): super(SkypeContactsParser, self).__init__(contact_db.runQuery( """ 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 """ ) @@ -379,39 +364,25 @@ class SkypeMessagesParser(TskMessagesParser): """ super(SkypeMessagesParser, self).__init__(message_db.runQuery( """ - SELECT contacts_list_with_groups.conversation_id, - contacts_list_with_groups.participant_ids, - contacts_list_with_groups.participants, - time, - content, - device_gallery_path, - is_sender_me, - person_id as sender_id, - sender_name.name AS sender_name - FROM (SELECT conversation_id, - Group_concat(person_id) AS participant_ids, - Group_concat("""+_format_user_name()+""") AS participants - FROM particiapnt AS PART - JOIN person AS P - ON PART.person_id = P.entry_id - GROUP BY conversation_id - UNION - 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 + SELECT contact_book_w_groups.conversation_id, + contact_book_w_groups.participant_ids, + messages.time, + messages.content, + messages.device_gallery_path, + messages.is_sender_me, + messages.person_id as sender_id + FROM (SELECT conversation_id, + Group_concat(person_id) AS participant_ids + FROM particiapnt + GROUP BY conversation_id + UNION + SELECT entry_id as conversation_id, + NULL + FROM person) AS contact_book_w_groups + JOIN chatitem AS messages + ON messages.conversation_link = contact_book_w_groups.conversation_id WHERE message_type != 3 - """ + """ ) ) self._SKYPE_MESSAGE_TYPE = "Skype Message" @@ -469,25 +440,3 @@ class SkypeMessagesParser(TskMessagesParser): if group_ids is not None: return self.result_set.getString("conversation_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 - """ - - diff --git a/InternalPythonModules/android/textnow.py b/InternalPythonModules/android/textnow.py index ad9704cece..1890c7ae42 100644 --- a/InternalPythonModules/android/textnow.py +++ b/InternalPythonModules/android/textnow.py @@ -290,53 +290,50 @@ class TextNowMessagesParser(TskMessagesParser): """ super(TextNowMessagesParser, self).__init__(message_db.runQuery( """ - - SELECT CASE - WHEN message_direction == 2 THEN "" - WHEN to_addresses IS NULL THEN M.contact_value - ELSE contact_name - end from_address, - CASE - WHEN message_direction == 1 THEN "" - WHEN to_addresses IS NULL THEN M.contact_value - ELSE to_addresses - end to_address, - message_direction, - message_text, - M.READ, - M.date, - M.attach, - thread_id - FROM (SELECT group_info.contact_value, - group_info.to_addresses, - G.contact_value AS thread_id - FROM (SELECT GM.contact_value, - Group_concat(GM.member_contact_value) AS to_addresses - FROM group_members AS GM - GROUP BY GM.contact_value) AS group_info - JOIN groups AS G - ON G.contact_value = group_info.contact_value - UNION - SELECT c.contact_value, - NULL, - "-1" - FROM contacts AS c) AS to_from_map - JOIN messages AS M - ON M.contact_value = to_from_map.contact_value - WHERE message_type NOT IN ( 102, 100 ) + SELECT CASE + WHEN messages.message_direction == 2 THEN NULL + WHEN contact_book_w_groups.to_addresses IS NULL THEN + messages.contact_value + END from_address, + CASE + WHEN messages.message_direction == 1 THEN NULL + WHEN contact_book_w_groups.to_addresses IS NULL THEN + messages.contact_value + ELSE contact_book_w_groups.to_addresses + END to_address, + messages.message_direction, + messages.message_text, + messages.READ, + messages.DATE, + messages.attach, + thread_id + FROM (SELECT GM.contact_value, + Group_concat(GM.member_contact_value) AS to_addresses, + G.contact_value AS thread_id + FROM group_members AS GM + join GROUPS AS G + ON G.contact_value = GM.contact_value + GROUP BY GM.contact_value + UNION + SELECT contact_value, + NULL, + NULL + FROM contacts) AS contact_book_w_groups + join messages + ON messages.contact_value = contact_book_w_groups.contact_value + WHERE message_type NOT IN ( 102, 100 ) """ ) ) self._TEXTNOW_MESSAGE_TYPE = "TextNow Message" self._INCOMING_MESSAGE_TYPE = 1 self._OUTGOING_MESSAGE_TYPE = 2 - self._UNKNOWN_THREAD_ID = "-1" def get_message_type(self): return self._TEXTNOW_MESSAGE_TYPE 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 self.result_set.getString("from_address") @@ -347,10 +344,9 @@ class TextNowMessagesParser(TskMessagesParser): return self.OUTGOING 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() - recipients = self.result_set.getString("to_address").split(",") - return recipients + return self.result_set.getString("to_address").split(",") def get_message_date_time(self): #convert ms to s @@ -359,7 +355,7 @@ class TextNowMessagesParser(TskMessagesParser): def get_message_read_status(self): read = self.result_set.getBoolean("read") if self.get_message_direction() == self.INCOMING: - if read == True: + if read: return self.READ return self.UNREAD @@ -375,6 +371,6 @@ class TextNowMessagesParser(TskMessagesParser): def get_thread_id(self): 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 thread_id diff --git a/InternalPythonModules/android/whatsapp.py b/InternalPythonModules/android/whatsapp.py index 9cb7ea3d73..438784f3e2 100644 --- a/InternalPythonModules/android/whatsapp.py +++ b/InternalPythonModules/android/whatsapp.py @@ -433,31 +433,28 @@ class WhatsAppMessagesParser(TskMessagesParser): def __init__(self, message_db): super(WhatsAppMessagesParser, self).__init__(message_db.runQuery( """ - SELECT M.key_remote_jid AS id, - contact_info.recipients, - key_from_me AS direction, - CASE - WHEN M.data IS NULL THEN "" - ELSE M.data - END AS content, - M.timestamp AS send_timestamp, - M.received_timestamp, - M.remote_resource AS group_sender, - M.media_url As attachment - FROM (SELECT jid, - recipients - FROM wadb.wa_contacts AS WC - LEFT JOIN (SELECT gjid, - group_concat(CASE - WHEN jid == "" THEN NULL - ELSE jid - END) AS recipients - FROM group_participants - GROUP BY gjid) AS group_map - ON WC.jid = group_map.gjid - GROUP BY jid) AS contact_info - JOIN messages AS M - ON M.key_remote_jid = contact_info.jid + SELECT messages.key_remote_jid AS id, + contact_book_w_groups.recipients, + key_from_me AS direction, + messages.data AS content, + messages.timestamp AS send_timestamp, + messages.received_timestamp, + messages.remote_resource AS group_sender, + messages.media_url AS attachment + FROM (SELECT jid, + recipients + FROM wadb.wa_contacts AS contacts + left join (SELECT gjid, + Group_concat(CASE + WHEN jid == "" THEN NULL + ELSE jid + END) AS recipients + FROM group_participants + GROUP BY gjid) AS groups + ON contacts.jid = groups.gjid + GROUP BY jid) AS contact_book_w_groups + join messages + ON messages.key_remote_jid = contact_book_w_groups.jid """ ) ) @@ -503,6 +500,8 @@ class WhatsAppMessagesParser(TskMessagesParser): def get_message_text(self): message = self.result_set.getString("content") + if message is None: + message = super(WhatsAppMessagesParser, self).get_message_text() attachment = self.result_set.getString("attachment") if attachment is not None: return general.appendAttachmentList(message, [attachment]) diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox index 122e14fb0f..61d555ace4 100644 --- a/docs/doxygen-user/main.dox +++ b/docs/doxygen-user/main.dox @@ -6,6 +6,8 @@ Overview This is the User's Guide for the open source Autopsy platform. 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 developer's guide 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 ------- The following topics are available here: diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java index 40f2fc0933..09a6637e6e 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java @@ -83,7 +83,7 @@ class EmailMessage { void setSubject(String subject) { if (subject != null) { 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(); replySubject = true; } else { @@ -93,19 +93,19 @@ class EmailMessage { this.simplifiedSubject = ""; } } - + /** * Returns the orginal subject with the "RE:" stripped off". - * + * * @return Message subject with the "RE" stripped off */ String getSimplifiedSubject() { return simplifiedSubject; } - + /** * Returns whether or not the message subject started with "RE:" - * + * * @return true if the original subject started with RE otherwise false. */ boolean isReplySubject() { @@ -121,6 +121,7 @@ class EmailMessage { this.headers = headers; } } + String getTextBody() { return textBody; } @@ -211,75 +212,80 @@ class EmailMessage { this.localPath = localPath; } } - + /** - * Returns the value of the Message-ID header field of this message or - * empty string if it is not present. - * + * Returns the value of the Message-ID header field of this message or empty + * string if it is not present. + * * @return the identifier of this message. */ String getMessageID() { return messageID; } - + /** * Sets the identifier of this message. - * + * * @param messageID identifer of this message */ 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 */ String getInReplyToID() { return inReplyToID; } - + /** * Sets the messageID of the parent message. - * + * * @param inReplyToID messageID of the parent message. */ void setInReplyToID(String inReplyToID) { this.inReplyToID = inReplyToID; } - + /** - * Returns a list of Message-IDs listing the parent, grandparent, - * great-grandparent, and so on, of this message. - * + * Returns a list of Message-IDs listing the parent, grandparent, + * great-grandparent, and so on, of this message. + * * @return The reference list or empty string if none is available. */ List getReferences() { return references; } - + /** * Set the list of reference message-IDs from the email message header. - * - * @param references + * + * @param references */ void setReferences(List references) { this.references = references; } - + /** * Sets the ThreadID of this message. - * + * * @param threadID - the thread ID to set */ void setMessageThreadID(String threadID) { this.messageThreadID = threadID; } - + /** * Returns the ThreadID for this message. - * + * * @return - the message thread ID or "" is non is available */ String getMessageThreadID() { @@ -308,7 +314,7 @@ class EmailMessage { private long aTime = 0L; private long mTime = 0L; - + private TskData.EncodingType encodingType = TskData.EncodingType.NONE; String getName() { @@ -394,14 +400,14 @@ class EmailMessage { this.mTime = mTime.getTime() / 1000; } } - - void setEncodingType(TskData.EncodingType encodingType){ + + void setEncodingType(TskData.EncodingType encodingType) { this.encodingType = encodingType; } - - TskData.EncodingType getEncodingType(){ + + TskData.EncodingType getEncodingType() { return encodingType; } - + } }