Compare commits

..
633 Commits
Author SHA1 Message Date
Sebastian Stenzel c6d88f1dc4 Merge branch 'release/1.5.0-beta3' 2020-02-27 10:02:37 +01:00
Sebastian Stenzel 4a2d71405d preparing 1.5.0-beta3 2020-02-27 09:58:37 +01:00
Sebastian StenzelandGitHub f98c3aa5ca New Crowdin translations (#1064)
[ci skip]
2020-02-27 09:54:48 +01:00
Sebastian Stenzel 7d389b561d use shorter names for rtf files to make sure the name doesn't get collapsed in the file explorer 2020-02-27 09:40:52 +01:00
Sebastian Stenzel 7056d35254 refer to docs.cryptomator.org instead of google.com in README file 2020-02-27 09:39:03 +01:00
Sebastian Stenzel 6a8234ddd9 fixes #1046 2020-02-26 16:09:08 +01:00
Tobias Hagemann 3d3eed0ed2 updated logo and removed unused assets 2020-02-21 12:08:58 +01:00
Sebastian Stenzel e479b35846 typo
[ci skip]
2020-02-20 17:43:19 +01:00
Sebastian Stenzel 83c7421a96 renamed keys
[ci skip]
2020-02-20 17:19:56 +01:00
Sebastian StenzelandGitHub 116091f2e0 New Crowdin translations (#1041)
* New translations
[ci skip]
2020-02-20 17:17:49 +01:00
Sebastian Stenzel ce11017609 Moved masterkey-related vault options to its own tab
Also made remaining vault recovery labels localizable
2020-02-20 16:27:21 +01:00
Sebastian Stenzel caa8c84d8a Can now use a recovery key to reset a vault's password 2020-02-20 15:31:22 +01:00
Sebastian Stenzel e14fc56b37 fixed autocompletion for pasting text 2020-02-20 14:09:32 +01:00
Sebastian Stenzel 0d29e56948 Implemented word auto-completion for recovery key entry field 2020-02-20 11:10:42 +01:00
Sebastian Stenzel d2a27c782d No longer warn about this method invocation 2020-02-20 11:09:52 +01:00
Sebastian Stenzel 864e92ea59 updated funding.yml 2020-02-20 09:51:43 +01:00
Sebastian Stenzel 97dfe9a1d4 fixed unit test 2020-02-20 00:28:48 +01:00
Sebastian Stenzel d7edfd13a7 Began vault recovery wizard 2020-02-19 17:27:07 +01:00
Sebastian Stenzel 35207de7cc fixes #1056 2020-02-17 22:36:13 +01:00
Sebastian Stenzel adf0e3720d dedup 2020-02-17 20:34:20 +01:00
Sebastian Stenzel f73c1889b7 Show generic error screen in case of exceptions during migration (references #1056) 2020-02-17 20:23:19 +01:00
Sebastian Stenzel 18ff1d2898 fixes #986, fixes #1062 2020-02-17 18:45:31 +01:00
Sebastian Stenzel c6e2fa11a4 updated cryptofs to 1.9.2, fixes #1057, references #1056 2020-02-17 18:14:08 +01:00
Sebastian Stenzel 3fac8b7cd6 updated dependencies 2020-02-17 18:12:58 +01:00
Sebastian Stenzel f62faa72ce (Auto)Unlock via VaultService (fixes #1044) 2020-02-13 23:50:26 +01:00
Sebastian Stenzel 57256d0733 began implementation of new auto unlocker (references #1044) 2020-02-13 17:16:09 +01:00
Sebastian Stenzel e1f44fb48a App lifecycle fixes 2020-02-13 16:29:49 +01:00
Sebastian Stenzel 62676d5a83 minor ui state tweaks
[ci skip]
2020-02-13 14:32:23 +01:00
Armin Schrenk 7755256956 further additions to #1060 2020-02-12 16:19:42 +01:00
Armin Schrenk e1cf8546b7 adding recover vault dialogue stub 2020-02-12 15:02:06 +01:00
Tobias Hagemann e40837da2c updated primary_d2 color (somehow there was a small discrepancy in the corporate design manual) [ci skip] 2020-01-31 13:25:11 +01:00
Tobias Hagemann e7c3c0ab53 updated color palette 2020-01-30 15:33:54 +01:00
Sebastian Stenzel 2336b9c622 write vault migration events to additional upgrade log (references #1058) 2020-01-30 09:24:46 +01:00
Sebastian StenzelandGitHub 4e2122a64f Merge pull request #1050 from zhouer/close_request
Handle close request event for main window, fixes #1040
2020-01-16 12:22:56 +01:00
Sebastian Stenzel 833bb085e6 Using disting ExecutorService and ScheduledExecutorService since the latter doesn't scale beyond its pre-defined number of core threads
fixes #1051
2020-01-15 13:07:19 +01:00
En-Jan Chou 1ec9b0507d Prevent window close if user choose cancel in quit window 2020-01-14 17:12:36 -05:00
En-Jan Chou b6a4f70ec9 Handle close request event for main window
When receiving close request event, main window should quit program
if there is no system tray available, and just like the close button
was clicked. The window close request event happens when user close
window by pressing alt-f4 (Gnome) or when receiving WM_DELETE_WINDOW
xwindow event. Current behavior is just close the window and let the
program keep running, and there is no way to quit program without
system tray.
2020-01-14 16:54:47 -05:00
Sebastian Stenzel eb047e29a0 jcenter requires https now
[ci skip]
2020-01-14 22:06:01 +01:00
Sebastian Stenzel 5dc1d9b2e0 Fail fast (ammendment for #1047) 2020-01-14 21:46:52 +01:00
Sebastian Stenzel 362f3eac63 Certain app lifecycle event handlers are now now longer @TrayMenuScoped but rather @Singleton, so they get set up even if there is no TrayIcon (references #1035) 2020-01-14 21:43:45 +01:00
Sebastian StenzelandGitHub f0b26c60c4 Merge pull request #1045 from zhouer/minimize
Add minimize button, fixes #1035
2020-01-14 20:59:10 +01:00
Sebastian StenzelandGitHub e827d86967 Merge pull request #1047 from zhouer/icons
Add multiple icons and let system choose the best size
2020-01-14 20:54:18 +01:00
Tobias Hagemann 04533c0338 downgrading font awesome from pro to free 2020-01-14 16:57:13 +01:00
En-Jan Chou e148e39fef Rollback TrayMenuController to FxApplication code moving 2020-01-13 03:48:34 -05:00
En-Jan Chou cb97905c98 Use static emptyList and close InputStream 2020-01-12 21:17:56 -05:00
En-Jan Chou 7ec7cecbfd Add multiple icons and let system choose the best size 2020-01-12 13:42:17 -05:00
En-Jan Chou 3369401f1c Add minimize button and tray menu refactoring
* Add minimize button and change close button behavior
  * close main window (minimize to tray) if system tray available,
    quit application otherwise.
  * show minimize button if system tray unavailable
* Move some codes from TrayMenuController to FxApplication, includes:
  * Desktop integrations (shortcut, quit handlers...)
  * vaults change listener for sudden termination
  * public method showPreferenceWindow change to showPerferenceTab
    due to name conflict
  * public method quitApplication for both main window and system tray
  * shutdown hook for unmounting vaults on shutdown
* Add a new i18n string: main.minimizeBtn.tooltip
2020-01-12 05:29:56 -05:00
Sebastian Stenzel 09b9cfe027 onboarding text in empty vault list is now centered 2020-01-10 16:12:00 +01:00
Sebastian Stenzel e94ac3720b onboarding text in empty vault list is now centered 2020-01-10 14:46:25 +01:00
Sebastian Stenzel 4414d60026 Merge branch 'master' into develop 2020-01-09 17:16:40 +01:00
Sebastian Stenzel a6e680e32d Merge branch 'release/1.5.0-beta2' 2020-01-09 17:15:55 +01:00
Sebastian Stenzel d60a024c63 preparing 1.5.0-beta2 2020-01-09 16:50:38 +01:00
Sebastian StenzelandGitHub cc1b536656 New Crowdin translations (#1039)
[ci skip]
2020-01-09 16:49:39 +01:00
Sebastian Stenzel 85e773abef enable themes for donators 2020-01-09 16:34:31 +01:00
Sebastian Stenzel 4690bc56aa fixes #1038 2020-01-09 16:04:46 +01:00
Sebastian Stenzel 292c2676fd fixes #980 2020-01-09 15:44:25 +01:00
Sebastian Stenzel f7c6604c7c fixes #1029 2020-01-09 15:16:13 +01:00
Sebastian Stenzel 22310eb957 updated donation key store uri 2020-01-09 14:38:23 +01:00
Sebastian Stenzel 6d17d1298b removed unused fields 2020-01-09 13:03:22 +01:00
Sebastian Stenzel e90a680620 Split up MainWindowController, as it grew too big. The title bar is now handled by MainWindowTitleController
(@zhouer add the minimize button for #1035 to this new controller and fxml file)
2020-01-09 12:18:24 +01:00
Sebastian Stenzel 35d0ccfe89 allow resizing main window on all four corners
fixes #995
2020-01-09 12:06:12 +01:00
Tobias Hagemann 62cc454367 updated license public key 2020-01-09 10:35:27 +01:00
Tobias Hagemann 8bb277fe56 added iCloud Drive and OneDrive location presets 2020-01-09 10:27:56 +01:00
Sebastian Stenzel 67485b76af Make sure that adding a vault located at ~/foo is noop when a vault located at /home/user/foo already exists
fixes #1032
2020-01-08 16:50:08 +01:00
Sebastian Stenzel 270e60d7cf updated logging
[ci skip]
2020-01-08 15:41:26 +01:00
Sebastian Stenzel 11ffec862e Added autostart support for windows
fixes #418
2020-01-07 17:56:05 +01:00
Sebastian Stenzel d37e0dd5e1 Determine autostart state async (#418) 2020-01-07 17:55:36 +01:00
Sebastian Stenzel 960c3665e0 Localizations 2020-01-06 12:54:51 +01:00
Sebastian Stenzel 3396c79c38 updated intellij config
[ci skip]
2020-01-06 12:45:01 +01:00
Sebastian Stenzel cc1517f1b8 Added checkbox for auto start on macOS (#418) 2020-01-06 12:37:36 +01:00
Sebastian Stenzel 4f3ff55ae7 updated stale config
[ci skip]
2020-01-06 09:48:18 +01:00
Sebastian Stenzel f51362e95e cleanup
[ci skip]
2020-01-06 09:46:14 +01:00
Sebastian Stenzel 415dfece58 sames as in 9b6145ce:
We only want the user to trigger the listener
2020-01-06 09:46:01 +01:00
Sebastian StenzelandGitHub 3bdb5353da New Crowdin translations (#1015)
[ci skip]
2020-01-04 22:11:48 +01:00
Sebastian StenzelandGitHub 6ddf1b59ef Merge pull request #1031 from zhouer/hide_start_hidden
Hide startHidden setting when system tray is not supported
2019-12-29 14:18:35 +01:00
En-Jan Chou ef88f11c4f Hide startHidden setting when system tray is not supported
For platform not supporting system tray, users will have no chance to show
window and modify settings back again if the window is hidden at startup.
2019-12-26 09:40:33 -05:00
Sebastian Stenzel ec800c5439 Enforce minimum password length (amendment to #1018) 2019-12-18 15:31:52 +01:00
Sebastian Stenzel a2f3f5d254 fixes #1018, references #979 2019-12-18 14:58:36 +01:00
Sebastian Stenzel f475f70adf Updated zxcvbn4j to 1.3.0, which allows us to swipe the password from memory even after measuring its strength 2019-12-18 14:40:04 +01:00
Sebastian StenzelandGitHub 93c3da66da Merge pull request #1024 from zhouer/fix_mount_point
Initialize radio button before binding settings to it
2019-12-18 12:16:12 +01:00
En-Jan Chou 9b6145ce3f Initialize radio button before binding settings to it
The usesIndividualMountPath setting will be cleared if it bind to
an uninitialized radio button. This patch sets radio button with
current settings before binding the setting to the radio button.
2019-12-18 02:39:03 -05:00
Sebastian Stenzel 148eed172a Deduplicated UI code
Made "Enter Password" and "Reenter Password" a reusable component that is included in its original places
preparation for #1018
2019-12-17 17:35:16 +01:00
Sebastian Stenzel c97f146964 enabled "lock all" from tray menu (#297, #863) 2019-12-16 14:44:14 +01:00
Sebastian Stenzel a1034f5663 Refactored "lock all" task 2019-12-16 14:43:33 +01:00
Sebastian Stenzel 938b351f33 enabled "reveal" from tray menu (#297) 2019-12-16 13:10:43 +01:00
Sebastian Stenzel d870ecbcdb moved reveal() to VaultService 2019-12-16 13:03:28 +01:00
Sebastian Stenzel 5dff2126c4 cleanup of new VaultService 2019-12-16 11:22:30 +01:00
Sebastian Stenzel 41a6cc15e8 run "lock all" service on shared executorservice 2019-12-16 10:32:35 +01:00
Sebastian Stenzel f21b30c009 removed unused field 2019-12-16 10:31:04 +01:00
Sebastian Stenzel d69b63acc3 enabled "lock" from tray menu (#297) 2019-12-11 15:57:39 +01:00
Sebastian Stenzel f95b2baad5 "lock" deduplication 2019-12-11 15:57:14 +01:00
Sebastian Stenzel 1a4d1fffb3 shutdown logback manually 2019-12-11 13:53:32 +01:00
Sebastian Stenzel ac536ba125 Some shutdown hook refactorings for #980 2019-12-04 17:50:30 +01:00
Sebastian Stenzel 362b225d66 Fixes #939 2019-11-27 16:39:40 +01:00
Sebastian Stenzel 7d0bdc1a63 Show stack trace in case of unexpected exceptions 2019-11-27 15:44:53 +01:00
Sebastian Stenzel 94e3b21e44 Typo
[ci skip]
2019-11-27 13:20:44 +01:00
Sebastian Stenzel 23f89c1dc9 Reevaluate vault state when selecting vault 2019-11-26 17:21:28 +01:00
Sebastian Stenzel ce9c2f2c7a removed unused import
[ci skip]
2019-11-26 17:21:08 +01:00
Tobias Hagemann 44e97ab046 smaller text size for path in reveal button [ci skip] 2019-11-26 17:02:13 +01:00
Sebastian Stenzel 5d0a1fd49f tabpane styling
[ci skip]
2019-11-26 16:39:32 +01:00
Sebastian Stenzel ffc1d8dc1e simplified 2019-11-22 14:23:55 +01:00
Sebastian Stenzel 54fca93bba deduplicated code 2019-11-22 14:21:51 +01:00
Sebastian Stenzel c281687910 added copy/print buttons to recovery key dialog 2019-11-22 13:43:40 +01:00
Sebastian Stenzel 6229d7abbe Updated password and recovery key instructions 2019-11-22 13:10:50 +01:00
Sebastian Stenzel 01644eddb6 increase wizard dimensions
[ci skip]
2019-11-21 16:58:05 +01:00
Sebastian Stenzel 6e30df3796 Let the user choose whether a recovery key should be shown during vault creation 2019-11-21 16:57:43 +01:00
Sebastian Stenzel f87fa319ff renamed class 2019-11-20 17:14:31 +01:00
Sebastian Stenzel 9b019726bb show badge for missing license key in window title 2019-11-20 17:12:11 +01:00
Sebastian StenzelandGitHub cceafb76ed New Crowdin translations
New translations strings.properties
[ci skip]
2019-11-20 17:09:01 +01:00
Tobias Hagemann 39e2994c69 updated drag-n-drop-indicator styling 2019-11-20 16:53:02 +01:00
Sebastian Stenzel 1ddfcc3219 reworked drag'n'drop l&f 2019-11-20 15:03:03 +01:00
Tobias Hagemann 687f11596e added disabled styling to choice-box 2019-11-19 21:52:52 +01:00
Sebastian Stenzel b445f614fb Made mount point settings localizable
(and some fxml cleanup)
[ci skip]
2019-11-19 21:39:00 +01:00
Sebastian Stenzel c6fc1d93a0 save donation key to settings 2019-11-19 21:20:00 +01:00
Sebastian Stenzel 28d58922e3 TextArea needs a prefRowCount, otherwise it attempts to use all available space, displacing other elements.
fixes #1012
2019-11-18 10:11:20 +01:00
Sebastian Stenzel b884ea7ddc added registration key to preferences 2019-11-15 15:56:35 +01:00
Sebastian Stenzel 1717e20b61 Add JWT verifier 2019-11-14 17:02:41 +01:00
Sebastian Stenzel 6222f96b01 Merge branch 'master' into develop 2019-11-12 13:40:34 +01:00
Sebastian Stenzel 88f83c5cdb Merge branch 'release/1.5.0-beta1' 2019-11-12 13:33:08 +01:00
Sebastian Stenzel c8059bef78 bumped version
[ci skip]
2019-11-12 13:32:51 +01:00
Sebastian Stenzel 638b731cf8 always set version to SNAPSHOT-{commit} for untagged CI builds 2019-11-12 13:23:05 +01:00
Sebastian Stenzel b2b6e304ed updated CI config 2019-11-12 13:13:58 +01:00
Sebastian Stenzel 0748709f40 Updated dependencies 2019-11-11 16:42:56 +01:00
Sebastian StenzelandGitHub d8ee446d37 New Crowdin translations (#987)
* New translations strings.properties
[ci skip]
2019-11-11 16:36:48 +01:00
Sebastian Stenzel 9932185ccc fixed typo
kudos to our translation team
[ci skip]
2019-10-31 22:10:37 +01:00
Sebastian Stenzel 77afd4688a ide settings
[ci skip]
2019-10-31 22:10:09 +01:00
Sebastian Stenzel 9a5ef3f6ff updated cryptofs to 1.9.0-rc1 2019-10-31 01:29:30 +01:00
Sebastian Stenzel c99f23a4c7 Merge branch 'support/1.4.x' into develop
# Conflicts:
#	main/buildkit/pom.xml
#	main/commons/pom.xml
#	main/keychain/pom.xml
#	main/launcher/pom.xml
#	main/pom.xml
#	main/ui/pom.xml
2019-10-31 01:11:02 +01:00
Sebastian Stenzel 274c3886f5 Merge branch 'hotfix/1.4.17' into support/1.4.x 2019-10-31 01:01:24 +01:00
Sebastian Stenzel 95db29158c fixes #997 2019-10-31 00:54:49 +01:00
Sebastian Stenzel 872680a737 updated dependencies, removed commons-io 2019-10-31 00:54:37 +01:00
Sebastian Stenzel 0ba12f6301 bumped version to 1.4.17 2019-10-31 00:54:09 +01:00
Armin Schrenk 248df9da51 TODOs 2019-10-23 14:23:53 +02:00
Armin Schrenk 702408d488 phrasing 2019-10-23 14:23:44 +02:00
Armin Schrenk dd1506f17a windows: fixing that already used drive letters are not shown as such in drop down menu 2019-10-23 13:07:41 +02:00
Sebastian Stenzel 7fb5c741ad get rid of some legacy code with too much if/else 2019-10-18 14:34:48 +02:00
Sebastian Stenzel aa61ab2b6e allow changing of user interface orientation via settings 2019-10-18 12:51:41 +02:00
Armin Schrenk 7a1e20d732 showing custom mountPath if selected 2019-10-16 16:58:50 +02:00
Armin Schrenk c1a5e187b6 localization 2019-10-16 16:49:21 +02:00
Armin Schrenk 57d3f788e6 further improving windows mount options 2019-10-16 16:41:28 +02:00
Armin Schrenk ca73c3ad90 Updating WinDriveLetters since Dokany be installed in version 1.3.x 2019-10-16 15:57:08 +02:00
Armin Schrenk aec367dcc7 adding TODO 2019-10-16 15:49:17 +02:00
Armin Schrenk 56e7c13cb1 including windows mount option for selecting mount point (without functionality) 2019-10-16 14:32:10 +02:00
Sebastian Stenzel 3b4f384bfd there shouldn't be a "."
[ci skip]
2019-10-14 10:51:16 +02:00
Sebastian Stenzel d1a20da7e0 Localization [ci skip] 2019-10-14 10:22:23 +02:00
Tobias HagemannandGitHub 4b3533f717 say my name [ci skip] 2019-10-14 00:31:49 +02:00
Sebastian Stenzel 6535adef44 Merge branch 'master' into develop 2019-10-13 15:42:09 +02:00
Sebastian Stenzel f69cde1469 Merge branch 'release/1.5.0-alpha2' 2019-10-13 15:41:27 +02:00
Sebastian Stenzel 59643b762f Preparing 1.5.0-alpha2 2019-10-13 15:36:52 +02:00
Sebastian Stenzel ec69c1411b Merge commit 'ad28d4510d8b99db02c1883f1061817b658f3407' into develop
# Conflicts:
#	main/ui/src/main/resources/i18n/strings_de.properties
2019-10-13 15:35:00 +02:00
Sebastian Stenzel ad28d4510d New translations strings.properties (German)
[ci skip]
2019-10-13 15:26:53 +02:00
Sebastian Stenzel 111e500928 only show migration's progressbar when there actually is any progress 2019-10-13 15:20:38 +02:00
Sebastian Stenzel b5cb129ff0 updated to lastest cryptofs beta 2019-10-13 14:46:25 +02:00
Sebastian Stenzel 8a1586e5e8 New translations strings.properties (Japanese)
[ci skip]
2019-10-12 08:15:37 +02:00
Sebastian Stenzel e1959211de New translations strings.properties (French)
[ci skip]
2019-10-11 22:50:43 +02:00
Sebastian Stenzel 53494fa141 New translations strings.properties (French)
[ci skip]
2019-10-11 22:08:38 +02:00
Sebastian Stenzel 7fdab3a2ab New translations strings.properties (French)
[ci skip]
2019-10-11 21:27:17 +02:00
Sebastian Stenzel cb749c2fba New translations strings.properties (French)
[ci skip]
2019-10-11 19:58:32 +02:00
Sebastian Stenzel 453efe9998 New translations strings.properties (French)
[ci skip]
2019-10-11 19:26:49 +02:00
Sebastian Stenzel 2948b78cbe showing the window shouldn't be a decision made by the vault list controller.
when opening a .cryptomator file, the handler for open file requests is responsible for showing the main window.
2019-10-11 15:48:28 +02:00
Sebastian Stenzel 5a9f993df8 updated cryptofs to fix a migration problem 2019-10-11 13:23:19 +02:00
Sebastian Stenzel 4936cc76d0 New translations strings.properties (Korean)
[ci skip]
2019-10-11 00:45:55 +02:00
Sebastian Stenzel 4a15467ff5 New translations strings.properties (Italian)
[ci skip]
2019-10-10 22:41:06 +02:00
Sebastian Stenzel bb09b32885 New translations strings.properties (French)
[ci skip]
2019-10-10 18:57:37 +02:00
Sebastian Stenzel 4c9372747c New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-10 12:59:48 +02:00
Sebastian Stenzel 1c04c22617 New translations strings.properties (Japanese)
[ci skip]
2019-10-10 10:18:14 +02:00
Sebastian Stenzel 675146d20c New translations strings.properties (Japanese)
[ci skip]
2019-10-10 09:48:14 +02:00
Sebastian Stenzel 56ee6af9de New translations strings.properties (Russian)
[ci skip]
2019-10-10 08:32:02 +02:00
Sebastian Stenzel 926a0b3717 New translations strings.properties (Spanish)
[ci skip]
2019-10-10 06:26:05 +02:00
Sebastian Stenzel eae28ce76d New translations strings.properties (Spanish)
[ci skip]
2019-10-10 05:57:57 +02:00
Sebastian Stenzel 99c34539e6 New translations strings.properties (German)
[ci skip]
2019-10-09 21:28:50 +02:00
Sebastian Stenzel 0dfafdd874 New translations strings.properties (German)
[ci skip]
2019-10-09 21:02:41 +02:00
Sebastian Stenzel a8cb40831e New translations strings.properties (Dutch)
[ci skip]
2019-10-09 20:21:07 +02:00
Sebastian Stenzel 3b1bed9345 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 19:29:01 +02:00
Sebastian Stenzel f18b81bdb7 New translations strings.properties (Spanish)
[ci skip]
2019-10-09 19:01:38 +02:00
Armin Schrenk 4a61fe372e enhancing error handling in storage location selection when creating a new vault 2019-10-09 18:24:23 +02:00
Sebastian Stenzel 0bd0543d10 New translations strings.properties (Russian)
[ci skip]
2019-10-09 17:49:00 +02:00
Sebastian Stenzel 06a3a04840 New translations strings.properties (Italian)
[ci skip]
2019-10-09 17:48:58 +02:00
Sebastian Stenzel 3eb379b1e9 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 17:07:27 +02:00
Sebastian Stenzel 52e7707f81 New translations strings.properties (Greek)
[ci skip]
2019-10-09 17:07:25 +02:00
Sebastian Stenzel 28d2424cd5 New translations strings.properties (Japanese)
[ci skip]
2019-10-09 17:07:23 +02:00
Sebastian Stenzel 187e9f17fc New translations strings.properties (Korean)
[ci skip]
2019-10-09 17:07:22 +02:00
Sebastian Stenzel b9776a1017 New translations strings.properties (Croatian)
[ci skip]
2019-10-09 17:07:21 +02:00
Sebastian Stenzel 3fbf28eea9 New translations strings.properties (Portuguese)
[ci skip]
2019-10-09 17:07:19 +02:00
Sebastian Stenzel 99e7ec7dc2 New translations strings.properties (Turkish)
[ci skip]
2019-10-09 17:07:18 +02:00
Sebastian Stenzel a9b4512ce6 New translations strings.properties (Dutch)
[ci skip]
2019-10-09 17:07:16 +02:00
Sebastian Stenzel feb9ee238d New translations strings.properties (French)
[ci skip]
2019-10-09 17:07:14 +02:00
Sebastian Stenzel 748f7ca889 New translations strings.properties (Czech)
[ci skip]
2019-10-09 17:07:12 +02:00
Sebastian Stenzel 87a469f264 New translations strings.properties (Spanish)
[ci skip]
2019-10-09 17:07:11 +02:00
Sebastian Stenzel 59e0175d65 New translations strings.properties (Russian)
[ci skip]
2019-10-09 17:07:09 +02:00
Sebastian Stenzel 7595f5317d New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-09 17:07:07 +02:00
Sebastian Stenzel 3ba4e87f1d New translations strings.properties (Italian)
[ci skip]
2019-10-09 17:07:06 +02:00
Sebastian Stenzel 0a7aec26cc New translations strings.properties (German)
[ci skip]
2019-10-09 17:07:04 +02:00
Armin Schrenk d70b7e12ef add TODO 2019-10-09 16:48:20 +02:00
Armin Schrenk 54d2591391 adding error screen for adding existing vault 2019-10-09 16:48:07 +02:00
Armin Schrenk 4f70695ceb reducing possible mistake when adding new controller to module 2019-10-09 16:45:50 +02:00
Sebastian Stenzel 8f0a151018 New translations strings.properties (Spanish)
[ci skip]
2019-10-09 16:24:33 +02:00
Sebastian Stenzel cd64460e62 added shake animation 2019-10-09 16:15:58 +02:00
Sebastian Stenzel db3c0622c6 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 15:55:05 +02:00
Sebastian Stenzel ac0092b178 New translations strings.properties (Korean)
[ci skip]
2019-10-09 15:55:02 +02:00
Sebastian Stenzel fab32e9e93 New translations strings.properties (Russian)
[ci skip]
2019-10-09 15:55:00 +02:00
Sebastian Stenzel 3d1129b0f3 New translations strings.properties (Italian)
[ci skip]
2019-10-09 15:54:57 +02:00
Sebastian Stenzel 89b2ff74f3 New translations strings.properties (German)
[ci skip]
2019-10-09 15:54:54 +02:00
Sebastian Stenzel a43e8fc461 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 15:21:38 +02:00
Sebastian Stenzel 98d3fddfcc New translations strings.properties (Greek)
[ci skip]
2019-10-09 15:21:36 +02:00
Sebastian Stenzel ff36fff091 New translations strings.properties (Japanese)
[ci skip]
2019-10-09 15:21:35 +02:00
Sebastian Stenzel 7d02108c8b New translations strings.properties (Korean)
[ci skip]
2019-10-09 15:21:33 +02:00
Sebastian Stenzel 593f37d5fe New translations strings.properties (Croatian)
[ci skip]
2019-10-09 15:21:32 +02:00
Sebastian Stenzel 7abe9c627b New translations strings.properties (Portuguese)
[ci skip]
2019-10-09 15:21:30 +02:00
Sebastian Stenzel a0923c3c9b New translations strings.properties (Turkish)
[ci skip]
2019-10-09 15:21:29 +02:00
Sebastian Stenzel 439042ab95 New translations strings.properties (Dutch)
[ci skip]
2019-10-09 15:21:28 +02:00
Sebastian Stenzel 113e505c52 New translations strings.properties (French)
[ci skip]
2019-10-09 15:21:26 +02:00
Sebastian Stenzel f01e0ae194 New translations strings.properties (Czech)
[ci skip]
2019-10-09 15:21:25 +02:00
Sebastian Stenzel 5540cef257 New translations strings.properties (Spanish)
[ci skip]
2019-10-09 15:21:23 +02:00
Sebastian Stenzel ab16ee493f New translations strings.properties (Russian)
[ci skip]
2019-10-09 15:21:21 +02:00
Sebastian Stenzel b1a1d1029c New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-09 15:21:20 +02:00
Sebastian Stenzel a46b4aa768 New translations strings.properties (Italian)
[ci skip]
2019-10-09 15:21:18 +02:00
Sebastian Stenzel 245fe4b525 New translations strings.properties (German)
[ci skip]
2019-10-09 15:21:17 +02:00
Sebastian Stenzel c5a9926652 Added localizable labels to recovery key display window 2019-10-09 15:15:13 +02:00
Sebastian Stenzel 7032862a65 Added basic CSS for TextArea 2019-10-09 15:01:43 +02:00
Sebastian Stenzel 2012229c46 fixed tests 2019-10-09 14:14:38 +02:00
Sebastian Stenzel a6672ddbfc oxfordTop5000WordsSortedByLength.stream().filter(Pattern.compile("[a-z]{2,8}").asMatchPredicate()).distinct().limit(4096) 2019-10-09 14:04:28 +02:00
Sebastian Stenzel 8dd2147638 AppleScriptEngineFactory no longer exists since Java 9 (see https://bugs.openjdk.java.net/browse/JDK-8143404)
Formerly used in TrayIconUtil.java, which no longer exists either
2019-10-09 11:30:01 +02:00
Sebastian Stenzel 7a29a1b680 New translations strings.properties (Swedish)
[ci skip]
2019-10-09 08:48:14 +02:00
Sebastian Stenzel f4983f7862 New translations strings.properties (Spanish)
[ci skip]
2019-10-08 23:46:34 +02:00
Sebastian Stenzel 488bd1087a New translations strings.properties (German)
[ci skip]
2019-10-08 22:23:21 +02:00
Sebastian Stenzel d557136295 New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-08 21:42:26 +02:00
Sebastian Stenzel 87f4b639a0 New translations strings.properties (Italian)
[ci skip]
2019-10-08 20:38:54 +02:00
Sebastian Stenzel dbb9379a2e New translations strings.properties (Swedish)
[ci skip]
2019-10-08 20:10:14 +02:00
Sebastian Stenzel fa86ae68ea Removed old 1.4.x localizations 2019-10-08 20:03:02 +02:00
Sebastian Stenzel f82fddc8fe Made recovery key creation a two-step wizard 2019-10-08 19:18:49 +02:00
Sebastian Stenzel e6f0b321cb New translations strings.properties (German)
[ci skip]
2019-10-08 18:36:38 +02:00
Sebastian Stenzel 41fef58450 New translations strings.properties (Dutch)
[ci skip]
2019-10-08 18:01:08 +02:00
Sebastian Stenzel 268026629d New translations strings.properties (Spanish)
[ci skip]
2019-10-08 18:01:07 +02:00
Sebastian Stenzel b95fa1868a New translations strings.properties (German)
[ci skip]
2019-10-08 18:01:06 +02:00
Sebastian Stenzel d2b7376e37 New translations strings.properties (Korean)
[ci skip]
2019-10-08 17:19:31 +02:00
Sebastian Stenzel 8d6eac63e2 New translations strings.properties (Turkish)
[ci skip]
2019-10-08 17:19:29 +02:00
Sebastian Stenzel b8fc2dcb64 New translations strings.properties (Russian)
[ci skip]
2019-10-08 17:19:27 +02:00
Sebastian Stenzel 671f934934 New translations strings.properties (Swedish)
[ci skip]
2019-10-08 16:28:17 +02:00
Sebastian Stenzel 58ff6554fc New translations strings.properties (Japanese)
[ci skip]
2019-10-08 16:28:14 +02:00
Sebastian Stenzel cad6b221d9 New translations strings.properties (Korean)
[ci skip]
2019-10-08 16:28:13 +02:00
Sebastian Stenzel c02d5a9794 New translations strings.properties (Turkish)
[ci skip]
2019-10-08 16:28:09 +02:00
Sebastian Stenzel aac71277e3 New translations strings.properties (Dutch)
[ci skip]
2019-10-08 16:28:07 +02:00
Sebastian Stenzel aa61be7bf5 New translations strings.properties (French)
[ci skip]
2019-10-08 16:28:05 +02:00
Sebastian Stenzel 3a4f796c79 New translations strings.properties (Czech)
[ci skip]
2019-10-08 16:28:04 +02:00
Sebastian Stenzel f070cdc12d New translations strings.properties (Spanish)
[ci skip]
2019-10-08 16:28:02 +02:00
Sebastian Stenzel e14911d4d5 New translations strings.properties (Russian)
[ci skip]
2019-10-08 16:28:00 +02:00
Sebastian Stenzel aa666ad025 New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-08 16:27:59 +02:00
Sebastian Stenzel cd55c2666f New translations strings.properties (Italian)
[ci skip]
2019-10-08 16:27:57 +02:00
Sebastian Stenzel 0dc6032209 New translations strings.properties (German)
[ci skip]
2019-10-08 16:27:56 +02:00
Sebastian Stenzel f7630c28d6 Generate recovery key during vault creation 2019-10-08 16:26:57 +02:00
Sebastian Stenzel 08d9beb6b8 Externalized logic of recovery key creation to reusable utility class 2019-10-08 14:58:55 +02:00
Sebastian Stenzel 5808239416 Refactored shortcut handlers 2019-10-08 12:46:10 +02:00
Sebastian Stenzel e2f400340b added test for issue #979 2019-10-08 11:16:13 +02:00
Sebastian Stenzel 5854b24c44 updated badge link 2019-10-08 11:16:07 +02:00
Tobias Hagemann 7adaf44fb3 added progress bar 2019-10-08 01:06:50 +02:00
Sebastian Stenzel 0bd74056a0 New translations strings.properties (Swedish)
[ci skip]
2019-10-07 13:42:39 +02:00
Sebastian Stenzel bbeea7a2ee New translations strings.properties (Swedish)
[ci skip]
2019-10-06 17:55:46 +02:00
Sebastian Stenzel e3548737f1 New translations strings.properties (Russian)
[ci skip]
2019-10-06 17:55:44 +02:00
Sebastian Stenzel 75d7656824 New translations strings.properties (Swedish)
[ci skip]
2019-10-06 17:26:01 +02:00
Sebastian Stenzel c292f18915 New translations strings.properties (Russian)
[ci skip]
2019-10-06 17:25:59 +02:00
Sebastian Stenzel 9636e7c700 New translations strings.properties (Swedish)
[ci skip]
2019-10-06 16:56:15 +02:00
Sebastian Stenzel eb8f7840cc New translations strings.properties (Korean)
[ci skip]
2019-10-06 11:55:39 +02:00
Sebastian Stenzel 180d79f49e New translations strings.properties (Korean)
[ci skip]
2019-10-06 11:25:49 +02:00
Sebastian Stenzel 29cb1c96b1 New translations strings.properties (Swedish)
[ci skip]
2019-10-06 10:26:28 +02:00
Sebastian Stenzel f61432fc52 New translations strings.properties (French)
[ci skip]
2019-10-05 12:16:15 +02:00
Sebastian Stenzel 8e7dbf4640 New translations strings.properties (French)
[ci skip]
2019-10-05 11:50:53 +02:00
Sebastian Stenzel d2528faf3a New translations strings.properties (French)
[ci skip]
2019-10-05 10:07:47 +02:00
Sebastian Stenzel c1a3fe66ef New translations strings.properties (Japanese)
[ci skip]
2019-10-05 09:36:19 +02:00
Sebastian Stenzel ecb4c114a2 New translations strings.properties (French)
[ci skip]
2019-10-05 09:36:17 +02:00
Sebastian Stenzel 1ecaf5ae6e New translations strings.properties (Spanish)
[ci skip]
2019-10-05 00:55:54 +02:00
Sebastian Stenzel e88b7f00b3 New translations strings.properties (Spanish)
[ci skip]
2019-10-05 00:22:30 +02:00
Sebastian Stenzel 91aaabc7fb New translations strings.properties (French)
[ci skip]
2019-10-04 23:45:54 +02:00
Sebastian Stenzel 7414c29593 New translations strings.properties (French)
[ci skip]
2019-10-04 23:17:06 +02:00
Sebastian Stenzel 79ae5b7bff New translations strings.properties (French)
[ci skip]
2019-10-04 15:26:28 +02:00
Sebastian Stenzel 486a3a07f5 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 14:50:00 +02:00
Sebastian Stenzel 02d44eb0e7 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 13:56:22 +02:00
Sebastian Stenzel 3aff7bd956 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 13:20:39 +02:00
Sebastian Stenzel cd6a6cc55a New translations strings.properties (Korean)
[ci skip]
2019-10-04 07:36:24 +02:00
Sebastian Stenzel d0a30a1779 New translations strings.properties (Korean)
[ci skip]
2019-10-04 03:26:03 +02:00
Sebastian Stenzel a5fb30d7c7 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 01:55:05 +02:00
Sebastian Stenzel ba4e813a35 New translations strings.properties (French)
[ci skip]
2019-10-04 01:16:25 +02:00
Sebastian Stenzel f6d4caee07 New translations strings.properties (Japanese)
[ci skip]
2019-10-04 00:46:37 +02:00
Sebastian Stenzel d3a00e726f New translations strings.properties (French)
[ci skip]
2019-10-04 00:46:35 +02:00
Sebastian Stenzel d71c0695f4 New translations strings.properties (French)
[ci skip]
2019-10-04 00:19:26 +02:00
Sebastian Stenzel 394c91c50c New translations strings.properties (Czech)
[ci skip]
2019-10-03 18:06:06 +02:00
Sebastian Stenzel 71c35eaf4c New translations strings.properties (Greek)
[ci skip]
2019-10-03 15:44:58 +02:00
Sebastian Stenzel 70790d0e6a New translations strings.properties (Japanese)
[ci skip]
2019-10-03 14:08:44 +02:00
Sebastian Stenzel 8fbc00a417 New translations strings.properties (Japanese)
[ci skip]
2019-10-03 13:41:10 +02:00
Sebastian Stenzel 080bf7f540 New translations strings.properties (Japanese)
[ci skip]
2019-10-03 13:08:50 +02:00
Sebastian Stenzel 9d988294f3 New translations strings.properties (Czech)
[ci skip]
2019-10-03 12:30:27 +02:00
Sebastian Stenzel a712013507 New translations strings.properties (Czech)
[ci skip]
2019-10-03 12:00:54 +02:00
Sebastian Stenzel 0aab22975a New translations strings.properties (Czech)
[ci skip]
2019-10-03 11:25:33 +02:00
Sebastian Stenzel fde6555e4d New translations strings.properties (Czech)
[ci skip]
2019-10-03 10:55:07 +02:00
Sebastian Stenzel 748e76e187 New translations strings.properties (Czech)
[ci skip]
2019-10-03 10:24:17 +02:00
Sebastian Stenzel 55a627f718 New translations strings.properties (Czech)
[ci skip]
2019-10-03 08:58:08 +02:00
Sebastian Stenzel a19cbff12f New translations strings.properties (Czech)
[ci skip]
2019-10-03 08:27:39 +02:00
Sebastian Stenzel d5e098a6cd New translations strings.properties (Czech)
[ci skip]
2019-10-03 08:00:05 +02:00
Sebastian Stenzel 3f9fc28e3c New translations strings.properties (Spanish)
[ci skip]
2019-10-02 17:59:29 +02:00
Sebastian Stenzel 0bf4081d71 New translations strings.properties (Spanish)
[ci skip]
2019-10-02 17:29:19 +02:00
Sebastian Stenzel de3e392aa3 New translations strings.properties (Czech)
[ci skip]
2019-10-02 16:48:00 +02:00
Sebastian Stenzel 44a5f51c93 New translations strings.properties (Korean)
[ci skip]
2019-10-02 14:34:11 +02:00
Sebastian Stenzel 9c8ea3c2e5 New translations strings.properties (Croatian)
[ci skip]
2019-10-02 14:34:10 +02:00
Sebastian Stenzel 4208d1e036 New translations strings.properties (Portuguese)
[ci skip]
2019-10-02 14:34:08 +02:00
Sebastian Stenzel 154198fd5c New translations strings.properties (Turkish)
[ci skip]
2019-10-02 14:34:06 +02:00
Sebastian Stenzel 0ad0d79a54 New translations strings.properties (Dutch)
[ci skip]
2019-10-02 14:34:05 +02:00
Sebastian Stenzel 966538e47b New translations strings.properties (French)
[ci skip]
2019-10-02 14:34:03 +02:00
Sebastian Stenzel 181a1e7248 New translations strings.properties (Czech)
[ci skip]
2019-10-02 14:34:01 +02:00
Sebastian Stenzel 1a13e03a08 New translations strings.properties (Spanish)
[ci skip]
2019-10-02 14:33:59 +02:00
Sebastian Stenzel 0fa0568f24 New translations strings.properties (Russian)
[ci skip]
2019-10-02 14:33:58 +02:00
Sebastian Stenzel d7e76bee6d New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-02 14:33:56 +02:00
Sebastian Stenzel 77dcf4da62 New translations strings.properties (Italian)
[ci skip]
2019-10-02 14:33:55 +02:00
Sebastian Stenzel 6eb0384fa6 New translations strings.properties (German)
[ci skip]
2019-10-02 14:33:54 +02:00
Armin Schrenk 5af5a6c5b0 enhancing fxml loading to add local specific stylesheets, for #754 2019-10-02 14:27:10 +02:00
Armin Schrenk 0e4c833be0 loaading dosis-bold font via @font-face in css 2019-10-02 13:39:38 +02:00
Sebastian Stenzel a2e4a2c78a New translations strings.properties (Korean)
[ci skip]
2019-10-02 13:07:55 +02:00
Sebastian Stenzel f4ab2b590f New translations strings.properties (Korean)
[ci skip]
2019-10-02 12:32:27 +02:00
Sebastian Stenzel 80b3d7ab6b New translations strings.properties (Korean)
[ci skip]
2019-10-02 10:54:40 +02:00
Sebastian Stenzel b23fbd4507 New translations strings.properties (Korean)
[ci skip]
2019-10-02 10:29:20 +02:00
Sebastian Stenzel 5b56ff6d55 New translations strings.properties (Korean)
[ci skip]
2019-10-02 09:58:16 +02:00
Sebastian Stenzel 143b691c61 New translations strings.properties (Korean)
[ci skip]
2019-10-02 09:31:50 +02:00
Sebastian Stenzel 88e4c88a87 New translations strings.properties (Croatian)
[ci skip]
2019-10-01 22:37:58 +02:00
Sebastian Stenzel c9e06ad2d2 New translations strings.properties (Turkish)
[ci skip]
2019-10-01 22:07:35 +02:00
Sebastian Stenzel bd814f729d New translations strings.properties (Italian)
[ci skip]
2019-10-01 22:07:33 +02:00
Sebastian Stenzel 8c8b50b83a New translations strings.properties (Portuguese)
[ci skip]
2019-10-01 21:39:03 +02:00
Sebastian Stenzel e9f4790b40 New translations strings.properties (Turkish)
[ci skip]
2019-10-01 21:39:01 +02:00
Sebastian Stenzel ac1149b873 New translations strings.properties (Dutch)
[ci skip]
2019-10-01 21:39:00 +02:00
Sebastian Stenzel 51b6f828ed New translations strings.properties (Italian)
[ci skip]
2019-10-01 21:38:59 +02:00
Sebastian Stenzel b97a99d4e2 New translations strings.properties (Turkish)
[ci skip]
2019-10-01 21:09:42 +02:00
Sebastian Stenzel 6ed661aa13 New translations strings.properties (Dutch)
[ci skip]
2019-10-01 20:44:03 +02:00
Sebastian Stenzel f3d46ed767 New translations strings.properties (Italian)
[ci skip]
2019-10-01 20:44:02 +02:00
Sebastian Stenzel 3bde55c4df New translations strings.properties (Dutch)
[ci skip]
2019-10-01 20:08:57 +02:00
Sebastian Stenzel dd591ec258 New translations strings.properties (Spanish)
[ci skip]
2019-10-01 20:08:56 +02:00
Sebastian Stenzel 4ce035b554 New translations strings.properties (French)
[ci skip]
2019-10-01 18:38:52 +02:00
Sebastian Stenzel cdc451fdca New translations strings.properties (Czech)
[ci skip]
2019-10-01 18:38:50 +02:00
Sebastian Stenzel 0680d16708 New translations strings.properties (Italian)
[ci skip]
2019-10-01 18:09:13 +02:00
Sebastian Stenzel 1f1792522e New translations strings.properties (German)
[ci skip]
2019-10-01 18:09:12 +02:00
Sebastian Stenzel db53a7a3dc New translations strings.properties (Russian)
[ci skip]
2019-10-01 17:18:13 +02:00
Sebastian Stenzel 9362196d4c New translations strings.properties (Spanish)
[ci skip]
2019-10-01 16:51:17 +02:00
Sebastian Stenzel 527d387640 New translations strings.properties (Russian)
[ci skip]
2019-10-01 16:51:16 +02:00
Sebastian Stenzel 5f9dda4bbb New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2019-10-01 16:51:14 +02:00
Sebastian Stenzel 6a991f3ab5 New translations strings.properties (Italian)
[ci skip]
2019-10-01 16:51:13 +02:00
Sebastian Stenzel 4c023f792c New translations strings.properties (German)
[ci skip]
2019-10-01 15:27:40 +02:00
Sebastian Stenzel 6b5f7d37ca more crowdin fine tuning
[ci skip]
2019-10-01 15:26:18 +02:00
Sebastian Stenzel 87158b1e7a Update Crowdin configuration file 2019-10-01 13:53:37 +02:00
Sebastian Stenzel 0b132b7d10 updaten OpenJFX to version 13 2019-10-01 13:04:44 +02:00
Sebastian Stenzel cd4cb70896 show progress during vault migration (ugly prototype) 2019-09-27 21:44:24 +02:00
Sebastian Stenzel 633470b0d6 reduced visibility of scope annotation 2019-09-27 21:43:56 +02:00
Sebastian Stenzel 1930090044 Added first prototyp for recovery key generation 2019-09-27 21:43:42 +02:00
Sebastian Stenzel ccefb3613e renamed file 2019-09-27 20:01:02 +02:00
Sebastian Stenzel 9e79350b9e added the strongest avenger 😜
[ci skip]
2019-09-27 10:56:44 +02:00
Sebastian Stenzel 9092c2325a Merge branch 'master' into develop 2019-09-19 11:23:58 +02:00
Sebastian Stenzel 5cd3a6dbaf Merge branch 'release/1.5.0-alpha1' 2019-09-19 11:22:25 +02:00
Sebastian Stenzel 9934a4ea3a Preparing 1.5.0-alpha1 2019-09-19 11:13:07 +02:00
Sebastian Stenzel 3468a88268 Made most Window-specific Qualifiers package-private 2019-09-19 11:11:51 +02:00
Sebastian Stenzel 4d916cd5cc stage cleanup 2019-09-19 11:00:24 +02:00
Sebastian Stenzel 24f13e2afe added stage title for display in window managers 2019-09-19 11:00:03 +02:00
Sebastian Stenzel ad29c009b2 Disable custom theme for now 2019-09-19 10:42:14 +02:00
Sebastian Stenzel d87955adb6 Disable custom theme for now 2019-09-19 10:40:20 +02:00
Sebastian Stenzel 07aa71bb9d temporarily disabled vault migration 2019-09-19 09:19:07 +02:00
Sebastian Stenzel ea3ef8b60f removed outdated german localization 2019-09-19 09:18:45 +02:00
Tobias Hagemann a33df54ed3 updated localization, fixed wrongfilealert layout 2019-09-19 01:12:50 +02:00
Tobias Hagemann 3107f16768 fixed color palette in dark theme, caused by 04800407 [ci skip] 2019-09-19 00:55:17 +02:00
Sebastian Stenzel 230436be8a Merge branch 'feature/new-ui' into develop
fixes #931, fixes #930, fixes #927, fixes #926, fixes #925, fixes #458, fixes #621, fixes #957, fixes #225, fixes #341, fixes #329, fixes #577, fixes #283 and last but not least: fixes #891
2019-09-19 00:21:43 +02:00
Sebastian Stenzel 7fefeee145 Supress ugly error log during unit tests 2019-09-18 23:44:01 +02:00
Sebastian Stenzel db836a9dc3 JNI interfaces are now singletons 2019-09-18 23:40:53 +02:00
Tobias Hagemann ba22f0ed3c updated jni lib to 2.2.1 2019-09-18 23:16:11 +02:00
Sebastian Stenzel a731b1b569 fixed illegalstate exceptions when spamming "check for updates" button 2019-09-18 23:11:03 +02:00
Sebastian Stenzel 8bb3530928 major cleanup of old 1.4.x code 2019-09-18 23:10:38 +02:00
Sebastian Stenzel 2567ca50e7 Merged from develop 2019-09-18 22:29:58 +02:00
Sebastian Stenzel 8d00b926c4 Merge branch 'master' into develop 2019-09-18 22:09:14 +02:00
Sebastian Stenzel 7636d6874b Merge branch 'release/1.4.16' 2019-09-18 22:08:26 +02:00
Sebastian Stenzel 66c098c746 Preparing 1.4.16 2019-09-18 22:02:57 +02:00
Sebastian Stenzel 7534bb3c7e fixes #955 2019-09-18 22:01:16 +02:00
Armin Schrenk 200fc1a563 refactoring forget password dialog ( when unlocking a vault) 2019-09-18 19:12:01 +02:00
Armin Schrenk c961eb0ca0 adding version number to settings dialogue 2019-09-18 14:36:35 +02:00
Sebastian Stenzel d2086d100e Added utility to encode a recovery key to a human-friendly sequence of words 2019-09-16 17:39:10 +02:00
Sebastian Stenzel 2a33705cc6 removed irrelevant comment [ci skip] 2019-09-16 14:25:34 +02:00
Sebastian Stenzel 4b48f75aed Fine foreground/background switching, added reopen listener 2019-09-13 16:02:05 +02:00
Sebastian Stenzel 2bbc3e5834 Refactored adding vaults 2019-09-12 17:01:50 +02:00
Sebastian Stenzel 9e14b5e70f bulk changes
[ci skip]
2019-09-12 15:48:38 +02:00
Sebastian Stenzel 161a4cd511 Handling launch events (such as doubleclicking a .cryptomator file)
Also refactored dagger component graph during launch
2019-09-11 16:44:41 +02:00
Sebastian Stenzel 2cf97b5f77 fixed test on headless ci system 2019-09-11 16:25:50 +02:00
Sebastian Stenzel 0d6df6e6c1 Fixed tests 2019-09-11 16:22:10 +02:00
Sebastian Stenzel 6a4edbf73d drop multipe vaults at once 2019-09-10 17:08:00 +02:00
Sebastian Stenzel d6ffb890e3 preparations for reacting to externally triggered requests to open a masterkey file 2019-09-10 16:59:51 +02:00
Sebastian Stenzel 5b83456e6d fxml cleanup 2019-09-10 10:07:51 +02:00
Sebastian Stenzel 3ad6784961 improved sudden termination handling 2019-09-10 09:41:25 +02:00
Sebastian Stenzel 3fa1ed1928 clean up vault list controller 2019-09-10 09:41:01 +02:00
Sebastian Stenzel d27466d49e replaced gap + element + gap by larger gap
[ci skip]
2019-09-06 19:32:47 +02:00
Sebastian Stenzel d9677fe7c4 Split up vault detail view 2019-09-06 19:25:25 +02:00
Sebastian Stenzel fa7421b1b0 Added migration workflow 2019-09-06 18:55:47 +02:00
Sebastian Stenzel 602bccae2a fixed NiceSecurePasswordField#requestFocus() 2019-09-06 18:50:24 +02:00
Sebastian Stenzel 0acd3b427f stop the madness! new approach to scene-independent shortcuts 2019-09-06 17:43:01 +02:00
Sebastian Stenzel f35f04851e Hook up migration window 2019-09-06 15:26:01 +02:00
Sebastian Stenzel cb4717b770 cancel stats task on any state other than UNLOCKED 2019-09-06 15:20:32 +02:00
Sebastian Stenzel 68e69790cf Don't trigger quit handler on new non-unlocked vault states 2019-09-06 14:05:17 +02:00
Tobias Hagemann 2bd93469bb added cmd+w shortcuts to more dialogs 2019-09-06 13:59:22 +02:00
Tobias Hagemann 6fc37d48ff adjusted min-height of some dialogs to prevent the smallest jitter ;) 2019-09-06 13:36:21 +02:00
Tobias Hagemann df8fbe6440 added disabled state to reveal button in unlock success 2019-09-06 13:29:22 +02:00
Sebastian Stenzel 9eee7883c5 Introducing more vault states 2019-09-06 10:40:57 +02:00
Sebastian Stenzel 7de08f52df Vault Format 7 2019-09-06 10:21:37 +02:00
Sebastian Stenzel b4bf5415bc Refactored Vault's observable accessPoint property 2019-09-05 15:42:45 +02:00
Tobias Hagemann c8e22fe2e3 updated reveal button in vault detail 2019-09-05 15:27:34 +02:00
Tobias Hagemann af89cee619 reverted orange glyph icons 2019-09-05 11:09:10 +02:00
Sebastian Stenzel 216f3620e1 We need to know what happened
[ci skip]
2019-09-05 09:38:47 +02:00
Armin Schrenk ae97e42d11 adding round corners to dragNdrop area 2019-09-04 17:25:50 +02:00
Armin Schrenk c6ad677e2b monster commit: Adding drag'n'drop functionality to add already existing vaults to cryptomator 2019-09-04 17:19:30 +02:00
Armin Schrenk e1a1f0bbdb renaming method in removeVaultModule 2019-09-04 17:19:30 +02:00
Sebastian Stenzel efaf5a1553 Pimped Password Field
Added reveal icon as well, capslock warnings as well a warning for unprintable chars (fixes #458)
2019-09-04 17:16:24 +02:00
Tobias Hagemann 9c104beeba replaced text-flow with label+graphic 2019-09-04 16:54:40 +02:00
Tobias Hagemann 31bad3191d added menu item disabled state styling 2019-09-04 16:01:50 +02:00
Tobias Hagemann 1089497c08 added context-menu styling (basically the same as the one from choice-box) 2019-09-04 15:42:20 +02:00
Tobias Hagemann 73bc2d06a7 updated "add vault" button styling 2019-09-04 15:27:42 +02:00
Armin Schrenk 040d913693 adding text to button 2019-09-04 14:13:32 +02:00
Armin Schrenk 52a488f967 removing minus button from vault list 2019-09-04 13:53:29 +02:00
Armin Schrenk 7511a676d4 adding context menu to vault list 2019-09-04 12:59:46 +02:00
Tobias Hagemann 6b7964f5d9 added hdd icon to vault detail for unlocked state 2019-09-03 23:30:09 +02:00
Tobias Hagemann 98e843aa0d added window shake animation when entering invalid passphrase 2019-09-03 21:33:06 +02:00
Tobias Hagemann c364a743ae finetuning almost all dialogs 2019-09-03 16:31:45 +02:00
Tobias Hagemann b36c5fa60f added disabled checkbox styling [ci skip] 2019-09-03 15:06:07 +02:00
Tobias Hagemann bd0ece8020 added highlight fill to text fields [ci skip] 2019-09-03 13:58:22 +02:00
Tobias Hagemann 9b15254880 added wrap text to formatted labels 2019-09-03 10:32:12 +02:00
Tobias Hagemann 4f039b9708 new vault detail layout for unlocked state 2019-09-03 10:31:42 +02:00
Tobias Hagemann 2ab823239c updated localization [ci skip] 2019-09-03 09:21:31 +02:00
Tobias Hagemann 78d4f2e479 exchanged icon in unlock/lock button [ci skip] 2019-09-02 14:52:24 +02:00
Tobias Hagemann 6513fc6ed4 added color to icon in password match label [ci skip] 2019-09-02 14:49:33 +02:00
Tobias Hagemann e41ddedce7 updated colors of password strength indicator [ci skip] 2019-09-02 14:45:26 +02:00
Sebastian Stenzel 88220cabee Added access location readme file 2019-08-30 10:59:37 +02:00
Tobias Hagemann 33252c1775 changed icon of stacked glyphs to white instead of main bg 2019-08-30 10:23:38 +02:00
Sebastian Stenzel 57553bbda1 Replaced TransformationList by simpler List with listener, because Property Extractor didn't work properly 2019-08-29 23:18:03 +02:00
Sebastian Stenzel 4a02bf529d Add readme file to vault storage directory 2019-08-29 22:20:27 +02:00
Sebastian Stenzel e55b1f8ff9 all vaults are locked when starting the application
[ci skip]
2019-08-29 21:33:31 +02:00
Sebastian Stenzel 73314ee985 Thread safety when quitting the application: Altering vault states (unlocking/locking/...) will now trigger a listener (on the fx application thread) which sets an AtomicBoolean. Previously the value has been read by an AWT thread from a (non-volatile) BooleanBinding 2019-08-29 17:13:32 +02:00
Sebastian Stenzel 5196dbe9af button labels
[ci skip]
2019-08-29 16:33:15 +02:00
Sebastian Stenzel 68454e914b Clarified some steps of create vault workflow 2019-08-29 16:29:53 +02:00
Sebastian Stenzel d0924e246f Added success screen to add vault workflow 2019-08-29 15:59:47 +02:00
Sebastian Stenzel 59a277f0c7 added missing $ sign
[ci skip]
2019-08-29 15:31:27 +02:00
Sebastian Stenzel 5ef0dc5505 same design for unlock success and remove vault dialog 2019-08-29 14:15:49 +02:00
Sebastian Stenzel 08434c36ee adjusted unlock success screen 2019-08-28 16:22:09 +02:00
Sebastian Stenzel 2f288dc92b replaced glyph="CIRCLE" by circle. 2019-08-28 16:21:52 +02:00
Armin Schrenk 421a21aced adding german localization 2019-08-28 16:08:45 +02:00
Armin Schrenk 98a3a1a372 adding section in strings.properties 2019-08-28 16:08:25 +02:00
Armin Schrenk da65e98030 set wrapText attribute in addExistingVault dialogue 2019-08-28 16:07:57 +02:00
Sebastian Stenzel 7bbbd3c849 no longer change window size during unlock workflow 2019-08-28 16:06:33 +02:00
Armin Schrenk 0481bb02af renaming string property for volume type 2019-08-28 15:35:47 +02:00
Tobias Hagemann c47d9c95c7 restyled disabled button & glyph icons 2019-08-28 14:41:55 +02:00
Sebastian Stenzel a122159268 Made tray menu localizable 2019-08-28 14:17:34 +02:00
Armin Schrenk 474064df3a adding window icon to vaultOptions dialogue 2019-08-28 14:12:03 +02:00
Armin Schrenk 1f65a626e4 changePassword window is now subcomponent of vaultOptions 2019-08-28 14:08:14 +02:00
Tobias Hagemann 04800407d7 added color palette 2019-08-28 14:05:42 +02:00
Armin Schrenk f306184b53 adding icon to main window 2019-08-28 13:55:14 +02:00
Armin Schrenk c1a8844f27 improving removeVault dialogue 2019-08-28 13:51:09 +02:00
Sebastian Stenzel d4e118f331 quit refinements 2019-08-28 13:35:57 +02:00
Armin Schrenk 2900a9672a refining english localization 2019-08-28 13:32:22 +02:00
Armin Schrenk e8ba94942d setting managed property to false if no dropbox or gdrive are found 2019-08-28 13:32:22 +02:00
Armin Schrenk b3a96aed74 WebDavScheme setting only shown under linux 2019-08-28 13:32:22 +02:00
Sebastian Stenzel 0db2068344 Added window icon to dialogs 2019-08-28 13:27:23 +02:00
Sebastian Stenzel 6da0d023ff Localized quit dialog 2019-08-28 13:05:20 +02:00
Armin Schrenk 88d522501f if vault is added (new or existing), it is selected in the vault list 2019-08-28 11:53:43 +02:00
Armin Schrenk 6b31e6cdc2 closing window after vault is removed 2019-08-28 11:53:43 +02:00
Sebastian Stenzel 1ef8668c4d Replaced TextFlows by FormattedLabel if applicable 2019-08-28 11:42:26 +02:00
Sebastian Stenzel 6721baf293 Replaced TextFlows by FormattedLabel if applicable 2019-08-28 11:35:07 +02:00
Sebastian Stenzel f4ec9c277f Formatting throughput stats properly 2019-08-28 11:18:21 +02:00
Sebastian Stenzel c9d897edd1 added window prefix "main.*" to localizable strings in main window 2019-08-28 11:17:45 +02:00
Tobias Hagemann ef07edf8c3 made button-group into an actual button 2019-08-27 16:11:24 +02:00
Armin Schrenk 36d49a6e1e vault options: read only only selectable when custom mount options are _not_ used 2019-08-27 15:51:22 +02:00
Sebastian Stenzel f4ee8d0a15 Added I/O stats (work in progress) 2019-08-27 15:50:15 +02:00
Armin Schrenk 71e414ae5c formatting 2019-08-27 15:23:48 +02:00
Armin Schrenk acda11f110 adding confirmation dialogue when removing a vault 2019-08-27 15:23:35 +02:00
Armin Schrenk a96935bfb5 unused imports removed 2019-08-27 14:35:51 +02:00
Armin Schrenk b74b4ce244 bug fix when switching vault during lock operation 2019-08-27 14:02:56 +02:00
Armin Schrenk 638dac9cb6 adding missing acces location text 2019-08-27 13:48:05 +02:00
Tobias Hagemann 249af4c397 decreased width of add vault wizard 2019-08-27 13:18:50 +02:00
Tobias Hagemann 4130dbea91 added checkmark icon to unlock success 2019-08-27 12:57:51 +02:00
Sebastian Stenzel ee57cd7d3b don't mark blocked issues as stale
[ci skip]
2019-08-27 10:32:31 +02:00
Tobias Hagemann 8939b196c0 reverted 5dcdaf45 2019-08-26 20:36:47 +02:00
Sebastian Stenzel f1b5ec6481 Updated FFI package to 2.2.0 2019-08-26 20:30:42 +02:00
Tobias Hagemann 50ace8c6a4 added ffi-version to buildkit 2019-08-26 20:22:52 +02:00
Sebastian Stenzel 264e81b4a0 macOS: Only show application menu and dock icon when a Window is visible (related to #957, #283, #346, #62) 2019-08-26 18:03:26 +02:00
Sebastian Stenzel 282b80fe24 downgraded jni lib (due to incompatibilites on macOS 10.13.x) 2019-08-26 17:59:46 +02:00
Tobias Hagemann 547e5dae52 Merge branch 'feature/jni-mac-tray-icon' into ‘feature/new-ui' 2019-08-26 16:58:28 +02:00
Tobias Hagemann 5dcdaf459f reverted jni version for merging, kept new code as comment 2019-08-26 16:56:23 +02:00
Tobias Hagemann ab89f59dd1 defined color constants for password strength indicator, updated labels 2019-08-26 16:29:50 +02:00
Tobias Hagemann 98451759f0 updated unlock success layout, reverted back from buttonbar to hbox in some dialogs 2019-08-26 15:54:21 +02:00
Sebastian Stenzel b22ddaf13e reveal vault access and storage location from vault detail view 2019-08-26 15:22:54 +02:00
Tobias Hagemann 7eaf598089 added open sans font 2019-08-26 15:15:01 +02:00
Sebastian Stenzel 3d7b504504 Added styleable PasswordStrengthIndicator 2019-08-26 14:47:25 +02:00
Tobias Hagemann 4dc8693216 updated change password layout & some other stuff 2019-08-26 13:33:24 +02:00
Sebastian Stenzel ac472393aa Began error handling during add vault workflow 2019-08-26 13:03:19 +02:00
Tobias Hagemann 477fd7d2e9 increased paddings in add vault wizard 2019-08-26 00:40:00 +02:00
Tobias Hagemann 468723ad92 centered check for updates button 2019-08-26 00:34:12 +02:00
Tobias Hagemann d58ad1e0a2 updated add vault wizard layout & stylings 2019-08-26 00:28:01 +02:00
Tobias Hagemann 8c94245102 updated menu item stylings of choicebox, updated window sizes & disabled resizable of preferences and vault options 2019-08-25 00:34:16 +02:00
Tobias Hagemann b4f697e052 updated vault detail layout, disabled focus traversable in main window's title buttons, updated other minor stylings 2019-08-23 17:57:02 +02:00
Tobias Hagemann 428ffbf705 updated vault detail layout 2019-08-23 00:56:39 +02:00
Tobias Hagemann d25a903556 updated vault detail layout, moved change password button to vault options, updated other minor stylings 2019-08-23 00:22:57 +02:00
Tobias Hagemann 3db906de10 updated vault list cell styling & added text-flow style class in vault detail 2019-08-20 16:09:17 +02:00
Sebastian Stenzel 486d3170e2 Relaxed vault list cell layout and added highlight colors (inspired by google backup & sync settings) 2019-08-19 17:21:49 +02:00
Sebastian Stenzel 15bdba85bb less annoying "armed" indicator 2019-08-19 17:19:37 +02:00
Sebastian Stenzel db1a2538cb removed unused logger 2019-08-19 17:18:33 +02:00
Sebastian Stenzel 3d61fab8b6 Deduplication 2019-08-19 16:27:35 +02:00
Sebastian Stenzel 644f2d9a89 Removed dependency to fontawesomefx, using fontawesome 5 instead (which we have a pro license for) 2019-08-19 16:15:10 +02:00
Sebastian Stenzel 46a4060814 fixed unlock 2019-08-19 16:14:11 +02:00
Tobias Hagemann 5c26c23619 added mac native function for listening to system interface theme changes and changing the tray icon accordingly [ci skip] 2019-08-19 15:55:07 +02:00
Sebastian Stenzel 60bdafca94 Merge branch 'develop' into feature/new-ui
# Conflicts:
#	main/pom.xml
2019-08-18 16:24:48 +02:00
Sebastian Stenzel 59cd3defce Merge branch 'master' into develop 2019-08-18 15:22:28 +02:00
Armin Schrenk fc51e5caff #926 replacing boolean by a string for current vault state 2019-08-16 15:54:08 +02:00
Armin Schrenk e32ddd07ea #926 Adding information to the main window 2019-08-16 15:51:39 +02:00
Armin Schrenk 6b47cf54e2 adding references for Label nodes in fxml 2019-08-16 12:24:47 +02:00
Armin Schrenk 40c3e7a417 #929, simplyfing add existing vault dialogue 2019-08-16 12:00:51 +02:00
Armin Schrenk 4bfb543419 #927 adding logic to stub 2019-08-14 18:27:31 +02:00
Armin Schrenk 8c9d78337e name the reenter field properly 2019-08-14 17:37:42 +02:00
Sebastian Stenzel 0a132351a8 load bundled font for "Cryptomator" title 2019-08-14 17:11:18 +02:00
Sebastian Stenzel ded43f7e7b apply default text color to all font awesome icons (except for title buttons) 2019-08-14 17:10:40 +02:00
Sebastian Stenzel cf91a8b62c Moved volume type settings to custom preferences controller
Added icons to preferences
2019-08-14 16:42:06 +02:00
Armin Schrenk 948659f3cd #927 :adding stubs for change password dialogue 2019-08-14 13:10:38 +02:00
Armin Schrenk c7e0015ffd Merge branch 'feature/new-ui' of https://github.com/cryptomator/cryptomator into feature/new-ui 2019-08-14 03:08:58 +02:00
Sebastian Stenzel 2fd671f513 New approach to shortcuts 2019-08-13 16:54:00 +02:00
Sebastian Stenzel 5fa86fad5e style adjustments
[ci skip]
2019-08-13 16:14:38 +02:00
Sebastian Stenzel c6d90cdb17 Added vault location presets to dialog (references #929) 2019-08-13 15:43:16 +02:00
Armin Schrenk dcf44aa134 add indicator when passwords match 2019-08-13 14:36:27 +02:00
Sebastian Stenzel 50b2b0dc57 Merge branch 'develop' into feature/new-ui
new keychain API
2019-08-12 16:32:24 +02:00
Tobias Hagemann 111d20a1cf updated button paddings, unlock dialog layout, localizations [ci skip] 2019-08-12 16:08:00 +02:00
Sebastian Stenzel 19840663a8 fixes #948 2019-08-11 22:01:29 +02:00
armin 4b7527bf0c refactoring PasswordStrengthUtil to use ressource bundle 2019-08-11 18:32:46 +02:00
armin 0a5bfa1ccb implementing password strength indicator 2019-08-11 18:12:47 +02:00
armin 21e1d303aa replacing hardcoded descriptions by strings.properties references for addvault new password controller 2019-08-11 16:42:41 +02:00
armin e0d5641d26 replacing hardcoded descriptions by strings.properties references for addvault new location controller 2019-08-11 16:40:37 +02:00
armin 529780f2c9 replacing hardcoded descriptions by strings.properties references for addvault new name controller 2019-08-11 16:35:07 +02:00
armin 19f1093d2c replacing hardcoded descriptions by reference to strings.properties for addvault existing controller 2019-08-11 16:29:04 +02:00
armin 5807e10286 replacing hardcoded descriptions by reference to strings.properties for addvault welcome controller 2019-08-11 16:19:58 +02:00
armin 0a1ad9f4d6 completing functionality of addVault wizard 2019-08-11 13:33:31 +02:00
armin 2a3e2e315e implementing finish-button logic for addNewVault wizard 2019-08-11 13:25:04 +02:00
armin 23c868b646 fix spelling 2019-08-11 13:23:41 +02:00
Tobias Hagemann ed8af54324 updated theme [ci skip] 2019-08-10 10:52:07 +02:00
Tobias Hagemann d8c3ba1e36 updated theme [ci skip] 2019-08-08 16:07:49 +02:00
Sebastian Stenzel 17f45c6dab Implemented some mount options (references #931) 2019-08-07 17:04:56 +02:00
Sebastian Stenzel 86745c5962 fixed copy/pasted comment
[ci skip]
2019-08-07 14:49:33 +02:00
Tobias Hagemann ca72c7a51f added button default styling [ci skip] 2019-08-07 00:47:16 +02:00
Tobias Hagemann 012c7cede9 updated update indicator styling [ci skip] 2019-08-07 00:35:20 +02:00
Sebastian Stenzel 15a55e996b Merge branch 'feature/new-ui' of https://github.com/cryptomator/cryptomator into feature/new-ui 2019-08-06 17:05:42 +02:00
Sebastian Stenzel cb50b2011f Lock remaining vaults on shutdown (references #838) 2019-08-06 17:03:05 +02:00
Sebastian Stenzel 04c45756b9 removed unused imports 2019-08-06 17:02:21 +02:00
Sebastian Stenzel 8d08d81f45 more specific throws statement 2019-08-06 17:02:06 +02:00
Tobias Hagemann d77403c403 updated tab pane styling [ci skip] 2019-08-06 16:15:43 +02:00
Sebastian Stenzel 1d94069144 Added proper Quit dialog (not yet fully implemented), fixes #838 2019-08-06 13:34:29 +02:00
Sebastian Stenzel 6e10d6bcd7 simplification 2019-08-06 13:32:56 +02:00
Sebastian Stenzel 7453431bcd Trigger list change events when a vault changes its state, thus removing need for additional state listeners 2019-08-06 13:09:42 +02:00
Sebastian Stenzel 2f953061b3 Experimenting with ⌘W shortcuts on windows (TODO: we should find some better way to do this) 2019-08-06 12:06:03 +02:00
Sebastian Stenzel 67610b935e Added vault settings window (references #931) 2019-08-06 11:09:00 +02:00
Sebastian Stenzel 09e5ee9e26 Merge branch 'develop' into feature/new-ui
[ci skip]
2019-08-02 16:50:23 +02:00
Sebastian Stenzel e23dc72856 Can now unlock directly from menu bar 2019-08-01 16:14:38 +02:00
Sebastian Stenzel b86d4b5a37 rebuild tray menu when vault state changes 2019-08-01 16:02:48 +02:00
Sebastian Stenzel 2024c95be5 add actionlistener to tray icon 2019-08-01 16:02:09 +02:00
Sebastian Stenzel d51debf736 fixed exception when scheduling the next regular update check while auto check is disabled
[ci skip]
2019-08-01 13:24:47 +02:00
Sebastian Stenzel 70b4b5fb2d only check for updates automatically if set so in preferences 2019-08-01 13:14:01 +02:00
Sebastian Stenzel e32fbc1d79 Select updates tab when showing preferences window and an update is available 2019-07-31 19:28:27 +02:00
Sebastian Stenzel be43279574 cleanup [ci skip] 2019-07-31 19:17:57 +02:00
Sebastian Stenzel 900e556369 Show TrayMenu sooner, don't wait for FxApplication to initialize 2019-07-31 19:16:14 +02:00
Sebastian Stenzel 09aca188fe Implemented manual update check button (references #272) 2019-07-31 18:39:13 +02:00
Sebastian Stenzel fc9565e13c Prepared "check for updates" button 2019-07-31 17:32:34 +02:00
Sebastian Stenzel e7f7321393 Split preferences into multiple tabs 2019-07-31 17:15:27 +02:00
infeo ddbe1c4546 implement first stub for password dialogue in create new vault wizard 2019-07-31 16:57:29 +02:00
infeo e3fa31b701 Merge branch 'feature/new-ui' of https://github.com/cryptomator/cryptomator into feature/new-ui 2019-07-31 16:55:29 +02:00
Sebastian Stenzel bfe8a22b48 fixed style class of preferences window
[ci skip]
2019-07-31 16:20:39 +02:00
Sebastian Stenzel dab779cbef Added new UpdateChecker that checks periodically (fixes #272) and added it to the main window (references #925) 2019-07-31 15:47:21 +02:00
infeo 66104bb753 removing wrong doc 2019-07-31 15:29:54 +02:00
infeo 37c69f991f implement next window of create new vault workflow 2019-07-31 15:26:41 +02:00
infeo 610460b3e4 add suffix to to create new vault workflow since there will be more than one step 2019-07-31 14:33:12 +02:00
infeo 43750e2bad adding first createNewVault dialogue 2019-07-31 14:14:51 +02:00
infeo a7df100e56 simplifying bindings 2019-07-31 12:49:21 +02:00
Sebastian Stenzel 4afb9d86c7 Added unlock success message (references #939) 2019-07-31 11:45:04 +02:00
Sebastian Stenzel 1b7a7060a8 moved Unlock component from MainWindow to FxApplication component 2019-07-31 11:44:39 +02:00
Sebastian Stenzel 1f6b94003e bring window to front
[ci skip]
2019-07-31 09:46:15 +02:00
Tobias Hagemann 2f1a7821b0 forgot to add text flow class in 4931e670 2019-07-30 21:11:30 +02:00
Sebastian Stenzel 1ec887092f Moved various non-ui related stuff to commons 2019-07-30 14:49:35 +02:00
Sebastian Stenzel 71afb088b5 show vault list in tray menu 2019-07-30 11:26:37 +02:00
Armin Schrenk 01ca501607 disabling open button in addExistingVault wizard if no file is selected. 2019-07-29 19:15:10 +02:00
Sebastian Stenzel ba34b37f8a Added options to start Cryptomator in background (references #418) 2019-07-29 16:28:29 +02:00
Sebastian Stenzel 62ff954558 Minimize mainwindow (instead of closing) when tray icon is not supported by the OS 2019-07-29 16:17:18 +02:00
Sebastian Stenzel fead9a48eb Reuse existing Stages and Scenes 2019-07-29 16:07:34 +02:00
Sebastian Stenzel 10e842e457 added tray icon, refactored dagger component graph:
singleton -> tray -> fxapplication -> mainwindow
2019-07-29 12:38:24 +02:00
Tobias Hagemann 4931e6707a added text flow styling, localized preferences title, renamed some i18n keys 2019-07-26 14:05:19 +02:00
Tobias Hagemann 036dca33be mac appearance changes with light/dark theme, updated jni 2019-07-26 11:42:46 +02:00
Sebastian Stenzel 74565d7995 fixed unit tests 2019-07-25 15:48:10 +02:00
Sebastian Stenzel 65ab09e63c added style classes for vault list cells during a drag'n'drop-event
[ci skip]
2019-07-25 15:29:25 +02:00
Sebastian Stenzel 8e305b0e21 minor layout adjustments
[ci skip]
2019-07-25 14:32:15 +02:00
Sebastian Stenzel ff66e07c65 Added save password checkbox to unlock dialog 2019-07-25 13:26:34 +02:00
Sebastian Stenzel 4138747fc3 Localizable file picker title 2019-07-25 12:41:02 +02:00
Sebastian Stenzel 87c654b43d using vaultlist from parent scope, no need to duplicate variable in subscope 2019-07-25 12:29:14 +02:00
Sebastian Stenzel d0062b7e22 inject resource bundle 2019-07-25 12:25:53 +02:00
Tobias Hagemann 1d3900b86c added dark theme 2019-07-24 23:28:16 +02:00
Armin Schrenk b799ce34f6 renaming methods 2019-07-24 17:14:28 +02:00
Armin Schrenk 8d6cf9581b reformatting 2019-07-24 17:12:51 +02:00
Armin Schrenk 1a5853766a #929, implemented functionality to add existing vault 2019-07-24 17:12:09 +02:00
Armin Schrenk cd284304cd replacing selfplaced buttons by button bar 2019-07-24 17:02:50 +02:00
Armin Schrenk 363e4152a5 implementing remove vault functionality 2019-07-24 16:34:27 +02:00
Tobias Hagemann 0ce4ddb874 added scrollbar to theme 2019-07-24 15:04:31 +02:00
Sebastian Stenzel 3c574b97cb Added unlock window (issue #928) 2019-07-24 14:58:43 +02:00
Sebastian Stenzel b7b8f26992 cleanup and simplification 2019-07-24 14:57:56 +02:00
Sebastian Stenzel 4eca5069b3 moved class 2019-07-24 14:56:52 +02:00
Armin Schrenk 03618f547d fitting the layout for the existing vault controller 2019-07-24 11:41:54 +02:00
Armin Schrenk 12477c07d6 refactoring ChooseExistingVaultControllor to use a scoped ObjectProperty for the vault path 2019-07-24 11:31:49 +02:00
Armin Schrenk 0fb2d445b0 cleaning up addExistingVault controller 2019-07-24 11:21:05 +02:00
Armin Schrenk 43c5482542 move text labeling of addExisting vault wizard to string properties file 2019-07-24 11:12:29 +02:00
Sebastian Stenzel dc8e7a86b0 Merge branch 'develop' into feature/new-ui 2019-07-24 10:21:17 +02:00
Sebastian Stenzel 2a69740559 sharper gradient stops on hi-res displays
[ci skip]
2019-07-23 18:15:12 +02:00
Sebastian Stenzel dd0e548353 schön is German for beautiful
[ci skip]
2019-07-23 17:14:22 +02:00
Tobias Hagemann 0eb19a287f updated theme 2019-07-23 17:02:05 +02:00
Sebastian Stenzel 14de8ffd59 make window resizable (fixes #932)
[ci skip]
2019-07-23 16:51:25 +02:00
Sebastian Stenzel ac6249f541 toolbar styling 2019-07-23 16:17:30 +02:00
Sebastian Stenzel 5af2a392ea show stuff in vault list cells 2019-07-23 14:21:08 +02:00
Sebastian Stenzel edfdca6e5f Load vault list cells from FXML 2019-07-23 13:13:36 +02:00
Armin Schrenk c2ed9958c7 changing test functionality by intended showing now selected file 2019-07-22 17:12:22 +02:00
Armin Schrenk fddae7d0e1 adding back, open and file chooser button. 2019-07-22 16:53:06 +02:00
Tobias Hagemann fa729e220c updated theme 2019-07-22 16:16:15 +02:00
Sebastian Stenzel c917fb6a57 Added UI theme switcher (issue #930) 2019-07-22 15:05:23 +02:00
Sebastian Stenzel cf2e026f75 NONONO
[ci skip]
2019-07-19 16:23:29 +02:00
Sebastian Stenzel c340544152 Add Vault Wizard: Navigation between scenes
[ci skip]
2019-07-19 16:22:27 +02:00
Sebastian Stenzel 00c7cae923 cleanup [ci skip] 2019-07-19 14:15:45 +02:00
Sebastian Stenzel 80cff6912b Hooked up "add vault" wizard 2019-07-19 13:35:08 +02:00
Sebastian Stenzel b76e1d4167 Each window is now a separate @Subcomponent of the FxApplicationComponent 2019-07-19 12:46:34 +02:00
Sebastian Stenzel f7759d2547 hooked up all controls in Preferences window 2019-07-17 16:17:23 +02:00
Sebastian Stenzel d51e3979ff Added SplitPane css
[ci skip]
2019-07-17 10:54:40 +02:00
Sebastian Stenzel 01275fce9f add movable divider between vault list and vault detail view 2019-07-16 17:16:19 +02:00
Sebastian Stenzel 261b18f4eb First CSS for basic UI controls 2019-07-16 16:08:47 +02:00
Sebastian Stenzel fdc20aa84f Added controls to preferences screen 2019-07-16 16:08:34 +02:00
Sebastian Stenzel a3382c2d5b hook up preferences window 2019-07-15 16:39:21 +02:00
Sebastian Stenzel 52a9a2f018 starting with blank css 2019-07-15 11:32:58 +02:00
Sebastian Stenzel cfea1c16bc added vault list and vault detail view 2019-07-15 11:32:49 +02:00
Sebastian Stenzel 180c07cf61 new class layout for viewcontrollers 2019-07-11 00:19:18 +02:00
Sebastian Stenzel f880db4902 fixes #842 2019-07-10 15:14:23 +02:00
381 changed files with 21927 additions and 11374 deletions
+5
View File
@@ -0,0 +1,5 @@
commit_message: "[ci skip]"
escape_special_characters: 0
files:
- source: /main/ui/src/main/resources/i18n/strings.properties
translation: /main/ui/src/main/resources/i18n/strings_%two_letters_code%.properties
+1 -1
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: [cryptomator] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
+4 -1
View File
@@ -1,11 +1,14 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 90
daysUntilStale: 180
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- type:security-issue # never close automatically
- type:feature-request # never close automatically
- state:awaiting-response # handled by different bot
- state:blocked
- state:confirmed
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking an issue as stale
+18 -10
View File
@@ -7,26 +7,34 @@
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.22.1/dagger-compiler-2.22.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.22.1/dagger-2.22.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.26/dagger-compiler-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.26/dagger-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.22.1/dagger-producers-2.22.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/25.0-jre/guava-25.0-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.1.3/error_prone_annotations-2.1.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.26/dagger-producers-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/27.1-jre/guava-27.1-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-qual/2.5.2/checker-qual-2.5.2.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.2.0/error_prone_annotations-2.2.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.22.1/dagger-spi-2.22.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.17/animal-sniffer-annotations-1.17.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.26/dagger-spi-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.3.50/kotlin-stdlib-1.3.50.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.50/kotlin-stdlib-common-1.3.50.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0/kotlinx-metadata-jvm-0.1.0.jar" />
</processorPath>
<module name="keychain" />
<module name="launcher" />
<module name="commons" />
<module name="ui" />
<module name="launcher" />
</profile>
</annotationProcessing>
</component>
+8 -6
View File
@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM">
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="file://$PROJECT_DIR$/main" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/buildkit" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/commons" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/keychain" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/launcher" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/ui" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/commons/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/keychain/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/keychain/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/launcher/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/ui/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/ui/src/main/resources" charset="UTF-8" />
<file url="PROJECT" charset="UTF-8" />
</component>
</project>
+1 -1
View File
@@ -2,8 +2,8 @@
<configuration default="false" name="Cryptomator macOS" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.mountPointsDir=&quot;/Volumes/&quot; -Xss2m -Xmx512m" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.mountPointsDir=&quot;/Volumes/&quot; -Xss2m -Xmx512m -ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>
+11 -10
View File
@@ -1,4 +1,4 @@
dist: xenial
dist: bionic
language: java
sudo: false
jdk:
@@ -17,24 +17,25 @@ addons:
- haveged
install:
- curl -o $HOME/.m2/settings.xml https://gist.githubusercontent.com/cryptobot/cf5fbd909c4782aaeeeb7c7f4a1a43da/raw/e60ee486e34ee0c79f89f947abe2c83b4290c6bb/settings.xml
- mvn -fmain/pom.xml clean install -DskipTests org.codehaus.mojo:versions-maven-plugin:help dependency:go-offline -Pcoverage,release # "clean install" needed until we can exclude artifacts currently in the reactor, see https://maven.apache.org/plugins/maven-dependency-plugin/go-offline-mojo.html#excludeReactor and https://issues.apache.org/jira/browse/MDEP-568
- mvn -fmain/pom.xml clean install -DskipTests -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN org.codehaus.mojo:versions-maven-plugin:help dependency:go-offline -Pcoverage,release # "clean install" needed until we can exclude artifacts currently in the reactor, see https://maven.apache.org/plugins/maven-dependency-plugin/go-offline-mojo.html#excludeReactor and https://issues.apache.org/jira/browse/MDEP-568
before_script:
- |
if [[ -n "$TRAVIS_TAG" ]]; then
mvn -fmain/pom.xml org.codehaus.mojo:versions-maven-plugin:set -DnewVersion=$TRAVIS_TAG
else
mvn -fmain/pom.xml org.codehaus.mojo:versions-maven-plugin:set -DnewVersion=SNAPSHOT-$(echo $TRAVIS_COMMIT | head -c7)
fi
script:
- mvn --update-snapshots -fmain/pom.xml clean test verify -Pcoverage
after_success:
- curl -o ~/codacy-coverage-reporter.jar https://oss.sonatype.org/service/local/repositories/releases/content/com/codacy/codacy-coverage-reporter/4.0.2/codacy-coverage-reporter-4.0.2-assembly.jar
- curl -o ~/codacy-coverage-reporter.jar https://oss.sonatype.org/service/local/repositories/releases/content/com/codacy/codacy-coverage-reporter/6.0.7/codacy-coverage-reporter-6.0.7-assembly.jar
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/commons/target/site/jacoco/jacoco.xml --partial
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/keychain/target/site/jacoco/jacoco.xml --partial
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/ui/target/site/jacoco/jacoco.xml --partial
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/launcher/target/site/jacoco/jacoco.xml --partial
- $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar final
before_deploy:
- |
if [[ -n "$TRAVIS_TAG" ]]; then
mvn -fmain/pom.xml org.codehaus.mojo:versions-maven-plugin:set -DnewVersion=$TRAVIS_TAG
elif [[ $TRAVIS_BRANCH == "develop" ]] && [[ $TRAVIS_PULL_REQUEST == "false" ]]; then
mvn -fmain/pom.xml org.codehaus.mojo:versions-maven-plugin:set -DnewVersion=SNAPSHOT-$(echo $TRAVIS_COMMIT | head -c7)
fi
- mvn -fmain/pom.xml clean package -Prelease -DskipTests
- mvn -fmain/pom.xml package -Prelease -DskipTests
- export TODAY=`date +'%Y-%m-%d'`; envsubst '$TRAVIS_TAG $TODAY' < .travis-deploy-release.tmpl.json > .travis-deploy-release.json
deploy:
- provider: bintray # SNAPSHOTS
+1 -1
View File
@@ -4,7 +4,7 @@
[![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptomator/badge.svg?targetFile=main%2Fpom.xml)](https://snyk.io/test/github/cryptomator/cryptomator?targetFile=main%2Fpom.xml)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/2a0adf3cec6a4143b91035d3924178f1)](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=cryptomator/cryptomator&amp;utm_campaign=Badge_Grade)
[![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator)
[![POEditor](https://img.shields.io/badge/POEditor-Help%20Translate-blue.svg?style=flat)](https://poeditor.com/join/project/bHwbvJmx0E)
[![Crowdin](https://badges.crowdin.net/cryptomator/localized.svg)](https://translate.cryptomator.org/)
[![Latest Release](https://img.shields.io/github/release/cryptomator/cryptomator.svg)](https://github.com/cryptomator/cryptomator/releases/latest)
[![Community](https://img.shields.io/badge/help-Community-orange.svg)](https://community.cryptomator.org)
+7
View File
@@ -14,6 +14,13 @@
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>
<include>ffi-version.txt</include>
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>
+7
View File
@@ -14,6 +14,13 @@
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>
<include>ffi-version.txt</include>
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>
+7
View File
@@ -14,6 +14,13 @@
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>
<include>ffi-version.txt</include>
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>
+2 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.15</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>buildkit</artifactId>
<packaging>pom</packaging>
@@ -39,6 +39,7 @@
<directory>${project.basedir}/src/main/resources</directory>
<includes>
<include>version.txt</include>
<include>ffi-version.txt</include>
<include>launcher-mac.sh</include>
<include>launcher-linux.sh</include>
<include>launcher-win.bat</include>
@@ -0,0 +1 @@
${cryptomator.jni.version}
+43 -8
View File
@@ -4,35 +4,70 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.15</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator Commons</name>
<description>Shared utilities</description>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>fuse-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>dokany-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>webdav-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<!-- JavaFx -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
</dependency>
<!-- Libs -->
<!-- EasyBind -->
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<!-- Google -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- DI -->
@@ -5,22 +5,126 @@
*******************************************************************************/
package org.cryptomator.common;
import java.util.Comparator;
import dagger.Module;
import dagger.Provides;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultComponent;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
import java.net.InetSocketAddress;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import dagger.Module;
import dagger.Provides;
@Module(subcomponents = {VaultComponent.class})
public abstract class CommonsModule {
@Module
public class CommonsModule {
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
private static final int NUM_SCHEDULER_THREADS = 2;
private static final int NUM_CORE_BG_THREADS = 6;
private static final long BG_THREAD_KEEPALIVE_SECONDS = 60l;
@Provides
@Singleton
@Named("licensePublicKey")
static String provideLicensePublicKey() {
// in PEM format without the dash-escaped begin/end lines
return "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW" //
+ "fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV" //
+ "gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE" //
+ "r0DzRyj4ixPIt38CQB8=";
}
@Provides
@Singleton
@Named("SemVer")
Comparator<String> providesSemVerComparator() {
static Comparator<String> providesSemVerComparator() {
return new SemVerComparator();
}
@Provides
@Singleton
static Settings provideSettings(SettingsProvider settingsProvider) {
return settingsProvider.get();
}
@Provides
@Singleton
static ObservableList<Vault> provideVaultList(VaultListManager vaultListManager) {
return vaultListManager.getVaultList();
}
@Provides
@Singleton
static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
String name = String.format("App Scheduled Executor %02d", threadNumber.getAndIncrement());
Thread t = new Thread(r);
t.setName(name);
t.setUncaughtExceptionHandler(CommonsModule::handleUncaughtExceptionInBackgroundThread);
t.setDaemon(true);
LOG.debug("Starting {}", t.getName());
return t;
});
shutdownHook.runOnShutdown(executorService::shutdown);
return executorService;
}
@Provides
@Singleton
static ExecutorService provideExecutorService(ShutdownHook shutdownHook) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ExecutorService executorService = new ThreadPoolExecutor(NUM_CORE_BG_THREADS, Integer.MAX_VALUE, BG_THREAD_KEEPALIVE_SECONDS, TimeUnit.SECONDS, new SynchronousQueue<>(), r -> {
String name = String.format("App Background Thread %03d", threadNumber.getAndIncrement());
Thread t = new Thread(r);
t.setName(name);
t.setUncaughtExceptionHandler(CommonsModule::handleUncaughtExceptionInBackgroundThread);
t.setDaemon(true);
LOG.debug("Starting {}", t.getName());
return t;
});
shutdownHook.runOnShutdown(executorService::shutdown);
return executorService;
}
private static void handleUncaughtExceptionInBackgroundThread(Thread thread, Throwable throwable) {
LOG.error("Uncaught exception in " + thread.getName(), throwable);
}
@Provides
@Singleton
static Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
return Bindings.createObjectBinding(() -> {
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
return InetSocketAddress.createUnresolved(host, settings.port().intValue());
}, settings.port());
}
@Provides
@Singleton
static WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
WebDavServer server = WebDavServer.create();
// no need to unsubscribe eventually, because server is a singleton
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
return server;
}
}
@@ -25,9 +25,11 @@ public class Environment {
private static final Path RELATIVE_HOME_DIR = Paths.get("~");
private static final Path ABSOLUTE_HOME_DIR = Paths.get(USER_HOME);
private static final char PATH_LIST_SEP = ':';
private static final int DEFAULT_MIN_PW_LENGTH = 8;
@Inject
public Environment() {
LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
LOG.debug("user.language: {}", System.getProperty("user.language"));
LOG.debug("user.region: {}", System.getProperty("user.region"));
LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
@@ -36,6 +38,7 @@ public class Environment {
LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir"));
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
}
public boolean useCustomLogbackConfig() {
@@ -62,6 +65,19 @@ public class Environment {
return getPath("cryptomator.mountPointsDir").map(this::replaceHomeDir);
}
public int getMinPwLength() {
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
}
private int getInt(String propertyName, int defaultValue) {
String value = System.getProperty(propertyName);
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) { // includes "null" values
return defaultValue;
}
}
private Optional<Path> getPath(String propertyName) {
String value = System.getProperty(propertyName);
return Optional.ofNullable(value).map(Paths::get);
@@ -0,0 +1,32 @@
/*******************************************************************************
* Copyright (c) 2019 Skymatic GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.jni.JniFunctions;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.WinFunctions;
import javax.inject.Singleton;
import java.util.Optional;
@Module
public class JniModule {
@Provides
@Singleton
Optional<MacFunctions> provideOptionalMacFunctions() {
return JniFunctions.macFunctions();
}
@Provides
@Singleton
Optional<WinFunctions> provideOptionalWinFunctions() {
return JniFunctions.winFunctions();
}
}
@@ -0,0 +1,56 @@
package org.cryptomator.common;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.google.common.io.BaseEncoding;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Optional;
@Singleton
class LicenseChecker {
private final JWTVerifier verifier;
@Inject
public LicenseChecker(@Named("licensePublicKey") String pemEncodedPublicKey) {
Algorithm algorithm = Algorithm.ECDSA512(decodePublicKey(pemEncodedPublicKey), null);
this.verifier = JWT.require(algorithm).build();
}
private static ECPublicKey decodePublicKey(String pemEncodedPublicKey) {
try {
byte[] keyBytes = BaseEncoding.base64().decode(pemEncodedPublicKey);
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes));
if (key instanceof ECPublicKey) {
return (ECPublicKey) key;
} else {
throw new IllegalStateException("Key not an EC public key.");
}
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException("Invalid license public key", e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
public Optional<DecodedJWT> check(String licenseKey) {
try {
return Optional.of(verifier.verify(licenseKey));
} catch (JWTVerificationException exception) {
return Optional.empty();
}
}
}
@@ -0,0 +1,79 @@
package org.cryptomator.common;
import com.auth0.jwt.interfaces.DecodedJWT;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.cryptomator.common.settings.Settings;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class LicenseHolder {
private final Settings settings;
private final LicenseChecker licenseChecker;
private final ObjectProperty<DecodedJWT> validJwtClaims;
private final StringBinding licenseSubject;
private final BooleanBinding validLicenseProperty;
@Inject
public LicenseHolder(LicenseChecker licenseChecker, Settings settings) {
this.settings = settings;
this.licenseChecker = licenseChecker;
this.validJwtClaims = new SimpleObjectProperty<>();
this.licenseSubject = Bindings.createStringBinding(this::getLicenseSubject, validJwtClaims);
this.validLicenseProperty = validJwtClaims.isNotNull();
Optional<DecodedJWT> claims = licenseChecker.check(settings.licenseKey().get());
validJwtClaims.set(claims.orElse(null));
}
public boolean validateAndStoreLicense(String licenseKey) {
Optional<DecodedJWT> claims = licenseChecker.check(licenseKey);
validJwtClaims.set(claims.orElse(null));
if (claims.isPresent()) {
settings.licenseKey().set(licenseKey);
return true;
} else {
return false;
}
}
/* Observable Properties */
public Optional<String> getLicenseKey() {
DecodedJWT claims = validJwtClaims.get();
if (claims != null) {
return Optional.of(claims.getToken());
} else {
return Optional.empty();
}
}
public StringBinding licenseSubjectProperty() {
return licenseSubject;
}
public String getLicenseSubject() {
DecodedJWT claims = validJwtClaims.get();
if (claims != null) {
return claims.getSubject();
} else {
return null;
}
}
public BooleanBinding validLicenseProperty() {
return validLicenseProperty;
}
public boolean isValidLicense() {
return validLicenseProperty.get();
}
}
@@ -1,28 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common;
import java.util.Optional;
import java.util.function.Function;
public final class Optionals {
private Optionals() {
}
/**
* Returns a function that is equivalent to the input function but immediately gets the value of the returned optional when invoked.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
* @param function An {@code Optional}-bearing input function {@code Function<Foo, Optional<Bar>>}
* @return A {@code Function<Foo, Bar>}, that may throw a NoSuchElementException, if the original function returns an empty optional.
*/
public static <T, R> Function<T, R> unwrap(Function<T, Optional<R>> function) {
return t -> function.apply(t).get();
}
}
@@ -0,0 +1,96 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common;
import com.google.common.util.concurrent.Runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Queue;
import java.util.concurrent.PriorityBlockingQueue;
@Singleton
public class ShutdownHook extends Thread {
private static final int PRIO_VERY_LAST = Integer.MIN_VALUE;
public static final int PRIO_LAST = PRIO_VERY_LAST + 1;
public static final int PRIO_DEFAULT = 0;
public static final int PRIO_FIRST = Integer.MAX_VALUE;
private static final Logger LOG = LoggerFactory.getLogger(ShutdownHook.class);
private static final OrderedTask POISON = new OrderedTask(PRIO_VERY_LAST, Runnables.doNothing());
private final Queue<OrderedTask> tasks = new PriorityBlockingQueue<>();
@Inject
ShutdownHook() {
super(null, null, "ShutdownTasks", 0);
Runtime.getRuntime().addShutdownHook(this);
LOG.debug("Registered shutdown hook.");
}
@Override
public void run() {
LOG.debug("Running graceful shutdown tasks...");
tasks.add(POISON);
Runnable task;
while ((task = tasks.remove()) != POISON) {
try {
task.run();
} catch (RuntimeException e) {
LOG.error("Exception while shutting down.", e);
}
}
}
/**
* Schedules a task to be run during shutdown with default order
*
* @param task The task to be scheduled
*/
public void runOnShutdown(Runnable task) {
runOnShutdown(PRIO_DEFAULT, task);
}
/**
* Schedules a task to be run with the given priority
*
* @param priority Tasks with high priority will be run before task with lower priority
* @param task The task to be scheduled
*/
public void runOnShutdown(int priority, Runnable task) {
tasks.add(new OrderedTask(priority, task));
}
private static class OrderedTask implements Comparable<OrderedTask>, Runnable {
private final int priority;
private final Runnable task;
public OrderedTask(int priority, Runnable task) {
this.priority = priority;
this.task = task;
}
@Override
public int compareTo(OrderedTask other) {
// overflow-safe signum impl:
if (this.priority > other.priority) {
return -1; // higher prio -> this before other
} else if (this.priority < other.priority) {
return +1; // lower prio -> other before this
} else {
return 0; // same prio
}
}
@Override
public void run() {
task.run();
}
}
}
@@ -8,11 +8,18 @@
******************************************************************************/
package org.cryptomator.common.settings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.NodeOrientation;
import java.util.function.Consumer;
@@ -22,20 +29,28 @@ public class Settings {
public static final int MAX_PORT = 65535;
public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
public static final boolean DEFAULT_CHECK_FOR_UDPATES = false;
public static final boolean DEFAULT_START_HIDDEN = false;
public static final int DEFAULT_PORT = 42427;
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
public static final String DEFAULT_GVFS_SCHEME = "dav";
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
public static final boolean DEFAULT_DEBUG_MODE = false;
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
private static final String DEFAULT_LICENSE_KEY = "";
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES);
private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN);
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
private final StringProperty preferredGvfsScheme = new SimpleStringProperty(DEFAULT_GVFS_SCHEME);
private final ObjectProperty<WebDavUrlScheme> preferredGvfsScheme = new SimpleObjectProperty<>(DEFAULT_GVFS_SCHEME);
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
private Consumer<Settings> saveCmd;
@@ -43,21 +58,25 @@ public class Settings {
* Package-private constructor; use {@link SettingsProvider}.
*/
Settings() {
directories.addListener((ListChangeListener.Change<? extends VaultSettings> change) -> this.save());
directories.addListener(this::somethingChanged);
askedForUpdateCheck.addListener(this::somethingChanged);
checkForUpdates.addListener(this::somethingChanged);
startHidden.addListener(this::somethingChanged);
port.addListener(this::somethingChanged);
numTrayNotifications.addListener(this::somethingChanged);
preferredGvfsScheme.addListener(this::somethingChanged);
debugMode.addListener(this::somethingChanged);
preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged);
licenseKey.addListener(this::somethingChanged);
}
void setSaveCmd(Consumer<Settings> saveCmd) {
this.saveCmd = saveCmd;
}
private void somethingChanged(ObservableValue<?> observable, Object oldValue, Object newValue) {
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
this.save();
}
@@ -80,6 +99,10 @@ public class Settings {
public BooleanProperty checkForUpdates() {
return checkForUpdates;
}
public BooleanProperty startHidden() {
return startHidden;
}
public IntegerProperty port() {
return port;
@@ -89,7 +112,7 @@ public class Settings {
return numTrayNotifications;
}
public StringProperty preferredGvfsScheme() {
public ObjectProperty<WebDavUrlScheme> preferredGvfsScheme() {
return preferredGvfsScheme;
}
@@ -101,4 +124,15 @@ public class Settings {
return preferredVolumeImpl;
}
public ObjectProperty<UiTheme> theme() {
return theme;
}
public ObjectProperty<NodeOrientation> userInterfaceOrientation() {
return userInterfaceOrientation;
}
public StringProperty licenseKey() {
return licenseKey;
}
}
@@ -9,6 +9,7 @@ import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import javafx.geometry.NodeOrientation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,11 +30,15 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
writeVaultSettingsArray(out, value.getDirectories());
out.name("askedForUpdateCheck").value(value.askedForUpdateCheck().get());
out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
out.name("startHidden").value(value.startHidden().get());
out.name("port").value(value.port().get());
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get());
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
out.name("debugMode").value(value.debugMode().get());
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name());
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
out.name("licenseKey").value(value.licenseKey().get());
out.endObject();
}
@@ -62,6 +67,9 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "checkForUpdatesEnabled":
settings.checkForUpdates().set(in.nextBoolean());
break;
case "startHidden":
settings.startHidden().set(in.nextBoolean());
break;
case "port":
settings.port().set(in.nextInt());
break;
@@ -69,7 +77,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
settings.numTrayNotifications().set(in.nextInt());
break;
case "preferredGvfsScheme":
settings.preferredGvfsScheme().set(in.nextString());
settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
break;
case "debugMode":
settings.debugMode().set(in.nextBoolean());
@@ -77,6 +85,15 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "preferredVolumeImpl":
settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
break;
case "theme":
settings.theme().set(parseUiTheme(in.nextString()));
break;
case "uiOrientation":
settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
break;
case "licenseKey":
settings.licenseKey().set(in.nextString());
break;
default:
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
@@ -90,12 +107,40 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
try {
return VolumeImpl.valueOf(nioAdapterName);
return VolumeImpl.valueOf(nioAdapterName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid volume type {}. Defaulting to {}.", nioAdapterName, Settings.DEFAULT_PREFERRED_VOLUME_IMPL);
return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
}
}
private WebDavUrlScheme parseWebDavUrlSchemePrefix(String webDavUrlSchemeName) {
try {
return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid WebDAV url scheme {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
return Settings.DEFAULT_GVFS_SCHEME;
}
}
private UiTheme parseUiTheme(String uiThemeName) {
try {
return UiTheme.valueOf(uiThemeName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid ui theme {}. Defaulting to {}.", uiThemeName, Settings.DEFAULT_THEME);
return Settings.DEFAULT_THEME;
}
}
private NodeOrientation parseUiOrientation(String uiOrientationName) {
try {
return NodeOrientation.valueOf(uiOrientationName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid ui orientation {}. Defaulting to {}.", uiOrientationName, Settings.DEFAULT_USER_INTERFACE_ORIENTATION);
return Settings.DEFAULT_USER_INTERFACE_ORIENTATION;
}
}
private List<VaultSettings> readVaultSettingsArray(JsonReader in) throws IOException {
List<VaultSettings> result = new ArrayList<>();
in.beginArray();
@@ -17,7 +17,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
@@ -33,29 +32,30 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Stream;
@Singleton
public class SettingsProvider implements Provider<Settings> {
public class SettingsProvider implements Supplier<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
private static final long SAVE_DELAY_MS = 1000;
private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
private final AtomicReference<Settings> settings = new AtomicReference<>();
private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter();
private final Environment env;
private final ScheduledExecutorService scheduler;
private final Gson gson;
@Inject
public SettingsProvider(Environment env) {
public SettingsProvider(Environment env, ScheduledExecutorService scheduler) {
this.env = env;
this.scheduler = scheduler;
this.gson = new GsonBuilder() //
.setPrettyPrinting().setLenient().disableHtmlEscaping() //
.registerTypeAdapter(Settings.class, settingsJsonAdapter) //
@@ -98,7 +98,7 @@ public class SettingsProvider implements Provider<Settings> {
final Optional<Path> settingsPath = env.getSettingsPath().findFirst(); // alway save to preferred (first) path
settingsPath.ifPresent(path -> {
Runnable saveCommand = () -> this.save(settings, path);
ScheduledFuture<?> scheduledTask = saveScheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
if (previouslyScheduledTask != null) {
previouslyScheduledTask.cancel(false);
@@ -0,0 +1,18 @@
package org.cryptomator.common.settings;
public enum UiTheme {
LIGHT("Light"),
DARK("Dark");
// CUSTOM("Custom (%s)");
private String displayName;
UiTheme(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
@@ -6,6 +6,7 @@
package org.cryptomator.common.settings;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
@@ -23,6 +24,7 @@ import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
/**
@@ -36,6 +38,8 @@ public class VaultSettings {
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
public static final boolean DEFAULT_USES_READONLY_MODE = false;
public static final String DEFAULT_MOUNT_FLAGS = "";
private static final Random RNG = new Random();
private final String id;
private final ObjectProperty<Path> path = new SimpleObjectProperty();
@@ -69,20 +73,9 @@ public class VaultSettings {
}
private static String generateId() {
return asBase64String(nineBytesFrom(UUID.randomUUID()));
}
private static String asBase64String(byte[] bytes) {
byte[] base64Bytes = Base64.getUrlEncoder().encode(bytes);
return new String(base64Bytes, StandardCharsets.US_ASCII);
}
private static byte[] nineBytesFrom(UUID uuid) {
ByteBuffer uuidBuffer = ByteBuffer.allocate(9);
uuidBuffer.putLong(uuid.getMostSignificantBits());
uuidBuffer.put((byte) (uuid.getLeastSignificantBits() & 0xFF));
uuidBuffer.flip();
return uuidBuffer.array();
byte[] randomBytes = new byte[9];
RNG.nextBytes(randomBytes);
return BaseEncoding.base64Url().encode(randomBytes);
}
public static String normalizeMountName(String mountName) {
@@ -0,0 +1,37 @@
package org.cryptomator.common.settings;
import java.util.Arrays;
public enum WebDavUrlScheme {
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
private final String prefix;
private final String displayName;
WebDavUrlScheme(String prefix, String displayName) {this.prefix = prefix;
this.displayName = displayName;
}
public String getPrefix() {
return prefix;
}
public String getDisplayName() {
return displayName;
}
/**
* Finds a WebDavUrlScheme by prefix.
*
* @param prefix Prefix of the WebDavUrlScheme
* @return WebDavUrlScheme with the given <code>prefix</code>.
* @throws IllegalArgumentException if not WebDavUrlScheme with the given <code>prefix</code> was found.
*/
public static WebDavUrlScheme forPrefix(String prefix) throws IllegalArgumentException {
return Arrays.stream(values()) //
.filter(impl -> impl.prefix.equals(prefix)) //
.findAny() //
.orElseThrow(IllegalArgumentException::new);
}
}
@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import com.google.common.base.Strings;
import org.cryptomator.common.settings.VaultSettings;
@@ -30,6 +30,7 @@ public class DokanyVolume implements Volume {
private final MountFactory mountFactory;
private final WindowsDriveLetters windowsDriveLetters;
private Mount mount;
private Path mountPoint;
@Inject
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, WindowsDriveLetters windowsDriveLetters) {
@@ -45,19 +46,19 @@ public class DokanyVolume implements Volume {
@Override
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException, IOException {
Path mountPath = getMountPoint();
this.mountPoint = determineMountPoint();
String mountName = vaultSettings.mountName().get();
try {
this.mount = mountFactory.mount(fs.getPath("/"), mountPath, mountName, FS_TYPE_NAME, mountFlags.strip());
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, mountName, FS_TYPE_NAME, mountFlags.strip());
} catch (MountFailedException e) {
if (vaultSettings.getIndividualMountPath().isPresent()) {
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPath);
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
}
throw new VolumeException("Unable to mount Filesystem", e);
}
}
private Path getMountPoint() throws VolumeException, IOException {
private Path determineMountPoint() throws VolumeException, IOException {
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
if (optionalCustomMountPoint.isPresent()) {
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
@@ -68,8 +69,9 @@ public class DokanyVolume implements Volume {
} else {
//auto assign drive letter
if (!windowsDriveLetters.getAvailableDriveLetters().isEmpty()) {
return windowsDriveLetters.getAvailableDriveLetters().iterator().next();
return Path.of(windowsDriveLetters.getAvailableDriveLetters().iterator().next() + ":\\");
} else {
//TODO: Error Handling
throw new VolumeException("No free drive letter available.");
}
}
@@ -99,6 +101,11 @@ public class DokanyVolume implements Volume {
mount.close();
}
@Override
public Optional<Path> getMountPoint() {
return Optional.ofNullable(mountPoint);
}
public static boolean isSupportedStatic() {
return MountFactory.isApplicable();
}
@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import com.google.common.base.Splitter;
import org.apache.commons.lang3.SystemUtils;
@@ -100,8 +100,7 @@ public class FuseVolume implements Volume {
try {
Mounter mounter = FuseMountFactory.getMounter();
EnvironmentVariables envVars = EnvironmentVariables.create() //
.withFlags(splitFlags(mountFlags))
.withMountPoint(mountPoint) //
.withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) //
.build();
this.fuseMnt = mounter.mount(root, envVars);
} catch (CommandFailedException e) {
@@ -166,6 +165,11 @@ public class FuseVolume implements Volume {
return FuseVolume.isSupportedStatic();
}
@Override
public Optional<Path> getMountPoint() {
return Optional.ofNullable(mountPoint);
}
public static boolean isSupportedStatic() {
return (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_LINUX) && FuseMountFactory.isFuseSupported();
}
@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import javax.inject.Scope;
import java.lang.annotation.Documented;
@@ -6,14 +6,16 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import com.google.common.base.Strings;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
@@ -39,36 +41,51 @@ import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.function.Supplier;
@PerVault
public class Vault {
public static final Predicate<Vault> NOT_LOCKED = hasState(State.LOCKED).negate();
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
private final VaultSettings vaultSettings;
private final Provider<Volume> volumeProvider;
private final Supplier<String> defaultMountFlags;
private final AtomicReference<CryptoFileSystem> cryptoFileSystem = new AtomicReference<>();
private final ObjectProperty<State> state = new SimpleObjectProperty<State>(State.LOCKED);
private final StringBinding defaultMountFlags;
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
private final ObjectProperty<VaultState> state;
private final VaultStats stats;
private final StringBinding displayableName;
private final StringBinding displayablePath;
private final BooleanBinding locked;
private final BooleanBinding processing;
private final BooleanBinding unlocked;
private final BooleanBinding needsMigration;
private final ObjectBinding<Path> accessPoint;
private Volume volume;
public enum State {
LOCKED, PROCESSING, UNLOCKED
}
private volatile Volume volume;
@Inject
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags Supplier<String> defaultMountFlags) {
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, VaultStats stats) {
this.vaultSettings = vaultSettings;
this.volumeProvider = volumeProvider;
this.defaultMountFlags = defaultMountFlags;
this.cryptoFileSystem = cryptoFileSystem;
this.state = state;
this.stats = stats;
this.displayableName = Bindings.createStringBinding(this::getDisplayableName, vaultSettings.path());
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
this.processing = Bindings.createBooleanBinding(this::isProcessing, state);
this.unlocked = Bindings.createBooleanBinding(this::isUnlocked, state);
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
this.accessPoint = Bindings.createObjectBinding(this::getAccessPoint, state);
}
// ******************************************************************************
@@ -80,7 +97,7 @@ public class Vault {
}
private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
List<FileSystemFlags> flags = new ArrayList<>();
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
if (vaultSettings.usesReadOnlyMode().get()) {
flags.add(FileSystemFlags.READONLY);
}
@@ -92,40 +109,16 @@ public class Vault {
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
}
public void create(CharSequence passphrase) throws IOException {
if (!isValidVaultDirectory()) {
CryptoFileSystemProvider.initialize(getPath(), MASTERKEY_FILENAME, passphrase);
} else {
throw new FileAlreadyExistsException(getPath().toString());
}
}
public void changePassphrase(CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException {
CryptoFileSystemProvider.changePassphrase(getPath(), MASTERKEY_FILENAME, oldPassphrase, newPassphrase);
}
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
Platform.runLater(() -> state.set(State.PROCESSING));
try {
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
throw new NotDirectoryException("");
}
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
volume = volumeProvider.get();
volume.mount(fs, getMountFlags());
Platform.runLater(() -> {
state.set(State.UNLOCKED);
});
} catch (Exception e) {
Platform.runLater(() -> state.set(State.LOCKED));
throw e;
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
throw new NotDirectoryException("");
}
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
volume = volumeProvider.get();
volume.mount(fs, getEffectiveMountFlags());
}
public synchronized void lock(boolean forced) throws Volume.VolumeException {
Platform.runLater(() -> {
state.set(State.PROCESSING);
});
if (forced && volume.supportsForcedUnmount()) {
volume.unmountForced();
} else {
@@ -139,28 +132,6 @@ public class Vault {
LOG.error("Error closing file system.", e);
}
}
Platform.runLater(() -> {
state.set(State.LOCKED);
});
}
/**
* Ejects any mounted drives and locks this vault. no-op if this vault is currently locked.
*/
public void prepareForShutdown() {
try {
lock(false);
} catch (Volume.VolumeException e) {
if (volume.supportsForcedUnmount()) {
try {
lock(true);
} catch (Volume.VolumeException e1) {
LOG.warn("Failed to force lock vault.", e1);
}
} else {
LOG.warn("Failed to gracefully lock vault.", e);
}
}
}
public void reveal() throws Volume.VolumeException {
@@ -168,21 +139,96 @@ public class Vault {
}
// ******************************************************************************
// Getter/Setter
// *******************************************************************************/
// Observable Properties
// *******************************************************************************
public State getState() {
return state.get();
}
public ReadOnlyObjectProperty<State> stateProperty() {
public ObjectProperty<VaultState> stateProperty() {
return state;
}
public static Predicate<Vault> hasState(State state) {
return vault -> {
return vault.getState() == state;
};
public VaultState getState() {
return state.get();
}
public void setState(VaultState value) {
state.setValue(value);
}
public BooleanBinding lockedProperty() {
return locked;
}
public boolean isLocked() {
return state.get() == VaultState.LOCKED;
}
public BooleanBinding processingProperty() {
return processing;
}
public boolean isProcessing() {
return state.get() == VaultState.PROCESSING;
}
public BooleanBinding unlockedProperty() {
return unlocked;
}
public boolean isUnlocked() {
return state.get() == VaultState.UNLOCKED;
}
public BooleanBinding needsMigrationProperty() {
return needsMigration;
}
public boolean isNeedsMigration() {
return state.get() == VaultState.NEEDS_MIGRATION;
}
public StringBinding displayableNameProperty() {
return displayableName;
}
public String getDisplayableName() {
Path p = vaultSettings.path().get();
return p.getFileName().toString();
}
public ObjectBinding<Path> accessPointProperty() {
return accessPoint;
}
public Path getAccessPoint() {
if (state.get() == VaultState.UNLOCKED) {
assert volume != null;
return volume.getMountPoint().orElse(Path.of(""));
} else {
return Path.of("");
}
}
public StringBinding displayablePathProperty() {
return displayablePath;
}
public String getDisplayablePath() {
Path p = vaultSettings.path().get();
if (p.startsWith(HOME_DIR)) {
Path relativePath = HOME_DIR.relativize(p);
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
return homePrefix + relativePath.toString();
} else {
return p.toString();
}
}
// ******************************************************************************
// Getter/Setter
// *******************************************************************************/
public VaultStats getStats() {
return stats;
}
public Observable[] observables() {
@@ -197,106 +243,31 @@ public class Vault {
return vaultSettings.path().getValue();
}
public Binding<String> displayablePath() {
Path homeDir = Paths.get(SystemUtils.USER_HOME);
return EasyBind.map(vaultSettings.path(), p -> {
if (p.startsWith(homeDir)) {
Path relativePath = homeDir.relativize(p);
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
return homePrefix + relativePath.toString();
} else {
return p.toString();
}
});
}
/**
* @return Directory name without preceeding path components and file extension
*/
public Binding<String> name() {
return EasyBind.map(vaultSettings.path(), Path::getFileName).map(Path::toString);
}
public boolean doesVaultDirectoryExist() {
return Files.isDirectory(getPath());
}
public boolean isValidVaultDirectory() {
return CryptoFileSystemProvider.containsVault(getPath(), MASTERKEY_FILENAME);
}
public long pollBytesRead() {
CryptoFileSystem fs = cryptoFileSystem.get();
if (fs != null) {
return fs.getStats().pollBytesRead();
} else {
return 0l;
}
}
public long pollBytesWritten() {
CryptoFileSystem fs = cryptoFileSystem.get();
if (fs != null) {
return fs.getStats().pollBytesWritten();
} else {
return 0l;
}
}
public String getCustomMountPath() {
return vaultSettings.individualMountPath().getValueSafe();
}
public void setCustomMountPath(String mountPath) {
vaultSettings.individualMountPath().set(mountPath);
}
public String getMountName() {
return vaultSettings.mountName().get();
}
public void setMountName(String mountName) throws IllegalArgumentException {
if (StringUtils.isBlank(mountName)) {
throw new IllegalArgumentException("mount name is empty");
} else {
vaultSettings.mountName().set(VaultSettings.normalizeMountName(mountName));
}
}
public boolean isHavingCustomMountFlags() {
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
}
public String getMountFlags() {
public StringBinding defaultMountFlagsProperty() {
return defaultMountFlags;
}
public String getDefaultMountFlags() {
return defaultMountFlags.get();
}
public String getEffectiveMountFlags() {
String mountFlags = vaultSettings.mountFlags().get();
if (Strings.isNullOrEmpty(mountFlags)) {
return defaultMountFlags.get();
return getDefaultMountFlags();
} else {
return mountFlags;
}
}
public void setMountFlags(String mountFlags) {
public void setCustomMountFlags(String mountFlags) {
vaultSettings.mountFlags().set(mountFlags);
}
public Character getWinDriveLetter() {
if (vaultSettings.winDriveLetter().get() == null) {
return null;
} else {
return vaultSettings.winDriveLetter().get().charAt(0);
}
}
public void setWinDriveLetter(Path root) {
if (root == null) {
vaultSettings.winDriveLetter().set(null);
} else {
char winDriveLetter = root.toString().charAt(0);
vaultSettings.winDriveLetter().set(String.valueOf(winDriveLetter));
}
}
public String getId() {
return vaultSettings.getId();
}
@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import dagger.BindsInstance;
import org.cryptomator.common.settings.VaultSettings;
@@ -22,6 +22,9 @@ public interface VaultComponent {
@BindsInstance
Builder vaultSettings(VaultSettings vaultSettings);
@BindsInstance
Builder initialVaultState(VaultState vaultState);
VaultComponent build();
}
@@ -0,0 +1,33 @@
package org.cryptomator.common.vaults;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.cryptomator.common.settings.VaultSettings;
import java.util.List;
import java.util.stream.Collectors;
/**
* This listener makes sure to reflect any changes to the vault list back to the settings.
*/
class VaultListChangeListener implements ListChangeListener<Vault> {
private final ObservableList<VaultSettings> vaultSettingsList;
public VaultListChangeListener(ObservableList<VaultSettings> vaultSettingsList) {
this.vaultSettingsList = vaultSettingsList;
}
@Override
public void onChanged(Change<? extends Vault> c) {
while(c.next()) {
if (c.wasAdded()) {
List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
vaultSettingsList.addAll(c.getFrom(), addedSettings);
} else if (c.wasRemoved()) {
List<VaultSettings> removedSettings = c.getRemoved().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
vaultSettingsList.removeAll(removedSettings);
}
}
}
}
@@ -0,0 +1,100 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.common.vaults;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.migration.Migrators;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
@Singleton
public class VaultListManager {
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private final VaultComponent.Builder vaultComponentBuilder;
private final ObservableList<Vault> vaultList;
@Inject
public VaultListManager(VaultComponent.Builder vaultComponentBuilder, Settings settings) {
this.vaultComponentBuilder = vaultComponentBuilder;
this.vaultList = FXCollections.observableArrayList(Vault::observables);
addAll(settings.getDirectories());
vaultList.addListener(new VaultListChangeListener(settings.getDirectories()));
}
public ObservableList<Vault> getVaultList() {
return vaultList;
}
public Vault add(Path pathToVault) throws NoSuchFileException {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
throw new NoSuchFileException(pathToVault.toString(), null, "Not a vault directory");
}
Optional<Vault> alreadyExistingVault = get(pathToVault);
if (alreadyExistingVault.isPresent()) {
return alreadyExistingVault.get();
} else {
VaultSettings vaultSettings = VaultSettings.withRandomId();
vaultSettings.path().set(pathToVault);
Vault newVault = create(vaultSettings);
vaultList.add(newVault);
return newVault;
}
}
private void addAll(Collection<VaultSettings> vaultSettings) {
Collection<Vault> vaults = vaultSettings.stream().map(this::create).collect(Collectors.toList());
vaultList.addAll(vaults);
}
private Optional<Vault> get(Path vaultPath) {
return vaultList.stream().filter(v -> {
try {
return Files.isSameFile(vaultPath, v.getPath());
} catch (IOException e) {
return false;
}
}).findAny();
}
private Vault create(VaultSettings vaultSettings) {
VaultState vaultState = determineVaultState(vaultSettings.path().get());
VaultComponent comp = vaultComponentBuilder.vaultSettings(vaultSettings).initialVaultState(vaultState).build();
return comp.vault();
}
public static VaultState determineVaultState(Path pathToVault) {
try {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.MISSING;
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.NEEDS_MIGRATION;
} else {
return VaultState.LOCKED;
}
} catch (IOException e) {
return VaultState.ERROR;
}
}
}
@@ -3,14 +3,23 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import dagger.Module;
import dagger.Provides;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,13 +27,25 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Supplier;
import java.util.concurrent.atomic.AtomicReference;
@Module
public class VaultModule {
private static final Logger LOG = LoggerFactory.getLogger(VaultModule.class);
@Provides
@PerVault
public AtomicReference<CryptoFileSystem> provideCryptoFileSystemReference() {
return new AtomicReference<>();
}
@Provides
@PerVault
public ObjectProperty<VaultState> provideVaultState(VaultState initialState) {
return new SimpleObjectProperty<>(initialState);
}
@Provides
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
@@ -44,33 +65,33 @@ public class VaultModule {
@Provides
@PerVault
@DefaultMountFlags
public Supplier<String> provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
return () -> {
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
switch (preferredImpl) {
case FUSE:
if (SystemUtils.IS_OS_MAC_OSX) {
return getMacFuseDefaultMountFlags(settings, vaultSettings);
} else if (SystemUtils.IS_OS_LINUX) {
return getLinuxFuseDefaultMountFlags(settings, vaultSettings);
}
case DOKANY:
return getDokanyDefaultMountFlags(settings, vaultSettings);
default:
return "--flags-supported-on-FUSE-or-DOKANY-only";
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
StringProperty mountName = vaultSettings.mountName();
BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
return Bindings.createStringBinding(() -> {
VolumeImpl v = preferredVolumeImpl.get();
if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_MAC) {
return getMacFuseDefaultMountFlags(mountName, readOnly);
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_LINUX) {
return getLinuxFuseDefaultMountFlags(readOnly);
} else if (v == VolumeImpl.DOKANY && SystemUtils.IS_OS_WINDOWS) {
return getDokanyDefaultMountFlags(readOnly);
} else {
return "--flags-supported-on-FUSE-or-DOKANY-only";
}
};
}, mountName, readOnly, preferredVolumeImpl);
}
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
private String getMacFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
private String getMacFuseDefaultMountFlags(ReadOnlyStringProperty mountName, ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_MAC_OSX;
StringBuilder flags = new StringBuilder();
if (vaultSettings.usesReadOnlyMode().get()) {
if (readOnly.get()) {
flags.append(" -ordonly");
}
flags.append(" -ovolname=").append(vaultSettings.mountName().get());
flags.append(" -ovolname=").append(mountName.get());
flags.append(" -oatomic_o_trunc");
flags.append(" -oauto_xattr");
flags.append(" -oauto_cache");
@@ -92,11 +113,10 @@ public class VaultModule {
}
// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
private String getLinuxFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
private String getLinuxFuseDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_LINUX;
StringBuilder flags = new StringBuilder();
if (vaultSettings.usesReadOnlyMode().get()) {
if (readOnly.get()) {
flags.append(" -oro");
}
flags.append(" -oauto_unmount");
@@ -115,12 +135,11 @@ public class VaultModule {
}
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
private String getDokanyDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
private String getDokanyDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_WINDOWS;
StringBuilder flags = new StringBuilder();
flags.append(" --options CURRENT_SESSION");
if (vaultSettings.usesReadOnlyMode().get()) {
if (readOnly.get()) {
flags.append(",WRITE_PROTECTION");
}
flags.append(" --thread-count 5");
@@ -0,0 +1,34 @@
package org.cryptomator.common.vaults;
public enum VaultState {
/**
* No vault found at the provided path
*/
MISSING,
/**
* Vault requires migration to a newer vault format
*/
NEEDS_MIGRATION,
/**
* Vault ready to be unlocked
*/
LOCKED,
/**
* Vault in transition between two other states
*/
PROCESSING,
/**
* Vault is unlocked
*/
UNLOCKED,
/**
* Unknown state due to preceeding unrecoverable exceptions.
*/
ERROR;
}
@@ -0,0 +1,100 @@
package org.cryptomator.common.vaults;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.util.Duration;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@PerVault
public class VaultStats {
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
private final AtomicReference<CryptoFileSystem> fs;
private final ObjectProperty<VaultState> state;
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
@Inject
VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
this.fs = fs;
this.state = state;
this.updateService = new UpdateStatsService();
updateService.setExecutor(executor);
updateService.setPeriod(Duration.seconds(1));
state.addListener(this::vaultStateChanged);
}
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
switch (state.get()) {
case UNLOCKED:
assert fs.get() != null;
LOG.debug("start recording stats");
updateService.restart();
break;
default:
LOG.debug("stop recording stats");
updateService.cancel();
break;
}
}
private void updateStats(Optional<CryptoFileSystemStats> stats) {
assert Platform.isFxApplicationThread();
bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0l));
bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0l));
}
private class UpdateStatsService extends ScheduledService<Optional<CryptoFileSystemStats>> {
@Override
protected Task<Optional<CryptoFileSystemStats>> createTask() {
return new Task<>() {
@Override
protected Optional<CryptoFileSystemStats> call() {
return Optional.ofNullable(fs.get()).map(CryptoFileSystem::getStats);
}
};
}
@Override
protected void succeeded() {
assert getValue() != null;
updateStats(getValue());
super.succeeded();
}
}
/* Observables */
public LongProperty bytesPerSecondReadProperty() {
return bytesPerSecondRead;
}
public long getBytesPerSecondRead() {
return bytesPerSecondRead.get();
}
public LongProperty bytesPerSecondWrittenProperty() {
return bytesPerSecondWritten;
}
public long getBytesPerSecondWritten() {
return bytesPerSecondWritten.get();
}
}
@@ -1,9 +1,11 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.stream.Stream;
/**
@@ -28,6 +30,8 @@ public interface Volume {
void unmount() throws VolumeException;
Optional<Path> getMountPoint();
// optional forced unmounting:
default boolean supportsForcedUnmount() {
@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import org.cryptomator.common.settings.Settings;
@@ -13,6 +13,8 @@ import javax.inject.Inject;
import javax.inject.Provider;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.util.Optional;
public class WebDavVolume implements Volume {
@@ -25,6 +27,7 @@ public class WebDavVolume implements Volume {
private WebDavServer server;
private WebDavServletController servlet;
private Mounter.Mount mount;
private Path mountPoint;
@Inject
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings) {
@@ -52,7 +55,7 @@ public class WebDavVolume implements Volume {
}
MountParams mountParams = MountParams.create() //
.withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) //
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get())//
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
.withWebdavHostname(getLocalhostAliasOrNull()) //
.build();
try {
@@ -93,6 +96,11 @@ public class WebDavVolume implements Volume {
cleanup();
}
@Override
public Optional<Path> getMountPoint() {
return Optional.ofNullable(mountPoint);
}
private String getLocalhostAliasOrNull() {
try {
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
@@ -3,31 +3,31 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
@FxApplicationScoped
@Singleton
public final class WindowsDriveLetters {
private static final Logger LOG = LoggerFactory.getLogger(WindowsDriveLetters.class);
private static final Set<Path> D_TO_Z;
private static final Set<String> A_TO_Z;
static {
try (IntStream stream = IntStream.rangeClosed('D', 'Z')) {
D_TO_Z = stream.mapToObj(i -> Path.of(((char) i)+":\\")).collect(Collectors.toSet());
try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
A_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(Collectors.toSet());
}
}
@@ -35,20 +35,21 @@ public final class WindowsDriveLetters {
public WindowsDriveLetters() {
}
public Set<Path> getOccupiedDriveLetters() {
public Set<String> getAllDriveLetters() {
return A_TO_Z;
}
public Set<String> getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) {
LOG.warn("Attempted to get occupied drive letters on non-Windows machine.");
return Set.of();
} else {
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).collect(Collectors.toSet());
return StreamSupport.stream(rootDirs.spliterator(), false).map(p -> p.toString().substring(0,1)).collect(Collectors.toSet());
}
}
public Set<Path> getAvailableDriveLetters() {
Set<Path> occupiedDriveLetters = getOccupiedDriveLetters();
Predicate<Path> isOccupiedDriveLetter = occupiedDriveLetters::contains;
return D_TO_Z.stream().filter(isOccupiedDriveLetter.negate()).collect(Collectors.toSet());
public Set<String> getAvailableDriveLetters() {
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
}
}
@@ -0,0 +1,62 @@
package org.cryptomator.common;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.cryptomator.common.LicenseChecker;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Optional;
class LicenseCheckerTest {
private static final String PUBLIC_KEY = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ" //
+ "PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47" //
+ "6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM" //
+ "Al8G7CqwoJOsW7Kddns=";
private LicenseChecker licenseChecker;
@BeforeEach
public void setup() {
licenseChecker = new LicenseChecker(PUBLIC_KEY);
}
@Test
public void testCheckValidLicense() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertTrue(decoded.isPresent());
Assertions.assertEquals("cryptobot@example.com", decoded.get().getSubject());
}
@Test
public void testCheckInvalidLicenseHeader() {
String license = "EyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertFalse(decoded.isPresent());
}
@Test
public void testCheckInvalidLicensePayload() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.EyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertFalse(decoded.isPresent());
}
@Test
public void testCheckInvalidLicenseSignature() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.aQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertFalse(decoded.isPresent());
}
}
@@ -30,7 +30,7 @@ public class SettingsJsonAdapterTest {
Assertions.assertEquals(2, settings.getDirectories().size());
Assertions.assertEquals(8080, settings.port().get());
Assertions.assertEquals(42, settings.numTrayNotifications().get());
Assertions.assertEquals("dav", settings.preferredGvfsScheme().get());
Assertions.assertEquals(WebDavUrlScheme.DAV, settings.preferredGvfsScheme().get());
Assertions.assertEquals(VolumeImpl.FUSE, settings.preferredVolumeImpl().get());
}
@@ -8,22 +8,20 @@ package org.cryptomator.common.settings;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.io.IOException;
import java.util.function.Consumer;
public class SettingsTest {
@Test
public void testAutoSave() throws IOException {
@SuppressWarnings("unchecked")
Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
public void testAutoSave() {
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
Settings settings = new Settings();
settings.setSaveCmd(changeListener);
VaultSettings vaultSettings = VaultSettings.withRandomId();
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
// first change (to property):
settings.preferredGvfsScheme().set("asd");
settings.preferredGvfsScheme().set(WebDavUrlScheme.WEBDAV);
Mockito.verify(changeListener, Mockito.times(1)).accept(settings);
// second change (to list):
@@ -0,0 +1,70 @@
package org.cryptomator.common.vaults;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
import java.nio.file.Path;
public class VaultModuleTest {
private final Settings settings = Mockito.mock(Settings.class);
private final VaultSettings vaultSettings = Mockito.mock(VaultSettings.class);
private final VaultModule module = new VaultModule();
@BeforeEach
public void setup(@TempDir Path tmpDir) {
Mockito.when(vaultSettings.mountName()).thenReturn(new SimpleStringProperty("TEST"));
Mockito.when(vaultSettings.usesReadOnlyMode()).thenReturn(new SimpleBooleanProperty(true));
System.setProperty("user.home", tmpDir.toString());
}
@Test
@DisplayName("provideDefaultMountFlags on Mac/FUSE")
@EnabledOnOs(OS.MAC)
public void testMacFuseDefaultMountFlags() {
Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.FUSE));
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=TEST"));
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ordonly"));
}
@Test
@DisplayName("provideDefaultMountFlags on Linux/FUSE")
@EnabledOnOs(OS.LINUX)
public void testLinuxFuseDefaultMountFlags() {
Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.FUSE));
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-oro"));
}
@Test
@DisplayName("provideDefaultMountFlags on Windows/Dokany")
@EnabledOnOs(OS.WINDOWS)
public void testWinDokanyDefaultMountFlags() {
Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.DOKANY));
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("--options CURRENT_SESSION,WRITE_PROTECTION"));
}
}
+2 -6
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.15</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>keychain</artifactId>
<name>System Keychain Access</name>
@@ -23,11 +23,7 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<!-- Google -->
<dependency>
<groupId>com.google.guava</groupId>
@@ -5,30 +5,19 @@
*******************************************************************************/
package org.cryptomator.keychain;
import java.util.Optional;
import java.util.Set;
import com.google.common.collect.Sets;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
import org.cryptomator.jni.JniFunctions;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.WinFunctions;
import org.cryptomator.common.JniModule;
@Module
import javax.inject.Singleton;
import java.util.Optional;
import java.util.Set;
@Module(includes = {JniModule.class})
public class KeychainModule {
@Provides
Optional<MacFunctions> provideOptionalMacFunctions() {
return JniFunctions.macFunctions();
}
@Provides
Optional<WinFunctions> provideOptionalWinFunctions() {
return JniFunctions.winFunctions();
}
@Provides
@ElementsIntoSet
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {
@@ -36,6 +25,7 @@ public class KeychainModule {
}
@Provides
@Singleton
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.15</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>launcher</artifactId>
<name>Cryptomator Launcher</name>
@@ -1,40 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.launcher;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class CleanShutdownPerformer extends Thread {
private static final Logger LOG = LoggerFactory.getLogger(CleanShutdownPerformer.class);
static final ConcurrentMap<Runnable, Boolean> SHUTDOWN_TASKS = new ConcurrentHashMap<>();
@Override
public void run() {
LOG.debug("Running graceful shutdown tasks...");
SHUTDOWN_TASKS.keySet().forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("Exception while shutting down.", e);
}
});
SHUTDOWN_TASKS.clear();
LOG.info("Goodbye.");
}
static void scheduleShutdownTask(Runnable task) {
SHUTDOWN_TASKS.put(task, Boolean.TRUE);
}
static void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new CleanShutdownPerformer());
}
}
@@ -5,12 +5,10 @@
*******************************************************************************/
package org.cryptomator.launcher;
import javafx.application.Application;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.logging.LoggerConfiguration;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.launcher.UiLauncher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,6 +17,7 @@ import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
@Singleton
public class Cryptomator {
@@ -32,13 +31,17 @@ public class Cryptomator {
private final DebugMode debugMode;
private final IpcFactory ipcFactory;
private final Optional<String> applicationVersion;
private final CountDownLatch shutdownLatch;
private final UiLauncher uiLauncher;
@Inject
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion) {
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) {
this.logConfig = logConfig;
this.debugMode = debugMode;
this.ipcFactory = ipcFactory;
this.applicationVersion = applicationVersion;
this.shutdownLatch = shutdownLatch;
this.uiLauncher = uiLauncher;
}
public static void main(String[] args) {
@@ -48,6 +51,7 @@ public class Cryptomator {
/**
* Main entry point of the application launcher.
*
* @param args The arguments passed to this program via {@link #main(String[])}.
* @return Nonzero exit code in case of an error.
*/
@@ -63,6 +67,7 @@ public class Cryptomator {
try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) {
endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
if (endpoint.isConnectedToRemote()) {
endpoint.getRemote().revealRunningApp();
LOG.info("Found running application instance. Shutting down...");
return 2;
} else {
@@ -77,45 +82,21 @@ public class Cryptomator {
/**
* Launches the JavaFX application and waits until shutdown is requested.
*
* @return Nonzero exit code in case of an error.
* @implNote This method blocks until {@link #shutdownLatch} reached zero.
*/
private int runGuiApplication() {
try {
CleanShutdownPerformer.registerShutdownHook();
Application.launch(MainApp.class);
LOG.info("Shutting down...");
uiLauncher.launch();
shutdownLatch.await();
LOG.info("UI shut down");
return 0;
} catch (Throwable e) {
LOG.error("Terminating due to error", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return 1;
}
}
// We need a separate FX Application class, until we can use the module system. See https://stackoverflow.com/q/54756176/4014509
public static class MainApp extends Application {
@Override
public void start(Stage primaryStage) {
LOG.info("JavaFX application started.");
primaryStage.setMinWidth(652.0);
primaryStage.setMinHeight(440.0);
FxApplicationComponent fxApplicationComponent = CRYPTOMATOR_COMPONENT.fxApplicationComponent() //
.fxApplication(this) //
.mainWindow(primaryStage) //
.build();
MainController mainCtrl = fxApplicationComponent.fxmlLoader().load("/fxml/main.fxml");
mainCtrl.initStage(primaryStage);
primaryStage.show();
}
@Override
public void stop() {
LOG.info("JavaFX application stopped.");
}
}
}
@@ -3,15 +3,14 @@ package org.cryptomator.launcher;
import dagger.Component;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.logging.LoggerModule;
import org.cryptomator.ui.launcher.UiLauncherModule;
import javax.inject.Singleton;
@Singleton
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class})
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class, UiLauncherModule.class})
public interface CryptomatorComponent {
Cryptomator application();
FxApplicationComponent.Builder fxApplicationComponent();
}
@@ -2,31 +2,20 @@ package org.cryptomator.launcher;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.ui.model.AppLaunchEvent;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
@Module
class CryptomatorModule {
@Provides
@Singleton
static Settings provideSettings(SettingsProvider settingsProvider) {
return settingsProvider.get();
}
@Provides
@Singleton
@Named("launchEventQueue")
static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
return new ArrayBlockingQueue<>(10);
@Named("shutdownLatch")
static CountDownLatch provideShutdownLatch() {
return new CountDownLatch(1);
}
@Provides
@@ -6,7 +6,7 @@
*******************************************************************************/
package org.cryptomator.launcher;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,8 +21,10 @@ import java.nio.file.FileSystems;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Singleton
@@ -34,16 +36,14 @@ class FileOpenRequestHandler {
@Inject
public FileOpenRequestHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.launchEventQueue = launchEventQueue;
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_FILE)) {
Desktop.getDesktop().setOpenFileHandler(this::openFiles);
} catch (UnsupportedOperationException e) {
LOG.info("Unable to setOpenFileHandler, probably not supported on this OS.");
}
}
private void openFiles(final OpenFilesEvent evt) {
Stream<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath);
AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen);
private void openFiles(OpenFilesEvent evt) {
Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).collect(Collectors.toList());
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
@@ -53,16 +53,18 @@ class FileOpenRequestHandler {
// visible for testing
void handleLaunchArgs(FileSystem fs, String[] args) {
Stream<Path> pathsToOpen = Arrays.stream(args).map(str -> {
Collection<Path> pathsToOpen = Arrays.stream(args).map(str -> {
try {
return fs.getPath(str);
} catch (InvalidPathException e) {
LOG.trace("Argument not a valid path: {}", str);
return null;
}
}).filter(Objects::nonNull);
AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}).filter(Objects::nonNull).collect(Collectors.toList());
if (!pathsToOpen.isEmpty()) {
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
}
@@ -1,26 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.launcher;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.ui.UiModule;
import javax.inject.Named;
import java.util.function.Consumer;
@Module(includes = {UiModule.class})
class FxApplicationModule {
@Provides
@FxApplicationScoped
@Named("shutdownTaskScheduler")
Consumer<Runnable> provideShutdownTaskScheduler() {
return CleanShutdownPerformer::scheduleShutdownTask;
}
}
@@ -10,6 +10,8 @@ import java.rmi.RemoteException;
interface IpcProtocol extends Remote {
void handleLaunchArgs(String[] args) throws RemoteException;
void revealRunningApp() throws RemoteException;
void handleLaunchArgs(String... args) throws RemoteException;
}
@@ -1,11 +1,16 @@
package org.cryptomator.launcher;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Stream;
@Singleton
class IpcProtocolImpl implements IpcProtocol {
@@ -13,15 +18,22 @@ class IpcProtocolImpl implements IpcProtocol {
private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
private final FileOpenRequestHandler fileOpenRequestHandler;
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
@Inject
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler) {
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.fileOpenRequestHandler = fileOpenRequestHandler;
this.launchEventQueue = launchEventQueue;
}
@Override
public void handleLaunchArgs(String[] args) {
LOG.info("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
public void revealRunningApp() {
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Collections.emptyList()));
}
@Override
public void handleLaunchArgs(String... args) {
LOG.debug("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
fileOpenRequestHandler.handleLaunchArgs(args);
}
@@ -5,9 +5,8 @@ import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.hook.DelayingShutdownHook;
import ch.qos.logback.core.util.Duration;
import org.cryptomator.common.Environment;
import org.cryptomator.common.ShutdownHook;
import javax.inject.Inject;
import javax.inject.Named;
@@ -16,26 +15,27 @@ import java.util.Map;
@Singleton
public class LoggerConfiguration {
private static final double SHUTDOWN_DELAY_MS = 100;
private final LoggerContext context;
private final Environment environment;
private final Appender<ILoggingEvent> stdout;
private final Appender<ILoggingEvent> upgrade;
private final Appender<ILoggingEvent> file;
private final ShutdownHook shutdownHook;
@Inject
LoggerConfiguration(LoggerContext context, //
Environment environment, //
@Named("stdoutAppender") Appender<ILoggingEvent> stdout, //
@Named("upgradeAppender") Appender<ILoggingEvent> upgrade, //
@Named("fileAppender") Appender<ILoggingEvent> file) {
@Named("fileAppender") Appender<ILoggingEvent> file, //
ShutdownHook shutdownHook) {
this.context = context;
this.environment = environment;
this.stdout = stdout;
this.upgrade = upgrade;
this.file = file;
this.shutdownHook = shutdownHook;
}
public void init() {
@@ -55,16 +55,15 @@ public class LoggerConfiguration {
}
// configure upgrade logger:
Logger upgrades = context.getLogger("org.cryptomator.ui.model.upgrade");
Logger upgrades = context.getLogger("org.cryptomator.cryptofs.migration");
upgrades.setLevel(Level.DEBUG);
upgrades.addAppender(stdout);
upgrades.addAppender(upgrade);
upgrades.addAppender(file);
upgrades.setAdditive(false);
// add shutdown hook
DelayingShutdownHook shutdownHook = new DelayingShutdownHook();
shutdownHook.setContext(context);
shutdownHook.setDelay(Duration.buildByMilliseconds(SHUTDOWN_DELAY_MS));
shutdownHook.runOnShutdown(ShutdownHook.PRIO_LAST, context::stop);
}
}
@@ -5,7 +5,7 @@
*******************************************************************************/
package org.cryptomator.launcher;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
@@ -15,16 +15,14 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FileOpenRequestHandlerTest {
@@ -39,32 +37,30 @@ public class FileOpenRequestHandlerTest {
@Test
@DisplayName("./cryptomator.exe foo bar")
public void testOpenArgsWithCorrectPaths() throws IOException {
public void testOpenArgsWithCorrectPaths() {
inTest.handleLaunchArgs(new String[]{"foo", "bar"});
AppLaunchEvent evt = queue.poll();
Assertions.assertNotNull(evt);
List<Path> paths = evt.getPathsToOpen().collect(Collectors.toList());
Collection<Path> paths = evt.getPathsToOpen();
MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Paths.get("foo"), Paths.get("bar")));
}
@Test
@DisplayName("./cryptomator.exe foo (with 'foo' being an invalid path)")
public void testOpenArgsWithIncorrectPaths() throws IOException {
public void testOpenArgsWithIncorrectPaths() {
FileSystem fs = Mockito.mock(FileSystem.class);
Mockito.when(fs.getPath("foo")).thenThrow(new InvalidPathException("foo", "foo is not a path"));
inTest.handleLaunchArgs(fs, new String[]{"foo"});
AppLaunchEvent evt = queue.poll();
Assertions.assertNotNull(evt);
List<Path> paths = evt.getPathsToOpen().collect(Collectors.toList());
Assertions.assertTrue(paths.isEmpty());
Assertions.assertNull(evt);
}
@Test
@DisplayName("./cryptomator.exe foo (with full event queue)")
public void testOpenArgsWithFullQueue() throws IOException {
queue.add(new AppLaunchEvent(Stream.empty()));
public void testOpenArgsWithFullQueue() {
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Collections.emptyList()));
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
inTest.handleLaunchArgs(new String[]{"foo"});
+36 -47
View File
@@ -3,13 +3,13 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.15</version>
<version>1.5.0-beta3</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
<organization>
<name>cryptomator.org</name>
<url>http://cryptomator.org</url>
<url>https://cryptomator.org</url>
</organization>
<developers>
@@ -23,47 +23,34 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- dependency versions -->
<cryptomator.cryptolib.version>1.2.1</cryptomator.cryptolib.version>
<cryptomator.cryptofs.version>1.8.7</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.0.0</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.0</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.11</cryptomator.dokany.version>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>1.9.3</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.2</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.2</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.12</cryptomator.dokany.version>
<cryptomator.webdav.version>1.0.10</cryptomator.webdav.version>
<javafx.version>12</javafx.version>
<commons-io.version>2.6</commons-io.version>
<commons-lang3.version>3.8.1</commons-lang3.version>
<!-- 3rd party dependencies -->
<javafx.version>14-ea+8</javafx.version>
<commons-lang3.version>3.9</commons-lang3.version>
<jwt.version>3.8.3</jwt.version>
<easybind.version>1.0.3</easybind.version>
<guava.version>27.1-jre</guava.version>
<dagger.version>2.22.1</dagger.version>
<gson.version>2.8.5</gson.version>
<slf4j.version>1.7.26</slf4j.version>
<guava.version>28.1-jre</guava.version>
<dagger.version>2.26</dagger.version>
<gson.version>2.8.6</gson.version>
<slf4j.version>1.7.29</slf4j.version>
<logback.version>1.2.3</logback.version>
<junit.jupiter.version>5.4.2</junit.jupiter.version>
<mockito.version>2.27.0</mockito.version>
<hamcrest.version>2.1</hamcrest.version>
<!-- test dependencies -->
<junit.jupiter.version>5.6.0</junit.jupiter.version>
<mockito.version>3.2.4</mockito.version>
<hamcrest.version>2.2</hamcrest.version>
</properties>
<repositories>
<repository>
<id>ossrh-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>jcenter</id>
<url>http://jcenter.bintray.com</url>
<url>https://jcenter.bintray.com</url>
</repository>
</repositories>
@@ -92,11 +79,6 @@
</dependency>
<!-- Cryptomator Libs -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
<version>${cryptomator.cryptolib.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
@@ -129,6 +111,11 @@
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
@@ -163,16 +150,18 @@
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- EasyBind -->
<dependency>
@@ -278,7 +267,7 @@
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.0</version>
<version>3.1.1</version>
<executions>
<execution>
<id>copy-libs</id>
@@ -307,7 +296,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<version>0.8.5</version>
<executions>
<execution>
<id>prepare-agent</id>
@@ -334,7 +323,7 @@
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<version>3.8.1</version>
<configuration>
<release>11</release>
<annotationProcessorPaths>
@@ -349,7 +338,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
+4 -31
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.15</version>
<version>1.5.0-beta3</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -18,33 +18,10 @@
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>fuse-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>dokany-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>webdav-nio-adapter</artifactId>
</dependency>
<!-- CryptoLib -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
</dependency>
<!-- JavaFx -->
<dependency>
@@ -60,7 +37,7 @@
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
</dependency>
</dependency>
<!-- Google -->
<dependency>
@@ -72,11 +49,7 @@
<artifactId>gson</artifactId>
</dependency>
<!-- apache commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
@@ -92,7 +65,7 @@
<dependency>
<groupId>com.nulab-inc</groupId>
<artifactId>zxcvbn</artifactId>
<version>1.2.2</version>
<version>1.3.0</version>
</dependency>
<!-- Logging -->
@@ -1,216 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - implementation of github issue #56
*******************************************************************************/
package org.cryptomator.ui;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.jni.JniException;
import org.cryptomator.jni.MacApplicationUiState;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.l10n.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.swing.SwingUtilities;
import java.awt.AWTException;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@FxApplicationScoped
public class ExitUtil {
private static final Logger LOG = LoggerFactory.getLogger(ExitUtil.class);
private final Stage mainWindow;
private final Localization localization;
private final Settings settings;
private final Optional<MacFunctions> macFunctions;
private TrayIcon trayIcon;
@Inject
public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, Optional<MacFunctions> macFunctions) {
this.mainWindow = mainWindow;
this.localization = localization;
this.settings = settings;
this.macFunctions = macFunctions;
}
public void initExitHandler(Runnable exitCommand) {
if (SystemUtils.IS_OS_LINUX) {
initMinimizeExitHandler(exitCommand);
} else {
initTrayIconExitHandler(exitCommand);
}
}
private void initMinimizeExitHandler(Runnable exitCommand) {
mainWindow.setOnCloseRequest(e -> {
if (Platform.isImplicitExit()) {
exitCommand.run();
} else {
mainWindow.setIconified(true);
e.consume();
}
});
}
private void initTrayIconExitHandler(Runnable exitCommand) {
trayIcon = createTrayIcon(exitCommand);
try {
// double clicking tray icon should open Cryptomator
if (SystemUtils.IS_OS_WINDOWS) {
trayIcon.addMouseListener(new TrayIconMouseListener());
}
SystemTray.getSystemTray().add(trayIcon);
mainWindow.setOnCloseRequest((e) -> {
if (Platform.isImplicitExit()) {
exitCommand.run();
} else {
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication));
mainWindow.close();
this.showTrayNotification(trayIcon);
}
});
} catch (SecurityException | AWTException ex) {
// not working? then just go ahead and close the app
mainWindow.setOnCloseRequest((ev) -> {
exitCommand.run();
});
}
}
private TrayIcon createTrayIcon(Runnable exitCommand) {
final PopupMenu popup = new PopupMenu();
final MenuItem showItem = new MenuItem(localization.getString("tray.menu.open"));
showItem.addActionListener(this::restoreFromTray);
popup.add(showItem);
final MenuItem exitItem = new MenuItem(localization.getString("tray.menu.quit"));
exitItem.addActionListener(e -> exitCommand.run());
popup.add(exitItem);
final Image image = getAppropriateTrayIconImage(true);
return new TrayIcon(image, localization.getString("app.name"), popup);
}
/**
* @return true if <code>defaults read -g AppleInterfaceStyle</code> has an exit status of <code>0</code> (i.e. _not_ returning "key not found").
*/
private boolean isMacMenuBarDarkMode() {
try {
// check for exit status only. Once there are more modes than "dark" and "default", we might need to analyze string contents..
final Process proc = Runtime.getRuntime().exec(new String[] {"defaults", "read", "-g", "AppleInterfaceStyle"});
proc.waitFor(100, TimeUnit.MILLISECONDS);
return proc.exitValue() == 0;
} catch (IOException | InterruptedException | IllegalThreadStateException ex) {
// IllegalThreadStateException thrown by proc.exitValue(), if process didn't terminate
LOG.warn("Determining MAC OS X dark mode settings failed. Assuming default (light) mode.");
return false;
}
}
private void showTrayNotification(TrayIcon trayIcon) {
int remainingTrayNotification = settings.numTrayNotifications().get();
if (remainingTrayNotification <= 0) {
return;
} else {
settings.numTrayNotifications().set(remainingTrayNotification - 1);
}
final Runnable notificationCmd;
if (SystemUtils.IS_OS_MAC_OSX) {
final String title = localization.getString("tray.infoMsg.title");
final String msg = localization.getString("tray.infoMsg.msg.osx");
final String notificationCenterAppleScript = String.format("display notification \"%s\" with title \"%s\"", msg, title);
notificationCmd = () -> {
try {
final ScriptEngineManager mgr = new ScriptEngineManager();
final ScriptEngine engine = mgr.getEngineByName("AppleScriptEngine");
if (engine != null) {
engine.eval(notificationCenterAppleScript);
} else {
Runtime.getRuntime().exec(new String[] {"/usr/bin/osascript", "-e", notificationCenterAppleScript});
}
} catch (ScriptException | IOException e) {
// ignore, user will notice the tray icon anyway.
}
};
} else {
final String title = localization.getString("tray.infoMsg.title");
final String msg = localization.getString("tray.infoMsg.msg");
notificationCmd = () -> {
trayIcon.displayMessage(title, msg, MessageType.INFO);
};
}
SwingUtilities.invokeLater(() -> {
notificationCmd.run();
});
}
private class TrayIconMouseListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
restoreFromTray(new ActionEvent(e.getSource(), e.getID(), e.paramString()));
}
}
}
private void restoreFromTray(ActionEvent event) {
Platform.runLater(() -> {
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication));
mainWindow.show();
mainWindow.requestFocus();
});
}
public void updateTrayIcon(boolean areAllVaultsLocked) {
if (trayIcon != null) {
Image image = getAppropriateTrayIconImage(areAllVaultsLocked);
trayIcon.setImage(image);
}
}
private Image getAppropriateTrayIconImage(boolean areAllVaultsLocked) {
String resourceName;
if (SystemUtils.IS_OS_MAC_OSX && isMacMenuBarDarkMode()) {
resourceName = areAllVaultsLocked ? "/tray_icon_mac_white.png" : "/tray_icon_unlocked_mac_white.png";
} else if (SystemUtils.IS_OS_MAC_OSX) {
resourceName = areAllVaultsLocked ? "/tray_icon_mac_black.png" : "/tray_icon_unlocked_mac_black.png";
} else {
resourceName = areAllVaultsLocked ? "/tray_icon.png" : "/tray_icon_unlocked.png";
}
return Toolkit.getDefaultToolkit().getImage(getClass().getResource(resourceName));
}
}
@@ -1,82 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui;
import dagger.Module;
import dagger.Provides;
import javafx.beans.binding.Binding;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.keychain.KeychainModule;
import org.cryptomator.ui.controllers.ViewControllerModule;
import org.cryptomator.ui.model.VaultComponent;
import org.fxmisc.easybind.EasyBind;
import javax.inject.Named;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@Module(includes = {ViewControllerModule.class, KeychainModule.class}, subcomponents = {VaultComponent.class})
public class UiModule {
private static final int NUM_SCHEDULER_THREADS = 4;
@Provides
@FxApplicationScoped
ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
Thread t = new Thread(r);
t.setName("Scheduler Thread " + threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
});
shutdownTaskScheduler.accept(executorService::shutdown);
return executorService;
}
@Provides
@FxApplicationScoped
ExecutorService provideExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ExecutorService executorService = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setName("Background Thread " + threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
});
shutdownTaskScheduler.accept(executorService::shutdown);
return executorService;
}
@Provides
@FxApplicationScoped
Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
return EasyBind.map(settings.port(), (Number port) -> {
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
return InetSocketAddress.createUnresolved(host, port.intValue());
});
}
@Provides
@FxApplicationScoped
WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
WebDavServer server = WebDavServer.create();
// no need to unsubscribe eventually, because server is a singleton
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
return server;
}
}
@@ -0,0 +1,51 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import java.nio.file.Path;
@AddVaultWizardScoped
public class AddVaultFailureExistingController implements FxController {
private final Stage window;
private final Lazy<Scene> previousScene;
private final StringBinding vaultName;
@Inject
AddVaultFailureExistingController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> previousScene, ObjectProperty<Path> pathOfFailedVault){
this.window = window;
this.previousScene = previousScene;
this.vaultName = Bindings.createStringBinding(() -> pathOfFailedVault.get().getFileName().toString(),pathOfFailedVault);
}
@FXML
public void close(){
window.close();
}
@FXML
public void back(){
window.setScene(previousScene.get());
}
// Getter & Setter
public StringBinding vaultNameProperty(){
return vaultName;
}
public String getVaultName(){
return vaultName.get();
}
}
@@ -0,0 +1,204 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController;
import javax.inject.Named;
import javax.inject.Provider;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
@Module
public abstract class AddVaultModule {
@Provides
@AddVaultWizardScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@AddVaultWizardScoped
static ObjectProperty<Path> provideVaultPath() {
return new SimpleObjectProperty<>();
}
@Provides
@Named("vaultName")
@AddVaultWizardScoped
static StringProperty provideVaultName() {
return new SimpleStringProperty("");
}
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static ObjectProperty<Vault> provideVault() {
return new SimpleObjectProperty<>();
}
@Provides
@Named("recoveryKey")
@AddVaultWizardScoped
static StringProperty provideRecoveryKey() {
return new SimpleStringProperty();
}
// ------------------
@Provides
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
@AddVaultWizardScoped
static Scene provideWelcomeScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_WELCOME.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_EXISTING)
@AddVaultWizardScoped
static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_EXISTING.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_EXISTING_ERROR)
@AddVaultWizardScoped
static Scene provideChooseExistingVaultErrorScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_EXISTING_ERROR.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_NAME)
@AddVaultWizardScoped
static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_NAME.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION)
@AddVaultWizardScoped
static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_LOCATION.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD)
@AddVaultWizardScoped
static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_PASSWORD.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY)
@AddVaultWizardScoped
static Scene provideCreateNewVaultRecoveryKeyScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY.getRessourcePathString());
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_SUCCESS)
@AddVaultWizardScoped
static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_SUCCESS.getRessourcePathString());
}
// ------------------
@Binds
@IntoMap
@FxControllerKey(AddVaultWelcomeController.class)
abstract FxController bindWelcomeController(AddVaultWelcomeController controller);
@Binds
@IntoMap
@FxControllerKey(ChooseExistingVaultController.class)
abstract FxController bindChooseExistingVaultController(ChooseExistingVaultController controller);
@Binds
@IntoMap
@FxControllerKey(AddVaultFailureExistingController.class)
abstract FxController bindAddVaultFailureExistingController(AddVaultFailureExistingController controller);
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultNameController.class)
abstract FxController bindCreateNewVaultNameController(CreateNewVaultNameController controller);
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultLocationController.class)
abstract FxController bindCreateNewVaultLocationController(CreateNewVaultLocationController controller);
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultPasswordController.class)
abstract FxController bindCreateNewVaultPasswordController(CreateNewVaultPasswordController controller);
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultRecoveryKeyController.class)
abstract FxController bindCreateNewVaultRecoveryKeyController(CreateNewVaultRecoveryKeyController controller);
@Provides
@IntoMap
@FxControllerKey(RecoveryKeyDisplayController.class)
static FxController provideRecoveryKeyDisplayController(@AddVaultWizardWindow Stage window, @Named("vaultName") StringProperty vaultName, @Named("recoveryKey") StringProperty recoveryKey, ResourceBundle localization) {
return new RecoveryKeyDisplayController(window, vaultName.get(), recoveryKey.get(), localization);
}
@Binds
@IntoMap
@FxControllerKey(AddVaultSuccessController.class)
abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller);
}
@@ -0,0 +1,47 @@
package org.cryptomator.ui.addvaultwizard;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import javax.inject.Inject;
@AddVaultWizardScoped
public class AddVaultSuccessController implements FxController {
private final FxApplication fxApplication;
private final Stage window;
private final ReadOnlyObjectProperty<Vault> vault;
@Inject
AddVaultSuccessController(FxApplication fxApplication, @AddVaultWizardWindow Stage window, @AddVaultWizardWindow ObjectProperty<Vault> vault) {
this.fxApplication = fxApplication;
this.window = window;
this.vault = vault;
}
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault.get());
}
@FXML
public void close() {
window.close();
}
/* Observables */
public ReadOnlyObjectProperty<Vault> vaultProperty() {
return vault;
}
public Vault getVault() {
return vault.get();
}
}
@@ -0,0 +1,38 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
@AddVaultWizardScoped
public class AddVaultWelcomeController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(AddVaultWelcomeController.class);
private final Stage window;
private final Lazy<Scene> chooseExistingVaultScene;
private final Lazy<Scene> createNewVaultScene;
@Inject
AddVaultWelcomeController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> chooseExistingVaultScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> createNewVaultScene) {
this.window = window;
this.chooseExistingVaultScene = chooseExistingVaultScene;
this.createNewVaultScene = createNewVaultScene;
}
public void createNewVault() {
LOG.debug("AddVaultWelcomeController.createNewVault()");
window.setScene(createNewVaultScene.get());
}
public void chooseExistingVault() {
LOG.debug("AddVaultWelcomeController.chooseExistingVault()");
window.setScene(chooseExistingVaultScene.get());
}
}
@@ -0,0 +1,38 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import dagger.Subcomponent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
@AddVaultWizardScoped
@Subcomponent(modules = {AddVaultModule.class})
public interface AddVaultWizardComponent {
@AddVaultWizardWindow
Stage window();
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
Lazy<Scene> scene();
default void showAddVaultWizard() {
Stage stage = window();
stage.setScene(scene().get());
stage.sizeToScene();
stage.show();
}
@Subcomponent.Builder
interface Builder {
AddVaultWizardComponent build();
}
}
@@ -0,0 +1,13 @@
package org.cryptomator.ui.addvaultwizard;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface AddVaultWizardScoped {
}
@@ -0,0 +1,14 @@
package org.cryptomator.ui.addvaultwizard;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface AddVaultWizardWindow {
}
@@ -0,0 +1,73 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.File;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ResourceBundle;
@AddVaultWizardScoped
public class ChooseExistingVaultController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(ChooseExistingVaultController.class);
private final Stage window;
private final Lazy<Scene> welcomeScene;
private final Lazy<Scene> successScene;
private final Lazy<Scene> errorScene;
private final ObjectProperty<Path> vaultPath;
private final ObjectProperty<Vault> vault;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
@Inject
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.ADDVAULT_EXISTING_ERROR) Lazy<Scene> errorScene, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
this.window = window;
this.welcomeScene = welcomeScene;
this.successScene = successScene;
this.errorScene = errorScene;
this.vaultPath = vaultPath;
this.vault = vault;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
}
@FXML
public void back() {
window.setScene(welcomeScene.get());
}
@FXML
public void chooseFileAndNext() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
vaultPath.setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
try {
Vault newVault = vaultListManager.add(vaultPath.get());
vault.set(newVault);
window.setScene(successScene.get());
} catch (NoSuchFileException e) {
LOG.error("Failed to open existing vault.", e);
window.setScene(errorScene.get());
}
}
}
}
@@ -0,0 +1,189 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ResourceBundle;
@AddVaultWizardScoped
public class CreateNewVaultLocationController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class);
private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home"));
private final Stage window;
private final Lazy<Scene> chooseNameScene;
private final Lazy<Scene> choosePasswordScene;
private final LocationPresets locationPresets;
private final ObjectProperty<Path> vaultPath;
private final StringProperty vaultName;
private final ResourceBundle resourceBundle;
private final BooleanBinding validVaultPath;
private final BooleanBinding invalidVaultPath;
private final BooleanProperty usePresetPath;
private final StringProperty warningText;
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
public ToggleGroup predefinedLocationToggler;
public RadioButton iclouddriveRadioButton;
public RadioButton dropboxRadioButton;
public RadioButton gdriveRadioButton;
public RadioButton onedriveRadioButton;
public RadioButton customRadioButton;
@Inject
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
this.window = window;
this.chooseNameScene = chooseNameScene;
this.choosePasswordScene = choosePasswordScene;
this.locationPresets = locationPresets;
this.vaultPath = vaultPath;
this.vaultName = vaultName;
this.resourceBundle = resourceBundle;
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
this.invalidVaultPath = validVaultPath.not();
this.usePresetPath = new SimpleBooleanProperty();
this.warningText = new SimpleStringProperty();
}
private boolean isValidVaultPath() {
return vaultPath.get() != null && Files.notExists(vaultPath.get());
}
@FXML
public void initialize() {
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
vaultPath.addListener(this::vaultPathDidChange);
}
private void vaultPathDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> observable, @SuppressWarnings("unused") Path oldValue, Path newValue) {
if (!Files.notExists(newValue)) {
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} else {
warningText.set(null);
}
}
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
if (iclouddriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get()));
} else if (dropboxRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get()));
} else if (gdriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
} else if (onedriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
} else if (customRadioButton.equals(newValue)) {
vaultPath.set(customVaultPath.resolve(vaultName.get()));
}
}
@FXML
public void back() {
window.setScene(chooseNameScene.get());
}
@FXML
public void next() {
try {
// check if we have write access AND the vaultPath doesn't already exist:
assert Files.isDirectory(vaultPath.get().getParent());
Path createdDir = Files.createDirectory(vaultPath.get());
Files.delete(createdDir); // assert: dir exists and is empty
window.setScene(choosePasswordScene.get());
} catch (FileAlreadyExistsException e) {
LOG.warn("Can not use already existing vault path {}", vaultPath.get());
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} catch (IOException e) {
LOG.warn("Can not create vault at path: {}", vaultPath.get());
LOG.warn("Thrown Exception:", e);
warningText.set(resourceBundle.getString("addvaultwizard.new.ioException"));
}
}
@FXML
public void chooseCustomVaultPath() {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle(resourceBundle.getString("addvaultwizard.new.directoryPickerTitle"));
directoryChooser.setInitialDirectory(customVaultPath.toFile());
final File file = directoryChooser.showDialog(window);
if (file != null) {
customVaultPath = file.toPath().toAbsolutePath();
vaultPath.set(customVaultPath.resolve(vaultName.get()));
}
}
/* Getter/Setter */
public Path getVaultPath() {
return vaultPath.get();
}
public ObjectProperty<Path> vaultPathProperty() {
return vaultPath;
}
public BooleanBinding invalidVaultPathProperty() {
return invalidVaultPath;
}
public Boolean getInvalidVaultPath() {
return invalidVaultPath.get();
}
public LocationPresets getLocationPresets() {
return locationPresets;
}
public BooleanProperty usePresetPathProperty() {
return usePresetPath;
}
public boolean getUsePresetPath() {
return usePresetPath.get();
}
public StringProperty warningTextProperty() {
return warningText;
}
public String getWarningText() {
return warningText.get();
}
public BooleanBinding showWarningProperty() {
return warningText.isNotEmpty();
}
public boolean isShowWarning() {
return showWarningProperty().get();
}
}
@@ -0,0 +1,106 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.regex.Pattern;
@AddVaultWizardScoped
public class CreateNewVaultNameController implements FxController {
private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[\\w -]+", Pattern.UNICODE_CHARACTER_CLASS);
public TextField textField;
private final Stage window;
private final Lazy<Scene> welcomeScene;
private final Lazy<Scene> chooseLocationScene;
private final ObjectProperty<Path> vaultPath;
private final StringProperty vaultName;
private final BooleanBinding validVaultName;
private final BooleanBinding invalidVaultName;
private final StringBinding warningText;
@Inject
CreateNewVaultNameController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
this.window = window;
this.welcomeScene = welcomeScene;
this.chooseLocationScene = chooseLocationScene;
this.vaultPath = vaultPath;
this.vaultName = vaultName;
this.validVaultName = Bindings.createBooleanBinding(this::isValidVaultName, vaultName);
this.invalidVaultName = validVaultName.not();
this.warningText = Bindings.when(vaultName.isNotEmpty().and(invalidVaultName)).then(resourceBundle.getString("addvaultwizard.new.invalidName")).otherwise((String) null);
}
@FXML
public void initialize() {
vaultName.bind(textField.textProperty());
vaultName.addListener(this::vaultNameChanged);
}
public boolean isValidVaultName() {
return vaultName.get() != null && VALID_NAME_PATTERN.matcher(vaultName.get().trim()).matches();
}
private void vaultNameChanged(@SuppressWarnings("unused") Observable observable) {
if (isValidVaultName()) {
if (vaultPath.get() != null) {
// update vaultPath if it is already set but the user went back to change its name:
vaultPath.set(vaultPath.get().resolveSibling(vaultName.get()));
}
}
}
@FXML
public void back() {
window.setScene(welcomeScene.get());
}
@FXML
public void next() {
window.setScene(chooseLocationScene.get());
}
/* Getter/Setter */
public BooleanBinding invalidVaultNameProperty() {
return invalidVaultName;
}
public boolean isInvalidVaultName() {
return invalidVaultName.get();
}
public StringBinding warningTextProperty() {
return warningText;
}
public String getWarningText() {
return warningText.get();
}
public BooleanBinding showWarningProperty() {
return warningText.isNotEmpty();
}
public boolean isShowWarning() {
return showWarningProperty().get();
}
}
@@ -0,0 +1,218 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.channels.WritableByteChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import static java.nio.charset.StandardCharsets.US_ASCII;
@AddVaultWizardScoped
public class CreateNewVaultPasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private final Stage window;
private final Lazy<Scene> chooseLocationScene;
private final Lazy<Scene> recoveryKeyScene;
private final Lazy<Scene> successScene;
private final ExecutorService executor;
private final RecoveryKeyFactory recoveryKeyFactory;
private final StringProperty vaultNameProperty;
private final ObjectProperty<Path> vaultPathProperty;
private final ObjectProperty<Vault> vaultProperty;
private final StringProperty recoveryKeyProperty;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
private final ObjectProperty<CharSequence> password;
private final ReadmeGenerator readmeGenerator;
private final BooleanProperty processing;
private final BooleanProperty readyToCreateVault;
private final ObjectBinding<ContentDisplay> createVaultButtonState;
public ToggleGroup recoveryKeyChoice;
public Toggle showRecoveryKey;
public Toggle skipRecoveryKey;
@Inject
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty<CharSequence> password, ReadmeGenerator readmeGenerator) {
this.window = window;
this.chooseLocationScene = chooseLocationScene;
this.recoveryKeyScene = recoveryKeyScene;
this.successScene = successScene;
this.executor = executor;
this.recoveryKeyFactory = recoveryKeyFactory;
this.vaultNameProperty = vaultName;
this.vaultPathProperty = vaultPath;
this.vaultProperty = vault;
this.recoveryKeyProperty = recoveryKey;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
this.password = password;
this.readmeGenerator = readmeGenerator;
this.processing = new SimpleBooleanProperty();
this.readyToCreateVault = new SimpleBooleanProperty();
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
}
@FXML
public void initialize() {
BooleanBinding isValidNewPassword = Bindings.createBooleanBinding(() -> password.get() != null && password.get().length() > 0, password);
readyToCreateVault.bind(isValidNewPassword.and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
}
@FXML
public void back() {
window.setScene(chooseLocationScene.get());
}
@FXML
public void next() {
Path pathToVault = vaultPathProperty.get();
try {
Files.createDirectory(pathToVault);
} catch (FileAlreadyExistsException e) {
LOG.error("Vault dir already exists.", e);
window.setScene(chooseLocationScene.get());
} catch (IOException e) {
// TODO show generic error screen
LOG.error("", e);
}
if (showRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) {
showRecoveryKeyScene();
} else if (skipRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) {
showSuccessScene();
} else {
throw new IllegalStateException("Unexpected toggle state");
}
}
private void showRecoveryKeyScene() {
Path pathToVault = vaultPathProperty.get();
processing.set(true);
Tasks.create(() -> {
initializeVault(pathToVault, password.get());
return recoveryKeyFactory.createRecoveryKey(pathToVault, password.get());
}).onSuccess(recoveryKey -> {
initializationSucceeded(pathToVault);
recoveryKeyProperty.set(recoveryKey);
window.setScene(recoveryKeyScene.get());
}).onError(IOException.class, e -> {
// TODO show generic error screen
LOG.error("", e);
}).andFinally(() -> {
processing.set(false);
}).runOnce(executor);
}
private void showSuccessScene() {
Path pathToVault = vaultPathProperty.get();
processing.set(true);
Tasks.create(() -> {
initializeVault(pathToVault, password.get());
}).onSuccess(() -> {
initializationSucceeded(pathToVault);
window.setScene(successScene.get());
}).onError(IOException.class, e -> {
// TODO show generic error screen
LOG.error("", e);
}).andFinally(() -> {
processing.set(false);
}).runOnce(executor);
}
private void initializeVault(Path path, CharSequence passphrase) throws IOException {
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withPassphrase(passphrase) //
.withFlags(Collections.emptySet()) //
.withMasterkeyFilename(MASTERKEY_FILENAME) //
.build();
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
}
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
}
LOG.info("Created vault at {}", path);
}
private void initializationSucceeded(Path pathToVault) {
try {
Vault newVault = vaultListManager.add(pathToVault);
vaultProperty.set(newVault);
} catch (NoSuchFileException e) {
throw new UncheckedIOException(e);
}
}
/* Getter/Setter */
public String getVaultName() {
return vaultNameProperty.get();
}
public StringProperty vaultNameProperty() {
return vaultNameProperty;
}
public BooleanProperty readyToCreateVaultProperty() {
return readyToCreateVault;
}
public boolean isReadyToCreateVault() {
return readyToCreateVault.get();
}
public ObjectBinding<ContentDisplay> createVaultButtonStateProperty() {
return createVaultButtonState;
}
public ContentDisplay getCreateVaultButtonState() {
return processing.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
}
}
@@ -0,0 +1,28 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
public class CreateNewVaultRecoveryKeyController implements FxController {
private final Stage window;
private final Lazy<Scene> successScene;
@Inject
CreateNewVaultRecoveryKeyController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene) {
this.window = window;
this.successScene = successScene;
}
@FXML
public void next() {
window.setScene(successScene.get());
}
}
@@ -0,0 +1,126 @@
package org.cryptomator.ui.addvaultwizard;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javax.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@AddVaultWizardScoped
public class LocationPresets {
private static final String USER_HOME = System.getProperty("user.home");
private static final String[] ICLOUDDRIVE_LOCATIONS = {"~/Library/Mobile Documents/iCloud~com~setolabs~Cryptomator/Documents"};
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
private final ReadOnlyObjectProperty<Path> dropboxLocation;
private final ReadOnlyObjectProperty<Path> gdriveLocation;
private final ReadOnlyObjectProperty<Path> onedriveLocation;
private final BooleanBinding foundIclouddrive;
private final BooleanBinding foundDropbox;
private final BooleanBinding foundGdrive;
private final BooleanBinding foundOnedrive;
@Inject
public LocationPresets() {
this.iclouddriveLocation = new SimpleObjectProperty<>(existingWritablePath(ICLOUDDRIVE_LOCATIONS));
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
this.foundIclouddrive = iclouddriveLocation.isNotNull();
this.foundDropbox = dropboxLocation.isNotNull();
this.foundGdrive = gdriveLocation.isNotNull();
this.foundOnedrive = onedriveLocation.isNotNull();
}
private static Path existingWritablePath(String... candidates) {
for (String candidate : candidates) {
Path path = Paths.get(resolveHomePath(candidate));
if (Files.isDirectory(path)) {
return path;
}
}
return null;
}
private static String resolveHomePath(String path) {
if (path.startsWith("~/")) {
return USER_HOME + path.substring(1);
} else {
return path;
}
}
/* Observables */
public ReadOnlyObjectProperty<Path> iclouddriveLocationProperty() {
return iclouddriveLocation;
}
public Path getIclouddriveLocation() {
return iclouddriveLocation.get();
}
public BooleanBinding foundIclouddriveProperty() {
return foundIclouddrive;
}
public boolean isFoundIclouddrive() {
return foundIclouddrive.get();
}
public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
return dropboxLocation;
}
public Path getDropboxLocation() {
return dropboxLocation.get();
}
public BooleanBinding foundDropboxProperty() {
return foundDropbox;
}
public boolean isFoundDropbox() {
return foundDropbox.get();
}
public ReadOnlyObjectProperty<Path> gdriveLocationProperty() {
return gdriveLocation;
}
public Path getGdriveLocation() {
return gdriveLocation.get();
}
public BooleanBinding foundGdriveProperty() {
return foundGdrive;
}
public boolean isFoundGdrive() {
return foundGdrive.get();
}
public ReadOnlyObjectProperty<Path> onedriveLocationProperty() {
return onedriveLocation;
}
public Path getOnedriveLocation() {
return onedriveLocation.get();
}
public BooleanBinding foundOnedriveProperty() {
return foundOnedrive;
}
public boolean isFoundOnedrive() {
return foundOnedrive.get();
}
}
@@ -0,0 +1,70 @@
package org.cryptomator.ui.addvaultwizard;
import javax.inject.Inject;
import java.util.List;
import java.util.ResourceBundle;
@AddVaultWizardScoped
public class ReadmeGenerator {
// specs: https://web.archive.org/web/20190708132914/http://www.kleinlercher.at/tools/Windows_Protocols/Word2007RTFSpec9.pdf
private static final String RTF_HEADER = "{\\rtf1\\fbidis\\ansi\\uc0\\fs32\n";
private static final String RTF_FOOTER = "}";
private static final String HELP_URL = "{\\field{\\*\\fldinst HYPERLINK \"http://docs.cryptoamtor.org/\"}{\\fldrslt docs.cryptoamtor.org}}";
private final ResourceBundle resourceBundle;
@Inject
public ReadmeGenerator(ResourceBundle resourceBundle){
this.resourceBundle = resourceBundle;
}
public String createVaultStorageLocationReadmeRtf() {
return createDocument(List.of( //
resourceBundle.getString("addvault.new.readme.storageLocation.1"), //
resourceBundle.getString("addvault.new.readme.storageLocation.2"), //
resourceBundle.getString("addvault.new.readme.storageLocation.3"), //
String.format(resourceBundle.getString("addvault.new.readme.storageLocation.4"), HELP_URL) //
));
}
public String createVaultAccessLocationReadmeRtf() {
return createDocument(List.of( //
resourceBundle.getString("addvault.new.readme.accessLocation.1"), //
resourceBundle.getString("addvault.new.readme.accessLocation.2"), //
resourceBundle.getString("addvault.new.readme.accessLocation.3") //
));
}
// visible for testing
String createDocument(Iterable<String> paragraphs) {
StringBuilder sb = new StringBuilder(RTF_HEADER);
for (String p : paragraphs) {
sb.append("\\par {\\sa80 ");
appendEscaped(sb, p);
sb.append("}\n");
}
sb.append(RTF_FOOTER);
return sb.toString();
}
// visible for testing
String escapeNonAsciiChars(CharSequence input) {
StringBuilder sb = new StringBuilder();
appendEscaped(sb, input);
return sb.toString();
}
private void appendEscaped(StringBuilder sb, CharSequence input) {
input.chars().forEachOrdered(c -> {
if (c < 128) {
sb.append((char) c);
} else if (c < 0xFFFF) {
sb.append("\\u").append(c);
}
});
}
}
@@ -0,0 +1,42 @@
package org.cryptomator.ui.changepassword;
import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named;
@ChangePasswordScoped
@Subcomponent(modules = {ChangePasswordModule.class})
public interface ChangePasswordComponent {
@ChangePasswordWindow
Stage window();
@FxmlScene(FxmlFile.CHANGEPASSWORD)
Lazy<Scene> scene();
default void showChangePasswordWindow() {
Stage stage = window();
stage.setScene(scene().get());
stage.show();
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@ChangePasswordWindow Vault vault);
@BindsInstance
Builder owner(@Named("changePasswordOwner") Stage owner);
ChangePasswordComponent build();
}
}
@@ -0,0 +1,85 @@
package org.cryptomator.ui.changepassword;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.ResourceBundle;
@ChangePasswordScoped
public class ChangePasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private final Stage window;
private final Vault vault;
private final ObjectProperty<CharSequence> newPassword;
public NiceSecurePasswordField oldPasswordField;
public CheckBox finalConfirmationCheckbox;
public Button finishButton;
@Inject
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword) {
this.window = window;
this.vault = vault;
this.newPassword = newPassword;
}
@FXML
public void initialize() {
BooleanBinding hasNotConfirmedCheckbox = finalConfirmationCheckbox.selectedProperty().not();
BooleanBinding isInvalidNewPassword = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
finishButton.disableProperty().bind(hasNotConfirmedCheckbox.or(isInvalidNewPassword));
}
@FXML
public void cancel() {
window.close();
}
@FXML
public void finish() {
try {
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
LOG.info("Successful changed password for {}", vault.getDisplayableName());
window.close();
} catch (IOException e) {
// TODO show generic error screen
LOG.error("IO error occured during password change. Unable to perform operation.", e);
e.printStackTrace();
} catch (InvalidPassphraseException e) {
// TODO shake
LOG.info("Wrong old password.");
}
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
}
@@ -0,0 +1,81 @@
package org.cryptomator.ui.changepassword;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import javax.inject.Named;
import javax.inject.Provider;
import java.nio.CharBuffer;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
@Module
abstract class ChangePasswordModule {
@Provides
@ChangePasswordScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("changepassword.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@FxmlScene(FxmlFile.CHANGEPASSWORD)
@ChangePasswordScoped
static Scene provideUnlockScene(@ChangePasswordWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/changepassword.fxml");
}
// ------------------
@Binds
@IntoMap
@FxControllerKey(ChangePasswordController.class)
abstract FxController bindUnlockController(ChangePasswordController controller);
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
}
@@ -0,0 +1,13 @@
package org.cryptomator.ui.changepassword;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface ChangePasswordScoped {
}
@@ -0,0 +1,14 @@
package org.cryptomator.ui.changepassword;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface ChangePasswordWindow {
}
@@ -0,0 +1,36 @@
package org.cryptomator.ui.common;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.value.WritableValue;
import javafx.stage.Window;
import javafx.util.Duration;
public class Animations {
public static Timeline createShakeWindowAnimation(Window window) {
WritableValue<Double> writableWindowX = new WritableValue<>() {
@Override
public Double getValue() {
return window.getX();
}
@Override
public void setValue(Double value) {
window.setX(value);
}
};
return new Timeline( //
new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), //
new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), //
new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), //
new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), //
new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), //
new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), //
new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), //
new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) //
);
}
}
@@ -0,0 +1,59 @@
package org.cryptomator.ui.common;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import java.util.function.Function;
@FxApplicationScoped
public class DefaultSceneFactory implements Function<Parent, Scene> {
protected static final KeyCodeCombination ALT_F4 = new KeyCodeCombination(KeyCode.F4, KeyCombination.ALT_DOWN);
protected static final KeyCodeCombination SHORTCUT_W = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
protected final Settings settings;
@Inject
public DefaultSceneFactory(Settings settings) {
this.settings = settings;
}
@Override
public Scene apply(Parent root) {
Scene scene = new Scene(root);
configureRoot(root);
configureScene(scene);
return scene;
}
protected void configureRoot(Parent root) {
root.nodeOrientationProperty().bind(settings.userInterfaceOrientation());
}
protected void configureScene(Scene scene) {
scene.windowProperty().addListener(observable -> {
Window window = scene.getWindow();
if (window instanceof Stage) {
setupDefaultAccelerators(scene, (Stage) window);
}
});
}
protected void setupDefaultAccelerators(Scene scene, Stage stage) {
if (SystemUtils.IS_OS_WINDOWS) {
scene.getAccelerators().put(ALT_F4, stage::close);
} else {
scene.getAccelerators().put(SHORTCUT_W, stage::close);
}
}
}
@@ -0,0 +1,80 @@
package org.cryptomator.ui.common;
import com.google.common.base.Splitter;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javax.inject.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.function.Function;
public class FXMLLoaderFactory {
private final Map<Class<? extends FxController>, Provider<FxController>> controllerFactories;
private final Function<Parent, Scene> sceneFactory;
private final ResourceBundle resourceBundle;
public FXMLLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> controllerFactories, Function<Parent, Scene> sceneFactory, ResourceBundle resourceBundle) {
this.controllerFactories = controllerFactories;
this.sceneFactory = sceneFactory;
this.resourceBundle = resourceBundle;
}
/**
* @return A new FXMLLoader instance
*/
public FXMLLoader construct() {
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(this::constructController);
loader.setResources(resourceBundle);
return loader;
}
/**
* Loads the FXML given fxml resource in a new FXMLLoader instance.
*
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
* @return The FXMLLoader used to load the file
* @throws IOException if an error occurs while loading the FXML file
*/
public FXMLLoader load(String fxmlResourceName) throws IOException {
FXMLLoader loader = construct();
try (InputStream in = getClass().getResourceAsStream(fxmlResourceName)) {
loader.load(in);
}
return loader;
}
/**
* {@link #load(String) Loads} the FXML file and creates a new Scene containing the loaded ui.
*
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
* @throws UncheckedIOException wrapping any IOException thrown by {@link #load(String)).
*/
public Scene createScene(String fxmlResourceName) {
final FXMLLoader loader;
try {
loader = load(fxmlResourceName);
} catch (IOException e) {
throw new UncheckedIOException("Failed to load " + fxmlResourceName, e);
}
Parent root = loader.getRoot();
List<String> addtionalStyleSheets = Splitter.on(',').omitEmptyStrings().splitToList(resourceBundle.getString("additionalStyleSheets"));
addtionalStyleSheets.forEach(styleSheet -> root.getStylesheets().add("/css/" + styleSheet));
return sceneFactory.apply(root);
}
private FxController constructController(Class<?> aClass) {
if (!controllerFactories.containsKey(aClass)) {
throw new IllegalArgumentException("ViewController not registered: " + aClass);
} else {
return controllerFactories.get(aClass).get();
}
}
}
@@ -0,0 +1,49 @@
package org.cryptomator.ui.common;
import javafx.scene.text.Font;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
public class FontLoader {
private static final Logger LOG = LoggerFactory.getLogger(FontLoader.class);
private static final double DEFAULT_FONT_SIZE = 12;
public static Font load(String resourcePath) throws FontLoaderException {
try (InputStream in = FontLoader.class.getResourceAsStream(resourcePath)) {
if (in == null) {
throw new FontLoaderException(resourcePath);
} else {
return load(resourcePath, in);
}
} catch (IOException e) {
throw new FontLoaderException(resourcePath, e);
}
}
private static Font load(String resourcePath, InputStream in) throws FontLoaderException {
Font font = Font.loadFont(in, DEFAULT_FONT_SIZE);
if (font != null) {
LOG.debug("Loaded family: {}", font.getFamily());
return font;
} else {
throw new FontLoaderException(resourcePath);
}
}
public static class FontLoaderException extends IOException {
private FontLoaderException(String resourceName) {
super("Failed to load font: " + resourceName);
}
private FontLoaderException(String resourceName, Throwable cause) {
super("Failed to load font: " + resourceName, cause);
}
}
}
@@ -0,0 +1,9 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.common;
public interface FxController {
}
@@ -3,21 +3,22 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.controllers;
package org.cryptomator.ui.common;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import dagger.MapKey;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import dagger.MapKey;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
// TODO rename after refactoring
@Documented
@Target(METHOD)
@Retention(RUNTIME)
@MapKey
public @interface ViewControllerKey {
Class<? extends ViewController> value();
public @interface FxControllerKey {
Class<? extends FxController> value();
}
@@ -0,0 +1,43 @@
package org.cryptomator.ui.common;
public enum FxmlFile {
ADDVAULT_WELCOME("/fxml/addvault_welcome.fxml"), //
ADDVAULT_EXISTING("/fxml/addvault_existing.fxml"), //
ADDVAULT_EXISTING_ERROR("/fxml/addvault_existing_error.fxml"),
ADDVAULT_NEW_NAME("/fxml/addvault_new_name.fxml"), //
ADDVAULT_NEW_LOCATION("/fxml/addvault_new_location.fxml"), //
ADDVAULT_NEW_PASSWORD("/fxml/addvault_new_password.fxml"), //
ADDVAULT_NEW_RECOVERYKEY("/fxml/addvault_new_recoverykey.fxml"), //
ADDVAULT_SUCCESS("/fxml/addvault_success.fxml"), //
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //
MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
MIGRATION_GENERIC_ERROR("/fxml/migration_generic_error.fxml"), //
MIGRATION_RUN("/fxml/migration_run.fxml"), //
MIGRATION_START("/fxml/migration_start.fxml"), //
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
PREFERENCES("/fxml/preferences.fxml"), //
QUIT("/fxml/quit.fxml"), //
RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), //
RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), //
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK("/fxml/unlock.fxml"),
UNLOCK_GENERIC_ERROR("/fxml/unlock_generic_error.fxml"), //
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
private final String ressourcePathString;
FxmlFile(String ressourcePathString) {
this.ressourcePathString = ressourcePathString;
}
public String getRessourcePathString(){
return ressourcePathString;
}
}
@@ -0,0 +1,16 @@
package org.cryptomator.ui.common;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface FxmlScene {
FxmlFile value();
}
@@ -0,0 +1,74 @@
package org.cryptomator.ui.common;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.fxmisc.easybind.EasyBind;
import java.util.ResourceBundle;
public class NewPasswordController implements FxController {
private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater;
private final ObjectProperty<CharSequence> password;
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
public NiceSecurePasswordField passwordField;
public NiceSecurePasswordField reenterField;
public Label passwordStrengthLabel;
public Label passwordMatchLabel;
public FontAwesome5IconView checkmark;
public FontAwesome5IconView cross;
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty<CharSequence> password) {
this.resourceBundle = resourceBundle;
this.strengthRater = strengthRater;
this.password = password;
}
@FXML
public void initialize() {
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(checkmark).otherwise(cross));
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("newPassword.passwordsMatch")).otherwise(resourceBundle.getString("newPassword.passwordsDoNotMatch")));
passwordField.textProperty().addListener(this::passwordsDidChange);
reenterField.textProperty().addListener(this::passwordsDidChange);
}
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
password.set(passwordField.getCharacters());
} else {
password.set("");
}
}
private boolean hasSamePasswordInBothFields() {
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
}
/* Getter/Setter */
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
public int getPasswordStrength() {
return passwordStrength.get();
}
}
@@ -0,0 +1,60 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Jean-Noël Charon - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.common;
import com.nulabinc.zxcvbn.Zxcvbn;
import org.cryptomator.common.Environment;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import java.util.List;
import java.util.ResourceBundle;
@FxApplicationScoped
public class PasswordStrengthUtil {
private static final int PW_TRUNC_LEN = 100; // truncate very long passwords, since zxcvbn memory and runtime depends vastly on the length
private static final String RESSOURCE_PREFIX = "passwordStrength.messageLabel.";
private static final List<String> SANITIZED_INPUTS = List.of("cryptomator");
private final ResourceBundle resourceBundle;
private final int minPwLength;
private final Zxcvbn zxcvbn;
@Inject
public PasswordStrengthUtil(ResourceBundle resourceBundle, Environment environment) {
this.resourceBundle = resourceBundle;
this.minPwLength = environment.getMinPwLength();
this.zxcvbn = new Zxcvbn();
}
public boolean fulfillsMinimumRequirements(CharSequence password) {
return password.length() >= minPwLength;
}
public int computeRate(CharSequence password) {
if (password == null || password.length() < minPwLength) {
return -1;
} else {
int numCharsToRate = Math.min(PW_TRUNC_LEN, password.length());
return zxcvbn.measure(password.subSequence(0, numCharsToRate), SANITIZED_INPUTS).getScore();
}
}
public String getStrengthDescription(Number score) {
if (score.intValue() == -1) {
return String.format(resourceBundle.getString(RESSOURCE_PREFIX + "tooShort"), minPwLength);
} else if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) {
return resourceBundle.getString(RESSOURCE_PREFIX + score.intValue());
} else {
return "";
}
}
}
@@ -0,0 +1,28 @@
package org.cryptomator.ui.common;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
public class StackTraceController implements FxController {
private final String stackTrace;
public StackTraceController(Throwable cause) {
this.stackTrace = provideStackTrace(cause);
}
private static String provideStackTrace(Throwable cause) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
cause.printStackTrace(new PrintStream(baos));
return baos.toString(StandardCharsets.UTF_8);
}
/* Getter/Setter */
public String getStackTrace() {
return stackTrace;
}
}
@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.util;
package org.cryptomator.ui.common;
import java.util.ArrayList;
import java.util.List;
@@ -73,21 +73,21 @@ public class Tasks {
return new TaskImpl<>(callable, successHandler, errorHandlers, finallyHandler);
}
public Task<T> runOnce(ExecutorService executorService) {
public Task<T> runOnce(ExecutorService executor) {
Task<T> task = build();
executorService.submit(task);
executor.submit(task);
return task;
}
public Task<T> scheduleOnce(ScheduledExecutorService executorService, long delay, TimeUnit unit) {
public Task<T> scheduleOnce(ScheduledExecutorService scheduler, long delay, TimeUnit unit) {
Task<T> task = build();
executorService.schedule(task, delay, unit);
scheduler.schedule(task, delay, unit);
return task;
}
public ScheduledService<T> schedulePeriodically(ExecutorService executorService, Duration initialDelay, Duration period) {
public ScheduledService<T> schedulePeriodically(ExecutorService executor, Duration initialDelay, Duration period) {
ScheduledService<T> service = new RestartingService<>(this::build);
service.setExecutor(executorService);
service.setExecutor(executor);
service.setDelay(initialDelay);
service.setPeriod(period);
Platform.runLater(service::start);
@@ -0,0 +1,350 @@
package org.cryptomator.ui.common;
import javafx.concurrent.Task;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
@FxApplicationScoped
public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
private final ExecutorService executorService;
private final Optional<KeychainAccess> keychain;
@Inject
public VaultService(ExecutorService executorService, Optional<KeychainAccess> keychain) {
this.executorService = executorService;
this.keychain = keychain;
}
public void reveal(Vault vault) {
executorService.execute(createRevealTask(vault));
}
/**
* Creates but doesn't start a reveal task.
*
* @param vault The vault to reveal
*/
public Task<Vault> createRevealTask(Vault vault) {
Task<Vault> task = new RevealVaultTask(vault);
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Attempts to unlock all given vaults in a background thread using passwords stored in the system keychain.
*
* @param vaults The vaults to unlock
* @implNote No-op if no system keychain is present
*/
public void attemptAutoUnlock(Collection<Vault> vaults) {
if (!keychain.isPresent()) {
LOG.debug("No system keychain found. Unable to auto unlock without saved passwords.");
} else {
for (Vault vault : vaults) {
attemptAutoUnlock(vault, keychain.get());
}
}
}
/**
* Unlocks a vault in a background thread using a stored passphrase
*
* @param vault The vault to unlock
* @param keychainAccess The system keychain holding the passphrase for the vault
*/
public void attemptAutoUnlock(Vault vault, KeychainAccess keychainAccess) {
executorService.execute(createAutoUnlockTask(vault, keychainAccess));
}
/**
* Creates but doesn't start an auto-unlock task.
*
* @param vault The vault to unlock
* @param keychainAccess The system keychain holding the passphrase for the vault
* @return The task
*/
public Task<Vault> createAutoUnlockTask(Vault vault, KeychainAccess keychainAccess) {
Task<Vault> task = new AutoUnlockVaultTask(vault, keychainAccess);
task.setOnSucceeded(evt -> LOG.info("Auto-unlocked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to auto-unlock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Unlocks a vault in a background thread
*
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public void unlock(Vault vault, CharSequence passphrase) {
executorService.execute(createUnlockTask(vault, passphrase));
}
/**
* Creates but doesn't start an unlock task.
*
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @return The task
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public Task<Vault> createUnlockTask(Vault vault, CharSequence passphrase) {
Task<Vault> task = new UnlockVaultTask(vault, passphrase);
task.setOnSucceeded(evt -> LOG.info("Unlocked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to unlock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Locks a vault in a background thread.
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
*/
public void lock(Vault vault, boolean forced) {
executorService.execute(createLockTask(vault, forced));
}
/**
* Creates but doesn't start a lock task.
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
* @return The task
*/
public Task<Vault> createLockTask(Vault vault, boolean forced) {
Task<Vault> task = new LockVaultTask(vault, forced);
task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to lock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Locks all given vaults in a background thread.
*
* @param vaults The vaults to lock
* @param forced Whether to attempt a forced lock
*/
public void lockAll(Collection<Vault> vaults, boolean forced) {
executorService.execute(createLockAllTask(vaults, forced));
}
/**
* Creates but doesn't start a lock-all task.
*
* @param vaults The list of vaults to be locked
* @param forced Whether to attempt a forced lock
* @return Meta-Task that waits until all vaults are locked or fails after the first failure of a subtask
*/
public Task<Collection<Vault>> createLockAllTask(Collection<Vault> vaults, boolean forced) {
List<Task<Vault>> lockTasks = vaults.stream().map(v -> new LockVaultTask(v, forced)).collect(Collectors.toUnmodifiableList());
lockTasks.forEach(executorService::execute);
Task<Collection<Vault>> task = new WaitForTasksTask(lockTasks);
String vaultNames = vaults.stream().map(Vault::getDisplayableName).collect(Collectors.joining(", "));
task.setOnSucceeded(evt -> LOG.info("Locked {}", vaultNames));
task.setOnFailed(evt -> LOG.error("Failed to lock vaults " + vaultNames, evt.getSource().getException()));
return task;
}
private static class RevealVaultTask extends Task<Vault> {
private final Vault vault;
/**
* @param vault The vault to lock
*/
public RevealVaultTask(Vault vault) {
this.vault = vault;
}
@Override
protected Vault call() throws Volume.VolumeException {
vault.reveal();
return vault;
}
}
/**
* A task that waits for completion of multiple other tasks
*/
private static class WaitForTasksTask extends Task<Collection<Vault>> {
private final Collection<Task<Vault>> startedTasks;
public WaitForTasksTask(Collection<Task<Vault>> tasks) {
this.startedTasks = List.copyOf(tasks);
}
@Override
protected Collection<Vault> call() throws Exception {
Iterator<Task<Vault>> remainingTasks = startedTasks.iterator();
Collection<Vault> completed = new ArrayList<>();
try {
// wait for all tasks:
while (remainingTasks.hasNext()) {
Vault lockedVault = remainingTasks.next().get();
completed.add(lockedVault);
}
} catch (ExecutionException e) {
// cancel all remaining:
while (remainingTasks.hasNext()) {
remainingTasks.next().cancel(true);
}
throw e;
}
return List.copyOf(completed);
}
}
private static class AutoUnlockVaultTask extends Task<Vault> {
private final Vault vault;
private final KeychainAccess keychain;
public AutoUnlockVaultTask(Vault vault, KeychainAccess keychain) {
this.vault = vault;
this.keychain = keychain;
}
@Override
protected Vault call() throws Exception {
char[] storedPw = null;
try {
storedPw = keychain.loadPassphrase(vault.getId());
if (storedPw == null) {
throw new InvalidPassphraseException();
}
vault.unlock(CharBuffer.wrap(storedPw));
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
}
private static class UnlockVaultTask extends Task<Vault> {
private final Vault vault;
private final CharBuffer passphrase;
/**
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public UnlockVaultTask(Vault vault, CharSequence passphrase) {
this.vault = vault;
this.passphrase = CharBuffer.allocate(passphrase.length());
for (int i = 0; i < passphrase.length(); i++) {
this.passphrase.put(i, passphrase.charAt(i));
}
}
@Override
protected Vault call() throws Exception {
try {
vault.unlock(passphrase);
} finally {
Arrays.fill(passphrase.array(), ' ');
}
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
}
/**
* A task that locks a vault
*/
private static class LockVaultTask extends Task<Vault> {
private final Vault vault;
private final boolean forced;
/**
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
*/
public LockVaultTask(Vault vault, boolean forced) {
this.vault = vault;
this.forced = forced;
}
@Override
protected Vault call() throws Volume.VolumeException {
vault.lock(forced);
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.LOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.UNLOCKED);
}
}
}
@@ -1,203 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - password strength meter
*******************************************************************************/
package org.cryptomator.ui.controllers;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
import java.util.Optional;
public class ChangePasswordController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
private final Application app;
private final PasswordStrengthUtil strengthRater;
private final Localization localization;
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // 0-4
private Optional<ChangePasswordListener> listener = Optional.empty();
private Vault vault;
@Inject
public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) {
this.app = app;
this.strengthRater = strengthRater;
this.localization = localization;
}
@FXML
private SecPasswordField oldPasswordField;
@FXML
private SecPasswordField newPasswordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button changePasswordButton;
@FXML
private Text messageText;
@FXML
private Hyperlink downloadsPageLink;
@FXML
private Label passwordStrengthLabel;
@FXML
private Region passwordStrengthLevel0;
@FXML
private Region passwordStrengthLevel1;
@FXML
private Region passwordStrengthLevel2;
@FXML
private Region passwordStrengthLevel3;
@FXML
private Region passwordStrengthLevel4;
@FXML
private GridPane root;
@Override
public void initialize() {
oldPasswordField.textProperty().addListener(this::passwordsChanged);
newPasswordField.textProperty().addListener(this::passwordsChanged);
retypePasswordField.textProperty().addListener(this::passwordsChanged);
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
}
private void passwordsChanged(@SuppressWarnings("unused") Observable observable) {
boolean oldPasswordEmpty = oldPasswordField.getCharacters().length() == 0;
boolean newPasswordEmpty = newPasswordField.getCharacters().length() == 0;
boolean passwordsEqual = newPasswordField.getCharacters().equals(retypePasswordField.getCharacters());
changePasswordButton.setDisable(oldPasswordEmpty || newPasswordEmpty || !passwordsEqual);
passwordStrength.set(strengthRater.computeRate(newPasswordField.getCharacters().toString()));
}
@Override
public Parent getRoot() {
return root;
}
@Override
public void focus() {
oldPasswordField.requestFocus();
}
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
// trigger "default" change to refresh key bindings:
changePasswordButton.setDefaultButton(false);
changePasswordButton.setDefaultButton(true);
messageText.setText(null);
}
// ****************************************
// Downloads link
// ****************************************
@FXML
public void didClickDownloadsLink(ActionEvent event) {
app.getHostServices().showDocument("https://cryptomator.org/downloads/");
}
// ****************************************
// Change password button
// ****************************************
@FXML
private void didClickChangePasswordButton(ActionEvent event) {
downloadsPageLink.setVisible(false);
try {
vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
messageText.setText(null);
listener.ifPresent(this::invokeListenerLater);
} catch (InvalidPassphraseException e) {
messageText.setText(localization.getString("changePassword.errorMessage.wrongPassword"));
Platform.runLater(oldPasswordField::requestFocus);
} catch (UncheckedIOException | IOException ex) {
messageText.setText(localization.getString("changePassword.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (UnsupportedVaultFormatException e) {
downloadsPageLink.setVisible(true);
LOG.warn("Unable to unlock vault: " + e.getMessage());
if (e.isVaultOlderThanSoftware()) {
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
} else if (e.isSoftwareOlderThanVault()) {
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
}
} finally {
oldPasswordField.swipe();
newPasswordField.swipe();
retypePasswordField.swipe();
}
}
/* Getter/Setter */
public ChangePasswordListener getListener() {
return listener.orElse(null);
}
public void setListener(ChangePasswordListener listener) {
this.listener = Optional.ofNullable(listener);
}
/* callback */
private void invokeListenerLater(ChangePasswordListener listener) {
Platform.runLater(() -> {
listener.didChangePassword();
});
}
@FunctionalInterface
interface ChangePasswordListener {
void didChangePassword();
}
}
@@ -1,167 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - password strength meter
******************************************************************************/
package org.cryptomator.ui.controllers;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.util.Objects;
import java.util.Optional;
public class InitializeController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private final Localization localization;
private final PasswordStrengthUtil strengthRater;
private IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // strengths: 0-4
private Optional<InitializationListener> listener = Optional.empty();
private Vault vault;
@Inject
public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) {
this.localization = localization;
this.strengthRater = strengthRater;
}
@FXML
private SecPasswordField passwordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button okButton;
@FXML
private Label messageLabel;
@FXML
private Label passwordStrengthLabel;
@FXML
private Region passwordStrengthLevel0;
@FXML
private Region passwordStrengthLevel1;
@FXML
private Region passwordStrengthLevel2;
@FXML
private Region passwordStrengthLevel3;
@FXML
private Region passwordStrengthLevel4;
@FXML
private GridPane root;
@Override
public void initialize() {
passwordField.textProperty().addListener(this::passwordsChanged);
retypePasswordField.textProperty().addListener(this::passwordsChanged);
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
}
private void passwordsChanged(@SuppressWarnings("unused") Observable observable) {
boolean passwordsEmpty = passwordField.getCharacters().length() == 0;
boolean passwordsEqual = passwordField.getCharacters().equals(retypePasswordField.getCharacters());
okButton.setDisable(passwordsEmpty || !passwordsEqual);
passwordStrength.set(strengthRater.computeRate(passwordField.getCharacters().toString()));
}
@Override
public Parent getRoot() {
return root;
}
@Override
public void focus() {
passwordField.requestFocus();
}
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
// trigger "default" change to refresh key bindings:
okButton.setDefaultButton(false);
okButton.setDefaultButton(true);
}
// ****************************************
// OK button
// ****************************************
@FXML
protected void initializeVault(ActionEvent event) {
final CharSequence passphrase = passwordField.getCharacters();
try {
vault.create(passphrase);
listener.ifPresent(this::invokeListenerLater);
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
messageLabel.setText(localization.getString("initialize.messageLabel.initializationFailed"));
} finally {
passwordField.swipe();
retypePasswordField.swipe();
}
}
/* Getter/Setter */
public InitializationListener getListener() {
return listener.orElse(null);
}
public void setListener(InitializationListener listener) {
this.listener = Optional.ofNullable(listener);
}
/* callback */
private void invokeListenerLater(InitializationListener listener) {
Platform.runLater(() -> {
listener.didInitialize();
});
}
@FunctionalInterface
interface InitializationListener {
void didInitialize();
}
}
@@ -1,554 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - confirmation dialog on vault removal
******************************************************************************/
package org.cryptomator.ui.controllers;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.binding.ObjectExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Cell;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.ui.ExitUtil;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.model.AutoUnlocker;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.model.VaultList;
import org.cryptomator.ui.model.upgrade.UpgradeStrategies;
import org.cryptomator.ui.model.upgrade.UpgradeStrategy;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.cryptomator.ui.util.Tasks;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.fxmisc.easybind.monadic.MonadicBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.stream.Stream;
import static org.cryptomator.ui.util.DialogBuilderUtil.buildErrorDialog;
@FxApplicationScoped
public class MainController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private static final String ACTIVE_WINDOW_STYLE_CLASS = "active-window";
private static final String INACTIVE_WINDOW_STYLE_CLASS = "inactive-window";
private final Stage mainWindow;
private final ExitUtil exitUtil;
private final Localization localization;
private final ExecutorService executorService;
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
private final VaultFactory vaultFactoy;
private final ViewControllerLoader viewControllerLoader;
private final ObjectProperty<ViewController> activeController = new SimpleObjectProperty<>();
private final ObservableList<Vault> vaults;
private final BooleanBinding areAllVaultsLocked;
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
private final ObjectExpression<Vault.State> selectedVaultState = ObjectExpression.objectExpression(EasyBind.select(selectedVault).selectObject(Vault::stateProperty));
private final BooleanExpression isSelectedVaultValid = BooleanExpression.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false));
private final BooleanExpression canEditSelectedVault = selectedVaultState.isEqualTo(Vault.State.LOCKED);
private final MonadicBinding<UpgradeStrategy> upgradeStrategyForSelectedVault;
private final BooleanBinding isShowingSettings;
private final Map<Vault, UnlockedController> unlockedVaults = new HashMap<>();
private Subscription subs = Subscription.EMPTY;
@Inject
public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExitUtil exitUtil, Localization localization,
VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults, AutoUnlocker autoUnlocker) {
this.mainWindow = mainWindow;
this.executorService = executorService;
this.launchEventQueue = launchEventQueue;
this.exitUtil = exitUtil;
this.localization = localization;
this.vaultFactoy = vaultFactoy;
this.viewControllerLoader = viewControllerLoader;
this.vaults = vaults;
// derived bindings:
this.isShowingSettings = Bindings.equal(SettingsController.class, EasyBind.monadic(activeController).map(ViewController::getClass));
this.upgradeStrategyForSelectedVault = EasyBind.monadic(selectedVault).map(upgradeStrategies::getUpgradeStrategy);
this.areAllVaultsLocked = Bindings.isEmpty(FXCollections.observableList(vaults, Vault::observables).filtered(Vault.NOT_LOCKED));
EasyBind.subscribe(areAllVaultsLocked, exitUtil::updateTrayIcon);
EasyBind.subscribe(areAllVaultsLocked, Platform::setImplicitExit);
autoUnlocker.unlockAllSilently();
try {
Desktop.getDesktop().setPreferencesHandler(e -> {
Platform.runLater(this::toggleShowSettings);
});
} catch (UnsupportedOperationException e) {
LOG.info("Unable to setPreferencesHandler, probably not supported on this OS.");
}
}
@FXML
private ContextMenu vaultListCellContextMenu;
@FXML
private MenuItem changePasswordMenuItem;
@FXML
private ContextMenu addVaultContextMenu;
@FXML
private HBox root;
@FXML
private ListView<Vault> vaultList;
@FXML
private ToggleButton addVaultButton;
@FXML
private Button removeVaultButton;
@FXML
private ToggleButton settingsButton;
@FXML
private Pane contentPane;
@FXML
private Pane emptyListInstructions;
@Override
public void initialize() {
vaultList.setItems(vaults);
vaultList.getSelectionModel().clearSelection();
vaultList.setOnKeyReleased(this::didPressKeyOnList);
vaultList.setCellFactory(this::createDirecoryListCell);
root.setOnKeyReleased(this::didPressKeyOnRoot);
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
removeVaultButton.disableProperty().bind(canEditSelectedVault.not());
emptyListInstructions.visibleProperty().bind(Bindings.isEmpty(vaults));
changePasswordMenuItem.visibleProperty().bind(isSelectedVaultValid.and(Bindings.isNull(upgradeStrategyForSelectedVault)));
subs = subs.and(EasyBind.subscribe(selectedVault, this::selectedVaultDidChange));
subs = subs.and(EasyBind.subscribe(activeController, this::activeControllerDidChange));
subs = subs.and(EasyBind.subscribe(isShowingSettings, settingsButton::setSelected));
subs = subs.and(EasyBind.subscribe(addVaultContextMenu.showingProperty(), addVaultButton::setSelected));
}
@Override
public Parent getRoot() {
return root;
}
public void initStage(Stage stage) {
stage.setScene(new Scene(getRoot()));
stage.sizeToScene();
stage.setTitle(localization.getString("app.name")); // set once before bind to avoid display bugs with Linux window managers
stage.titleProperty().bind(windowTitle());
stage.setResizable(false);
loadFont("/css/ionicons.ttf");
loadFont("/css/fontawesome-webfont.ttf");
if (SystemUtils.IS_OS_MAC_OSX) {
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), ACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty()));
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), INACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty().not()));
Application.setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
} else if (SystemUtils.IS_OS_LINUX) {
stage.getIcons().add(new Image(getClass().getResourceAsStream("/window_icon_512.png")));
Application.setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
} else if (SystemUtils.IS_OS_WINDOWS) {
stage.getIcons().add(new Image(getClass().getResourceAsStream("/window_icon_32.png")));
Application.setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
}
exitUtil.initExitHandler(() -> Platform.runLater(this::gracefulShutdown));
listenToFileOpenRequests(stage);
}
private void gracefulShutdown() {
vaults.filtered(Vault.NOT_LOCKED).forEach(Vault::prepareForShutdown);
if (!vaults.filtered(Vault.NOT_LOCKED).isEmpty()) {
mainWindow.show(); // to keep the application open
ButtonType tryAgainButtonType = new ButtonType(localization.getString("main.gracefulShutdown.button.tryAgain"));
ButtonType forceShutdownButtonType = new ButtonType(localization.getString("main.gracefulShutdown.button.forceShutdown"));
Alert gracefulShutdownDialog = DialogBuilderUtil.buildGracefulShutdownDialog(
localization.getString("main.gracefulShutdown.dialog.title"), localization.getString("main.gracefulShutdown.dialog.header"), localization.getString("main.gracefulShutdown.dialog.content"),
forceShutdownButtonType, ButtonType.CANCEL, forceShutdownButtonType, tryAgainButtonType);
Optional<ButtonType> choice = gracefulShutdownDialog.showAndWait();
choice.ifPresent(btnType -> {
if (tryAgainButtonType.equals(btnType)) {
gracefulShutdown();
} else if (forceShutdownButtonType.equals(btnType)) {
Platform.runLater(Platform::exit);
} else {
if (!vaults.filtered(Vault.NOT_LOCKED).isEmpty()) {
showUnlockedView(vaults.get(0), false); //if there are still unlocked vaults, show one of them
} else {
showUnlockView(UnlockController.State.UNLOCKING); //otherwise show any vault
}
}
});
} else {
Platform.runLater(Platform::exit);
}
}
private void loadFont(String resourcePath) {
try (InputStream in = getClass().getResourceAsStream(resourcePath)) {
Font.loadFont(in, 12.0);
} catch (IOException e) {
LOG.warn("Error loading font from path: " + resourcePath, e);
}
}
private void listenToFileOpenRequests(Stage stage) {
Tasks.create(launchEventQueue::take).onSuccess(event -> {
stage.setIconified(false);
stage.show();
stage.toFront();
stage.requestFocus();
event.getPathsToOpen().forEach(path -> addVault(path, true));
}).schedulePeriodically(executorService, Duration.ZERO, Duration.ZERO);
}
private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setVaultContextMenu(vaultListCellContextMenu);
cell.setOnMouseClicked(this::didClickOnListCell);
return cell;
}
// ****************************************
// UI Events
// ****************************************
@FXML
private void didClickAddVault() {
if (addVaultContextMenu.isShowing()) {
addVaultContextMenu.hide();
} else {
addVaultContextMenu.show(addVaultButton, Side.BOTTOM, 0.0, 0.0);
}
}
@FXML
private void didClickCreateNewVault() {
final FileChooser fileChooser = new FileChooser();
final File file = fileChooser.showSaveDialog(mainWindow);
if (file == null) {
return;
}
try {
final Path vaultDir = file.toPath();
if (Files.exists(vaultDir)) {
try (Stream<Path> stream = Files.list(vaultDir)) {
if (stream.filter(this::isNotHidden).findAny().isPresent()) {
buildErrorDialog( //
localization.getString("main.createVault.nonEmptyDir.title"), //
localization.getString("main.createVault.nonEmptyDir.header"), //
localization.getString("main.createVault.nonEmptyDir.content"), //
ButtonType.OK).show();
return;
}
}
} else {
Files.createDirectory(vaultDir);
}
addVault(vaultDir, true);
} catch (IOException e) {
LOG.error("Unable to create vault", e);
}
}
private boolean isNotHidden(Path file) {
return !file.getFileName().toString().startsWith(".");
}
@FXML
private void didClickAddExistingVaults() {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
final List<File> files = fileChooser.showOpenMultipleDialog(mainWindow);
if (files != null) {
for (final File file : files) {
addVault(file.toPath(), true);
}
}
}
/**
* adds the given directory or selects it if it is already in the list of directories.
*
* @param path to a vault directory or masterkey file
*/
public void addVault(final Path path, boolean select) {
final Path vaultPath;
if (path != null && Files.isDirectory(path)) {
vaultPath = path;
} else if (path != null && Files.isReadable(path)) {
vaultPath = path.getParent();
} else {
LOG.warn("Ignoring attempt to add vault with invalid path: {}", path);
return;
}
final Vault vault = vaults.stream().filter(v -> v.getPath().equals(vaultPath)).findAny().orElseGet(() -> {
VaultSettings vaultSettings = VaultSettings.withRandomId();
vaultSettings.path().set(vaultPath);
return vaultFactoy.get(vaultSettings);
});
if (!vaults.contains(vault)) {
vaults.add(vault);
}
if (select) {
vaultList.getSelectionModel().select(vault);
activeController.get().focus();
}
}
@FXML
private void didClickRemoveSelectedEntry() {
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
localization.getString("main.directoryList.remove.confirmation.title"), //
localization.getString("main.directoryList.remove.confirmation.header"), //
localization.getString("main.directoryList.remove.confirmation.content"), //
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.OK.equals(choice.get())) {
vaults.remove(selectedVault.get());
if (vaults.isEmpty()) {
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
} else {
activeController.get().focus();
}
}
}
@FXML
private void didClickChangePassword() {
showChangePasswordView();
}
@FXML
private void didClickShowSettings() {
toggleShowSettings();
}
private void toggleShowSettings() {
if (isShowingSettings.get()) {
showWelcomeView();
} else {
showPreferencesView();
}
vaultList.getSelectionModel().clearSelection();
}
// ****************************************
// Binding Listeners
// ****************************************
private void activeControllerDidChange(ViewController newValue) {
final Parent root = newValue.getRoot();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
}
private void selectedVaultDidChange(Vault newValue) {
if (newValue == null) {
return;
}
if (newValue.getState() != Vault.State.LOCKED) {
this.showUnlockedView(newValue, false);
} else if (!newValue.doesVaultDirectoryExist()) {
this.showNotFoundView();
} else if (newValue.isValidVaultDirectory() && upgradeStrategyForSelectedVault.isPresent()) {
this.showUpgradeView();
} else if (newValue.isValidVaultDirectory()) {
this.showUnlockView(UnlockController.State.UNLOCKING);
} else {
this.showInitializeView();
}
}
private void didPressKeyOnList(KeyEvent e) {
if (e.getCode() == KeyCode.ENTER || e.getCode() == KeyCode.SPACE) {
activeController.get().focus();
}
}
private void didPressKeyOnRoot(KeyEvent event) {
boolean triggered;
if (SystemUtils.IS_OS_MAC) {
triggered = event.isMetaDown();
} else {
triggered = event.isControlDown() && !event.isAltDown();
}
if (triggered && event.getCode().isDigitKey()) {
int digit = Integer.valueOf(event.getText());
switch (digit) {
case 0: {
vaultList.getSelectionModel().clearSelection();
showWelcomeView();
return;
}
default: {
vaultList.getSelectionModel().select(digit - 1);
activeController.get().focus();
return;
}
}
}
}
private void didClickOnListCell(MouseEvent e) {
if (MouseEvent.MOUSE_CLICKED.equals(e.getEventType()) && e.getSource() instanceof Cell && ((Cell<?>) e.getSource()).isSelected()) {
activeController.get().focus();
}
}
// ****************************************
// Public Bindings
// ****************************************
public Binding<String> windowTitle() {
return EasyBind.monadic(selectedVault).flatMap(Vault::name).orElse(localization.getString("app.name"));
}
// ****************************************
// Subcontroller for right panel
// ****************************************
private void showWelcomeView() {
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
}
private void showPreferencesView() {
activeController.set(viewControllerLoader.load("/fxml/settings.fxml"));
}
private void showNotFoundView() {
activeController.set(viewControllerLoader.load("/fxml/notfound.fxml"));
}
private void showInitializeView() {
final InitializeController ctrl = viewControllerLoader.load("/fxml/initialize.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didInitialize);
activeController.set(ctrl);
}
public void didInitialize() {
showUnlockView(UnlockController.State.INITIALIZED);
activeController.get().focus();
}
private void showUpgradeView() {
final UpgradeController ctrl = viewControllerLoader.load("/fxml/upgrade.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUpgrade);
activeController.set(ctrl);
}
public void didUpgrade() {
showUnlockView(UnlockController.State.UPGRADED);
activeController.get().focus();
}
private void showUnlockView(UnlockController.State state) {
final UnlockController ctrl = viewControllerLoader.load("/fxml/unlock.fxml");
ctrl.setVault(selectedVault.get(), state);
ctrl.setListener(this::didUnlock);
activeController.set(ctrl);
}
public void didUnlock(Vault vault) {
if (vault.equals(selectedVault.getValue())) {
this.showUnlockedView(vault, vault.getVaultSettings().revealAfterMount().getValue());
}
}
private void showUnlockedView(Vault vault, boolean reveal) {
final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> viewControllerLoader.load("/fxml/unlocked.fxml"));
ctrl.setVault(vault);
ctrl.setListener(this::didLock);
if (reveal) {
ctrl.revealVault(vault);
}
activeController.set(ctrl);
}
public void didLock(UnlockedController ctrl) {
unlockedVaults.remove(ctrl.getVault());
if (ctrl.getVault().getId() == selectedVault.get().getId()) {
showUnlockView(UnlockController.State.UNLOCKING);
}
activeController.get().focus();
}
private void showChangePasswordView() {
final ChangePasswordController ctrl = viewControllerLoader.load("/fxml/change_password.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didChangePassword);
activeController.set(ctrl);
Platform.runLater(ctrl::focus);
}
public void didChangePassword() {
showUnlockView(UnlockController.State.PASSWORD_CHANGED);
activeController.get().focus();
}
}
@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.controllers;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.layout.VBox;
import org.cryptomator.common.FxApplicationScoped;
import javax.inject.Inject;
@FxApplicationScoped
public class NotFoundController implements ViewController {
@Inject
public NotFoundController() {
// no-op
}
@FXML
VBox root;
@Override
public Parent getRoot() {
return root;
}
}
@@ -1,181 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui.controllers;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Volume;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Optional;
@FxApplicationScoped
public class SettingsController implements ViewController {
private static final CharMatcher DIGITS_MATCHER = CharMatcher.inRange('0', '9');
private final Localization localization;
private final Settings settings;
private final Optional<String> applicationVersion;
@Inject
public SettingsController(Localization localization, Settings settings, @Named("applicationVersion") Optional<String> applicationVersion) {
this.localization = localization;
this.settings = settings;
this.applicationVersion = applicationVersion;
this.webdavSettings = new Group();
}
@FXML
private CheckBox checkForUpdatesCheckbox;
private Group webdavSettings;
@FXML
private Label portFieldLabel;
@FXML
private TextField portField;
@FXML
private Button changePortButton;
@FXML
private Label versionLabel;
@FXML
private Label prefGvfsSchemeLabel;
@FXML
private ChoiceBox<String> prefGvfsScheme;
@FXML
private ChoiceBox<VolumeImpl> volume;
@FXML
private CheckBox debugModeCheckbox;
@FXML
private VBox root;
@Override
public void initialize() {
versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion.orElse("SNAPSHOT")));
checkForUpdatesCheckbox.setDisable(areUpdatesManagedExternally());
checkForUpdatesCheckbox.setSelected(settings.checkForUpdates().get() && !areUpdatesManagedExternally());
//NIOADAPTER
volume.getItems().addAll(Volume.getCurrentSupportedAdapters());
volume.setValue(settings.preferredVolumeImpl().get());
volume.setConverter(new NioAdapterImplStringConverter());
volume.valueProperty().addListener(this::setVisibilityGvfsElements);
//WEBDAV
webdavSettings.visibleProperty().bind(volume.valueProperty().isEqualTo(VolumeImpl.WEBDAV));
webdavSettings.managedProperty().bind(webdavSettings.visibleProperty());
prefGvfsScheme.managedProperty().bind(webdavSettings.visibleProperty());
prefGvfsSchemeLabel.managedProperty().bind(webdavSettings.visibleProperty());
portFieldLabel.managedProperty().bind(webdavSettings.visibleProperty());
portFieldLabel.visibleProperty().bind(webdavSettings.visibleProperty());
changePortButton.managedProperty().bind(webdavSettings.visibleProperty());
portField.managedProperty().bind(webdavSettings.visibleProperty());
portField.visibleProperty().bind(webdavSettings.visibleProperty());
portField.setText(String.valueOf(settings.port().intValue()));
portField.addEventFilter(KeyEvent.KEY_TYPED, this::filterNumericKeyEvents);
changePortButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(portField.textProperty()));
changePortButton.disableProperty().bind(Bindings.createBooleanBinding(this::isPortValid, portField.textProperty()).not());
prefGvfsScheme.getItems().add("dav");
prefGvfsScheme.getItems().add("webdav");
prefGvfsScheme.setValue(settings.preferredGvfsScheme().get());
prefGvfsSchemeLabel.setVisible(SystemUtils.IS_OS_LINUX);
prefGvfsScheme.setVisible(SystemUtils.IS_OS_LINUX);
debugModeCheckbox.setSelected(settings.debugMode().get());
settings.checkForUpdates().bind(checkForUpdatesCheckbox.selectedProperty());
settings.preferredGvfsScheme().bind(prefGvfsScheme.valueProperty());
settings.preferredVolumeImpl().bind(volume.valueProperty());
settings.debugMode().bind(debugModeCheckbox.selectedProperty());
}
@Override
public Parent getRoot() {
return root;
}
@FXML
private void changePort() {
assert isPortValid() : "Button must be disabled, if port is invalid.";
try {
int port = Integer.parseInt(portField.getText());
settings.port().set(port);
} catch (NumberFormatException e) {
throw new IllegalStateException("Button must be disabled, if port is invalid.", e);
}
}
private boolean isPortValid() {
try {
int port = Integer.parseInt(portField.getText());
return port == 0 // choose port automatically
|| port >= Settings.MIN_PORT && port <= Settings.MAX_PORT; // port within range
} catch (NumberFormatException e) {
return false;
}
}
private void filterNumericKeyEvents(KeyEvent t) {
if (!Strings.isNullOrEmpty(t.getCharacter()) && !DIGITS_MATCHER.matchesAllOf(t.getCharacter())) {
t.consume();
}
}
private void setVisibilityGvfsElements(@SuppressWarnings("unused") Observable obs, @SuppressWarnings("unused")Object oldValue, Object newValue) {
prefGvfsSchemeLabel.setVisible(SystemUtils.IS_OS_LINUX && ((VolumeImpl) newValue).getDisplayName().equals("WebDAV"));
prefGvfsScheme.setVisible(SystemUtils.IS_OS_LINUX && ((VolumeImpl) newValue).getDisplayName().equals("WebDAV"));
}
private boolean areUpdatesManagedExternally() {
return Boolean.parseBoolean(System.getProperty("cryptomator.updatesManagedExternally", "false"));
}
private static class NioAdapterImplStringConverter extends StringConverter<VolumeImpl> {
@Override
public String toString(VolumeImpl object) {
return object.getDisplayName();
}
@Override
public VolumeImpl fromString(String string) {
return VolumeImpl.forDisplayName(string);
}
}
}
@@ -1,581 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui.controllers;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.WindowsDriveLetters;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.cryptomator.ui.util.Tasks;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
public class UnlockController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private static final CharMatcher ALPHA_NUMERIC_MATCHER = CharMatcher.inRange('a', 'z') //
.or(CharMatcher.inRange('A', 'Z')) //
.or(CharMatcher.inRange('0', '9')) //
.or(CharMatcher.is('_')) //
.precomputed();
private final Application app;
private final Stage mainWindow;
private final Localization localization;
private final WindowsDriveLetters driveLetters;
private final ChangeListener<Path> driveLetterChangeListener = this::winDriveLetterDidChange;
private final Optional<KeychainAccess> keychainAccess;
private final Settings settings;
private final ExecutorService executor;
private Vault vault;
private Optional<UnlockListener> listener = Optional.empty();
private Subscription vaultSubs = Subscription.EMPTY;
private BooleanProperty unlocking = new SimpleBooleanProperty();
@Inject
public UnlockController(Application app, @Named("mainWindow") Stage mainWindow, Localization localization, WindowsDriveLetters driveLetters, Optional<KeychainAccess> keychainAccess, Settings settings, ExecutorService executor) {
this.app = app;
this.mainWindow = mainWindow;
this.localization = localization;
this.driveLetters = driveLetters;
this.keychainAccess = keychainAccess;
this.settings = settings;
this.executor = executor;
}
@FXML
private SecPasswordField passwordField;
@FXML
private Button advancedOptionsButton;
@FXML
private Button unlockButton;
@FXML
private Text messageText;
@FXML
private CheckBox savePassword;
@FXML
private TextField mountName;
@FXML
private CheckBox useCustomMountFlags;
@FXML
private TextField mountFlags;
@FXML
private CheckBox revealAfterMount;
@FXML
private CheckBox useCustomWinDriveLetter;
@FXML
private ChoiceBox<Path> winDriveLetter;
@FXML
private CheckBox useCustomMountPoint;
@FXML
private HBox customMountPoint;
@FXML
private Label customMountPointLabel;
@FXML
private Hyperlink downloadsPageLink;
@FXML
private VBox advancedOptions;
@FXML
private VBox root;
@FXML
private CheckBox unlockAfterStartup;
@FXML
private CheckBox useReadOnlyMode;
@Override
public void initialize() {
advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
advancedOptions.disableProperty().bind(unlocking);
unlockButton.disableProperty().bind(unlocking.or(passwordField.textProperty().isEmpty()));
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
mountName.textProperty().addListener(this::mountNameDidChange);
useReadOnlyMode.selectedProperty().addListener(this::useReadOnlyDidChange);
useCustomMountFlags.selectedProperty().addListener(this::useCustomMountFlagsDidChange);
mountFlags.disableProperty().bind(useCustomMountFlags.selectedProperty().not());
mountFlags.textProperty().addListener(this::mountFlagsDidChange);
savePassword.setDisable(!keychainAccess.isPresent());
unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not()));
downloadsPageLink.visibleProperty().bind(downloadsPageLink.managedProperty());
customMountPoint.visibleProperty().bind(useCustomMountPoint.selectedProperty());
customMountPoint.managedProperty().bind(useCustomMountPoint.selectedProperty());
winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
winDriveLetter.disableProperty().bind(useCustomWinDriveLetter.selectedProperty().not());
if (!SystemUtils.IS_OS_WINDOWS) {
useCustomWinDriveLetter.setVisible(false);
useCustomWinDriveLetter.setManaged(false);
winDriveLetter.setVisible(false);
winDriveLetter.setManaged(false);
}
}
@Override
public Parent getRoot() {
return root;
}
@Override
public void focus() {
passwordField.requestFocus();
}
void setVault(Vault vault, State state) {
vaultSubs.unsubscribe();
vaultSubs = Subscription.EMPTY;
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
if (Objects.equals(this.vault, Objects.requireNonNull(vault))) {
return;
}
assert vault != null;
this.vault = vault;
advancedOptions.setVisible(false);
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
state.successMessage().map(localization::getString).ifPresent(messageText::setText);
downloadsPageLink.setManaged(false);
mountName.setText(vault.getMountName());
useCustomMountFlags.setSelected(vault.isHavingCustomMountFlags());
mountFlags.setText(vault.getMountFlags());
savePassword.setSelected(false);
// auto-fill pw from keychain:
if (keychainAccess.isPresent()) {
try {
char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
if (storedPw != null) {
savePassword.setSelected(true);
passwordField.setPassword(storedPw);
passwordField.selectRange(storedPw.length, storedPw.length);
Arrays.fill(storedPw, ' ');
}
} catch (KeychainAccessException e) {
LOG.error("Failed to load stored password from system keychain.", e);
}
}
VaultSettings vaultSettings = vault.getVaultSettings();
unlockAfterStartup.setSelected(savePassword.isSelected() && vaultSettings.unlockAfterStartup().get());
revealAfterMount.setSelected(vaultSettings.revealAfterMount().get());
useReadOnlyMode.setSelected(vaultSettings.usesReadOnlyMode().get());
// WEBDAV-dependent controls:
if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) {
useCustomMountPoint.setVisible(false);
useCustomMountPoint.setManaged(false);
useCustomMountFlags.setVisible(false);
useCustomMountFlags.setManaged(false);
mountFlags.setVisible(false);
mountFlags.setManaged(false);
} else {
useCustomMountPoint.setVisible(true);
useCustomMountPoint.setSelected(vaultSettings.usesIndividualMountPath().get());
if (Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
customMountPointLabel.setText(localization.getString("unlock.label.chooseMountPath"));
} else {
customMountPointLabel.setText(displayablePath(vaultSettings.individualMountPath().getValueSafe()));
}
}
// OS-dependent controls:
if (SystemUtils.IS_OS_WINDOWS) {
winDriveLetter.valueProperty().removeListener(driveLetterChangeListener);
winDriveLetter.getItems().clear();
winDriveLetter.getItems().add(null);
winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters());
winDriveLetter.getItems().sort(new WinDriveLetterComparator());
winDriveLetter.valueProperty().addListener(driveLetterChangeListener);
chooseSelectedDriveLetter();
winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
useCustomWinDriveLetter.setSelected(!vaultSettings.usesIndividualMountPath().get());
useCustomWinDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
useCustomWinDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
}
vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), vaultSettings.unlockAfterStartup()::set));
vaultSubs = vaultSubs.and(EasyBind.subscribe(revealAfterMount.selectedProperty(), vaultSettings.revealAfterMount()::set));
vaultSubs = vaultSubs.and(EasyBind.subscribe(useCustomMountPoint.selectedProperty(), vaultSettings.usesIndividualMountPath()::set));
vaultSubs = vaultSubs.and(EasyBind.subscribe(useReadOnlyMode.selectedProperty(), vaultSettings.usesReadOnlyMode()::set));
}
private String displayablePath(String path) {
Path homeDir = Paths.get(SystemUtils.USER_HOME);
Path p = Paths.get(path);
if (p.startsWith(homeDir)) {
Path relativePath = homeDir.relativize(p);
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
return homePrefix + relativePath.toString();
} else {
return p.toString();
}
}
// ****************************************
// Downloads link
// ****************************************
@FXML
public void didClickDownloadsLink() {
app.getHostServices().showDocument("https://cryptomator.org/downloads/#allVersions");
}
// ****************************************
// Advanced options button
// ****************************************
@FXML
private void didClickAdvancedOptionsButton() {
messageText.setText(null);
downloadsPageLink.setManaged(false);
advancedOptions.setVisible(!advancedOptions.isVisible());
if (advancedOptions.isVisible()) {
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.hide"));
} else {
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
}
}
private void filterAlphanumericKeyEvents(KeyEvent t) {
if (!Strings.isNullOrEmpty(t.getCharacter()) && !ALPHA_NUMERIC_MATCHER.matchesAllOf(t.getCharacter())) {
t.consume();
}
}
private void mountNameDidChange(@SuppressWarnings("unused") ObservableValue<? extends String> property, @SuppressWarnings("unused") String oldValue, String newValue) {
// newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents
if (newValue.isEmpty()) {
mountName.setText(vault.getMountName());
} else {
vault.setMountName(newValue);
}
if (!useCustomMountFlags.isSelected()) {
mountFlags.setText(vault.getMountFlags()); // update default flags
}
}
private void useReadOnlyDidChange(@SuppressWarnings("unused") ObservableValue<? extends Boolean> property, @SuppressWarnings("unused") Boolean oldValue, Boolean newValue) {
vault.getVaultSettings().usesReadOnlyMode().setValue(newValue);
if (!useCustomMountFlags.isSelected()) {
mountFlags.setText(vault.getMountFlags()); // update default flags
}
}
private void useCustomMountFlagsDidChange(@SuppressWarnings("unused") ObservableValue<? extends Boolean> property, @SuppressWarnings("unused") Boolean oldValue, Boolean newValue) {
if (!newValue) {
vault.setMountFlags(VaultSettings.DEFAULT_MOUNT_FLAGS);
mountFlags.setText(vault.getMountFlags());
}
}
private void mountFlagsDidChange(@SuppressWarnings("unused") ObservableValue<? extends String> property, @SuppressWarnings("unused") String oldValue, String newValue) {
if (useCustomMountFlags.isSelected()) {
vault.setMountFlags(newValue);
}
}
@FXML
public void didClickChooseCustomMountPoint() {
DirectoryChooser dirChooser = new DirectoryChooser();
File file = dirChooser.showDialog(mainWindow);
if (file != null) {
customMountPointLabel.setText(displayablePath(file.toString()));
vault.setCustomMountPath(file.toString());
}
}
@FXML
public void didClickCustomWinDriveLetterCheckbox() {
if (!useCustomWinDriveLetter.isSelected()) {
winDriveLetter.setValue(null);
}
}
@FXML
public void didClickCustomMountPointCheckbox() {
useCustomWinDriveLetter.setSelected(vault.getWinDriveLetter() != null);
}
/**
* Converts 'C' to "C:" to translate between model and GUI.
*/
private class WinDriveLetterLabelConverter extends StringConverter<Path> {
@Override
public String toString(Path root) {
if (root == null) {
return localization.getString("unlock.choicebox.winDriveLetter.auto");
} else if (root.endsWith("occupied")) {
return root.getRoot().toString().substring(0, 1) + " (" + localization.getString("unlock.choicebox.winDriveLetter.occupied") + ")";
} else {
return root.toString().substring(0, 1);
}
}
@Override
public Path fromString(String string) {
if (localization.getString("unlock.choicebox.winDriveLetter.auto").equals(string)) {
return null;
} else {
return Path.of(string);
}
}
}
/**
* Natural sorting of ASCII letters, but <code>null</code> always on first, as this is "auto-assign".
*/
private static class WinDriveLetterComparator implements Comparator<Path> {
@Override
public int compare(Path c1, Path c2) {
if (c1 == null) {
return -1;
} else if (c2 == null) {
return 1;
} else {
return c1.compareTo(c2);
}
}
}
private void winDriveLetterDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> property, @SuppressWarnings("unused") Path oldValue, Path newValue) {
vault.setWinDriveLetter(newValue);
}
private void chooseSelectedDriveLetter() {
assert SystemUtils.IS_OS_WINDOWS;
// if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this:
if (vault.getWinDriveLetter() != null) {
final Path pickedRoot = Path.of(vault.getWinDriveLetter() + ":\\");
if (driveLetters.getOccupiedDriveLetters().contains(pickedRoot)) {
Path alteredPath = pickedRoot.resolve("occupied");
this.winDriveLetter.getItems().add(alteredPath);
this.winDriveLetter.getSelectionModel().select(alteredPath);
} else {
this.winDriveLetter.getSelectionModel().select(pickedRoot);
}
} else {
// first option is known to be 'auto-assign' due to #WinDriveLetterComparator.
this.winDriveLetter.getSelectionModel().selectFirst();
}
}
// ****************************************
// Save password checkbox
// ****************************************
@FXML
private void didClickSavePasswordCheckbox() {
if (!savePassword.isSelected() && hasStoredPassword()) {
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
localization.getString("unlock.savePassword.delete.confirmation.title"), //
localization.getString("unlock.savePassword.delete.confirmation.header"), //
localization.getString("unlock.savePassword.delete.confirmation.content"), //
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.OK.equals(choice.get())) {
try {
keychainAccess.get().deletePassphrase(vault.getId());
} catch (KeychainAccessException e) {
LOG.error("Failed to remove entry from system keychain.", e);
}
} else if (ButtonType.CANCEL.equals(choice.get())) {
savePassword.setSelected(true);
}
}
}
private boolean hasStoredPassword() {
try {
char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
boolean hasPw = (storedPw != null);
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
return hasPw;
} catch (KeychainAccessException e) {
return false;
}
}
// ****************************************
// Unlock button
// ****************************************
@FXML
private void didClickUnlockButton() {
unlocking.set(true);
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
unlockButton.setContentDisplay(ContentDisplay.LEFT);
CharSequence password = passwordField.getCharacters();
Tasks.create(() -> {
vault.unlock(password);
if (keychainAccess.isPresent() && savePassword.isSelected()) {
keychainAccess.get().storePassphrase(vault.getId(), password);
}
}).onSuccess(() -> {
messageText.setText(null);
downloadsPageLink.setManaged(false);
listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
passwordField.swipe();
}).onError(InvalidPassphraseException.class, e -> {
messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
downloadsPageLink.setManaged(false);
passwordField.selectAll();
passwordField.requestFocus();
}).onError(UnsupportedVaultFormatException.class, e -> {
if (e.isVaultOlderThanSoftware()) {
// whitespace after localized text used as separator between text and hyperlink
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
downloadsPageLink.setManaged(true);
} else if (e.isSoftwareOlderThanVault()) {
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
downloadsPageLink.setManaged(true);
} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
}
}).onError(NotDirectoryException.class, e -> {
LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage());
advancedOptions.setVisible(true);
messageText.setText(null);
downloadsPageLink.setManaged(false);
showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNonExisting");
}).onError(DirectoryNotEmptyException.class, e -> {
LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage());
advancedOptions.setVisible(true);
messageText.setText(null);
downloadsPageLink.setManaged(false);
showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNotEmpty");
}).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Unlock failed for technical reasons.", e);
messageText.setText(localization.getString("unlock.errorMessage.unlockFailed"));
downloadsPageLink.setManaged(false);
}).andFinally(() -> {
unlocking.set(false);
unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
}).runOnce(executor);
}
private void showUnlockFailedErrorDialog(String localizableContentKey) {
String title = localization.getString("unlock.failedDialog.title");
String header = localization.getString("unlock.failedDialog.header");
String content = localization.getString(localizableContentKey);
Alert alert = DialogBuilderUtil.buildErrorDialog(title, header, content, ButtonType.OK);
alert.show();
}
/* callback */
public void setListener(UnlockListener listener) {
this.listener = Optional.ofNullable(listener);
}
@FunctionalInterface
interface UnlockListener {
void didUnlock(Vault vault);
}
/* state */
public enum State {
UNLOCKING(null), //
INITIALIZED("unlock.successLabel.vaultCreated"), //
PASSWORD_CHANGED("unlock.successLabel.passwordChanged"), //
UPGRADED("unlock.successLabel.upgraded");
private Optional<String> successMessage;
State(String successMessage) {
this.successMessage = Optional.ofNullable(successMessage);
}
public Optional<String> successMessage() {
return successMessage;
}
}
}

Some files were not shown because too many files have changed in this diff Show More