/*
* Autopsy Forensic Browser
*
* Copyright 2014-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline.actions;
import java.awt.Desktop;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.function.Supplier;
import java.util.logging.Level;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Control;
import javafx.scene.control.TextInputDialog;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javax.swing.JOptionPane;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.HyperlinkLabel;
import org.controlsfx.control.action.Action;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.FileUtil;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.PromptDialogManager;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.snapshot.SnapShotReportWriter;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Action that saves a snapshot of the given node as an autopsy report.
* Delegates to SnapsHotReportWrite to actually generate and write the report.
*/
public class SaveSnapshotAsReport extends Action {
private static final Logger LOGGER = Logger.getLogger(SaveSnapshotAsReport.class.getName());
private static final Image SNAP_SHOT = new Image("org/sleuthkit/autopsy/timeline/images/image.png", 16, 16, true, true); //NON_NLS
private static final ButtonType OPEN = new ButtonType(Bundle.OpenReportAction_DisplayName(), ButtonBar.ButtonData.NO);
private static final ButtonType OK = new ButtonType(ButtonType.OK.getText(), ButtonBar.ButtonData.CANCEL_CLOSE);
private final TimeLineController controller;
private final Case currentCase;
/**
* Constructor
*
* @param controller The controller for this timeline action
* @param nodeSupplier The Supplier of the node to snapshot.
*/
@NbBundle.Messages({
"Timeline.ModuleName=Timeline",
"SaveSnapShotAsReport.action.dialogs.title=Timeline",
"SaveSnapShotAsReport.action.name.text=Snapshot Report",
"SaveSnapShotAsReport.action.longText=Save a screen capture of the current view of the timeline as a report.",
"# {0} - report file path",
"SaveSnapShotAsReport.ReportSavedAt=Report saved at [{0}]",
"SaveSnapShotAsReport.Success=Success",
"SaveSnapShotAsReport.FailedToAddReport=Failed to add snaphot to case as a report.",
"# {0} - report path",
"SaveSnapShotAsReport.ErrorWritingReport=Error writing report to disk at {0}.",
"# {0} - generated default report name",
"SaveSnapShotAsReport.reportName.prompt=leave empty for default report name: {0}.",
"SaveSnapShotAsReport.reportName.header=Enter a report name for the Timeline Snapshot Report.",
"SaveSnapShotAsReport.duplicateReportNameError.text=A report with that name already exists."
})
public SaveSnapshotAsReport(TimeLineController controller, Supplier<Node> nodeSupplier) {
super(Bundle.SaveSnapShotAsReport_action_name_text());
setLongText(Bundle.SaveSnapShotAsReport_action_longText());
setGraphic(new ImageView(SNAP_SHOT));
this.controller = controller;
this.currentCase = controller.getAutopsyCase();
setEventHandler(actionEvent -> {
//capture generation date and use to make default report name
Date generationDate = new Date();
final String defaultReportName = FileUtil.escapeFileName(currentCase.getName() + " " + new SimpleDateFormat("MM-dd-yyyy-HH-mm-ss").format(generationDate)); //NON_NLS
BufferedImage snapshot = SwingFXUtils.fromFXImage(nodeSupplier.get().snapshot(null, null), null);
//prompt user to pick report name
TextInputDialog textInputDialog = new TextInputDialog();
PromptDialogManager.setDialogIcons(textInputDialog);
textInputDialog.setTitle(Bundle.SaveSnapShotAsReport_action_dialogs_title());
textInputDialog.getEditor().setPromptText(Bundle.SaveSnapShotAsReport_reportName_prompt(defaultReportName));
textInputDialog.setHeaderText(Bundle.SaveSnapShotAsReport_reportName_header());
//keep prompt even if text field has focus, until user starts typing.
textInputDialog.getEditor().setStyle("-fx-prompt-text-fill: derive(-fx-control-inner-background, -30%);");//NON_NLS
/*
* Create a ValidationSupport to validate that a report with the
* entered name doesn't exist on disk already. Disable ok button if
* report name is not validated.
*/
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.registerValidator(textInputDialog.getEditor(), false, new Validator<String>() {
@Override
public ValidationResult apply(Control textField, String enteredReportName) {
String reportName = StringUtils.defaultIfBlank(enteredReportName, defaultReportName);
boolean exists = Files.exists(Paths.get(currentCase.getReportDirectory(), reportName));
return ValidationResult.fromErrorIf(textField, Bundle.SaveSnapShotAsReport_duplicateReportNameError_text(), exists);
}
});
textInputDialog.getDialogPane().lookupButton(ButtonType.OK).disableProperty().bind(validationSupport.invalidProperty());
//show dialog and handle result
textInputDialog.showAndWait().ifPresent(enteredReportName -> {
//reportName defaults to case name + timestamp if left blank
String reportName = StringUtils.defaultIfBlank(enteredReportName, defaultReportName);
Path reportFolderPath = Paths.get(currentCase.getReportDirectory(), reportName, "Timeline Snapshot"); //NON_NLS
Path reportMainFilePath;
try {
//generate and write report
reportMainFilePath = new SnapShotReportWriter(currentCase,
reportFolderPath,
reportName,
controller.getEventsModel().getZoomParamaters(),
generationDate, snapshot).writeReport();
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, "Error writing report to disk at " + reportFolderPath, ex); //NON_NLS
new Alert(Alert.AlertType.ERROR, Bundle.SaveSnapShotAsReport_ErrorWritingReport(reportFolderPath)).show();
return;
}
try {
//add main file as report to case
Case.getCurrentCase().addReport(reportMainFilePath.toString(), Bundle.Timeline_ModuleName(), reportName);
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to add " + reportMainFilePath.toString() + " to case as a report", ex); //NON_NLS
new Alert(Alert.AlertType.ERROR, Bundle.SaveSnapShotAsReport_FailedToAddReport()).show();
return;
}
//notify user of report location
final Alert alert = new Alert(Alert.AlertType.INFORMATION, null, OPEN, OK);
alert.setTitle(Bundle.SaveSnapShotAsReport_action_dialogs_title());
alert.setHeaderText(Bundle.SaveSnapShotAsReport_Success());
//make action to open report, and hyperlinklable to invoke action
final OpenReportAction openReportAction = new OpenReportAction(reportMainFilePath);
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(Bundle.SaveSnapShotAsReport_ReportSavedAt(reportMainFilePath.toString()));
hyperlinkLabel.setOnAction(openReportAction);
alert.getDialogPane().setContent(hyperlinkLabel);
alert.showAndWait().filter(OPEN::equals).ifPresent(buttonType -> openReportAction.handle(null));
});
});
}
/**
* Action that opens the given Path in the system default application.
*/
@NbBundle.Messages({
"OpenReportAction.DisplayName=Open Report",
"OpenReportAction.NoAssociatedEditorMessage=There is no associated editor for reports of this type or the associated application failed to launch.",
"OpenReportAction.MessageBoxTitle=Open Report Failure",
"OpenReportAction.NoOpenInEditorSupportMessage=This platform (operating system) does not support opening a file in an editor this way.",
"OpenReportAction.MissingReportFileMessage=The report file no longer exists.",
"OpenReportAction.ReportFileOpenPermissionDeniedMessage=Permission to open the report file was denied."})
private class OpenReportAction extends Action {
OpenReportAction(Path reportHTMLFIle) {
super(Bundle.OpenReportAction_DisplayName());
setEventHandler(actionEvent -> {
try {
Desktop.getDesktop().open(reportHTMLFIle.toFile());
} catch (IOException ex) {
JOptionPane.showMessageDialog(null,
Bundle.OpenReportAction_NoAssociatedEditorMessage(),
Bundle.OpenReportAction_MessageBoxTitle(),
JOptionPane.ERROR_MESSAGE);
} catch (UnsupportedOperationException ex) {
JOptionPane.showMessageDialog(null,
Bundle.OpenReportAction_NoOpenInEditorSupportMessage(),
Bundle.OpenReportAction_MessageBoxTitle(),
JOptionPane.ERROR_MESSAGE);
} catch (IllegalArgumentException ex) {
JOptionPane.showMessageDialog(null,
Bundle.OpenReportAction_MissingReportFileMessage(),
Bundle.OpenReportAction_MessageBoxTitle(),
JOptionPane.ERROR_MESSAGE);
} catch (SecurityException ex) {
JOptionPane.showMessageDialog(null,
Bundle.OpenReportAction_ReportFileOpenPermissionDeniedMessage(),
Bundle.OpenReportAction_MessageBoxTitle(),
JOptionPane.ERROR_MESSAGE);
}
});
}
}
}