Compare commits
25 Commits
2.7-java-6
...
2.7-java-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
554c0129a2 | ||
|
|
78956beb08 | ||
|
|
39dacc8e5a | ||
|
|
34042e5462 | ||
|
|
0a386d6fad | ||
|
|
ff17a1d637 | ||
|
|
46fe919476 | ||
|
|
8f35ac5f64 | ||
|
|
06100510c3 | ||
|
|
1bf6109038 | ||
|
|
f2fa2a25b2 | ||
|
|
2a0cfd3a32 | ||
|
|
3070967d34 | ||
|
|
e4837a284a | ||
|
|
06ebe954f1 | ||
|
|
48d4668575 | ||
|
|
af768329a3 | ||
|
|
9a04c28054 | ||
|
|
ec9c55ec4d | ||
|
|
d8a735e1b1 | ||
|
|
a1eee88a54 | ||
|
|
ac5286853a | ||
|
|
39f6893742 | ||
|
|
7bf7b8981c | ||
|
|
09abe21fed |
@@ -9,7 +9,7 @@ build_project:
|
|||||||
- "( ./lib/bin/build_libsodium-macos clean && ./lib/bin/build_libsodium-macos )"
|
- "( ./lib/bin/build_libsodium-macos clean && ./lib/bin/build_libsodium-macos )"
|
||||||
- "( ./lib/bin/build_libjson-c-macos clean && ./lib/bin/build_libjson-c-macos )"
|
- "( ./lib/bin/build_libjson-c-macos clean && ./lib/bin/build_libjson-c-macos )"
|
||||||
- "( cd ./platform-independent/c/cli && ./clean && targets=all ./build && ./mpw-tests && ./mpw-cli-tests )"
|
- "( cd ./platform-independent/c/cli && ./clean && targets=all ./build && ./mpw-tests && ./mpw-cli-tests )"
|
||||||
- "( cd ./gradle && ./gradlew --stacktrace clean test )"
|
- "( export JAVA_HOME=$(java_home -Fv 10 || java_home -Fv 9* ) && cd ./gradle && ./gradlew --stacktrace clean test )"
|
||||||
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword iOS' -sdk iphonesimulator clean build )"
|
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword iOS' -sdk iphonesimulator clean build )"
|
||||||
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword macOS' clean build )"
|
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword macOS' clean build )"
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ allprojects {
|
|||||||
apply plugin: 'findbugs'
|
apply plugin: 'findbugs'
|
||||||
|
|
||||||
group = 'com.lyndir.masterpassword'
|
group = 'com.lyndir.masterpassword'
|
||||||
version = '2.7.6'
|
version = '2.7.10'
|
||||||
|
|
||||||
tasks.withType( JavaCompile ) {
|
tasks.withType( JavaCompile ) {
|
||||||
options.encoding = 'UTF-8'
|
options.encoding = 'UTF-8'
|
||||||
|
|||||||
2
platform-darwin/External/Pearl
vendored
2
platform-darwin/External/Pearl
vendored
Submodule platform-darwin/External/Pearl updated: b713577cd6...3d04d775e0
@@ -3917,7 +3917,7 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = "/bin/sh -e";
|
shellPath = "/bin/sh -e";
|
||||||
shellScript = "exec Scripts/genassets";
|
shellScript = "exec Scripts/genassets\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
DAB7AE4C1F3D56AD00C856B1 /* ShellScript */ = {
|
DAB7AE4C1F3D56AD00C856B1 /* ShellScript */ = {
|
||||||
|
|||||||
@@ -120,21 +120,21 @@
|
|||||||
MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
|
MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
|
||||||
if (indexPath.section == 0) {
|
if (indexPath.section == 0) {
|
||||||
if (indexPath.item == 0) {
|
if (indexPath.item == 0) {
|
||||||
MPGlobalAnswersCell *cell = [MPGlobalAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
|
MPGlobalAnswersCell *cell = [MPGlobalAnswersCell dequeueFromTableView:tableView indexPath:indexPath];
|
||||||
[cell setSite:site];
|
[cell setSite:site];
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
if (indexPath.item == 1)
|
if (indexPath.item == 1)
|
||||||
return [MPSendAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
|
return [MPSendAnswersCell dequeueFromTableView:tableView indexPath:indexPath];
|
||||||
if (indexPath.item == 2) {
|
if (indexPath.item == 2) {
|
||||||
MPMultipleAnswersCell *cell = [MPMultipleAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
|
MPMultipleAnswersCell *cell = [MPMultipleAnswersCell dequeueFromTableView:tableView indexPath:indexPath];
|
||||||
cell.accessoryType = self.multiple? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone;
|
cell.accessoryType = self.multiple? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone;
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
Throw( @"Unsupported row index: %@", indexPath );
|
Throw( @"Unsupported row index: %@", indexPath );
|
||||||
}
|
}
|
||||||
|
|
||||||
MPAnswersQuestionCell *cell = [MPAnswersQuestionCell dequeueCellFromTableView:tableView indexPath:indexPath];
|
MPAnswersQuestionCell *cell = [MPAnswersQuestionCell dequeueFromTableView:tableView indexPath:indexPath];
|
||||||
MPSiteQuestionEntity *question = nil;
|
MPSiteQuestionEntity *question = nil;
|
||||||
if ([site.questions count] > indexPath.item)
|
if ([site.questions count] > indexPath.item)
|
||||||
question = site.questions[indexPath.item];
|
question = site.questions[indexPath.item];
|
||||||
|
|||||||
@@ -150,7 +150,7 @@
|
|||||||
|
|
||||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
|
||||||
MPGuideStepCell *cell = [MPGuideStepCell dequeueCellFromCollectionView:collectionView indexPath:indexPath];
|
MPGuideStepCell *cell = [MPGuideStepCell dequeueFromCollectionView:collectionView indexPath:indexPath];
|
||||||
cell.imageView.image = ((MPGuideStep *)self.steps[indexPath.item]).image;
|
cell.imageView.image = ((MPGuideStep *)self.steps[indexPath.item]).image;
|
||||||
cell.contentView.frame = cell.bounds;
|
cell.contentView.frame = cell.bounds;
|
||||||
|
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
|
|||||||
|
|
||||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
|
||||||
MPSiteCell *cell = [MPSiteCell dequeueCellFromCollectionView:collectionView indexPath:indexPath];
|
MPSiteCell *cell = [MPSiteCell dequeueFromCollectionView:collectionView indexPath:indexPath];
|
||||||
[cell setFuzzyGroups:self.fuzzyGroups];
|
[cell setFuzzyGroups:self.fuzzyGroups];
|
||||||
id item = self.dataSource[(NSUInteger)indexPath.section][(NSUInteger)indexPath.item];
|
id item = self.dataSource[(NSUInteger)indexPath.section][(NSUInteger)indexPath.item];
|
||||||
if ([item isKindOfClass:[MPSiteEntity class]])
|
if ([item isKindOfClass:[MPSiteEntity class]])
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ PearlEnum( MPDevelopmentFuelConsumption,
|
|||||||
SKProduct *product = content;
|
SKProduct *product = content;
|
||||||
MPStoreProductCell *cell;
|
MPStoreProductCell *cell;
|
||||||
if ([product.productIdentifier isEqualToString:MPProductFuel])
|
if ([product.productIdentifier isEqualToString:MPProductFuel])
|
||||||
cell = [MPStoreFuelProductCell dequeueCellFromTableView:tableView indexPath:indexPath];
|
cell = [MPStoreFuelProductCell dequeueFromTableView:tableView indexPath:indexPath];
|
||||||
else
|
else
|
||||||
cell = [MPStoreProductCell dequeueCellFromTableView:tableView indexPath:indexPath];
|
cell = [MPStoreProductCell dequeueFromTableView:tableView indexPath:indexPath];
|
||||||
[cell updateWithProduct:product transaction:self.transactions[product.productIdentifier]];
|
[cell updateWithProduct:product transaction:self.transactions[product.productIdentifier]];
|
||||||
|
|
||||||
return cell;
|
return cell;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# USAGE
|
# USAGE
|
||||||
# [targets='...'] [mpw_feature=0|1 ...] [CFLAGS='...'] [LDFLAGS='...'] ./build [cc arguments ...]
|
# [targets='...'] [mpw_feature=0|1 ...] [CFLAGS='...'] [LDFLAGS='...'] ./build [-v|-d|-h|--] [cc arguments ...]
|
||||||
#
|
#
|
||||||
# By default, you should only need to run ./build
|
# By default, you should only need to run ./build
|
||||||
#
|
#
|
||||||
|
# -v: verbose mode, outputs state information and compiler commands.
|
||||||
|
# -d: debug build, modifies default build flags to produce binaries best suited for debugging.
|
||||||
|
# -h: show this usage information.
|
||||||
|
#
|
||||||
# You can customize the targets that are built using targets='...'. Use targets='all' to build all targets.
|
# You can customize the targets that are built using targets='...'. Use targets='all' to build all targets.
|
||||||
# By default, we only build the 'mpw' target.
|
# By default, we only build the 'mpw' target.
|
||||||
# See targets_all for all possible targets as well as the features they support and require.
|
# See targets_all for all possible targets as well as the features they support and require.
|
||||||
@@ -27,29 +31,53 @@ set -e
|
|||||||
|
|
||||||
|
|
||||||
### CONFIGURATION
|
### CONFIGURATION
|
||||||
# Targets to build.
|
verbose=0
|
||||||
|
|
||||||
|
# Options
|
||||||
|
while getopts :vdh opt; do
|
||||||
|
case $opt in
|
||||||
|
v) verbose=1 ;;
|
||||||
|
d) debug=1 ;;
|
||||||
|
h|?) sed -n '/^[^#]/q;p' "${BASH_SOURCE##*/}"; exit ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift "$(( OPTIND - 1 ))"
|
||||||
|
|
||||||
|
# Targets to build
|
||||||
targets_all=(
|
targets_all=(
|
||||||
mpw # C CLI version of Master Password (needs: mpw_sodium, optional: mpw_color, mpw_json).
|
mpw # C CLI version of Master Password (needs: mpw_sodium, optional: mpw_color, mpw_json).
|
||||||
mpw-bench # C CLI Master Password benchmark utility (needs: mpw_sodium).
|
mpw-bench # C CLI Master Password benchmark utility (needs: mpw_sodium).
|
||||||
mpw-tests # C Master Password algorithm test suite (needs: mpw_sodium, mpw_xml).
|
mpw-tests # C Master Password algorithm test suite (needs: mpw_sodium, mpw_xml).
|
||||||
)
|
)
|
||||||
targets_default='mpw' # Override with: targets='...' ./build
|
targets_default='mpw' # Override with: targets='...' ./build
|
||||||
|
targets=${targets[*]:-$targets_default}
|
||||||
|
|
||||||
# Features.
|
# Features
|
||||||
mpw_sodium=${mpw_sodium:-1} # Implement crypto functions with sodium (depends on libsodium).
|
mpw_sodium=${mpw_sodium:-1} # Implement crypto functions with sodium (depends on libsodium).
|
||||||
mpw_json=${mpw_json:-1} # Support JSON-based user configuration format (depends on libjson-c).
|
mpw_json=${mpw_json:-1} # Support JSON-based user configuration format (depends on libjson-c).
|
||||||
mpw_color=${mpw_color:-1} # Colorized identicon (depends on libncurses).
|
mpw_color=${mpw_color:-1} # Colorized identicon (depends on libncurses).
|
||||||
mpw_xml=${mpw_xml:-1} # XML parsing (depends on libxml2).
|
mpw_xml=${mpw_xml:-1} # XML parsing (depends on libxml2).
|
||||||
|
|
||||||
# Default build flags.
|
# Default build flags
|
||||||
cflags=( -O3 $CFLAGS )
|
cflags=( -O3 $CFLAGS ); unset CFLAGS
|
||||||
ldflags=( $LDFLAGS )
|
ldflags=( $LDFLAGS ); unset LDFLAGS
|
||||||
|
if (( debug )); then
|
||||||
|
cflags+=( -O0 -g )
|
||||||
|
fi
|
||||||
|
|
||||||
# Version.
|
# Version
|
||||||
if { mpw_version=$(git describe --match '*-cli*' --long --dirty) || mpw_version=$(<VERSION); } 2>/dev/null; then
|
if { mpw_version=$(git describe --match '*-cli*' --long --dirty) || mpw_version=$(<VERSION); } 2>/dev/null; then
|
||||||
cflags+=( -D"MP_VERSION=$mpw_version" )
|
cflags+=( -D"MP_VERSION=$mpw_version" )
|
||||||
fi
|
fi
|
||||||
echo 2>&1 "Current mpw source version ${mpw_version:-<unknown>}..."
|
echo "Current mpw source version ${mpw_version:-<unknown>}..."
|
||||||
|
|
||||||
|
# Meta
|
||||||
|
if (( verbose )); then
|
||||||
|
echo "mpw_sodium=${mpw_sodium}, mpw_json=${mpw_json}, mpw_color=${mpw_color}, mpw_xml=${mpw_xml}"
|
||||||
|
echo "CFLAGS: ${cflags[*]}"
|
||||||
|
echo "LDFLAGS: ${ldflags[*]}"
|
||||||
|
echo "targets: ${targets[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
### TARGET: MPW
|
### TARGET: MPW
|
||||||
@@ -132,18 +160,20 @@ mpw-tests() {
|
|||||||
haslib() {
|
haslib() {
|
||||||
cc -x c "${ldflags[@]}" -l"$1" -o /dev/null - <<< 'int main() { return 0; }' &>/dev/null
|
cc -x c "${ldflags[@]}" -l"$1" -o /dev/null - <<< 'int main() { return 0; }' &>/dev/null
|
||||||
}
|
}
|
||||||
cc() {
|
cc() (
|
||||||
if hash llvm-gcc 2>/dev/null; then
|
(( verbose )) && set -x
|
||||||
|
|
||||||
|
if { hash llvm-gcc; } 2>/dev/null; then
|
||||||
llvm-gcc "$@"
|
llvm-gcc "$@"
|
||||||
elif hash gcc 2>/dev/null; then
|
elif { hash gcc; } 2>/dev/null; then
|
||||||
gcc -std=c11 "$@"
|
gcc -std=c11 "$@"
|
||||||
elif hash clang 2>/dev/null; then
|
elif { hash clang; } 2>/dev/null; then
|
||||||
clang "$@"
|
clang "$@"
|
||||||
else
|
else
|
||||||
echo >&2 "Need a compiler. Please install GCC or LLVM."
|
echo >&2 "Need a compiler. Please install GCC or LLVM."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
### DEPENDENCIES
|
### DEPENDENCIES
|
||||||
@@ -156,7 +186,7 @@ use() {
|
|||||||
for lib in "$lib" "$@"; do
|
for lib in "$lib" "$@"; do
|
||||||
haslib "$lib" && ldflags+=( -l"$lib" )
|
haslib "$lib" && ldflags+=( -l"$lib" )
|
||||||
done
|
done
|
||||||
echo >&2 "INFO: Enabled $option (lib$lib)."
|
echo "INFO: Enabled $option (lib$lib)."
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
elif [[ $requisite == required ]]; then
|
elif [[ $requisite == required ]]; then
|
||||||
@@ -174,7 +204,7 @@ use() {
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
else
|
else
|
||||||
echo >&2 "INFO: $option is supported but not enabled."
|
echo "INFO: $option is supported but not enabled."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -192,13 +222,13 @@ use_mpw_json() {
|
|||||||
}
|
}
|
||||||
use_mpw_xml() {
|
use_mpw_xml() {
|
||||||
local requisite=$1
|
local requisite=$1
|
||||||
use mpw_xml "$requisite" xml2 && cflags+=( -D"MPW_XML=1" -I"/usr/include/libxml2" -I"/usr/local/include/libxml2" ) ||:
|
use mpw_xml "$requisite" xml2 && cflags+=( $(xml2-config --cflags) ) ldflags+=( $(xml2-config --libs) ) ||:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
### BUILD TARGETS
|
### BUILD TARGETS
|
||||||
for target in "${targets_all[@]}"; do
|
for target in "${targets_all[@]}"; do
|
||||||
if [[ ${targets:-$targets_default} == 'all' || " ${targets:-$targets_default} " = *" $target "* ]]; then
|
if [[ $targets == 'all' || " $targets " = *" $target "* ]]; then
|
||||||
echo
|
echo
|
||||||
echo "Building target: $target..."
|
echo "Building target: $target..."
|
||||||
( "$target" "$@" )
|
( "$target" "$@" )
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const char *mpw_getenv(const char *variableName) {
|
|||||||
return envBuf? mpw_strdup( envBuf ): NULL;
|
return envBuf? mpw_strdup( envBuf ): NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *mpw_askpass(const char *prompt) {
|
const char *mpw_askpass(const char *prompt) {
|
||||||
|
|
||||||
const char *askpass = mpw_getenv( MP_ENV_askpass );
|
const char *askpass = mpw_getenv( MP_ENV_askpass );
|
||||||
if (!askpass)
|
if (!askpass)
|
||||||
@@ -74,7 +74,7 @@ char *mpw_askpass(const char *prompt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close( pipes[1] );
|
close( pipes[1] );
|
||||||
char *answer = mpw_read_fd( pipes[0] );
|
const char *answer = mpw_read_fd( pipes[0] );
|
||||||
close( pipes[0] );
|
close( pipes[0] );
|
||||||
int status;
|
int status;
|
||||||
if (waitpid( pid, &status, 0 ) == ERR) {
|
if (waitpid( pid, &status, 0 ) == ERR) {
|
||||||
@@ -86,7 +86,7 @@ char *mpw_askpass(const char *prompt) {
|
|||||||
if (WIFEXITED( status ) && WEXITSTATUS( status ) == EXIT_SUCCESS && answer && strlen( answer )) {
|
if (WIFEXITED( status ) && WEXITSTATUS( status ) == EXIT_SUCCESS && answer && strlen( answer )) {
|
||||||
// Remove trailing newline.
|
// Remove trailing newline.
|
||||||
if (answer[strlen( answer ) - 1] == '\n')
|
if (answer[strlen( answer ) - 1] == '\n')
|
||||||
answer[strlen( answer ) - 1] = '\0';
|
mpw_replace_string( answer, mpw_strndup( answer, strlen( answer ) - 1 ) );
|
||||||
return answer;
|
return answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,71 +97,74 @@ char *mpw_askpass(const char *prompt) {
|
|||||||
static const char *_mpw_getline(const char *prompt, bool silent) {
|
static const char *_mpw_getline(const char *prompt, bool silent) {
|
||||||
|
|
||||||
// Get answer from askpass.
|
// Get answer from askpass.
|
||||||
char *answer = mpw_askpass( prompt );
|
const char *answer = mpw_askpass( prompt );
|
||||||
if (answer)
|
if (answer)
|
||||||
return answer;
|
return answer;
|
||||||
|
|
||||||
#if MPW_COLOR
|
#if MPW_COLOR
|
||||||
// Initialize a curses screen.
|
// Initialize a curses screen.
|
||||||
SCREEN *screen = newterm( NULL, stderr, stdin );
|
SCREEN *screen = newterm( NULL, stderr, stdin );
|
||||||
start_color();
|
if (screen) {
|
||||||
init_pair( 1, COLOR_WHITE, COLOR_BLUE );
|
start_color();
|
||||||
init_pair( 2, COLOR_BLACK, COLOR_WHITE );
|
init_pair( 1, COLOR_WHITE, COLOR_BLUE );
|
||||||
int rows, cols;
|
init_pair( 2, COLOR_BLACK, COLOR_WHITE );
|
||||||
getmaxyx( stdscr, rows, cols );
|
int rows, cols;
|
||||||
|
getmaxyx( stdscr, rows, cols );
|
||||||
|
|
||||||
// Display a dialog box.
|
// Display a dialog box.
|
||||||
int width = max( prompt? (int)strlen( prompt ): 0, MPW_MAX_INPUT ) + 6;
|
int width = max( prompt? (int)strlen( prompt ): 0, MPW_MAX_INPUT ) + 6;
|
||||||
char *version = "mpw v" stringify_def( MP_VERSION );
|
char *version = "mpw v" stringify_def( MP_VERSION );
|
||||||
mvprintw( rows - 1, (cols - (int)strlen( version )) / 2, "%s", version );
|
mvprintw( rows - 1, (cols - (int)strlen( version )) / 2, "%s", version );
|
||||||
attron( A_BOLD );
|
attron( A_BOLD );
|
||||||
color_set( 2, NULL );
|
color_set( 2, NULL );
|
||||||
mvprintw( rows / 2 - 1, (cols - width) / 2, "%s%*s%s", "*", width - 2, "", "*" );
|
mvprintw( rows / 2 - 1, (cols - width) / 2, "%s%*s%s", "*", width - 2, "", "*" );
|
||||||
mvprintw( rows / 2 - 1, (cols - (int)strlen( prompt )) / 2, "%s", prompt );
|
mvprintw( rows / 2 - 1, (cols - (int)strlen( prompt )) / 2, "%s", prompt );
|
||||||
color_set( 1, NULL );
|
color_set( 1, NULL );
|
||||||
mvprintw( rows / 2 + 0, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
|
mvprintw( rows / 2 + 0, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
|
||||||
mvprintw( rows / 2 + 1, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
|
mvprintw( rows / 2 + 1, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
|
||||||
mvprintw( rows / 2 + 2, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
|
mvprintw( rows / 2 + 2, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
|
||||||
|
|
||||||
// Read response.
|
// Read response.
|
||||||
color_set( 2, NULL );
|
color_set( 2, NULL );
|
||||||
attron( A_STANDOUT );
|
attron( A_STANDOUT );
|
||||||
int result = ERR;
|
int result = ERR;
|
||||||
char str[MPW_MAX_INPUT + 1];
|
char str[MPW_MAX_INPUT + 1];
|
||||||
if (silent) {
|
if (silent) {
|
||||||
mvprintw( rows / 2 + 1, (cols - 5) / 2, "[ * ]" );
|
mvprintw( rows / 2 + 1, (cols - 5) / 2, "[ * ]" );
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
noecho();
|
noecho();
|
||||||
result = mvgetnstr( rows / 2 + 1, (cols - 1) / 2, str, MPW_MAX_INPUT );
|
result = mvgetnstr( rows / 2 + 1, (cols - 1) / 2, str, MPW_MAX_INPUT );
|
||||||
echo();
|
echo();
|
||||||
} else {
|
}
|
||||||
mvprintw( rows / 2 + 1, (cols - (MPW_MAX_INPUT + 2)) / 2, "%*s", MPW_MAX_INPUT + 2, "" );
|
else {
|
||||||
refresh();
|
mvprintw( rows / 2 + 1, (cols - (MPW_MAX_INPUT + 2)) / 2, "%*s", MPW_MAX_INPUT + 2, "" );
|
||||||
|
refresh();
|
||||||
|
|
||||||
echo();
|
echo();
|
||||||
result = mvgetnstr( rows / 2 + 1, (cols - MPW_MAX_INPUT) / 2, str, MPW_MAX_INPUT );
|
result = mvgetnstr( rows / 2 + 1, (cols - MPW_MAX_INPUT) / 2, str, MPW_MAX_INPUT );
|
||||||
|
}
|
||||||
|
attrset( 0 );
|
||||||
|
endwin();
|
||||||
|
delscreen( screen );
|
||||||
|
|
||||||
|
return result == ERR? NULL: mpw_strndup( str, MPW_MAX_INPUT );
|
||||||
}
|
}
|
||||||
attrset( 0 );
|
#endif
|
||||||
endwin();
|
|
||||||
delscreen( screen );
|
|
||||||
|
|
||||||
return result == ERR? NULL: mpw_strndup( str, MPW_MAX_INPUT );
|
|
||||||
#else
|
|
||||||
// Get password from terminal.
|
// Get password from terminal.
|
||||||
fprintf( stderr, "%s ", prompt );
|
fprintf( stderr, "%s ", prompt );
|
||||||
|
|
||||||
size_t bufSize = 0;
|
size_t bufSize = 0;
|
||||||
ssize_t lineSize = getline( &answer, &bufSize, stdin );
|
ssize_t lineSize = getline( (char **)&answer, &bufSize, stdin );
|
||||||
if (lineSize <= 1) {
|
if (lineSize <= 1) {
|
||||||
mpw_free_string( &answer );
|
mpw_free_string( &answer );
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove trailing newline.
|
// Remove trailing newline.
|
||||||
answer[lineSize - 1] = '\0';
|
mpw_replace_string( answer, mpw_strndup( answer, (size_t)lineSize - 1 ) );
|
||||||
return answer;
|
return answer;
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *mpw_getline(const char *prompt) {
|
const char *mpw_getline(const char *prompt) {
|
||||||
@@ -250,7 +253,7 @@ bool mpw_mkdirs(const char *filePath) {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *mpw_read_fd(int fd) {
|
const char *mpw_read_fd(int fd) {
|
||||||
|
|
||||||
char *buf = NULL;
|
char *buf = NULL;
|
||||||
size_t blockSize = 4096, bufSize = 0, bufOffset = 0;
|
size_t blockSize = 4096, bufSize = 0, bufOffset = 0;
|
||||||
@@ -263,7 +266,7 @@ char *mpw_read_fd(int fd) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *mpw_read_file(FILE *file) {
|
const char *mpw_read_file(FILE *file) {
|
||||||
|
|
||||||
if (!file)
|
if (!file)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const char *mpw_getenv(const char *variableName);
|
|||||||
|
|
||||||
/** Use the askpass program to prompt the user.
|
/** Use the askpass program to prompt the user.
|
||||||
* @return A newly allocated string or NULL if askpass is not supported or an error occurred. */
|
* @return A newly allocated string or NULL if askpass is not supported or an error occurred. */
|
||||||
char *mpw_askpass(const char *prompt);
|
const char *mpw_askpass(const char *prompt);
|
||||||
|
|
||||||
/** Ask the user a question.
|
/** Ask the user a question.
|
||||||
* @return A newly allocated string or NULL if an error occurred trying to read from the user. */
|
* @return A newly allocated string or NULL if an error occurred trying to read from the user. */
|
||||||
@@ -58,11 +58,11 @@ bool mpw_mkdirs(const char *filePath);
|
|||||||
|
|
||||||
/** Read until EOF from the given file descriptor.
|
/** Read until EOF from the given file descriptor.
|
||||||
* @return A newly allocated string or NULL the read buffer couldn't be allocated. */
|
* @return A newly allocated string or NULL the read buffer couldn't be allocated. */
|
||||||
char *mpw_read_fd(int fd);
|
const char *mpw_read_fd(int fd);
|
||||||
|
|
||||||
/** Read the file contents of a given file.
|
/** Read the file contents of a given file.
|
||||||
* @return A newly allocated string or NULL the read buffer couldn't be allocated. */
|
* @return A newly allocated string or NULL the read buffer couldn't be allocated. */
|
||||||
char *mpw_read_file(FILE *file);
|
const char *mpw_read_file(FILE *file);
|
||||||
|
|
||||||
/** Encode a visual fingerprint for a user.
|
/** Encode a visual fingerprint for a user.
|
||||||
* @return A newly allocated string. */
|
* @return A newly allocated string. */
|
||||||
|
|||||||
@@ -476,8 +476,8 @@ void cli_user(Arguments *args, Operation *operation) {
|
|||||||
|
|
||||||
else {
|
else {
|
||||||
// Read file.
|
// Read file.
|
||||||
char *sitesInputData = mpw_read_file( sitesFile );
|
const char *sitesInputData = mpw_read_file( sitesFile );
|
||||||
if (ferror( sitesFile ))
|
if (!sitesInputData || ferror( sitesFile ))
|
||||||
wrn( "Error while reading configuration file:\n %s: %d", operation->sitesPath, ferror( sitesFile ) );
|
wrn( "Error while reading configuration file:\n %s: %d", operation->sitesPath, ferror( sitesFile ) );
|
||||||
fclose( sitesFile );
|
fclose( sitesFile );
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ bool mpw_string_pushf(
|
|||||||
/** Push an integer onto a buffer. reallocs the given buffer and appends the given integer. */
|
/** Push an integer onto a buffer. reallocs the given buffer and appends the given integer. */
|
||||||
bool mpw_push_int(
|
bool mpw_push_int(
|
||||||
uint8_t **buffer, size_t *bufferSize, const uint32_t pushInt);
|
uint8_t **buffer, size_t *bufferSize, const uint32_t pushInt);
|
||||||
|
|
||||||
|
// These defines merely exist to force the void** cast (& do type-checking), since void** casts are not automatic.
|
||||||
/** Reallocate the given buffer from the given size by adding the delta size.
|
/** Reallocate the given buffer from the given size by adding the delta size.
|
||||||
* On success, the buffer size pointer will be updated to the buffer's new size
|
* On success, the buffer size pointer will be updated to the buffer's new size
|
||||||
* and the buffer pointer may be updated to a new memory address.
|
* and the buffer pointer may be updated to a new memory address.
|
||||||
@@ -124,26 +126,26 @@ bool mpw_push_int(
|
|||||||
* @param deltaSize The amount to increase the buffer's size by.
|
* @param deltaSize The amount to increase the buffer's size by.
|
||||||
* @return true if successful, false if reallocation failed.
|
* @return true if successful, false if reallocation failed.
|
||||||
*/
|
*/
|
||||||
#define mpw_realloc(buffer, bufferSize, deltaSize) \
|
#define mpw_realloc( \
|
||||||
|
/* const void** */buffer, /* size_t* */bufferSize, /* const size_t */deltaSize) \
|
||||||
({ __typeof__(buffer) _b = buffer; const void *__b = *_b; (void)__b; __mpw_realloc( (const void **)_b, bufferSize, deltaSize ); })
|
({ __typeof__(buffer) _b = buffer; const void *__b = *_b; (void)__b; __mpw_realloc( (const void **)_b, bufferSize, deltaSize ); })
|
||||||
bool __mpw_realloc(const void **buffer, size_t *bufferSize, const size_t deltaSize);
|
|
||||||
void mpw_zero(
|
|
||||||
void *buffer, size_t bufferSize);
|
|
||||||
/** Free a buffer after zero'ing its contents, then set the reference to NULL. */
|
/** Free a buffer after zero'ing its contents, then set the reference to NULL. */
|
||||||
#define mpw_free(buffer, bufferSize) \
|
#define mpw_free( \
|
||||||
|
/* void** */buffer, /* size_t */ bufferSize) \
|
||||||
({ __typeof__(buffer) _b = buffer; const void *__b = *_b; (void)__b; __mpw_free( (void **)_b, bufferSize ); })
|
({ __typeof__(buffer) _b = buffer; const void *__b = *_b; (void)__b; __mpw_free( (void **)_b, bufferSize ); })
|
||||||
bool __mpw_free(
|
|
||||||
void **buffer, size_t bufferSize);
|
|
||||||
/** Free a string after zero'ing its contents, then set the reference to NULL. */
|
/** Free a string after zero'ing its contents, then set the reference to NULL. */
|
||||||
#define mpw_free_string(string) \
|
#define mpw_free_string( \
|
||||||
|
/* char** */string) \
|
||||||
({ __typeof__(string) _s = string; const char *__s = *_s; (void)__s; __mpw_free_string( (char **)_s ); })
|
({ __typeof__(string) _s = string; const char *__s = *_s; (void)__s; __mpw_free_string( (char **)_s ); })
|
||||||
bool __mpw_free_string(
|
|
||||||
char **string);
|
|
||||||
/** Free strings after zero'ing their contents, then set the references to NULL. Terminate the va_list with NULL. */
|
/** Free strings after zero'ing their contents, then set the references to NULL. Terminate the va_list with NULL. */
|
||||||
#define mpw_free_strings(strings, ...) \
|
#define mpw_free_strings( \
|
||||||
|
/* char** */strings, ...) \
|
||||||
({ __typeof__(strings) _s = strings; const char *__s = *_s; (void)__s; __mpw_free_strings( (char **)_s, __VA_ARGS__ ); })
|
({ __typeof__(strings) _s = strings; const char *__s = *_s; (void)__s; __mpw_free_strings( (char **)_s, __VA_ARGS__ ); })
|
||||||
bool __mpw_free_strings(
|
/** Free a string after zero'ing its contents, then set the reference to the replacement string.
|
||||||
char **strings, ...);
|
* The replacement string is generated before the original is freed; it may be a derivative of the original. */
|
||||||
|
#define mpw_replace_string( \
|
||||||
|
/* char* */string, /* char* */replacement) \
|
||||||
|
do { const char *replacement_ = replacement; mpw_free_string( &string ); string = replacement_; } while (0)
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
#undef mpw_realloc
|
#undef mpw_realloc
|
||||||
#define mpw_realloc(buffer, bufferSize, deltaSize) \
|
#define mpw_realloc(buffer, bufferSize, deltaSize) \
|
||||||
@@ -158,6 +160,16 @@ bool __mpw_free_strings(
|
|||||||
#define mpw_free_strings(strings, ...) \
|
#define mpw_free_strings(strings, ...) \
|
||||||
__mpw_free_strings( (char **)strings, __VA_ARGS__ )
|
__mpw_free_strings( (char **)strings, __VA_ARGS__ )
|
||||||
#endif
|
#endif
|
||||||
|
bool __mpw_realloc(
|
||||||
|
const void **buffer, size_t *bufferSize, const size_t deltaSize);
|
||||||
|
bool __mpw_free(
|
||||||
|
void **buffer, size_t bufferSize);
|
||||||
|
bool __mpw_free_string(
|
||||||
|
char **string);
|
||||||
|
bool __mpw_free_strings(
|
||||||
|
char **strings, ...);
|
||||||
|
void mpw_zero(
|
||||||
|
void *buffer, size_t bufferSize);
|
||||||
|
|
||||||
//// Cryptographic functions.
|
//// Cryptographic functions.
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ public class MPMasterKey {
|
|||||||
* @return {@code null} if the result type is missing a required parameter.
|
* @return {@code null} if the result type is missing a required parameter.
|
||||||
*
|
*
|
||||||
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
|
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
|
||||||
|
* @throws MPAlgorithmException An internal system or algorithm error has occurred.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public String siteResult(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
|
public String siteResult(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
|
||||||
@@ -184,6 +185,7 @@ public class MPMasterKey {
|
|||||||
* {@link #siteResult(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)}.
|
* {@link #siteResult(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)}.
|
||||||
*
|
*
|
||||||
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
|
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
|
||||||
|
* @throws MPAlgorithmException An internal system or algorithm error has occurred.
|
||||||
*/
|
*/
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public String siteState(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
|
public String siteState(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ public final class Utilities {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String ifNotNullOrEmptyElse(@Nullable final String value, @Nonnull final String emptyValue) {
|
||||||
|
if ((value == null) || value.isEmpty())
|
||||||
|
return emptyValue;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public static <T, R> R ifNotNullElse(@Nullable final T value, final Function<T, R> consumer, @Nonnull final R nullValue) {
|
public static <T, R> R ifNotNullElse(@Nullable final T value, final Function<T, R> consumer, @Nonnull final R nullValue) {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ shadowJar {
|
|||||||
storepass: System.getenv( 'STORE_PW' ),
|
storepass: System.getenv( 'STORE_PW' ),
|
||||||
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
|
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
|
||||||
preservelastmodified: 'true',
|
preservelastmodified: 'true',
|
||||||
destdir: '.' )
|
signedJar: "${rootDir}/../public/site/${project.name}-${project.version}.jar" )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package com.lyndir.masterpassword.gui;
|
package com.lyndir.masterpassword.gui;
|
||||||
|
|
||||||
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
||||||
|
import com.lyndir.masterpassword.model.MPConfig;
|
||||||
import com.lyndir.masterpassword.model.MPModelConstants;
|
import com.lyndir.masterpassword.model.MPModelConstants;
|
||||||
|
|
||||||
|
|
||||||
@@ -26,15 +27,32 @@ import com.lyndir.masterpassword.model.MPModelConstants;
|
|||||||
* @author lhunath, 2014-08-31
|
* @author lhunath, 2014-08-31
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("CallToSystemGetenv")
|
@SuppressWarnings("CallToSystemGetenv")
|
||||||
public class MPConfig {
|
public class MPGuiConfig extends MPConfig {
|
||||||
|
|
||||||
private static final MPConfig instance = new MPConfig();
|
public static MPGuiConfig get() {
|
||||||
|
return get( MPGuiConfig.class );
|
||||||
public static MPConfig get() {
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Boolean checkForUpdates;
|
||||||
|
Boolean stayResident;
|
||||||
|
|
||||||
public boolean checkForUpdates() {
|
public boolean checkForUpdates() {
|
||||||
return ConversionUtils.toBoolean( System.getenv( MPModelConstants.env_checkUpdates ) ).orElse( true );
|
return (checkForUpdates != null)? checkForUpdates:
|
||||||
|
ConversionUtils.toBoolean( System.getenv( MPModelConstants.env_checkUpdates ) ).orElse( true );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCheckForUpdates(final boolean checkForUpdates) {
|
||||||
|
this.checkForUpdates = checkForUpdates;
|
||||||
|
MasterPassword.get().updateCheck();
|
||||||
|
setChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean stayResident() {
|
||||||
|
return (stayResident != null)? stayResident: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStayResident(final boolean stayResident) {
|
||||||
|
this.stayResident = stayResident;
|
||||||
|
setChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,10 +46,10 @@ import javax.swing.*;
|
|||||||
public final class MasterPassword {
|
public final class MasterPassword {
|
||||||
|
|
||||||
@SuppressWarnings("UnusedDeclaration")
|
@SuppressWarnings("UnusedDeclaration")
|
||||||
private static final Logger logger = Logger.get( MasterPassword.class );
|
private static final Logger logger = Logger.get( MasterPassword.class );
|
||||||
|
|
||||||
private static final MasterPassword instance = new MasterPassword();
|
private static final MasterPassword instance = new MasterPassword();
|
||||||
|
|
||||||
|
private final Provider keyMaster = Provider.getCurrentProvider( true );
|
||||||
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
|
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -97,7 +97,29 @@ public final class MasterPassword {
|
|||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkUpdate() {
|
public static void main(final String... args) {
|
||||||
|
//Thread.setDefaultUncaughtExceptionHandler(
|
||||||
|
// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) );
|
||||||
|
|
||||||
|
// Set the system look & feel, if available.
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
|
||||||
|
}
|
||||||
|
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and open the UI.
|
||||||
|
get().open();
|
||||||
|
|
||||||
|
// UI features.
|
||||||
|
get().updateResidence();
|
||||||
|
get().updateCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateCheck() {
|
||||||
|
if (!MPGuiConfig.get().checkForUpdates())
|
||||||
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String implementationVersion = version();
|
String implementationVersion = version();
|
||||||
String latestVersion = new ByteSource() {
|
String latestVersion = new ByteSource() {
|
||||||
@@ -127,26 +149,10 @@ public final class MasterPassword {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(final String... args) {
|
public void updateResidence() {
|
||||||
//Thread.setDefaultUncaughtExceptionHandler(
|
Platform.get().installAppForegroundHandler( get()::open );
|
||||||
// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) );
|
Platform.get().installAppReopenHandler( get()::open );
|
||||||
|
keyMaster.register( MPGuiConstants.ui_hotkey, hotKey -> get().open() );
|
||||||
// Try and set the system look & feel, if available.
|
|
||||||
try {
|
|
||||||
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
|
|
||||||
Platform.get().installAppForegroundHandler( get()::open );
|
|
||||||
Platform.get().installAppReopenHandler( get()::open );
|
|
||||||
Provider.getCurrentProvider( true ).register( MPGuiConstants.ui_hotkey, hotKey -> get().open() );
|
|
||||||
}
|
|
||||||
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a platform-specific GUI and open it.
|
|
||||||
get().open();
|
|
||||||
|
|
||||||
// Check online to see if this version has been superseded.
|
|
||||||
if (MPConfig.get().checkForUpdates())
|
|
||||||
get().checkUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("InterfaceMayBeAnnotatedFunctional")
|
@SuppressWarnings("InterfaceMayBeAnnotatedFunctional")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.lyndir.masterpassword.gui.model;
|
package com.lyndir.masterpassword.gui.model;
|
||||||
|
|
||||||
import com.lyndir.masterpassword.model.*;
|
import com.lyndir.masterpassword.model.*;
|
||||||
import com.lyndir.masterpassword.model.impl.*;
|
import com.lyndir.masterpassword.model.impl.MPBasicSite;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
|
||||||
@@ -19,4 +19,14 @@ public class MPNewSite extends MPBasicSite<MPUser<?>, MPQuestion> {
|
|||||||
public MPQuestion addQuestion(final String keyword) {
|
public MPQuestion addQuestion(final String keyword) {
|
||||||
throw new UnsupportedOperationException( "Cannot add a question to a site that hasn't been created yet." );
|
throw new UnsupportedOperationException( "Cannot add a question to a site that hasn't been created yet." );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <S extends MPSite<?>> S addTo(final MPUser<S> user) {
|
||||||
|
S site = user.addSite( getSiteName() );
|
||||||
|
site.setAlgorithm( getAlgorithm() );
|
||||||
|
site.setCounter( getCounter() );
|
||||||
|
site.setLoginType( getLoginType() );
|
||||||
|
site.setResultType( getResultType() );
|
||||||
|
|
||||||
|
return site;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
package com.lyndir.masterpassword.gui.util;
|
package com.lyndir.masterpassword.gui.util;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.*;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@@ -15,6 +21,8 @@ import javax.swing.event.ListSelectionListener;
|
|||||||
public class CollectionListModel<E> extends AbstractListModel<E>
|
public class CollectionListModel<E> extends AbstractListModel<E>
|
||||||
implements ComboBoxModel<E>, ListSelectionListener, Selectable<E, CollectionListModel<E>> {
|
implements ComboBoxModel<E>, ListSelectionListener, Selectable<E, CollectionListModel<E>> {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.get( CollectionListModel.class );
|
||||||
|
|
||||||
private final List<E> model = new LinkedList<>();
|
private final List<E> model = new LinkedList<>();
|
||||||
@Nullable
|
@Nullable
|
||||||
private JList<E> list;
|
private JList<E> list;
|
||||||
@@ -51,16 +59,11 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
|||||||
* This operation will mutate the internal model to reflect the given model.
|
* This operation will mutate the internal model to reflect the given model.
|
||||||
* The given model will remain untouched and independent from this object.
|
* The given model will remain untouched and independent from this object.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "unchecked", "SuspiciousToArrayCall" })
|
@SuppressWarnings({ "Guava", "AssignmentToForLoopParameter" })
|
||||||
public synchronized void set(final Collection<? extends E> elements) {
|
public synchronized void set(final Iterable<? extends E> elements) {
|
||||||
set( (E[]) elements.toArray( new Object[0] ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("AssignmentToForLoopParameter")
|
|
||||||
public synchronized void set(final E... elements) {
|
|
||||||
ListIterator<E> oldIt = model.listIterator();
|
ListIterator<E> oldIt = model.listIterator();
|
||||||
for (int from = 0; oldIt.hasNext(); ++from) {
|
for (int from = 0; oldIt.hasNext(); ++from) {
|
||||||
int to = Arrays.binarySearch( elements, oldIt.next() );
|
int to = Iterables.indexOf( elements, Predicates.equalTo( oldIt.next() ) );
|
||||||
|
|
||||||
if (to != from) {
|
if (to != from) {
|
||||||
oldIt.remove();
|
oldIt.remove();
|
||||||
@@ -69,33 +72,46 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int to = 0; to < elements.length; ++to) {
|
int to = 0;
|
||||||
E newSite = elements[to];
|
for (final E newSite : elements) {
|
||||||
|
|
||||||
if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) {
|
if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) {
|
||||||
model.add( to, newSite );
|
model.add( to, newSite );
|
||||||
fireIntervalAdded( this, to, to );
|
fireIntervalAdded( this, to, to );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
++to;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((selectedItem == null) || !model.contains( selectedItem ))
|
if ((selectedItem == null) || !model.contains( selectedItem ))
|
||||||
setSelectedItem( getElementAt( 0 ) );
|
selectItem( getElementAt( 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public final synchronized void set(final E... elements) {
|
||||||
|
set( ImmutableList.copyOf( elements ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings({ "unchecked", "SuspiciousMethodCalls" })
|
@Deprecated
|
||||||
public synchronized void setSelectedItem(@Nullable final Object newSelectedItem) {
|
@SuppressWarnings("unchecked")
|
||||||
if (!Objects.equals( selectedItem, newSelectedItem )) {
|
public synchronized void setSelectedItem(@Nullable final Object/* E */ newSelectedItem) {
|
||||||
selectedItem = (E) newSelectedItem;
|
selectItem( (E) newSelectedItem );
|
||||||
|
}
|
||||||
|
|
||||||
fireContentsChanged( this, -1, -1 );
|
public synchronized CollectionListModel<E> selectItem(@Nullable final E newSelectedItem) {
|
||||||
//noinspection ObjectEquality
|
if (Objects.equals( selectedItem, newSelectedItem ))
|
||||||
if ((list != null) && (list.getModel() == this))
|
return this;
|
||||||
list.setSelectedValue( selectedItem, true );
|
|
||||||
|
|
||||||
if (selectionConsumer != null)
|
selectedItem = newSelectedItem;
|
||||||
selectionConsumer.accept( selectedItem );
|
|
||||||
}
|
fireContentsChanged( this, -1, -1 );
|
||||||
|
//noinspection ObjectEquality
|
||||||
|
if ((list != null) && (list.getModel() == this))
|
||||||
|
list.setSelectedValue( selectedItem, true );
|
||||||
|
|
||||||
|
if (selectionConsumer != null)
|
||||||
|
selectionConsumer.accept( selectedItem );
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -104,11 +120,6 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
|||||||
return selectedItem;
|
return selectedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CollectionListModel<E> select(final E selectedItem) {
|
|
||||||
setSelectedItem( selectedItem );
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void registerList(final JList<E> list) {
|
public synchronized void registerList(final JList<E> list) {
|
||||||
// TODO: This class should probably implement ListSelectionModel instead.
|
// TODO: This class should probably implement ListSelectionModel instead.
|
||||||
if (this.list != null)
|
if (this.list != null)
|
||||||
@@ -131,7 +142,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
|||||||
@Override
|
@Override
|
||||||
public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
|
public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
|
||||||
this.selectionConsumer = null;
|
this.selectionConsumer = null;
|
||||||
setSelectedItem( selectedItem );
|
selectItem( selectedItem );
|
||||||
|
|
||||||
return selection( selectionConsumer );
|
return selection( selectionConsumer );
|
||||||
}
|
}
|
||||||
@@ -139,7 +150,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
|||||||
@Override
|
@Override
|
||||||
public synchronized void valueChanged(final ListSelectionEvent event) {
|
public synchronized void valueChanged(final ListSelectionEvent event) {
|
||||||
//noinspection ObjectEquality
|
//noinspection ObjectEquality
|
||||||
if (!event.getValueIsAdjusting() && (event.getSource() == list) && (list.getModel() == this)) {
|
if (!event.getValueIsAdjusting() && (event.getSource() == list) && (checkNotNull( list ).getModel() == this)) {
|
||||||
selectedItem = list.getSelectedValue();
|
selectedItem = list.getSelectedValue();
|
||||||
|
|
||||||
if (selectionConsumer != null)
|
if (selectionConsumer != null)
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ public abstract class Components {
|
|||||||
|
|
||||||
private static final Logger logger = Logger.get( Components.class );
|
private static final Logger logger = Logger.get( Components.class );
|
||||||
|
|
||||||
public static final float TEXT_SIZE_HEADING = 19f;
|
public static final int TEXT_SIZE_HEADING = 19;
|
||||||
public static final float TEXT_SIZE_CONTROL = 13f;
|
public static final int TEXT_SIZE_CONTROL = 13;
|
||||||
public static final int SIZE_MARGIN = 12;
|
public static final int SIZE_MARGIN = 12;
|
||||||
public static final int SIZE_PADDING = 8;
|
public static final int SIZE_PADDING = 8;
|
||||||
|
|
||||||
public static GradientPanel panel(final Component... components) {
|
public static GradientPanel panel(final Component... components) {
|
||||||
GradientPanel panel = panel( BoxLayout.LINE_AXIS, null, components );
|
GradientPanel panel = panel( BoxLayout.LINE_AXIS, null, components );
|
||||||
@@ -243,6 +243,9 @@ public abstract class Components {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
Dimension cellSize = getCellRenderer().getListCellRendererComponent( this, null, 0, false, false ).getPreferredSize();
|
||||||
|
setFixedCellWidth( cellSize.width );
|
||||||
|
setFixedCellHeight( cellSize.height );
|
||||||
|
|
||||||
if (model instanceof CollectionListModel)
|
if (model instanceof CollectionListModel)
|
||||||
((CollectionListModel<E>) model).registerList( this );
|
((CollectionListModel<E>) model).registerList( this );
|
||||||
@@ -423,10 +426,18 @@ public abstract class Components {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static JCheckBox checkBox(final String label) {
|
public static JCheckBox checkBox(final String label) {
|
||||||
|
return checkBox( label, false, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JCheckBox checkBox(final String label, final boolean selected, @Nullable final Consumer<Boolean> selectionConsumer) {
|
||||||
return new JCheckBox( label ) {
|
return new JCheckBox( label ) {
|
||||||
{
|
{
|
||||||
setBackground( null );
|
setBackground( null );
|
||||||
setAlignmentX( LEFT_ALIGNMENT );
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
|
setSelected( selected );
|
||||||
|
|
||||||
|
if (selectionConsumer != null)
|
||||||
|
addItemListener( e -> selectionConsumer.accept( isSelected() ) );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,8 +87,11 @@ public class DocumentModel implements Selectable<String, DocumentModel> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DocumentModel selection(@Nullable final String selectedItem, @Nullable final Consumer<String> selectionConsumer) {
|
public DocumentModel selection(@Nullable final String selectedItem, @Nullable final Consumer<String> selectionConsumer) {
|
||||||
selection( selectionConsumer );
|
|
||||||
setText( selectedItem );
|
setText( selectedItem );
|
||||||
|
selection( selectionConsumer );
|
||||||
|
|
||||||
|
if (selectionConsumer != null)
|
||||||
|
selectionConsumer.accept( selectedItem );
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,12 @@ package com.lyndir.masterpassword.gui.util;
|
|||||||
|
|
||||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||||
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
import com.google.common.io.Resources;
|
import com.google.common.io.Resources;
|
||||||
import com.google.common.util.concurrent.*;
|
import com.google.common.util.concurrent.*;
|
||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.masterpassword.MPIdenticon;
|
import com.lyndir.masterpassword.MPIdenticon;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.ref.SoftReference;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@@ -73,15 +70,6 @@ public abstract class Res {
|
|||||||
return job( job, 0, TimeUnit.MILLISECONDS );
|
return job( job, 0, TimeUnit.MILLISECONDS );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <V> void job(final Callable<V> job, final Consumer<V> callback) {
|
|
||||||
Futures.addCallback( job( job, 0, TimeUnit.MILLISECONDS ), new FailableCallback<V>( logger ) {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(@Nullable final V result) {
|
|
||||||
callback.accept( result );
|
|
||||||
}
|
|
||||||
}, uiExecutor() );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <V> ListenableFuture<V> job(final Callable<V> job, final long delay, final TimeUnit timeUnit) {
|
public static <V> ListenableFuture<V> job(final Callable<V> job, final long delay, final TimeUnit timeUnit) {
|
||||||
return jobExecutor.schedule( job, delay, timeUnit );
|
return jobExecutor.schedule( job, delay, timeUnit );
|
||||||
}
|
}
|
||||||
@@ -90,6 +78,15 @@ public abstract class Res {
|
|||||||
ui( true, job );
|
ui( true, job );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <V> void ui(final ListenableFuture<V> future, final Consumer<V> job) {
|
||||||
|
Futures.addCallback( future, new FailableCallback<V>( logger ) {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(@Nullable final V result) {
|
||||||
|
job.accept( result );
|
||||||
|
}
|
||||||
|
}, uiExecutor() );
|
||||||
|
}
|
||||||
|
|
||||||
public static void ui(final boolean immediate, final Runnable job) {
|
public static void ui(final boolean immediate, final Runnable job) {
|
||||||
uiExecutor( immediate ).execute( job );
|
uiExecutor( immediate ).execute( job );
|
||||||
}
|
}
|
||||||
@@ -184,88 +181,67 @@ public abstract class Res {
|
|||||||
|
|
||||||
public static final class Fonts {
|
public static final class Fonts {
|
||||||
|
|
||||||
public Font emoticonsFont(final float size) {
|
public Font emoticonsFont(final int size) {
|
||||||
return emoticonsRegular().deriveFont( size );
|
return MPFont.emoticonsRegular.get( size );
|
||||||
}
|
}
|
||||||
|
|
||||||
public Font controlFont(final float size) {
|
public Font controlFont(final int size) {
|
||||||
return exoRegular().deriveFont( size );
|
return MPFont.exoRegular.get( size );
|
||||||
}
|
}
|
||||||
|
|
||||||
public Font valueFont(final float size) {
|
public Font valueFont(final int size) {
|
||||||
return sourceSansProRegular().deriveFont( size );
|
return MPFont.sourceSansProRegular.get( size );
|
||||||
}
|
}
|
||||||
|
|
||||||
public Font bigValueFont(final float size) {
|
public Font bigValueFont(final int size) {
|
||||||
return sourceSansProBlack().deriveFont( size );
|
return MPFont.sourceSansProBlack.get( size );
|
||||||
}
|
}
|
||||||
|
|
||||||
public Font emoticonsRegular() {
|
private enum MPFont {
|
||||||
return font( "fonts/Emoticons-Regular.otf" );
|
emoticonsRegular( "Emoticons", "fonts/Emoticons-Regular.otf" ),
|
||||||
}
|
sourceCodeProRegular( "Source Code Pro", "fonts/SourceCodePro-Regular.otf" ),
|
||||||
|
sourceCodeProBlack( "Source Code Pro Bold", "fonts/SourceCodePro-Bold.otf" ),
|
||||||
|
sourceSansProRegular( "Source Sans Pro", "fonts/SourceSansPro-Regular.otf" ),
|
||||||
|
sourceSansProBlack( "Source Sans Pro Bold", "fonts/SourceSansPro-Bold.otf" ),
|
||||||
|
exoBold( "Exo 2.0 Bold", "fonts/Exo2.0-Bold.otf" ),
|
||||||
|
exoExtraBold( "Exo 2.0 Extra Bold", "fonts/Exo2.0-ExtraBold.otf" ),
|
||||||
|
exoRegular( "Exo 2.0", "fonts/Exo2.0-Regular.otf" ),
|
||||||
|
exoThin( "Exo 2.0 Thin", "fonts/Exo2.0-Thin.otf" ),
|
||||||
|
arimoBold( "Arimo Bold", "fonts/Arimo-Bold.ttf" ),
|
||||||
|
arimoBoldItalic( "Arimo Bold Italic", "fonts/Arimo-BoldItalic.ttf" ),
|
||||||
|
arimoItalic( "Arimo Italic", "fonts/Arimo-Italic.ttf" ),
|
||||||
|
arimoRegular( "Arimo", "fonts/Arimo-Regular.ttf" );
|
||||||
|
|
||||||
public Font sourceCodeProRegular() {
|
private final String fontName;
|
||||||
return font( "fonts/SourceCodePro-Regular.otf" );
|
private final String resourceName;
|
||||||
}
|
private boolean registered;
|
||||||
|
|
||||||
public Font sourceCodeProBlack() {
|
MPFont(final String fontName, final String resourceName) {
|
||||||
return font( "fonts/SourceCodePro-Bold.otf" );
|
this.fontName = fontName;
|
||||||
}
|
this.resourceName = resourceName;
|
||||||
|
}
|
||||||
|
|
||||||
public Font sourceSansProRegular() {
|
Font get(final int size) {
|
||||||
return font( "fonts/SourceSansPro-Regular.otf" );
|
return get( Font.PLAIN, size );
|
||||||
}
|
}
|
||||||
|
|
||||||
public Font sourceSansProBlack() {
|
Font get(final int style, final int size) {
|
||||||
return font( "fonts/SourceSansPro-Bold.otf" );
|
if (!registered)
|
||||||
}
|
register();
|
||||||
|
|
||||||
public Font exoBold() {
|
return new Font( fontName, style, size );
|
||||||
return font( "fonts/Exo2.0-Bold.otf" );
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public Font exoExtraBold() {
|
private void register() {
|
||||||
return font( "fonts/Exo2.0-ExtraBold.otf" );
|
|
||||||
}
|
|
||||||
|
|
||||||
public Font exoRegular() {
|
|
||||||
return font( "fonts/Exo2.0-Regular.otf" );
|
|
||||||
}
|
|
||||||
|
|
||||||
public Font exoThin() {
|
|
||||||
return font( "fonts/Exo2.0-Thin.otf" );
|
|
||||||
}
|
|
||||||
|
|
||||||
public Font arimoBold() {
|
|
||||||
return font( "fonts/Arimo-Bold.ttf" );
|
|
||||||
}
|
|
||||||
|
|
||||||
public Font arimoBoldItalic() {
|
|
||||||
return font( "fonts/Arimo-BoldItalic.ttf" );
|
|
||||||
}
|
|
||||||
|
|
||||||
public Font arimoItalic() {
|
|
||||||
return font( "fonts/Arimo-Italic.ttf" );
|
|
||||||
}
|
|
||||||
|
|
||||||
public Font arimoRegular() {
|
|
||||||
return font( "fonts/Arimo-Regular.ttf" );
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Font font(@NonNls final String fontResourceName) {
|
|
||||||
Map<String, SoftReference<Font>> fontsByResourceName = Maps.newHashMap();
|
|
||||||
SoftReference<Font> fontRef = fontsByResourceName.get( fontResourceName );
|
|
||||||
Font font = (fontRef == null)? null: fontRef.get();
|
|
||||||
if (font == null)
|
|
||||||
try {
|
try {
|
||||||
fontsByResourceName.put( fontResourceName, new SoftReference<>(
|
Font font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( resourceName ).openStream() );
|
||||||
font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) );
|
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont( font );
|
||||||
|
registered = true;
|
||||||
}
|
}
|
||||||
catch (final FontFormatException | IOException e) {
|
catch (final FontFormatException | IOException e) {
|
||||||
throw logger.bug( e );
|
throw logger.bug( e );
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return font;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,24 +17,47 @@ public class ApplePlatform implements IPlatform {
|
|||||||
private static final Application application = Preconditions.checkNotNull(
|
private static final Application application = Preconditions.checkNotNull(
|
||||||
Application.getApplication(), "Not an Apple Java application." );
|
Application.getApplication(), "Not an Apple Java application." );
|
||||||
|
|
||||||
|
private AppForegroundListener appForegroundHandler;
|
||||||
|
private AppReOpenedListener appReopenHandler;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean installAppForegroundHandler(final Runnable handler) {
|
public boolean installAppForegroundHandler(final Runnable handler) {
|
||||||
application.addAppEventListener( new AppForegroundListener() {
|
if (appForegroundHandler == null)
|
||||||
@Override
|
application.addAppEventListener( appForegroundHandler = new AppForegroundListener() {
|
||||||
public void appMovedToBackground(final AppEvent.AppForegroundEvent e) {
|
@Override
|
||||||
}
|
public void appMovedToBackground(final AppEvent.AppForegroundEvent e) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void appRaisedToForeground(final AppEvent.AppForegroundEvent e) {
|
public void appRaisedToForeground(final AppEvent.AppForegroundEvent e) {
|
||||||
handler.run();
|
handler.run();
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeAppForegroundHandler() {
|
||||||
|
if (appForegroundHandler == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
application.removeAppEventListener( appForegroundHandler );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean installAppReopenHandler(final Runnable handler) {
|
public boolean installAppReopenHandler(final Runnable handler) {
|
||||||
application.addAppEventListener( (AppReOpenedListener) e -> handler.run() );
|
application.addAppEventListener( appReopenHandler = e -> handler.run() );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeAppReopenHandler() {
|
||||||
|
if (appReopenHandler == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
application.removeAppEventListener( appReopenHandler );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,21 @@ public class BasePlatform implements IPlatform {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeAppForegroundHandler() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean installAppReopenHandler(final Runnable handler) {
|
public boolean installAppReopenHandler(final Runnable handler) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeAppReopenHandler() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean requestForeground() {
|
public boolean requestForeground() {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ public interface IPlatform {
|
|||||||
|
|
||||||
boolean installAppForegroundHandler(Runnable handler);
|
boolean installAppForegroundHandler(Runnable handler);
|
||||||
|
|
||||||
|
boolean removeAppForegroundHandler();
|
||||||
|
|
||||||
boolean installAppReopenHandler(Runnable handler);
|
boolean installAppReopenHandler(Runnable handler);
|
||||||
|
|
||||||
|
boolean removeAppReopenHandler();
|
||||||
|
|
||||||
boolean requestForeground();
|
boolean requestForeground();
|
||||||
|
|
||||||
boolean show(File file);
|
boolean show(File file);
|
||||||
|
|||||||
@@ -17,24 +17,49 @@ public class JDK9Platform implements IPlatform {
|
|||||||
private static final Logger logger = Logger.get( JDK9Platform.class );
|
private static final Logger logger = Logger.get( JDK9Platform.class );
|
||||||
private static final Desktop desktop = Desktop.getDesktop();
|
private static final Desktop desktop = Desktop.getDesktop();
|
||||||
|
|
||||||
|
private AppForegroundListener appForegroundHandler;
|
||||||
|
private AppReopenedListener appReopenHandler;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean installAppForegroundHandler(final Runnable handler) {
|
public boolean installAppForegroundHandler(final Runnable handler) {
|
||||||
desktop.addAppEventListener( new AppForegroundListener() {
|
if (appForegroundHandler == null)
|
||||||
@Override
|
desktop.addAppEventListener( appForegroundHandler = new AppForegroundListener() {
|
||||||
public void appRaisedToForeground(final AppForegroundEvent e) {
|
@Override
|
||||||
handler.run();
|
public void appRaisedToForeground(final AppForegroundEvent e) {
|
||||||
}
|
handler.run();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void appMovedToBackground(final AppForegroundEvent e) {
|
public void appMovedToBackground(final AppForegroundEvent e) {
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeAppForegroundHandler() {
|
||||||
|
if (appForegroundHandler == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
desktop.removeAppEventListener( appForegroundHandler );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean installAppReopenHandler(final Runnable handler) {
|
public boolean installAppReopenHandler(final Runnable handler) {
|
||||||
desktop.addAppEventListener( (AppReopenedListener) e -> handler.run() );
|
if (appReopenHandler == null)
|
||||||
|
desktop.addAppEventListener( appReopenHandler = e -> handler.run() );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeAppReopenHandler() {
|
||||||
|
if (appReopenHandler == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
desktop.removeAppEventListener( appReopenHandler );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUserSelected(@Nullable final MPUser<?> user) {
|
public void onUserSelected(@Nullable final MPUser<?> user) {
|
||||||
usersModel.setSelectedItem( user );
|
usersModel.selectItem( user );
|
||||||
avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
|
avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.lyndir.masterpassword.gui.view;
|
package com.lyndir.masterpassword.gui.view;
|
||||||
|
|
||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
|
import com.lyndir.masterpassword.gui.MPGuiConfig;
|
||||||
import com.lyndir.masterpassword.gui.util.Components;
|
import com.lyndir.masterpassword.gui.util.Components;
|
||||||
import com.lyndir.masterpassword.gui.util.Res;
|
import com.lyndir.masterpassword.gui.util.Res;
|
||||||
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
|
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.ComponentAdapter;
|
import java.awt.event.*;
|
||||||
import java.awt.event.ComponentEvent;
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.border.BevelBorder;
|
import javax.swing.border.BevelBorder;
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ public class MasterPasswordFrame extends JFrame {
|
|||||||
|
|
||||||
private static final Logger logger = Logger.get( MasterPasswordFrame.class );
|
private static final Logger logger = Logger.get( MasterPasswordFrame.class );
|
||||||
|
|
||||||
private final UserContentPanel userContent;
|
private final UserContentPanel userContent;
|
||||||
|
|
||||||
@SuppressWarnings("MagicNumber")
|
@SuppressWarnings("MagicNumber")
|
||||||
public MasterPasswordFrame() {
|
public MasterPasswordFrame() {
|
||||||
@@ -39,6 +39,7 @@ public class MasterPasswordFrame extends JFrame {
|
|||||||
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
|
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
|
||||||
|
|
||||||
addComponentListener( new ComponentHandler() );
|
addComponentListener( new ComponentHandler() );
|
||||||
|
addWindowListener( new WindowHandler() );
|
||||||
setPreferredSize( new Dimension( 800, 560 ) );
|
setPreferredSize( new Dimension( 800, 560 ) );
|
||||||
setDefaultCloseOperation( DISPOSE_ON_CLOSE );
|
setDefaultCloseOperation( DISPOSE_ON_CLOSE );
|
||||||
pack();
|
pack();
|
||||||
@@ -55,4 +56,14 @@ public class MasterPasswordFrame extends JFrame {
|
|||||||
userContent.transferFocus();
|
userContent.transferFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class WindowHandler extends WindowAdapter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void windowClosed(final WindowEvent e) {
|
||||||
|
if (!MPGuiConfig.get().stayResident())
|
||||||
|
System.exit( 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,16 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
|||||||
import com.google.common.base.*;
|
import com.google.common.base.*;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.primitives.UnsignedInteger;
|
import com.google.common.primitives.UnsignedInteger;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
|
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.gui.MPGuiConstants;
|
import com.lyndir.masterpassword.gui.*;
|
||||||
import com.lyndir.masterpassword.gui.MasterPassword;
|
|
||||||
import com.lyndir.masterpassword.gui.model.*;
|
import com.lyndir.masterpassword.gui.model.*;
|
||||||
import com.lyndir.masterpassword.gui.util.*;
|
import com.lyndir.masterpassword.gui.util.*;
|
||||||
import com.lyndir.masterpassword.gui.util.Platform;
|
import com.lyndir.masterpassword.gui.util.Platform;
|
||||||
import com.lyndir.masterpassword.model.*;
|
import com.lyndir.masterpassword.model.*;
|
||||||
import com.lyndir.masterpassword.model.impl.*;
|
import com.lyndir.masterpassword.model.impl.*;
|
||||||
import com.lyndir.masterpassword.util.Utilities;
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.datatransfer.StringSelection;
|
import java.awt.datatransfer.StringSelection;
|
||||||
import java.awt.datatransfer.Transferable;
|
import java.awt.datatransfer.Transferable;
|
||||||
@@ -28,7 +27,7 @@ import java.util.*;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.regex.Pattern;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
@@ -48,6 +47,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
private static final Logger logger = Logger.get( UserContentPanel.class );
|
private static final Logger logger = Logger.get( UserContentPanel.class );
|
||||||
private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
|
private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
|
||||||
private static final KeyStroke copyLoginKeyStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK );
|
private static final KeyStroke copyLoginKeyStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK );
|
||||||
|
private static final Pattern EACH_CHARACTER = Pattern.compile( "." );
|
||||||
|
|
||||||
private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(),
|
private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(),
|
||||||
"Add a new user to Master Password." );
|
"Add a new user to Master Password." );
|
||||||
@@ -406,6 +406,9 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
Res.job( () -> {
|
Res.job( () -> {
|
||||||
try {
|
try {
|
||||||
user.authenticate( masterPassword );
|
user.authenticate( masterPassword );
|
||||||
|
|
||||||
|
if (user instanceof MPFileUser)
|
||||||
|
((MPFileUser) user).migrateTo( MPMarshalFormat.DEFAULT );
|
||||||
}
|
}
|
||||||
catch (final MPIncorrectMasterPasswordException e) {
|
catch (final MPIncorrectMasterPasswordException e) {
|
||||||
logger.wrn( e, "During user authentication for: %s", user );
|
logger.wrn( e, "During user authentication for: %s", user );
|
||||||
@@ -461,7 +464,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener {
|
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener, KeyEventDispatcher {
|
||||||
|
|
||||||
private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(),
|
private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(),
|
||||||
"Show user preferences." );
|
"Show user preferences." );
|
||||||
@@ -479,16 +482,18 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
"Delete the site from the user." );
|
"Delete the site from the user." );
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private final MPUser<?> user;
|
private final MPUser<?> user;
|
||||||
private final JLabel passwordLabel;
|
private final JLabel resultLabel;
|
||||||
private final JLabel passwordField;
|
private final JLabel resultField;
|
||||||
private final JLabel answerLabel;
|
private final JLabel answerLabel;
|
||||||
private final JLabel answerField;
|
private final JLabel answerField;
|
||||||
private final JLabel queryLabel;
|
private final JLabel queryLabel;
|
||||||
private final JTextField queryField;
|
private final JTextField queryField;
|
||||||
private final CollectionListModel<MPSite<?>> sitesModel;
|
private final CollectionListModel<MPQuery.Result<? extends MPSite<?>>> sitesModel;
|
||||||
private final JList<MPSite<?>> sitesList;
|
private final CollectionListModel<MPQuery.Result<? extends MPQuestion>> questionsModel;
|
||||||
|
private final JList<MPQuery.Result<? extends MPSite<?>>> sitesList;
|
||||||
|
|
||||||
|
private boolean showLogin;
|
||||||
private Future<?> updateSitesJob;
|
private Future<?> updateSitesJob;
|
||||||
|
|
||||||
private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) {
|
private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) {
|
||||||
@@ -517,13 +522,14 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
answerField = Components.heading( SwingConstants.CENTER );
|
answerField = Components.heading( SwingConstants.CENTER );
|
||||||
answerField.setForeground( Res.colors().highlightFg() );
|
answerField.setForeground( Res.colors().highlightFg() );
|
||||||
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
||||||
|
questionsModel = new CollectionListModel<MPQuery.Result<? extends MPQuestion>>().selection( this::showQuestionItem );
|
||||||
|
|
||||||
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
|
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
|
||||||
|
|
||||||
add( passwordLabel = Components.label( SwingConstants.CENTER ) );
|
add( resultLabel = Components.label( SwingConstants.CENTER ) );
|
||||||
add( passwordField = Components.heading( SwingConstants.CENTER ) );
|
add( resultField = Components.heading( SwingConstants.CENTER ) );
|
||||||
passwordField.setForeground( Res.colors().highlightFg() );
|
resultField.setForeground( Res.colors().highlightFg() );
|
||||||
passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
resultField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
||||||
add( Box.createGlue() );
|
add( Box.createGlue() );
|
||||||
add( Components.strut() );
|
add( Components.strut() );
|
||||||
|
|
||||||
@@ -538,8 +544,12 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
add( Components.strut() );
|
add( Components.strut() );
|
||||||
|
|
||||||
add( Components.scrollPane( sitesList = Components.list(
|
add( Components.scrollPane( sitesList = Components.list(
|
||||||
sitesModel = new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ),
|
sitesModel = new CollectionListModel<MPQuery.Result<? extends MPSite<?>>>().selection( this::showSiteItem ),
|
||||||
this::getSiteDescription ) ) );
|
this::getSiteDescription ) ) );
|
||||||
|
sitesList.registerKeyboardAction( this::useSite, KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ),
|
||||||
|
JComponent.WHEN_FOCUSED );
|
||||||
|
sitesList.registerKeyboardAction( this::useSite, KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK ),
|
||||||
|
JComponent.WHEN_FOCUSED );
|
||||||
add( Components.strut() );
|
add( Components.strut() );
|
||||||
|
|
||||||
add( Components.label( strf(
|
add( Components.label( strf(
|
||||||
@@ -549,27 +559,42 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
KeyEvent.getKeyText( copyLoginKeyStroke.getKeyCode() ) ) ) );
|
KeyEvent.getKeyText( copyLoginKeyStroke.getKeyCode() ) ) ) );
|
||||||
|
|
||||||
addHierarchyListener( e -> {
|
addHierarchyListener( e -> {
|
||||||
if (null != SwingUtilities.windowForComponent( this ))
|
if (HierarchyEvent.DISPLAYABILITY_CHANGED == (e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED)) {
|
||||||
user.addListener( this );
|
if (null != SwingUtilities.windowForComponent( this )) {
|
||||||
else
|
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher( this );
|
||||||
user.removeListener( this );
|
user.addListener( this );
|
||||||
|
} else {
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher( this );
|
||||||
|
user.removeListener( this );
|
||||||
|
}
|
||||||
|
}
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showUserPreferences() {
|
public void showUserPreferences() {
|
||||||
ImmutableList.Builder<Component> components = ImmutableList.builder();
|
ImmutableList.Builder<Component> components = ImmutableList.builder();
|
||||||
|
|
||||||
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
|
|
||||||
if (fileUser != null)
|
|
||||||
components.add( Components.label( "Default Password Type:" ),
|
|
||||||
Components.comboBox( MPResultType.values(), MPResultType::getLongName,
|
|
||||||
fileUser.getDefaultType(), fileUser::setDefaultType ),
|
|
||||||
Components.strut() );
|
|
||||||
|
|
||||||
components.add( Components.label( "Default Algorithm:" ),
|
components.add( Components.label( "Default Algorithm:" ),
|
||||||
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
|
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
|
||||||
user.getAlgorithm().version(),
|
user.getAlgorithm().version(), version -> user.setAlgorithm( version.getAlgorithm() ) ) );
|
||||||
version -> user.setAlgorithm( version.getAlgorithm() ) ) );
|
|
||||||
|
components.add( Components.label( "Default Password Type:" ),
|
||||||
|
Components.comboBox( MPResultType.values(), MPResultType::getLongName,
|
||||||
|
user.getPreferences().getDefaultType(), user.getPreferences()::setDefaultType ),
|
||||||
|
Components.strut() );
|
||||||
|
|
||||||
|
components.add( Components.checkBox( "Hide Passwords",
|
||||||
|
user.getPreferences().isHidePasswords(), user.getPreferences()::setHidePasswords ) );
|
||||||
|
|
||||||
|
components.add( new JSeparator() );
|
||||||
|
|
||||||
|
components.add( Components.checkBox( "Check For Updates",
|
||||||
|
MPGuiConfig.get().checkForUpdates(), MPGuiConfig.get()::setCheckForUpdates ) );
|
||||||
|
|
||||||
|
components.add( Components.checkBox( strf( "<html>Stay Resident (reactivate with <strong><code>%s+%s</code></strong>)",
|
||||||
|
InputEvent.getModifiersExText( MPGuiConstants.ui_hotkey.getModifiers() ),
|
||||||
|
KeyEvent.getKeyText( MPGuiConstants.ui_hotkey.getKeyCode() ) ),
|
||||||
|
MPGuiConfig.get().stayResident(), MPGuiConfig.get()::setStayResident ) );
|
||||||
|
|
||||||
Components.showDialog( this, user.getFullName(), new JOptionPane( Components.panel(
|
Components.showDialog( this, user.getFullName(), new JOptionPane( Components.panel(
|
||||||
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
|
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
|
||||||
@@ -580,7 +605,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void showSiteSettings() {
|
public void showSiteSettings() {
|
||||||
MPSite<?> site = sitesModel.getSelectedItem();
|
MPSite<?> site = getSite();
|
||||||
if (site == null)
|
if (site == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -596,14 +621,14 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
Components.strut() );
|
Components.strut() );
|
||||||
|
|
||||||
components.add( Components.label( "Password Type:" ),
|
components.add( Components.label( "Password Type:" ),
|
||||||
Components.comboBox( MPResultType.values(), type -> getTypeDescription(
|
Components.comboBox( MPResultType.values(), type ->
|
||||||
type, user.getDefaultType(), user.getAlgorithm().mpw_default_result_type() ),
|
getTypeDescription( type, user.getPreferences().getDefaultType() ),
|
||||||
site.getResultType(), site::setResultType ),
|
site.getResultType(), site::setResultType ),
|
||||||
Components.strut() );
|
Components.strut() );
|
||||||
|
|
||||||
components.add( Components.label( "Login Type:" ),
|
components.add( Components.label( "Login Type:" ),
|
||||||
Components.comboBox( MPResultType.values(), type -> getTypeDescription(
|
Components.comboBox( MPResultType.values(), type ->
|
||||||
type, user.getAlgorithm().mpw_default_login_type() ),
|
getTypeDescription( type, user.getAlgorithm().mpw_default_login_type() ),
|
||||||
site.getLoginType(), site::setLoginType ),
|
site.getLoginType(), site::setLoginType ),
|
||||||
Components.strut() );
|
Components.strut() );
|
||||||
|
|
||||||
@@ -627,22 +652,24 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void showSiteQuestions() {
|
public void showSiteQuestions() {
|
||||||
MPSite<?> site = sitesModel.getSelectedItem();
|
MPSite<?> site = getSite();
|
||||||
if (site == null)
|
if (site == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
CollectionListModel<MPQuestion> questionsModel = new CollectionListModel<MPQuestion>().selection( this::showQuestionResult );
|
JList<MPQuery.Result<? extends MPQuestion>> questionsList =
|
||||||
JList<MPQuestion> questionsList = Components.list(
|
Components.list( questionsModel, this::getQuestionDescription );
|
||||||
questionsModel, question -> Strings.isNullOrEmpty( question.getKeyword() )? "<site>": question.getKeyword() );
|
JTextField queryField = Components.textField( null, queryText -> Res.job( () -> {
|
||||||
JTextField queryField = Components.textField( null, query -> Res.job( () -> {
|
MPQuery query = new MPQuery( queryText );
|
||||||
Collection<MPQuestion> questions = new LinkedList<>( site.findQuestions( query ) );
|
Collection<MPQuery.Result<? extends MPQuestion>> questionItems = new LinkedList<MPQuery.Result<? extends MPQuestion>>(
|
||||||
if (questions.stream().noneMatch( question -> question.getKeyword().equalsIgnoreCase( query ) ))
|
query.find( site.getQuestions(), MPQuestion::getKeyword ) );
|
||||||
questions.add( new MPNewQuestion( site, Utilities.ifNotNullElse( query, "" ) ) );
|
|
||||||
|
|
||||||
Res.ui( () -> questionsModel.set( questions ) );
|
if (questionItems.stream().noneMatch( MPQuery.Result::isExact ))
|
||||||
|
questionItems.add( MPQuery.Result.allOf( new MPNewQuestion( site, query.getQuery() ), query.getQuery() ) );
|
||||||
|
|
||||||
|
Res.ui( () -> questionsModel.set( questionItems ) );
|
||||||
} ) );
|
} ) );
|
||||||
queryField.putClientProperty( "JTextField.variant", "search" );
|
queryField.putClientProperty( "JTextField.variant", "search" );
|
||||||
queryField.addActionListener( event -> useQuestion( questionsModel.getSelectedItem() ) );
|
queryField.addActionListener( this::useQuestion );
|
||||||
queryField.addKeyListener( new KeyAdapter() {
|
queryField.addKeyListener( new KeyAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void keyPressed(final KeyEvent event) {
|
public void keyPressed(final KeyEvent event) {
|
||||||
@@ -672,7 +699,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void showSiteValues() {
|
public void showSiteValues() {
|
||||||
MPSite<?> site = sitesModel.getSelectedItem();
|
MPSite<?> site = getSite();
|
||||||
if (site == null)
|
if (site == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -717,7 +744,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void showSiteKeys() {
|
public void showSiteKeys() {
|
||||||
MPSite<?> site = sitesModel.getSelectedItem();
|
MPSite<?> site = getSite();
|
||||||
if (site == null)
|
if (site == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -787,7 +814,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteSite() {
|
public void deleteSite() {
|
||||||
MPSite<?> site = sitesModel.getSelectedItem();
|
MPSite<?> site = getSite();
|
||||||
if (site == null)
|
if (site == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -797,61 +824,92 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
user.deleteSite( site );
|
user.deleteSite( site );
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getSiteDescription(@Nonnull final MPSite<?> site) {
|
private String getSiteDescription(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
|
||||||
|
MPSite<?> site = (item != null)? item.getValue(): null;
|
||||||
|
if (site == null)
|
||||||
|
return " ";
|
||||||
if (site instanceof MPNewSite)
|
if (site instanceof MPNewSite)
|
||||||
return strf( "<html><strong>%s</strong> <Add new site></html>", queryField.getText() );
|
return strf( "<html><strong>%s</strong> <Add new site></html>", item.getKeyAsHTML() );
|
||||||
|
|
||||||
ImmutableList.Builder<Object> parameters = ImmutableList.builder();
|
ImmutableList.Builder<Object> parameters = ImmutableList.builder();
|
||||||
try {
|
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
|
||||||
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
|
if (fileSite != null)
|
||||||
if (fileSite != null)
|
parameters.add( Res.format( fileSite.getLastUsed() ) );
|
||||||
parameters.add( Res.format( fileSite.getLastUsed() ) );
|
parameters.add( site.getAlgorithm().version() );
|
||||||
parameters.add( site.getAlgorithm().version() );
|
parameters.add( strf( "#%d", site.getCounter().longValue() ) );
|
||||||
parameters.add( strf( "#%d", site.getCounter().longValue() ) );
|
if ((fileSite != null) && (fileSite.getUrl() != null))
|
||||||
parameters.add( strf( "<em>%s</em>", site.getLogin() ) );
|
parameters.add( fileSite.getUrl() );
|
||||||
if ((fileSite != null) && (fileSite.getUrl() != null))
|
|
||||||
parameters.add( fileSite.getUrl() );
|
|
||||||
}
|
|
||||||
catch (final MPAlgorithmException | MPKeyUnavailableException e) {
|
|
||||||
logger.err( e, "While generating site description." );
|
|
||||||
parameters.add( e.getLocalizedMessage() );
|
|
||||||
}
|
|
||||||
|
|
||||||
return strf( "<html><strong>%s</strong> (%s)</html>", site.getSiteName(),
|
return strf( "<html><strong>%s</strong> (%s)</html>", item.getKeyAsHTML(),
|
||||||
Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
|
Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getQuestionDescription(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
|
||||||
|
MPQuestion question = (item != null)? item.getValue(): null;
|
||||||
|
if (question == null)
|
||||||
|
return "<site>";
|
||||||
|
if (question instanceof MPNewQuestion)
|
||||||
|
return strf( "<html>%s <Add new question></html>", item.getKeyAsHTML() );
|
||||||
|
|
||||||
|
return strf( "<html>%s</html>", item.getKeyAsHTML() );
|
||||||
|
}
|
||||||
|
|
||||||
private void useSite(final ActionEvent event) {
|
private void useSite(final ActionEvent event) {
|
||||||
MPSite<?> site = sitesModel.getSelectedItem();
|
MPSite<?> site = getSite();
|
||||||
if (site instanceof MPNewSite) {
|
if (site instanceof MPNewSite) {
|
||||||
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
|
if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
|
||||||
this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ),
|
this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ),
|
||||||
"New Site", JOptionPane.YES_NO_OPTION )) {
|
"New Site", JOptionPane.YES_NO_OPTION ))
|
||||||
sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) );
|
return;
|
||||||
useSite( event );
|
|
||||||
}
|
site = ((MPNewSite) site).addTo( user );
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
|
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
|
||||||
showSiteResult( site, loginResult, result -> {
|
MPSite<?> fsite = site;
|
||||||
|
Res.ui( getSiteResult( site, loginResult ), result -> {
|
||||||
if (result == null)
|
if (result == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (site instanceof MPFileSite)
|
if (fsite instanceof MPFileSite)
|
||||||
((MPFileSite) site).use();
|
((MPFileSite) fsite).use();
|
||||||
|
|
||||||
copyResult( result );
|
copyResult( result );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showSiteResult(@Nullable final MPSite<?> site) {
|
private void setShowLogin(final boolean showLogin) {
|
||||||
showSiteResult( site, false, result -> {
|
if (showLogin == this.showLogin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.showLogin = showLogin;
|
||||||
|
showSiteItem( sitesModel.getSelectedItem() );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSiteItem(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
|
||||||
|
MPSite<?> site = (item != null)? item.getValue(): null;
|
||||||
|
Res.ui( getSiteResult( site, showLogin ), result -> {
|
||||||
|
if (!showLogin && (site != null))
|
||||||
|
resultLabel.setText( (result != null)? strf( "Your password for %s:", site.getSiteName() ): " " );
|
||||||
|
else if (showLogin && (site != null))
|
||||||
|
resultLabel.setText( (result != null)? strf( "Your login for %s:", site.getSiteName() ): " " );
|
||||||
|
|
||||||
|
if ((result == null) || result.isEmpty())
|
||||||
|
resultField.setText( " " );
|
||||||
|
else if (!showLogin && user.getPreferences().isHidePasswords())
|
||||||
|
resultField.setText( EACH_CHARACTER.matcher( result ).replaceAll( "•" ) );
|
||||||
|
else
|
||||||
|
resultField.setText( result );
|
||||||
|
settingsButton.setEnabled( result != null );
|
||||||
|
questionsButton.setEnabled( result != null );
|
||||||
|
editButton.setEnabled( result != null );
|
||||||
|
keyButton.setEnabled( result != null );
|
||||||
|
deleteButton.setEnabled( result != null );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showSiteResult(@Nullable final MPSite<?> site, final boolean loginResult, final Consumer<String> resultCallback) {
|
private ListenableFuture<String> getSiteResult(@Nullable final MPSite<?> site, final boolean loginResult) {
|
||||||
Res.job( () -> {
|
return Res.job( () -> {
|
||||||
try {
|
try {
|
||||||
if (site != null)
|
if (site != null)
|
||||||
return loginResult? site.getLogin(): site.getResult();
|
return loginResult? site.getLogin(): site.getResult();
|
||||||
@@ -861,57 +919,37 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, resultCallback.andThen( result -> Res.ui( () -> {
|
} );
|
||||||
passwordLabel.setText( ((result != null) && (site != null))? strf( "Your password for %s:", site.getSiteName() ): " " );
|
|
||||||
passwordField.setText( (result != null)? result: " " );
|
|
||||||
settingsButton.setEnabled( result != null );
|
|
||||||
questionsButton.setEnabled( result != null );
|
|
||||||
editButton.setEnabled( result != null );
|
|
||||||
keyButton.setEnabled( result != null );
|
|
||||||
deleteButton.setEnabled( result != null );
|
|
||||||
} ) ) );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void useQuestion(@Nullable final MPQuestion question) {
|
private void useQuestion(final ActionEvent event) {
|
||||||
|
MPQuestion question = getQuestion();
|
||||||
if (question instanceof MPNewQuestion) {
|
if (question instanceof MPNewQuestion) {
|
||||||
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
|
if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
|
||||||
this,
|
this,
|
||||||
strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>",
|
strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>",
|
||||||
Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ),
|
Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ),
|
||||||
"New Question", JOptionPane.YES_NO_OPTION )) {
|
"New Question", JOptionPane.YES_NO_OPTION ))
|
||||||
useQuestion( question.getSite().addQuestion( question.getKeyword() ) );
|
return;
|
||||||
}
|
|
||||||
return;
|
question = question.getSite().addQuestion( question.getKeyword() );
|
||||||
}
|
}
|
||||||
|
|
||||||
showQuestionResult( question, result -> {
|
MPQuestion fquestion = question;
|
||||||
|
Res.ui( getQuestionResult( question ), result -> {
|
||||||
if (result == null)
|
if (result == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (question instanceof MPFileQuestion)
|
if (fquestion instanceof MPFileQuestion)
|
||||||
((MPFileQuestion) question).use();
|
((MPFileQuestion) fquestion).use();
|
||||||
|
|
||||||
copyResult( result );
|
copyResult( result );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showQuestionResult(@Nullable final MPQuestion question) {
|
private void showQuestionItem(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
|
||||||
showQuestionResult( question, answer -> {
|
MPQuestion question = (item != null)? item.getValue(): null;
|
||||||
} );
|
Res.ui( getQuestionResult( question ), answer -> {
|
||||||
}
|
|
||||||
|
|
||||||
private void showQuestionResult(@Nullable final MPQuestion question, final Consumer<String> resultCallback) {
|
|
||||||
Res.job( () -> {
|
|
||||||
try {
|
|
||||||
if (question != null)
|
|
||||||
return question.getAnswer();
|
|
||||||
}
|
|
||||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
|
||||||
logger.err( e, "While resolving answer for: %s", question );
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, resultCallback.andThen( answer -> Res.ui( () -> {
|
|
||||||
if ((answer == null) || (question == null))
|
if ((answer == null) || (question == null))
|
||||||
answerLabel.setText( " " );
|
answerLabel.setText( " " );
|
||||||
else
|
else
|
||||||
@@ -921,7 +959,21 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
strf( "<html>Answer for site <b>%s</b>, of question with keyword <b>%s</b>:",
|
strf( "<html>Answer for site <b>%s</b>, of question with keyword <b>%s</b>:",
|
||||||
question.getSite().getSiteName(), question.getKeyword() ) );
|
question.getSite().getSiteName(), question.getKeyword() ) );
|
||||||
answerField.setText( (answer != null)? answer: " " );
|
answerField.setText( (answer != null)? answer: " " );
|
||||||
} ) ) );
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<String> getQuestionResult(@Nullable final MPQuestion question) {
|
||||||
|
return Res.job( () -> {
|
||||||
|
try {
|
||||||
|
if (question != null)
|
||||||
|
return question.getAnswer();
|
||||||
|
}
|
||||||
|
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||||
|
logger.err( e, "While resolving answer for: %s", question );
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyResult(final String result) {
|
private void copyResult(final String result) {
|
||||||
@@ -936,9 +988,29 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
|
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
|
||||||
if (window instanceof Frame)
|
if (window instanceof Frame)
|
||||||
((Frame) window).setExtendedState( Frame.ICONIFIED );
|
((Frame) window).setExtendedState( Frame.ICONIFIED );
|
||||||
|
|
||||||
|
setShowLogin( false );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MPSite<?> getSite() {
|
||||||
|
MPQuery.Result<? extends MPSite<?>> selectedSite = sitesModel.getSelectedItem();
|
||||||
|
if (selectedSite == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return selectedSite.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MPQuestion getQuestion() {
|
||||||
|
MPQuery.Result<? extends MPQuestion> selectedQuestion = questionsModel.getSelectedItem();
|
||||||
|
if (selectedQuestion == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return selectedQuestion.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void keyTyped(final KeyEvent event) {
|
public void keyTyped(final KeyEvent event) {
|
||||||
}
|
}
|
||||||
@@ -955,25 +1027,33 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
sitesList.dispatchEvent( event );
|
sitesList.dispatchEvent( event );
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void updateSites(@Nullable final String query) {
|
private synchronized void updateSites(@Nullable final String queryText) {
|
||||||
if (updateSitesJob != null)
|
if (updateSitesJob != null)
|
||||||
updateSitesJob.cancel( true );
|
updateSitesJob.cancel( true );
|
||||||
|
|
||||||
updateSitesJob = Res.job( () -> {
|
updateSitesJob = Res.job( () -> {
|
||||||
Collection<MPSite<?>> sites = new LinkedList<>( user.findSites( query ) );
|
MPQuery query = new MPQuery( queryText );
|
||||||
|
Collection<MPQuery.Result<? extends MPSite<?>>> siteItems = new LinkedList<MPQuery.Result<? extends MPSite<?>>>(
|
||||||
|
query.find( user.getSites(), MPSite::getSiteName ) );
|
||||||
|
|
||||||
if (!Strings.isNullOrEmpty( query ))
|
if (!Strings.isNullOrEmpty( queryText ))
|
||||||
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) ))
|
if (siteItems.stream().noneMatch( MPQuery.Result::isExact )) {
|
||||||
sites.add( new MPNewSite( user, query ) );
|
MPQuery.Result<? extends MPSite<?>> selectedItem = sitesModel.getSelectedItem();
|
||||||
|
if ((selectedItem != null) && user.equals( selectedItem.getValue().getUser() ) &&
|
||||||
|
queryText.equals( selectedItem.getValue().getSiteName() ))
|
||||||
|
siteItems.add( selectedItem );
|
||||||
|
else
|
||||||
|
siteItems.add( MPQuery.Result.allOf( new MPNewSite( user, query.getQuery() ), query.getQuery() ) );
|
||||||
|
}
|
||||||
|
|
||||||
Res.ui( () -> sitesModel.set( sites ) );
|
Res.ui( () -> sitesModel.set( siteItems ) );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUserUpdated(final MPUser<?> user) {
|
public void onUserUpdated(final MPUser<?> user) {
|
||||||
updateSites( queryField.getText() );
|
updateSites( queryField.getText() );
|
||||||
showSiteResult( sitesModel.getSelectedItem() );
|
showSiteItem( sitesModel.getSelectedItem() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -983,5 +1063,13 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
|||||||
@Override
|
@Override
|
||||||
public void onUserInvalidated(final MPUser<?> user) {
|
public void onUserInvalidated(final MPUser<?> user) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchKeyEvent(final KeyEvent e) {
|
||||||
|
if (e.getKeyCode() == KeyEvent.VK_SHIFT)
|
||||||
|
setShowLogin( e.isShiftDown() );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.lyndir.masterpassword.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.google.common.collect.ClassToInstanceMap;
|
||||||
|
import com.google.common.collect.MutableClassToInstanceMap;
|
||||||
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
|
import com.lyndir.masterpassword.model.impl.Changeable;
|
||||||
|
import com.lyndir.masterpassword.model.impl.MPJSONAnyObject;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lhunath, 2018-10-14
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("CallToSystemGetenv")
|
||||||
|
public class MPConfig extends MPJSONAnyObject {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.get( MPConfig.class );
|
||||||
|
private static final ClassToInstanceMap<MPConfig> instances = MutableClassToInstanceMap.create();
|
||||||
|
private static final File configFile = new File( rcDir(), "config.json" );
|
||||||
|
|
||||||
|
private final Changeable changeable = new Changeable() {
|
||||||
|
@Override
|
||||||
|
protected void onChanged() {
|
||||||
|
try {
|
||||||
|
objectMapper.writerWithDefaultPrettyPrinter().writeValue( configFile, MPConfig.this );
|
||||||
|
instances.clear();
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
logger.err( e, "While saving config to: %s", configFile );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static synchronized <C extends MPConfig> C get(final Class<C> type) {
|
||||||
|
C instance = instances.getInstance( type );
|
||||||
|
|
||||||
|
if (instance == null)
|
||||||
|
if (configFile.exists())
|
||||||
|
try {
|
||||||
|
instances.putInstance( type, instance = objectMapper.readValue( configFile, type ) );
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
logger.wrn( e, "While reading config file: %s", configFile );
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance == null)
|
||||||
|
try {
|
||||||
|
instance = type.getConstructor().newInstance();
|
||||||
|
}
|
||||||
|
catch (final ReflectiveOperationException e) {
|
||||||
|
throw logger.bug( e );
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setChanged() {
|
||||||
|
changeable.setChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MPConfig get() {
|
||||||
|
return get( MPConfig.class );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File rcDir() {
|
||||||
|
String rcDir = System.getenv( MPModelConstants.env_rcDir );
|
||||||
|
if (rcDir != null)
|
||||||
|
return new File( rcDir );
|
||||||
|
|
||||||
|
String home = System.getProperty( "user.home" );
|
||||||
|
if (home == null)
|
||||||
|
home = System.getenv( "HOME" );
|
||||||
|
|
||||||
|
return new File( home, ".mpw.d" );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package com.lyndir.masterpassword.model;
|
||||||
|
|
||||||
|
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableCollection;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lhunath, 2018-09-11
|
||||||
|
*/
|
||||||
|
public class MPQuery {
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private final String query;
|
||||||
|
|
||||||
|
public MPQuery(@Nullable final String query) {
|
||||||
|
this.query = (query != null)? query: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public String getQuery() {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if this query is contained wholly inside the given {@code key}.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public <V> Optional<Result<V>> matches(final V value, final CharSequence key) {
|
||||||
|
Result<V> result = Result.noneOf( value, key );
|
||||||
|
if (query.isEmpty())
|
||||||
|
return Optional.of( result );
|
||||||
|
if (key.length() == 0)
|
||||||
|
return Optional.empty();
|
||||||
|
|
||||||
|
// Consume query and key characters until one of them runs out, recording any matches against the result's key.
|
||||||
|
int q = 0, k = 0;
|
||||||
|
while ((q < query.length()) && (k < key.length())) {
|
||||||
|
if (query.charAt( q ) == key.charAt( k )) {
|
||||||
|
result.keyMatchedAt( k );
|
||||||
|
++q;
|
||||||
|
}
|
||||||
|
|
||||||
|
++k;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the match against the query broke before the end of the query, it failed.
|
||||||
|
return (q < query.length())? Optional.empty(): Optional.of( result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Results for values that matched against the query, in the original values' order.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public <V> ImmutableCollection<Result<? extends V>> find(final Iterable<? extends V> values,
|
||||||
|
final Function<V, CharSequence> valueToKey) {
|
||||||
|
ImmutableList.Builder<Result<? extends V>> results = ImmutableList.builder();
|
||||||
|
for (final V value : values)
|
||||||
|
matches( value, valueToKey.apply( value ) ).ifPresent( results::add );
|
||||||
|
|
||||||
|
return results.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Result<V> {
|
||||||
|
|
||||||
|
private final V value;
|
||||||
|
private final CharSequence key;
|
||||||
|
private final boolean[] keyMatches;
|
||||||
|
|
||||||
|
Result(final V value, final CharSequence key) {
|
||||||
|
this.value = value;
|
||||||
|
this.key = key;
|
||||||
|
|
||||||
|
keyMatches = new boolean[key.length()];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Result<T> noneOf(final T value, final CharSequence key) {
|
||||||
|
return new Result<>( value, key );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Result<T> allOf(final T value, final CharSequence key) {
|
||||||
|
Result<T> result = noneOf( value, key );
|
||||||
|
Arrays.fill( result.keyMatches, true );
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public V getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public CharSequence getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKeyAsHTML() {
|
||||||
|
return getKeyAsHTML( "u" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "MagicCharacter", "HardcodedFileSeparator" })
|
||||||
|
public String getKeyAsHTML(final String mark) {
|
||||||
|
String closeMark = mark.contains( " " )? mark.substring( 0, mark.indexOf( ' ' ) ): mark;
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
boolean marked = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < key.length(); ++i) {
|
||||||
|
if (keyMatches[i] && !marked) {
|
||||||
|
html.append( '<' ).append( mark ).append( '>' );
|
||||||
|
marked = true;
|
||||||
|
} else if (!keyMatches[i] && marked) {
|
||||||
|
html.append( '<' ).append( '/' ).append( closeMark ).append( '>' );
|
||||||
|
marked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.append( key.charAt( i ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marked)
|
||||||
|
html.append( '<' ).append( '/' ).append( closeMark ).append( '>' );
|
||||||
|
|
||||||
|
return html.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean[] getKeyMatches() {
|
||||||
|
return keyMatches.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExact() {
|
||||||
|
for (final boolean keyMatch : keyMatches)
|
||||||
|
if (!keyMatch)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void keyMatchedAt(final int k) {
|
||||||
|
keyMatches[k] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object o) {
|
||||||
|
if (!(o instanceof Result))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Result<?> r = (Result<?>) o;
|
||||||
|
return Objects.equals( value, r.value ) && Objects.equals( key, r.key ) && Arrays.equals( keyMatches, r.keyMatches );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return getValue().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return strf( "{Result: %s}", key );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,14 +40,14 @@ public interface MPQuestion extends Comparable<MPQuestion> {
|
|||||||
|
|
||||||
void setType(MPResultType type);
|
void setType(MPResultType type);
|
||||||
|
|
||||||
@Nonnull
|
@Nullable
|
||||||
default String getAnswer()
|
default String getAnswer()
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
|
||||||
return getAnswer( null );
|
return getAnswer( null );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nullable
|
||||||
String getAnswer(@Nullable String state)
|
String getAnswer(@Nullable String state)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException;
|
throws MPKeyUnavailableException, MPAlgorithmException;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
package com.lyndir.masterpassword.model;
|
package com.lyndir.masterpassword.model;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableCollection;
|
|
||||||
import com.google.common.primitives.UnsignedInteger;
|
import com.google.common.primitives.UnsignedInteger;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -80,6 +79,9 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
|
|||||||
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
|
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException;
|
throws MPKeyUnavailableException, MPAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see MPMasterKey#siteResult(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)
|
||||||
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext,
|
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext,
|
||||||
@Nullable UnsignedInteger counter, MPResultType type, @Nullable String state)
|
@Nullable UnsignedInteger counter, MPResultType type, @Nullable String state)
|
||||||
@@ -90,13 +92,13 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
|
|||||||
@Nullable UnsignedInteger counter, MPResultType type, String state)
|
@Nullable UnsignedInteger counter, MPResultType type, String state)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException;
|
throws MPKeyUnavailableException, MPAlgorithmException;
|
||||||
|
|
||||||
@Nonnull
|
@Nullable
|
||||||
default String getLogin()
|
default String getLogin()
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
return getLogin( null );
|
return getLogin( null );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nullable
|
||||||
String getLogin(@Nullable String state)
|
String getLogin(@Nullable String state)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException;
|
throws MPKeyUnavailableException, MPAlgorithmException;
|
||||||
|
|
||||||
@@ -115,7 +117,4 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
|
|||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
Collection<Q> getQuestions();
|
Collection<Q> getQuestions();
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
ImmutableCollection<Q> findQuestions(@Nullable String query);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
package com.lyndir.masterpassword.model;
|
package com.lyndir.masterpassword.model;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableCollection;
|
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
@@ -39,6 +38,9 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
String getFullName();
|
String getFullName();
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
MPUserPreferences getPreferences();
|
||||||
|
|
||||||
// - Algorithm
|
// - Algorithm
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@@ -46,11 +48,6 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
|
|||||||
|
|
||||||
void setAlgorithm(MPAlgorithm algorithm);
|
void setAlgorithm(MPAlgorithm algorithm);
|
||||||
|
|
||||||
@Nullable
|
|
||||||
default MPResultType getDefaultType() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
byte[] getKeyID();
|
byte[] getKeyID();
|
||||||
|
|
||||||
@@ -111,9 +108,6 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
Collection<S> getSites();
|
Collection<S> getSites();
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
ImmutableCollection<S> findSites(@Nullable String query);
|
|
||||||
|
|
||||||
void addListener(Listener listener);
|
void addListener(Listener listener);
|
||||||
|
|
||||||
void removeListener(Listener listener);
|
void removeListener(Listener listener);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.lyndir.masterpassword.model;
|
||||||
|
|
||||||
|
import com.lyndir.masterpassword.MPResultType;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lhunath, 2018-10-13
|
||||||
|
*/
|
||||||
|
public interface MPUserPreferences {
|
||||||
|
|
||||||
|
MPResultType getDefaultType();
|
||||||
|
|
||||||
|
void setDefaultType(@Nullable MPResultType defaultType);
|
||||||
|
|
||||||
|
boolean isHidePasswords();
|
||||||
|
|
||||||
|
void setHidePasswords(boolean hidePasswords);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import java.util.concurrent.Executors;
|
|||||||
/**
|
/**
|
||||||
* @author lhunath, 2018-07-08
|
* @author lhunath, 2018-07-08
|
||||||
*/
|
*/
|
||||||
public class Changeable {
|
public abstract class Changeable {
|
||||||
|
|
||||||
private static final ExecutorService changeExecutor = Executors.newSingleThreadExecutor();
|
private static final ExecutorService changeExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
@@ -15,7 +15,9 @@ public class Changeable {
|
|||||||
private Grouping grouping = Grouping.APPLY;
|
private Grouping grouping = Grouping.APPLY;
|
||||||
private boolean changed;
|
private boolean changed;
|
||||||
|
|
||||||
void setChanged() {
|
protected abstract void onChanged();
|
||||||
|
|
||||||
|
public void setChanged() {
|
||||||
synchronized (mutex) {
|
synchronized (mutex) {
|
||||||
if (changed)
|
if (changed)
|
||||||
return;
|
return;
|
||||||
@@ -37,9 +39,6 @@ public class Changeable {
|
|||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onChanged() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void beginChanges() {
|
public void beginChanges() {
|
||||||
synchronized (mutex) {
|
synchronized (mutex) {
|
||||||
grouping = Grouping.BATCH;
|
grouping = Grouping.BATCH;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
|
|||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String getAnswer(@Nullable final String state)
|
public String getAnswer(@Nullable final String state)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
@@ -82,8 +82,6 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onChanged() {
|
protected void onChanged() {
|
||||||
super.onChanged();
|
|
||||||
|
|
||||||
if (site instanceof Changeable)
|
if (site instanceof Changeable)
|
||||||
((Changeable) site).setChanged();
|
((Changeable) site).setChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ package com.lyndir.masterpassword.model.impl;
|
|||||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.common.collect.ImmutableCollection;
|
|
||||||
import com.google.common.collect.ImmutableSortedSet;
|
|
||||||
import com.google.common.primitives.UnsignedInteger;
|
import com.google.common.primitives.UnsignedInteger;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.*;
|
import com.lyndir.masterpassword.model.*;
|
||||||
@@ -40,7 +37,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
|
|||||||
|
|
||||||
private final U user;
|
private final U user;
|
||||||
private final String siteName;
|
private final String siteName;
|
||||||
private final Collection<Q> questions = new LinkedHashSet<>();
|
private final Collection<Q> questions = new TreeSet<>();
|
||||||
|
|
||||||
private MPAlgorithm algorithm;
|
private MPAlgorithm algorithm;
|
||||||
private UnsignedInteger counter;
|
private UnsignedInteger counter;
|
||||||
@@ -58,17 +55,20 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
|
|||||||
this.siteName = siteName;
|
this.siteName = siteName;
|
||||||
this.algorithm = (algorithm != null)? algorithm: this.user.getAlgorithm();
|
this.algorithm = (algorithm != null)? algorithm: this.user.getAlgorithm();
|
||||||
this.counter = (counter != null)? counter: this.algorithm.mpw_default_counter();
|
this.counter = (counter != null)? counter: this.algorithm.mpw_default_counter();
|
||||||
this.resultType = (resultType != null)? resultType:
|
this.resultType = (resultType != null)? resultType: this.user.getPreferences().getDefaultType();
|
||||||
ifNotNullElse( this.user.getDefaultType(), this.algorithm.mpw_default_result_type() );
|
|
||||||
this.loginType = (loginType != null)? loginType: this.algorithm.mpw_default_login_type();
|
this.loginType = (loginType != null)? loginType: this.algorithm.mpw_default_login_type();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// - Meta
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getSiteName() {
|
public String getSiteName() {
|
||||||
return siteName;
|
return siteName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// - Algorithm
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MPAlgorithm getAlgorithm() {
|
public MPAlgorithm getAlgorithm() {
|
||||||
@@ -159,7 +159,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
|
|||||||
keyPurpose, keyContext, type, state );
|
keyPurpose, keyContext, type, state );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String getLogin(@Nullable final String state)
|
public String getLogin(@Nullable final String state)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
@@ -167,6 +167,14 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
|
|||||||
return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state );
|
return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// - Relations
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public U getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Q addQuestion(final Q question) {
|
public Q addQuestion(final Q question) {
|
||||||
@@ -191,27 +199,8 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
|
|||||||
return Collections.unmodifiableCollection( questions );
|
return Collections.unmodifiableCollection( questions );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public ImmutableCollection<Q> findQuestions(@Nullable final String query) {
|
|
||||||
ImmutableSortedSet.Builder<Q> results = ImmutableSortedSet.naturalOrder();
|
|
||||||
for (final Q question : getQuestions())
|
|
||||||
if (Strings.isNullOrEmpty( query ) || question.getKeyword().startsWith( query ))
|
|
||||||
results.add( question );
|
|
||||||
|
|
||||||
return results.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public U getUser() {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onChanged() {
|
protected void onChanged() {
|
||||||
super.onChanged();
|
|
||||||
|
|
||||||
if (user instanceof Changeable)
|
if (user instanceof Changeable)
|
||||||
((Changeable) user).setChanged();
|
((Changeable) user).setChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ package com.lyndir.masterpassword.model.impl;
|
|||||||
|
|
||||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.common.collect.ImmutableCollection;
|
|
||||||
import com.google.common.collect.ImmutableSortedSet;
|
|
||||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
@@ -48,7 +45,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
|
|||||||
@Nullable
|
@Nullable
|
||||||
protected MPMasterKey masterKey;
|
protected MPMasterKey masterKey;
|
||||||
|
|
||||||
private final Map<String, S> sites = new LinkedHashMap<>();
|
private final Set<S> sites = new TreeSet<>();
|
||||||
|
|
||||||
protected MPBasicUser(final String fullName, final MPAlgorithm algorithm) {
|
protected MPBasicUser(final String fullName, final MPAlgorithm algorithm) {
|
||||||
this( 0, fullName, algorithm );
|
this( 0, fullName, algorithm );
|
||||||
@@ -80,6 +77,12 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
|
|||||||
return fullName;
|
return fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public MPUserPreferences getPreferences() {
|
||||||
|
return new MPBasicUserPreferences<MPBasicUser<?>>( this );
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MPAlgorithm getAlgorithm() {
|
public MPAlgorithm getAlgorithm() {
|
||||||
@@ -178,7 +181,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public S addSite(final S site) {
|
public S addSite(final S site) {
|
||||||
sites.put( site.getSiteName(), site );
|
sites.add( site );
|
||||||
|
|
||||||
setChanged();
|
setChanged();
|
||||||
return site;
|
return site;
|
||||||
@@ -186,7 +189,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean deleteSite(final MPSite<?> site) {
|
public boolean deleteSite(final MPSite<?> site) {
|
||||||
if (!sites.values().remove( site ))
|
if (!sites.remove( site ))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
setChanged();
|
setChanged();
|
||||||
@@ -196,18 +199,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Collection<S> getSites() {
|
public Collection<S> getSites() {
|
||||||
return Collections.unmodifiableCollection( sites.values() );
|
return Collections.unmodifiableCollection( sites );
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public ImmutableCollection<S> findSites(@Nullable final String query) {
|
|
||||||
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
|
|
||||||
for (final S site : getSites())
|
|
||||||
if (Strings.isNullOrEmpty( query ) || site.getSiteName().startsWith( query ))
|
|
||||||
results.add( site );
|
|
||||||
|
|
||||||
return results.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -222,8 +214,6 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onChanged() {
|
protected void onChanged() {
|
||||||
super.onChanged();
|
|
||||||
|
|
||||||
for (final Listener listener : listeners)
|
for (final Listener listener : listeners)
|
||||||
listener.onUserUpdated( this );
|
listener.onUserUpdated( this );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.lyndir.masterpassword.model.impl;
|
||||||
|
|
||||||
|
import com.lyndir.masterpassword.MPResultType;
|
||||||
|
import com.lyndir.masterpassword.model.MPUserPreferences;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lhunath, 2018-10-13
|
||||||
|
*/
|
||||||
|
public class MPBasicUserPreferences<U extends MPBasicUser<?>> implements MPUserPreferences {
|
||||||
|
|
||||||
|
private final U user;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MPResultType defaultType;
|
||||||
|
private boolean hidePasswords;
|
||||||
|
|
||||||
|
public MPBasicUserPreferences(final U user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected U getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MPResultType getDefaultType() {
|
||||||
|
return (defaultType != null)? defaultType: user.getAlgorithm().mpw_default_result_type();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDefaultType(@Nullable final MPResultType defaultType) {
|
||||||
|
this.defaultType = defaultType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isHidePasswords() {
|
||||||
|
return hidePasswords;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHidePasswords(final boolean hidePasswords) {
|
||||||
|
this.hidePasswords = hidePasswords;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ package com.lyndir.masterpassword.model.impl;
|
|||||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||||
|
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +32,7 @@ public class MPFileQuestion extends MPBasicQuestion {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private String answerState;
|
private String answerState;
|
||||||
|
|
||||||
|
@SuppressWarnings("TypeMayBeWeakened")
|
||||||
public MPFileQuestion(final MPFileSite site, final String keyword,
|
public MPFileQuestion(final MPFileSite site, final String keyword,
|
||||||
@Nullable final MPResultType type, @Nullable final String answerState) {
|
@Nullable final MPResultType type, @Nullable final String answerState) {
|
||||||
super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
|
super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
|
||||||
@@ -45,7 +45,7 @@ public class MPFileQuestion extends MPBasicQuestion {
|
|||||||
return answerState;
|
return answerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String getAnswer()
|
public String getAnswer()
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
|
|||||||
return getResult( keyPurpose, keyContext, getResultState() );
|
return getResult( keyPurpose, keyContext, getResultState() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String getLogin()
|
public String getLogin()
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ package com.lyndir.masterpassword.model.impl;
|
|||||||
|
|
||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
import com.lyndir.masterpassword.model.*;
|
||||||
import com.lyndir.masterpassword.model.MPUser;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.joda.time.Instant;
|
import org.joda.time.Instant;
|
||||||
@@ -41,44 +39,48 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private byte[] keyID;
|
private byte[] keyID;
|
||||||
private File path;
|
private File file;
|
||||||
private MPMarshalFormat format;
|
private MPMarshalFormat format;
|
||||||
private MPMarshaller.ContentMode contentMode;
|
private MPMarshaller.ContentMode contentMode;
|
||||||
|
private ReadableInstant lastUsed;
|
||||||
|
private boolean complete;
|
||||||
|
|
||||||
private MPResultType defaultType;
|
private final MPFileUserPreferences preferences;
|
||||||
private ReadableInstant lastUsed;
|
|
||||||
private boolean complete;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static MPFileUser load(final File file)
|
public static MPFileUser load(final File file)
|
||||||
throws IOException, MPMarshalException {
|
throws IOException, MPMarshalException {
|
||||||
for (final MPMarshalFormat format : MPMarshalFormat.values())
|
for (final MPMarshalFormat format : MPMarshalFormat.values())
|
||||||
if (file.getName().endsWith( format.fileSuffix() ))
|
if (format.matches( file ))
|
||||||
return format.unmarshaller().readUser( file );
|
return format.unmarshaller().readUser( file );
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MPFileUser(final String fullName, final File path) {
|
public MPFileUser(final String fullName, final File location) {
|
||||||
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), path );
|
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), location );
|
||||||
}
|
}
|
||||||
|
|
||||||
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) {
|
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File location) {
|
||||||
this( fullName, keyID, algorithm, 0, null, new Instant(),
|
this( fullName, keyID, algorithm, 0, null, new Instant(), false,
|
||||||
MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path );
|
MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, location );
|
||||||
}
|
}
|
||||||
|
|
||||||
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm,
|
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final int avatar,
|
||||||
final int avatar, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed,
|
@Nullable final MPResultType defaultType, final ReadableInstant lastUsed, final boolean hidePasswords,
|
||||||
final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) {
|
final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File location) {
|
||||||
super( avatar, fullName, algorithm );
|
super( avatar, fullName, algorithm );
|
||||||
|
|
||||||
this.keyID = (keyID != null)? keyID.clone(): null;
|
this.keyID = (keyID != null)? keyID.clone(): null;
|
||||||
this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type();
|
|
||||||
this.lastUsed = lastUsed;
|
this.lastUsed = lastUsed;
|
||||||
this.path = path;
|
this.preferences = new MPFileUserPreferences( this, defaultType, hidePasswords );
|
||||||
this.format = format;
|
this.format = format;
|
||||||
this.contentMode = contentMode;
|
this.contentMode = contentMode;
|
||||||
|
|
||||||
|
if (location.isDirectory())
|
||||||
|
this.file = new File( location, getFullName() + getFormat().fileSuffix() );
|
||||||
|
else
|
||||||
|
this.file = location;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -87,8 +89,10 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
return (keyID == null)? null: keyID.clone();
|
return (keyID == null)? null: keyID.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPath(final File path) {
|
@Nonnull
|
||||||
this.path = path;
|
@Override
|
||||||
|
public MPUserPreferences getPreferences() {
|
||||||
|
return preferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -115,39 +119,18 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFormat(final MPMarshalFormat format) {
|
|
||||||
if (Objects.equals( this.format, format ))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.format = format;
|
|
||||||
setChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MPMarshaller.ContentMode getContentMode() {
|
public MPMarshaller.ContentMode getContentMode() {
|
||||||
return contentMode;
|
return contentMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContentMode(final MPMarshaller.ContentMode contentMode) {
|
public void setContentMode(final MPMarshaller.ContentMode contentMode) {
|
||||||
if (Objects.equals( this.contentMode, contentMode ))
|
if (this.contentMode == contentMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.contentMode = contentMode;
|
this.contentMode = contentMode;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public MPResultType getDefaultType() {
|
|
||||||
return defaultType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDefaultType(final MPResultType defaultType) {
|
|
||||||
if (Objects.equals( this.defaultType, defaultType ))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.defaultType = defaultType;
|
|
||||||
setChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadableInstant getLastUsed() {
|
public ReadableInstant getLastUsed() {
|
||||||
return lastUsed;
|
return lastUsed;
|
||||||
}
|
}
|
||||||
@@ -166,7 +149,47 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public File getFile() {
|
public File getFile() {
|
||||||
return new File( path, getFullName() + getFormat().fileSuffix() );
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void migrateTo(final MPMarshalFormat format) {
|
||||||
|
if (this.format == format)
|
||||||
|
return;
|
||||||
|
|
||||||
|
migrateTo( file.getParentFile(), format );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void migrateTo(final File path) {
|
||||||
|
migrateTo( path, format );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the file for this user to the given path using a standard user-derived filename (ie. {@code [full name].[format suffix]})
|
||||||
|
*
|
||||||
|
* The user's old file is either moved to the new or deleted. If the user's file was already at the destination, it doesn't change.
|
||||||
|
* If a file already exists at the destination, it is overwritten.
|
||||||
|
*/
|
||||||
|
public void migrateTo(final File path, final MPMarshalFormat newFormat) {
|
||||||
|
MPMarshalFormat oldFormat = format;
|
||||||
|
File oldFile = file, newFile = new File( path, getFullName() + newFormat.fileSuffix() );
|
||||||
|
|
||||||
|
// If the format hasn't changed, migrate by moving the file: the contents doesn't need to change.
|
||||||
|
if ((oldFormat == newFormat) && !oldFile.equals( newFile ) && oldFile.exists())
|
||||||
|
if (!oldFile.renameTo( newFile ))
|
||||||
|
logger.wrn( "Couldn't move %s to %s for migration.", oldFile, newFile );
|
||||||
|
|
||||||
|
this.format = newFormat;
|
||||||
|
this.file = newFile;
|
||||||
|
|
||||||
|
// If the format has changed, save the new format into the new file and delete the old file. Revert if the user cannot be saved.
|
||||||
|
if ((oldFormat != newFormat) && !oldFile.equals( newFile ))
|
||||||
|
if (save()) {
|
||||||
|
if (oldFile.exists() && !oldFile.delete())
|
||||||
|
logger.wrn( "Couldn't delete %s after migration.", oldFile );
|
||||||
|
} else {
|
||||||
|
this.format = oldFormat;
|
||||||
|
this.file = oldFile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -187,10 +210,16 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() {
|
/**
|
||||||
|
* @return {@code false} if the user is not fully loaded (complete), authenticated, or an issue prevented the marshalling.
|
||||||
|
*/
|
||||||
|
public boolean save() {
|
||||||
|
if (!isComplete())
|
||||||
|
return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isComplete())
|
getFormat().marshaller().marshall( this );
|
||||||
getFormat().marshaller().marshall( this );
|
return true;
|
||||||
}
|
}
|
||||||
catch (final MPKeyUnavailableException e) {
|
catch (final MPKeyUnavailableException e) {
|
||||||
logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this );
|
logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this );
|
||||||
@@ -198,6 +227,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
catch (final IOException | MPMarshalException | MPAlgorithmException e) {
|
catch (final IOException | MPMarshalException | MPAlgorithmException e) {
|
||||||
logger.err( e, "Unable to write out changes for user: %s", this );
|
logger.err( e, "Unable to write out changes for user: %s", this );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -18,11 +18,9 @@
|
|||||||
|
|
||||||
package com.lyndir.masterpassword.model.impl;
|
package com.lyndir.masterpassword.model.impl;
|
||||||
|
|
||||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSortedSet;
|
import com.google.common.collect.ImmutableSortedSet;
|
||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.masterpassword.model.MPModelConstants;
|
import com.lyndir.masterpassword.model.MPConfig;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -38,19 +36,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
public class MPFileUserManager {
|
public class MPFileUserManager {
|
||||||
|
|
||||||
@SuppressWarnings("UnusedDeclaration")
|
@SuppressWarnings("UnusedDeclaration")
|
||||||
private static final Logger logger = Logger.get( MPFileUserManager.class );
|
private static final Logger logger = Logger.get( MPFileUserManager.class );
|
||||||
private static final MPFileUserManager instance;
|
private static final MPFileUserManager instance = create( MPConfig.get().rcDir() );
|
||||||
|
|
||||||
static {
|
|
||||||
String rcDir = System.getenv( MPModelConstants.env_rcDir );
|
|
||||||
|
|
||||||
if (rcDir != null)
|
|
||||||
instance = create( new File( rcDir ) );
|
|
||||||
else {
|
|
||||||
String home = ifNotNullElseNullable( System.getProperty( "user.home" ), System.getenv( "HOME" ) );
|
|
||||||
instance = create( new File( home, ".mpw.d" ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
|
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
|
||||||
private final Map<String, MPFileUser> userByName = new HashMap<>();
|
private final Map<String, MPFileUser> userByName = new HashMap<>();
|
||||||
@@ -78,18 +65,17 @@ public class MPFileUserManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final File file : pathFiles)
|
for (final MPMarshalFormat format : MPMarshalFormat.values())
|
||||||
try {
|
for (final File file : pathFiles)
|
||||||
MPFileUser user = MPFileUser.load( file );
|
if (format.matches( file ))
|
||||||
if (user != null) {
|
try {
|
||||||
MPFileUser previousUser = userByName.put( user.getFullName(), user );
|
MPFileUser user = MPFileUser.load( file );
|
||||||
if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal()))
|
if (user != null)
|
||||||
userByName.put( previousUser.getFullName(), previousUser );
|
add( user );
|
||||||
}
|
}
|
||||||
}
|
catch (final IOException | MPMarshalException e) {
|
||||||
catch (final IOException | MPMarshalException e) {
|
logger.err( e, "Couldn't read user from: %s", file );
|
||||||
logger.err( e, "Couldn't read user from: %s", file );
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fireUpdated();
|
fireUpdated();
|
||||||
}
|
}
|
||||||
@@ -99,12 +85,19 @@ public class MPFileUserManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MPFileUser add(final MPFileUser user) {
|
public MPFileUser add(final MPFileUser user) {
|
||||||
user.setPath( getPath() );
|
// We migrate in two steps to allow the first to complete even if the user is not in the right state to complete the latter.
|
||||||
user.save();
|
user.migrateTo( getPath() );
|
||||||
|
user.migrateTo( MPMarshalFormat.DEFAULT );
|
||||||
|
|
||||||
MPFileUser oldUser = userByName.put( user.getFullName(), user );
|
MPFileUser oldUser = userByName.put( user.getFullName(), user );
|
||||||
if (oldUser != null)
|
if (oldUser != null) {
|
||||||
oldUser.invalidate();
|
oldUser.invalidate();
|
||||||
|
|
||||||
|
// Delete old user, it is replaced by the new one.
|
||||||
|
if (!oldUser.getFile().equals( user.getFile() ) && oldUser.getFile().exists())
|
||||||
|
if (!oldUser.getFile().delete())
|
||||||
|
logger.err( "Couldn't delete file: %s, after replacing with: %s", oldUser.getFile(), user.getFile() );
|
||||||
|
}
|
||||||
fireUpdated();
|
fireUpdated();
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.lyndir.masterpassword.model.impl;
|
||||||
|
|
||||||
|
import com.lyndir.masterpassword.MPResultType;
|
||||||
|
import java.util.Objects;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lhunath, 2018-10-13
|
||||||
|
*/
|
||||||
|
public class MPFileUserPreferences extends MPBasicUserPreferences<MPFileUser> {
|
||||||
|
|
||||||
|
public MPFileUserPreferences(final MPFileUser user, @Nullable final MPResultType defaultType, final boolean hidePasswords) {
|
||||||
|
super( user );
|
||||||
|
|
||||||
|
setDefaultType( defaultType );
|
||||||
|
setHidePasswords( hidePasswords );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDefaultType(@Nullable final MPResultType defaultType) {
|
||||||
|
if (getDefaultType() == defaultType)
|
||||||
|
return;
|
||||||
|
|
||||||
|
super.setDefaultType( defaultType );
|
||||||
|
getUser().setChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHidePasswords(final boolean hidePasswords) {
|
||||||
|
if (Objects.equals( isHidePasswords(), hidePasswords ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
super.setHidePasswords( hidePasswords );
|
||||||
|
getUser().setChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ public class MPFlatMarshaller implements MPMarshaller {
|
|||||||
content.append( "# Avatar: " ).append( user.getAvatar() ).append( '\n' );
|
content.append( "# Avatar: " ).append( user.getAvatar() ).append( '\n' );
|
||||||
content.append( "# Key ID: " ).append( user.exportKeyID() ).append( '\n' );
|
content.append( "# Key ID: " ).append( user.exportKeyID() ).append( '\n' );
|
||||||
content.append( "# Algorithm: " ).append( user.getAlgorithm().version().toInt() ).append( '\n' );
|
content.append( "# Algorithm: " ).append( user.getAlgorithm().version().toInt() ).append( '\n' );
|
||||||
content.append( "# Default Type: " ).append( user.getDefaultType().getType() ).append( '\n' );
|
content.append( "# Default Type: " ).append( user.getPreferences().getDefaultType().getType() ).append( '\n' );
|
||||||
content.append( "# Passwords: " ).append( user.getContentMode().name() ).append( '\n' );
|
content.append( "# Passwords: " ).append( user.getContentMode().name() ).append( '\n' );
|
||||||
content.append( "##\n" );
|
content.append( "##\n" );
|
||||||
content.append( "#\n" );
|
content.append( "#\n" );
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import com.lyndir.lhunath.opal.system.CodeUtils;
|
|||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.MPModelConstants;
|
|
||||||
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
||||||
|
import com.lyndir.masterpassword.model.MPModelConstants;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -56,6 +56,7 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
int mpVersion = 0, avatar = 0;
|
int mpVersion = 0, avatar = 0;
|
||||||
boolean clearContent = false, headerStarted = false;
|
boolean clearContent = false, headerStarted = false;
|
||||||
MPResultType defaultType = null;
|
MPResultType defaultType = null;
|
||||||
|
Instant date = null;
|
||||||
|
|
||||||
//noinspection HardcodedLineSeparator
|
//noinspection HardcodedLineSeparator
|
||||||
for (final String line : CharStreams.readLines( reader ))
|
for (final String line : CharStreams.readLines( reader ))
|
||||||
@@ -66,10 +67,11 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
headerStarted = true;
|
headerStarted = true;
|
||||||
else if ((fullName != null) && (keyID != null))
|
else if ((fullName != null) && (keyID != null))
|
||||||
// Ends the header.
|
// Ends the header.
|
||||||
return new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(),
|
return new MPFileUser(
|
||||||
avatar, defaultType, new Instant( 0 ),
|
fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(), avatar, defaultType,
|
||||||
clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED,
|
date, false, clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED,
|
||||||
MPMarshalFormat.Flat, file.getParentFile() );
|
MPMarshalFormat.Flat, file
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comment.
|
// Comment.
|
||||||
@@ -91,6 +93,8 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
clearContent = "visible".equalsIgnoreCase( value );
|
clearContent = "visible".equalsIgnoreCase( value );
|
||||||
else if ("Default Type".equalsIgnoreCase( name ))
|
else if ("Default Type".equalsIgnoreCase( name ))
|
||||||
defaultType = MPResultType.forType( ConversionUtils.toIntegerNN( value ) );
|
defaultType = MPResultType.forType( ConversionUtils.toIntegerNN( value ) );
|
||||||
|
else if ("Date".equalsIgnoreCase( name ))
|
||||||
|
date = MPModelConstants.dateTimeFormatter.parseDateTime( value ).toInstant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,28 +154,32 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
MPFileSite site;
|
MPFileSite site;
|
||||||
switch (importFormat) {
|
switch (importFormat) {
|
||||||
case 0:
|
case 0:
|
||||||
site = new MPFileSite( user, //
|
site = new MPFileSite(
|
||||||
siteMatcher.group( 5 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN(
|
user, siteMatcher.group( 5 ),
|
||||||
colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(),
|
MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN(
|
||||||
user.getAlgorithm().mpw_default_counter(),
|
colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(),
|
||||||
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
|
user.getAlgorithm().mpw_default_counter(),
|
||||||
clearContent? null: siteMatcher.group( 6 ),
|
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
|
||||||
null, null, null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
|
clearContent? null: siteMatcher.group( 6 ),
|
||||||
MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
|
null, null, null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
|
||||||
|
MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
|
||||||
if (clearContent)
|
if (clearContent)
|
||||||
site.setSitePassword( site.getResultType(), siteMatcher.group( 6 ) );
|
site.setSitePassword( site.getResultType(), siteMatcher.group( 6 ) );
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
site = new MPFileSite( user, //
|
site = new MPFileSite(
|
||||||
siteMatcher.group( 7 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN(
|
user, siteMatcher.group( 7 ),
|
||||||
colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(),
|
MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN(
|
||||||
UnsignedInteger.valueOf( colon.matcher( siteMatcher.group( 5 ) ).replaceAll( "" ) ),
|
colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(),
|
||||||
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
|
UnsignedInteger.valueOf(
|
||||||
clearContent? null: siteMatcher.group( 8 ),
|
colon.matcher( siteMatcher.group( 5 ) ).replaceAll( "" ) ),
|
||||||
MPResultType.GeneratedName, clearContent? null: siteMatcher.group( 6 ), null,
|
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
|
||||||
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
|
clearContent? null: siteMatcher.group( 8 ),
|
||||||
MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
|
MPResultType.GeneratedName,
|
||||||
|
clearContent? null: siteMatcher.group( 6 ),
|
||||||
|
null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
|
||||||
|
MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
|
||||||
if (clearContent) {
|
if (clearContent) {
|
||||||
site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) );
|
site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) );
|
||||||
site.setLoginName( MPResultType.StoredPersonal, siteMatcher.group( 6 ) );
|
site.setLoginName( MPResultType.StoredPersonal, siteMatcher.group( 6 ) );
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
package com.lyndir.masterpassword.model.impl;
|
package com.lyndir.masterpassword.model.impl;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.*;
|
import com.fasterxml.jackson.annotation.*;
|
||||||
|
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
|
||||||
|
import com.fasterxml.jackson.core.util.Separators;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@@ -27,31 +30,59 @@ import java.util.*;
|
|||||||
* @author lhunath, 2018-05-14
|
* @author lhunath, 2018-05-14
|
||||||
*/
|
*/
|
||||||
@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = MPJSONAnyObject.MPJSONEmptyValue.class)
|
@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = MPJSONAnyObject.MPJSONEmptyValue.class)
|
||||||
class MPJSONAnyObject {
|
public class MPJSONAnyObject {
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
protected static final ObjectMapper objectMapper = new ObjectMapper() {
|
||||||
|
{
|
||||||
|
setDefaultPrettyPrinter( new DefaultPrettyPrinter() {
|
||||||
|
@Override
|
||||||
|
public DefaultPrettyPrinter withSeparators(final Separators separators) {
|
||||||
|
super.withSeparators( separators );
|
||||||
|
_objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " ";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
setVisibility( PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE );
|
||||||
|
setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@JsonAnySetter
|
@JsonAnySetter
|
||||||
final Map<String, Object> any = new LinkedHashMap<>();
|
final Map<String, Object> any = new LinkedHashMap<>();
|
||||||
|
|
||||||
@JsonAnyGetter
|
@JsonAnyGetter
|
||||||
public Map<String, Object> getAny() {
|
public Map<String, Object> any() {
|
||||||
return Collections.unmodifiableMap( any );
|
return Collections.unmodifiableMap( any );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <V> V any(final String key) {
|
||||||
|
return (V) any.get( key );
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("EqualsAndHashcode")
|
@SuppressWarnings("EqualsAndHashcode")
|
||||||
public static class MPJSONEmptyValue {
|
public static class MPJSONEmptyValue {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings({ "ChainOfInstanceofChecks", "Contract" })
|
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
|
||||||
@SuppressFBWarnings({ "EQ_UNUSUAL", "EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS", "HE_EQUALS_USE_HASHCODE" })
|
@SuppressFBWarnings({ "EQ_UNUSUAL", "EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS", "HE_EQUALS_USE_HASHCODE" })
|
||||||
public boolean equals(final Object obj) {
|
public boolean equals(final Object obj) {
|
||||||
|
return isEmpty( obj );
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "ChainOfInstanceofChecks", "ConstantConditions" })
|
||||||
|
private static boolean isEmpty(final Object obj) {
|
||||||
|
if (obj == null)
|
||||||
|
return true;
|
||||||
if (obj instanceof Collection<?>)
|
if (obj instanceof Collection<?>)
|
||||||
return ((Collection<?>) obj).isEmpty();
|
return ((Collection<?>) obj).isEmpty();
|
||||||
if (obj instanceof Map<?, ?>)
|
if (obj instanceof Map<?, ?>)
|
||||||
return ((Map<?, ?>) obj).isEmpty();
|
return ((Map<?, ?>) obj).isEmpty();
|
||||||
if (obj instanceof MPJSONFile.Site.Ext)
|
if (obj instanceof MPJSONAnyObject)
|
||||||
return ((MPJSONAnyObject) obj).any.isEmpty();
|
return ((MPJSONAnyObject) obj).any.isEmpty() && (objectMapper.valueToTree( obj ).size() == 0);
|
||||||
|
|
||||||
return obj == null;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.google.common.primitives.UnsignedInteger;
|
import com.google.common.primitives.UnsignedInteger;
|
||||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.MPModelConstants;
|
|
||||||
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
||||||
|
import com.lyndir.masterpassword.model.MPModelConstants;
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -44,22 +44,6 @@ import org.joda.time.Instant;
|
|||||||
@SuppressFBWarnings("URF_UNREAD_FIELD")
|
@SuppressFBWarnings("URF_UNREAD_FIELD")
|
||||||
public class MPJSONFile extends MPJSONAnyObject {
|
public class MPJSONFile extends MPJSONAnyObject {
|
||||||
|
|
||||||
protected static final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
static {
|
|
||||||
objectMapper.setDefaultPrettyPrinter( new DefaultPrettyPrinter() {
|
|
||||||
private static final long serialVersionUID = 1;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DefaultPrettyPrinter withSeparators(final Separators separators) {
|
|
||||||
super.withSeparators( separators );
|
|
||||||
_objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " ";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
|
|
||||||
}
|
|
||||||
|
|
||||||
MPJSONFile() {
|
MPJSONFile() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +63,12 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
user.last_used = MPModelConstants.dateTimeFormatter.print( modelUser.getLastUsed() );
|
user.last_used = MPModelConstants.dateTimeFormatter.print( modelUser.getLastUsed() );
|
||||||
user.key_id = modelUser.exportKeyID();
|
user.key_id = modelUser.exportKeyID();
|
||||||
user.algorithm = modelUser.getAlgorithm().version();
|
user.algorithm = modelUser.getAlgorithm().version();
|
||||||
user.default_type = modelUser.getDefaultType();
|
user._ext_mpw = new User.Ext() {
|
||||||
|
{
|
||||||
|
default_type = modelUser.getPreferences().getDefaultType();
|
||||||
|
hide_passwords = modelUser.getPreferences().isHidePasswords();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Section "sites"
|
// Section "sites"
|
||||||
sites = new LinkedHashMap<>();
|
sites = new LinkedHashMap<>();
|
||||||
@@ -130,8 +119,11 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
site._ext_mpw = new Site.Ext();
|
site._ext_mpw = new Site.Ext() {
|
||||||
site._ext_mpw.url = modelSite.getUrl();
|
{
|
||||||
|
url = modelSite.getUrl();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,10 +132,11 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
|
|
||||||
return new MPFileUser(
|
return new MPFileUser(
|
||||||
user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar,
|
user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar,
|
||||||
(user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(),
|
(user._ext_mpw != null)? user._ext_mpw.default_type: null,
|
||||||
(user.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
|
(user.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
|
||||||
|
(user._ext_mpw != null) && user._ext_mpw.hide_passwords,
|
||||||
export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE,
|
export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE,
|
||||||
MPMarshalFormat.JSON, file.getParentFile()
|
MPMarshalFormat.JSON, file
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,8 +202,17 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
String key_id;
|
String key_id;
|
||||||
@Nullable
|
@Nullable
|
||||||
MPAlgorithm.Version algorithm;
|
MPAlgorithm.Version algorithm;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
MPResultType default_type;
|
Ext _ext_mpw;
|
||||||
|
|
||||||
|
|
||||||
|
public static class Ext extends MPJSONAnyObject {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MPResultType default_type;
|
||||||
|
boolean hide_passwords;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
package com.lyndir.masterpassword.model.impl;
|
package com.lyndir.masterpassword.model.impl;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author lhunath, 2017-09-20
|
* @author lhunath, 2017-09-20
|
||||||
*
|
*
|
||||||
@@ -72,4 +75,8 @@ public enum MPMarshalFormat {
|
|||||||
|
|
||||||
@SuppressWarnings("MethodReturnAlwaysConstant")
|
@SuppressWarnings("MethodReturnAlwaysConstant")
|
||||||
public abstract String fileSuffix();
|
public abstract String fileSuffix();
|
||||||
|
|
||||||
|
public boolean matches(final File file) {
|
||||||
|
return file.getName().endsWith( fileSuffix() );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule public/site updated: 8c54a29cfb...ebd2c741fd
Reference in New Issue
Block a user