package ch.ge.ve.offlineadmin.controller;
/*-
* #%L
* Admin offline
* %%
* Copyright (C) 2015 - 2016 République et Canton de Genève
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import ch.ge.ve.offlineadmin.exception.ProcessInterruptedException;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import java.util.Optional;
import java.util.ResourceBundle;
/**
* Dialog to enter password(s)
*/
public class PasswordDialogController {
public static final int LABEL_COL = 0;
public static final int INPUT_COL = 1;
private static final double GRID_GAP = 10.0;
private static final Insets GRID_INSETS = new Insets(20, 150, 10, 10);
private final ResourceBundle resources;
private ConsoleOutputControl consoleOutputController;
public PasswordDialogController(ResourceBundle resources, ConsoleOutputControl consoleOutputController) {
this.resources = resources;
this.consoleOutputController = consoleOutputController;
}
private static boolean isPasswordValid(String newValue) {
// Length should be between 9 and 10 (incl)
boolean validLength = newValue.length() >= 9 && newValue.length() <= 10;
// Password should contain at least one upper, one lower and one digit
boolean validPattern = newValue.matches(".*[A-Z].*") && newValue.matches(".*[a-z].*") && newValue.matches(".*[0-9].*");
return validLength && validPattern;
}
/**
* Display the dialogs to enter the two passwords
* <p/>
* Whenever requested a password confirmation input-box is displayed
*
* @param password1 first password
* @param password2 second password
* @param withConfirmation indicates if password confirmation is needed
* @throws ProcessInterruptedException
*/
public void promptForPasswords(StringProperty password1, StringProperty password2, boolean withConfirmation) throws ProcessInterruptedException {
openPasswordInputDialog(password1, 1, withConfirmation);
log("key_generation.password1_entered", withConfirmation);
openPasswordInputDialog(password2, 2, withConfirmation);
log("key_generation.password2_entered", withConfirmation);
}
private void log(String key, boolean withConfirmation) {
if (withConfirmation) {
consoleOutputController.logOnScreen(resources.getString(key));
} else {
consoleOutputController.progressMessage(resources.getString(key));
}
}
private void openPasswordInputDialog(StringProperty target, int groupNumber, boolean withConfirmation) throws ProcessInterruptedException {
Dialog<String> dialog = new Dialog<>();
String title = String.format(resources.getString("password_dialog.title"), groupNumber);
dialog.setTitle(title);
dialog.getDialogPane().getStylesheets().add("/ch/ge/ve/offlineadmin/styles/offlineadmin.css");
dialog.getDialogPane().getStyleClass().addAll("background", "password-dialog");
// Define and add buttons
ButtonType confirmPasswordButtonType = new ButtonType(resources.getString("password_dialog.confirm_button"), ButtonBar.ButtonData.OK_DONE);
dialog.getDialogPane().getButtonTypes().addAll(confirmPasswordButtonType, ButtonType.CANCEL);
// Create header label
Label headerLabel = new Label(title);
headerLabel.getStyleClass().add("header-label");
// Create the input labels and fields
GridPane grid = new GridPane();
grid.setHgap(GRID_GAP);
grid.setVgap(GRID_GAP);
grid.setPadding(GRID_INSETS);
TextField electionOfficer1Password = new TextField();
String electionOfficer1Label = withConfirmation ? resources.getString("password_dialog.election_officer_1")
: resources.getString("password_dialog.election_officer");
electionOfficer1Password.setPromptText(electionOfficer1Label);
electionOfficer1Password.setId("passwordField1");
TextField electionOfficer2Password = new TextField();
String electionOfficer2Label = resources.getString("password_dialog.election_officer_2");
electionOfficer2Password.setPromptText(electionOfficer2Label);
electionOfficer2Password.setId("passwordField2");
// Create error message label
Label errorMessage = createErrorMessage(withConfirmation);
errorMessage.setId("errorLabel");
// Position the labels and fields
int row = 0;
grid.add(headerLabel, LABEL_COL, row, 2, 1);
row++;
grid.add(new Label(electionOfficer1Label), LABEL_COL, row);
grid.add(electionOfficer1Password, INPUT_COL, row);
if (withConfirmation) {
row++;
grid.add(new Label(electionOfficer2Label), LABEL_COL, row);
grid.add(electionOfficer2Password, INPUT_COL, row);
}
row++;
grid.add(errorMessage, LABEL_COL, row, 2, 1);
dialog.getDialogPane().setContent(grid);
// Perform input validation
Node confirmButton = dialog.getDialogPane().lookupButton(confirmPasswordButtonType);
confirmButton.setDisable(true);
BooleanBinding booleanBinding = bindForValidity(withConfirmation, electionOfficer1Password, electionOfficer2Password, errorMessage, confirmButton);
// Convert the result
dialog.setResultConverter(dialogButton -> {
if (dialogButton == confirmPasswordButtonType) {
return electionOfficer1Password.textProperty().getValueSafe();
} else {
return null;
}
});
Platform.runLater(electionOfficer1Password::requestFocus);
Optional<String> result = dialog.showAndWait();
//if not disposed then we do have binding errors if this method is run again
booleanBinding.dispose();
result.ifPresent(target::setValue);
if (!result.isPresent()) {
throw new ProcessInterruptedException("Password input cancelled");
}
}
private BooleanBinding bindForValidity(boolean withConfirmation, TextField electionOfficer1Password, TextField electionOfficer2Password, Label errorMessage, Node confirmButton) {
BooleanBinding passwordsValid = Bindings.createBooleanBinding(
() -> withConfirmation ? arePasswordsEqualAndValid(electionOfficer1Password.textProperty(), electionOfficer2Password.textProperty()) : isPasswordValid(electionOfficer1Password.getText()),
electionOfficer1Password.textProperty(),
electionOfficer2Password.textProperty());
passwordsValid.addListener((observable, werePasswordsValid, arePasswordsValid) -> {
confirmButton.setDisable(!arePasswordsValid);
errorMessage.setVisible(!arePasswordsValid && withConfirmation);
});
return passwordsValid;
}
private Label createErrorMessage(boolean withConfirmation) {
Label errorMessage = new Label();
String passwordStrengthMessage;
if (withConfirmation) {
passwordStrengthMessage = resources.getString("password_dialog.strength_requirements");
} else {
passwordStrengthMessage = resources.getString("password_dialog.strength_requirements_no_conf");
}
errorMessage.setText(passwordStrengthMessage);
errorMessage.setWrapText(true);
return errorMessage;
}
private boolean arePasswordsEqualAndValid(StringProperty stringProperty1, StringProperty stringProperty2) {
return stringProperty1.getValueSafe().equals(stringProperty2.getValueSafe()) && isPasswordValid(stringProperty1.getValueSafe());
}
}