You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-26 23:38:08 +02:00
Compare commits
84 Commits
sharing_e2
...
server_sha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93fe919300 | ||
|
|
a756db2ef4 | ||
|
|
843b52000e | ||
|
|
e8e8ea3780 | ||
|
|
3c13c8d080 | ||
|
|
97bfd5ef04 | ||
|
|
e3fd34e5d6 | ||
|
|
f144daed96 | ||
|
|
add9d884e6 | ||
|
|
62f81b4315 | ||
|
|
f33088fbe0 | ||
|
|
31b6d06418 | ||
|
|
06cd5ffa2d | ||
|
|
f3d4d8eaed | ||
|
|
dc08e1ded5 | ||
|
|
31c3fec8d8 | ||
|
|
4487cb85fc | ||
|
|
56cac1f729 | ||
|
|
3ade7ed849 | ||
|
|
a7eea9fc21 | ||
|
|
7fac1941cd | ||
|
|
061761f224 | ||
|
|
63e88c05d9 | ||
|
|
a6b1cffd50 | ||
|
|
8cc720963a | ||
|
|
da884752a8 | ||
|
|
818c7d4640 | ||
|
|
4577c9c161 | ||
|
|
03b4b6eb2d | ||
|
|
4d38397cd5 | ||
|
|
37d446b970 | ||
|
|
c91d4bda3c | ||
|
|
3e537967ee | ||
|
|
0cbc261051 | ||
|
|
542fdb496a | ||
|
|
d850eedd78 | ||
|
|
9429b51694 | ||
|
|
72e58ee195 | ||
|
|
56be4d59f4 | ||
|
|
bb740c75ec | ||
|
|
4244f712e1 | ||
|
|
e447acc076 | ||
|
|
87f83236cf | ||
|
|
6d981864ef | ||
|
|
0d40026d8b | ||
|
|
7a9ec627ee | ||
|
|
2d72d1435e | ||
|
|
12ec7b0c1d | ||
|
|
afe1cf747d | ||
|
|
c99aba0dff | ||
|
|
43c594b6b2 | ||
|
|
024967ce60 | ||
|
|
cd877f64cd | ||
|
|
f91b4edb30 | ||
|
|
b56177a4e3 | ||
|
|
4e70ca6fd0 | ||
|
|
2e04656b54 | ||
|
|
5e8b7420ff | ||
|
|
8ae4e30fd2 | ||
|
|
3ce947e82c | ||
|
|
c2298213d7 | ||
|
|
9679f03cfa | ||
|
|
3cddac3931 | ||
|
|
41c1e3bec9 | ||
|
|
25c5892e74 | ||
|
|
a661a73511 | ||
|
|
b00959e143 | ||
|
|
f6f5d6808d | ||
|
|
01b653fc34 | ||
|
|
4e7fe66883 | ||
|
|
cd99e675d9 | ||
|
|
a7130ce17a | ||
|
|
20f8743079 | ||
|
|
660b53575e | ||
|
|
6c43b78496 | ||
|
|
9d5d891fe3 | ||
|
|
96ac12b460 | ||
|
|
4b93664240 | ||
|
|
a2c6461af8 | ||
|
|
d33b99cffb | ||
|
|
267c32143b | ||
|
|
9260b2a9ab | ||
|
|
0a54854f54 | ||
|
|
496039f15c |
@@ -76,6 +76,9 @@ packages/app-cli/app/command-e2ee.js.map
|
||||
packages/app-cli/app/command-settingschema.d.ts
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
packages/app-cli/app/command-settingschema.js.map
|
||||
packages/app-cli/app/command-testing.d.ts
|
||||
packages/app-cli/app/command-testing.js
|
||||
packages/app-cli/app/command-testing.js.map
|
||||
packages/app-cli/app/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js.map
|
||||
@@ -109,6 +112,9 @@ packages/app-cli/tests/services/plugins/sandboxProxy.js.map
|
||||
packages/app-cli/tests/testUtils.d.ts
|
||||
packages/app-cli/tests/testUtils.js
|
||||
packages/app-cli/tests/testUtils.js.map
|
||||
packages/app-cli/tools/populateDatabase.d.ts
|
||||
packages/app-cli/tools/populateDatabase.js
|
||||
packages/app-cli/tools/populateDatabase.js.map
|
||||
packages/app-desktop/ElectronAppWrapper.d.ts
|
||||
packages/app-desktop/ElectronAppWrapper.js
|
||||
packages/app-desktop/ElectronAppWrapper.js.map
|
||||
@@ -208,9 +214,9 @@ packages/app-desktop/gui/DialogTitle.js.map
|
||||
packages/app-desktop/gui/DropboxLoginScreen.d.ts
|
||||
packages/app-desktop/gui/DropboxLoginScreen.js
|
||||
packages/app-desktop/gui/DropboxLoginScreen.js.map
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.js
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/ErrorBoundary.d.ts
|
||||
packages/app-desktop/gui/ErrorBoundary.js
|
||||
packages/app-desktop/gui/ErrorBoundary.js.map
|
||||
@@ -328,9 +334,6 @@ packages/app-desktop/gui/MainScreen/commands/toggleSideBar.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js.map
|
||||
packages/app-desktop/gui/MasterPasswordDialog/Dialog.d.ts
|
||||
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
|
||||
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js.map
|
||||
packages/app-desktop/gui/MenuBar.d.ts
|
||||
packages/app-desktop/gui/MenuBar.js
|
||||
packages/app-desktop/gui/MenuBar.js.map
|
||||
@@ -931,15 +934,12 @@ packages/lib/commands/historyForward.js.map
|
||||
packages/lib/commands/index.d.ts
|
||||
packages/lib/commands/index.js
|
||||
packages/lib/commands/index.js.map
|
||||
packages/lib/commands/openMasterPasswordDialog.d.ts
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js.map
|
||||
packages/lib/commands/synchronize.d.ts
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/synchronize.js.map
|
||||
packages/lib/components/shared/encryption-config-shared.d.ts
|
||||
packages/lib/components/shared/encryption-config-shared.js
|
||||
packages/lib/components/shared/encryption-config-shared.js.map
|
||||
packages/lib/components/EncryptionConfigScreen/utils.d.ts
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js.map
|
||||
packages/lib/database.d.ts
|
||||
packages/lib/database.js
|
||||
packages/lib/database.js.map
|
||||
@@ -1000,6 +1000,9 @@ packages/lib/import-enex-md-gen.js.map
|
||||
packages/lib/import-enex-md-gen.test.d.ts
|
||||
packages/lib/import-enex-md-gen.test.js
|
||||
packages/lib/import-enex-md-gen.test.js.map
|
||||
packages/lib/import-enex.d.ts
|
||||
packages/lib/import-enex.js
|
||||
packages/lib/import-enex.js.map
|
||||
packages/lib/locale.d.ts
|
||||
packages/lib/locale.js
|
||||
packages/lib/locale.js.map
|
||||
@@ -1237,12 +1240,6 @@ packages/lib/services/e2ee/EncryptionService.js.map
|
||||
packages/lib/services/e2ee/EncryptionService.test.d.ts
|
||||
packages/lib/services/e2ee/EncryptionService.test.js
|
||||
packages/lib/services/e2ee/EncryptionService.test.js.map
|
||||
packages/lib/services/e2ee/ppk.d.ts
|
||||
packages/lib/services/e2ee/ppk.js
|
||||
packages/lib/services/e2ee/ppk.js.map
|
||||
packages/lib/services/e2ee/ppk.test.d.ts
|
||||
packages/lib/services/e2ee/ppk.test.js
|
||||
packages/lib/services/e2ee/ppk.test.js.map
|
||||
packages/lib/services/e2ee/types.d.ts
|
||||
packages/lib/services/e2ee/types.js
|
||||
packages/lib/services/e2ee/types.js.map
|
||||
@@ -1579,9 +1576,6 @@ packages/lib/services/synchronizer/Synchronizer.conflicts.test.js.map
|
||||
packages/lib/services/synchronizer/Synchronizer.e2ee.test.d.ts
|
||||
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js.map
|
||||
packages/lib/services/synchronizer/Synchronizer.ppk.test.d.ts
|
||||
packages/lib/services/synchronizer/Synchronizer.ppk.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.ppk.test.js.map
|
||||
packages/lib/services/synchronizer/Synchronizer.resources.test.d.ts
|
||||
packages/lib/services/synchronizer/Synchronizer.resources.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.resources.test.js.map
|
||||
@@ -1822,6 +1816,9 @@ packages/renderer/utils.js.map
|
||||
packages/tools/buildServerDocker.d.ts
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.js.map
|
||||
packages/tools/buildServerDocker.test.d.ts
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.test.js.map
|
||||
packages/tools/convertThemesToCss.d.ts
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/convertThemesToCss.js.map
|
||||
@@ -1855,6 +1852,9 @@ packages/tools/release-server.js.map
|
||||
packages/tools/setupNewRelease.d.ts
|
||||
packages/tools/setupNewRelease.js
|
||||
packages/tools/setupNewRelease.js.map
|
||||
packages/tools/tagServerLatest.d.ts
|
||||
packages/tools/tagServerLatest.js
|
||||
packages/tools/tagServerLatest.js.map
|
||||
packages/tools/tool-utils.d.ts
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/tool-utils.js.map
|
||||
|
||||
@@ -190,7 +190,7 @@ module.exports = {
|
||||
selector: 'enumMember',
|
||||
format: null,
|
||||
'filter': {
|
||||
'regex': '^(GET|POST|PUT|DELETE|PATCH|HEAD|SQLite|PostgreSQL|ASC|DESC|E2EE|OR|AND|UNION|INTERSECT|EXCLUSION|INCLUSION|EUR|GBP|USD|SJCL.*)$',
|
||||
'regex': '^(GET|POST|PUT|DELETE|PATCH|HEAD|SQLite|PostgreSQL|ASC|DESC|E2EE|OR|AND|UNION|INTERSECT|EXCLUSION|INCLUSION|EUR|GBP|USD)$',
|
||||
'match': true,
|
||||
},
|
||||
},
|
||||
|
||||
8
.github/workflows/github-actions-main.yml
vendored
8
.github/workflows/github-actions-main.yml
vendored
@@ -35,14 +35,6 @@ jobs:
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
|
||||
# the next line enables multi-architecture support for docker, it basically makes it use qemu for non native platforms
|
||||
# See https://hub.docker.com/r/tonistiigi/binfmt for more info
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
|
||||
# this just prints the info about what platforms are supported in the builder (can help debugging if something isn't working right)
|
||||
# and also proves the above worked properly
|
||||
sudo docker buildx ls
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: olegtarasov/get-tag@v2.1
|
||||
- uses: actions/setup-node@v2
|
||||
|
||||
42
.gitignore
vendored
42
.gitignore
vendored
@@ -61,6 +61,9 @@ packages/app-cli/app/command-e2ee.js.map
|
||||
packages/app-cli/app/command-settingschema.d.ts
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
packages/app-cli/app/command-settingschema.js.map
|
||||
packages/app-cli/app/command-testing.d.ts
|
||||
packages/app-cli/app/command-testing.js
|
||||
packages/app-cli/app/command-testing.js.map
|
||||
packages/app-cli/app/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js.map
|
||||
@@ -94,6 +97,9 @@ packages/app-cli/tests/services/plugins/sandboxProxy.js.map
|
||||
packages/app-cli/tests/testUtils.d.ts
|
||||
packages/app-cli/tests/testUtils.js
|
||||
packages/app-cli/tests/testUtils.js.map
|
||||
packages/app-cli/tools/populateDatabase.d.ts
|
||||
packages/app-cli/tools/populateDatabase.js
|
||||
packages/app-cli/tools/populateDatabase.js.map
|
||||
packages/app-desktop/ElectronAppWrapper.d.ts
|
||||
packages/app-desktop/ElectronAppWrapper.js
|
||||
packages/app-desktop/ElectronAppWrapper.js.map
|
||||
@@ -193,9 +199,9 @@ packages/app-desktop/gui/DialogTitle.js.map
|
||||
packages/app-desktop/gui/DropboxLoginScreen.d.ts
|
||||
packages/app-desktop/gui/DropboxLoginScreen.js
|
||||
packages/app-desktop/gui/DropboxLoginScreen.js.map
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.js
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/ErrorBoundary.d.ts
|
||||
packages/app-desktop/gui/ErrorBoundary.js
|
||||
packages/app-desktop/gui/ErrorBoundary.js.map
|
||||
@@ -313,9 +319,6 @@ packages/app-desktop/gui/MainScreen/commands/toggleSideBar.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js.map
|
||||
packages/app-desktop/gui/MasterPasswordDialog/Dialog.d.ts
|
||||
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
|
||||
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js.map
|
||||
packages/app-desktop/gui/MenuBar.d.ts
|
||||
packages/app-desktop/gui/MenuBar.js
|
||||
packages/app-desktop/gui/MenuBar.js.map
|
||||
@@ -916,15 +919,12 @@ packages/lib/commands/historyForward.js.map
|
||||
packages/lib/commands/index.d.ts
|
||||
packages/lib/commands/index.js
|
||||
packages/lib/commands/index.js.map
|
||||
packages/lib/commands/openMasterPasswordDialog.d.ts
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js.map
|
||||
packages/lib/commands/synchronize.d.ts
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/synchronize.js.map
|
||||
packages/lib/components/shared/encryption-config-shared.d.ts
|
||||
packages/lib/components/shared/encryption-config-shared.js
|
||||
packages/lib/components/shared/encryption-config-shared.js.map
|
||||
packages/lib/components/EncryptionConfigScreen/utils.d.ts
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js.map
|
||||
packages/lib/database.d.ts
|
||||
packages/lib/database.js
|
||||
packages/lib/database.js.map
|
||||
@@ -985,6 +985,9 @@ packages/lib/import-enex-md-gen.js.map
|
||||
packages/lib/import-enex-md-gen.test.d.ts
|
||||
packages/lib/import-enex-md-gen.test.js
|
||||
packages/lib/import-enex-md-gen.test.js.map
|
||||
packages/lib/import-enex.d.ts
|
||||
packages/lib/import-enex.js
|
||||
packages/lib/import-enex.js.map
|
||||
packages/lib/locale.d.ts
|
||||
packages/lib/locale.js
|
||||
packages/lib/locale.js.map
|
||||
@@ -1222,12 +1225,6 @@ packages/lib/services/e2ee/EncryptionService.js.map
|
||||
packages/lib/services/e2ee/EncryptionService.test.d.ts
|
||||
packages/lib/services/e2ee/EncryptionService.test.js
|
||||
packages/lib/services/e2ee/EncryptionService.test.js.map
|
||||
packages/lib/services/e2ee/ppk.d.ts
|
||||
packages/lib/services/e2ee/ppk.js
|
||||
packages/lib/services/e2ee/ppk.js.map
|
||||
packages/lib/services/e2ee/ppk.test.d.ts
|
||||
packages/lib/services/e2ee/ppk.test.js
|
||||
packages/lib/services/e2ee/ppk.test.js.map
|
||||
packages/lib/services/e2ee/types.d.ts
|
||||
packages/lib/services/e2ee/types.js
|
||||
packages/lib/services/e2ee/types.js.map
|
||||
@@ -1564,9 +1561,6 @@ packages/lib/services/synchronizer/Synchronizer.conflicts.test.js.map
|
||||
packages/lib/services/synchronizer/Synchronizer.e2ee.test.d.ts
|
||||
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js.map
|
||||
packages/lib/services/synchronizer/Synchronizer.ppk.test.d.ts
|
||||
packages/lib/services/synchronizer/Synchronizer.ppk.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.ppk.test.js.map
|
||||
packages/lib/services/synchronizer/Synchronizer.resources.test.d.ts
|
||||
packages/lib/services/synchronizer/Synchronizer.resources.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.resources.test.js.map
|
||||
@@ -1807,6 +1801,9 @@ packages/renderer/utils.js.map
|
||||
packages/tools/buildServerDocker.d.ts
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.js.map
|
||||
packages/tools/buildServerDocker.test.d.ts
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.test.js.map
|
||||
packages/tools/convertThemesToCss.d.ts
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/convertThemesToCss.js.map
|
||||
@@ -1840,6 +1837,9 @@ packages/tools/release-server.js.map
|
||||
packages/tools/setupNewRelease.d.ts
|
||||
packages/tools/setupNewRelease.js
|
||||
packages/tools/setupNewRelease.js.map
|
||||
packages/tools/tagServerLatest.d.ts
|
||||
packages/tools/tagServerLatest.js
|
||||
packages/tools/tagServerLatest.js.map
|
||||
packages/tools/tool-utils.d.ts
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/tool-utils.js.map
|
||||
|
||||
@@ -137,8 +137,8 @@ fi
|
||||
#-----------------------------------------------------
|
||||
print 'Downloading Joplin...'
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
wget -O ${TEMP_DIR}/Joplin.AppImage https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage
|
||||
wget -O ${TEMP_DIR}/joplin.png https://joplinapp.org/images/Icon512.png
|
||||
wget -O "${TEMP_DIR}/Joplin.AppImage" "https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage"
|
||||
wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
|
||||
|
||||
#-----------------------------------------------------
|
||||
print 'Installing Joplin...'
|
||||
@@ -149,7 +149,7 @@ rm -f ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop ~/.joplin/
|
||||
mkdir -p ~/.joplin/
|
||||
|
||||
# Download the latest version
|
||||
mv ${TEMP_DIR}/Joplin.AppImage ~/.joplin/Joplin.AppImage
|
||||
mv "${TEMP_DIR}/Joplin.AppImage" ~/.joplin/Joplin.AppImage
|
||||
|
||||
# Gives execution privileges
|
||||
chmod +x ~/.joplin/Joplin.AppImage
|
||||
@@ -159,7 +159,7 @@ print "${COLOR_GREEN}OK${COLOR_RESET}"
|
||||
#-----------------------------------------------------
|
||||
print 'Installing icon...'
|
||||
mkdir -p ~/.local/share/icons/hicolor/512x512/apps
|
||||
mv ${TEMP_DIR}/joplin.png ~/.local/share/icons/hicolor/512x512/apps/joplin.png
|
||||
mv "${TEMP_DIR}/joplin.png" ~/.local/share/icons/hicolor/512x512/apps/joplin.png
|
||||
print "${COLOR_GREEN}OK${COLOR_RESET}"
|
||||
|
||||
# Detect desktop environment
|
||||
@@ -222,7 +222,7 @@ fi
|
||||
print "${COLOR_GREEN}Joplin version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
|
||||
|
||||
# Record version
|
||||
echo $RELEASE_VERSION > ~/.joplin/VERSION
|
||||
echo "$RELEASE_VERSION" > ~/.joplin/VERSION
|
||||
|
||||
#-----------------------------------------------------
|
||||
if [[ "$SHOW_CHANGELOG" == true ]]; then
|
||||
@@ -232,5 +232,5 @@ fi
|
||||
|
||||
#-----------------------------------------------------
|
||||
print "Cleaning up..."
|
||||
rm -rf $TEMP_DIR
|
||||
rm -rf "$TEMP_DIR"
|
||||
print "${COLOR_GREEN}OK${COLOR_RESET}"
|
||||
|
||||
38
README.md
38
README.md
@@ -504,47 +504,47 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 28%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 71%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 56%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 55%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [marph91](mailto:martin.d@andix.de) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 54%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Mora](mailto:francisco.m.collao@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 31%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 96%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 36%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 83%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | Alexander Dawson | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 67%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Nicolas Suzuki](mailto:nicolas.suzuki@pm.me) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 63%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 62%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 42%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vi.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 92%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 81%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 80%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Po-Chiang Chao](mailto:BobChao%29%20%28bobchao@gmail.com) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [SiderealArt](mailto:nelson22768384@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 94%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Contributors
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"releaseIOS": "node packages/tools/release-ios.js",
|
||||
"releasePluginGenerator": "node packages/tools/release-plugin-generator.js",
|
||||
"releaseServer": "node packages/tools/release-server.js",
|
||||
"tagServerLatest": "node packages/tools/tagServerLatest.js",
|
||||
"buildServerDocker": "node packages/tools/buildServerDocker.js",
|
||||
"setupNewRelease": "node ./packages/tools/setupNewRelease",
|
||||
"test-ci": "lerna run test-ci --stream",
|
||||
|
||||
4
packages/app-cli/.gitignore
vendored
4
packages/app-cli/.gitignore
vendored
@@ -23,4 +23,6 @@ tests/support/dropbox-auth.txt
|
||||
tests/support/nextcloud-auth.json
|
||||
tests/support/onedrive-auth.txt
|
||||
build/
|
||||
patches/
|
||||
patches/
|
||||
createUsers-*.txt
|
||||
tools/temp/
|
||||
|
||||
@@ -89,7 +89,7 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
|
||||
flags = cliUtils.parseFlags(flags);
|
||||
|
||||
if (!flags.arg) {
|
||||
booleanFlags.push(flags.short);
|
||||
if (flags.short) booleanFlags.push(flags.short);
|
||||
if (flags.long) booleanFlags.push(flags.long);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.'); // `generate-ppk`
|
||||
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file` and `target-status`.');
|
||||
}
|
||||
|
||||
options() {
|
||||
@@ -151,19 +151,6 @@ class Command extends BaseCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// if (args.command === 'generate-ppk') {
|
||||
// const syncInfo = localSyncInfo();
|
||||
// if (syncInfo.ppk) throw new Error('This account already has a public-private key pair');
|
||||
|
||||
// const argPassword = options.password ? options.password.toString() : '';
|
||||
// if (!argPassword) throw new Error('Password must be provided'); // TODO: should get from prompt
|
||||
// const ppk = await generateKeyPair(EncryptionService.instance(), argPassword);
|
||||
|
||||
// syncInfo.ppk = ppk;
|
||||
// saveLocalSyncInfo(syncInfo);
|
||||
// await Setting.saveAll();
|
||||
// }
|
||||
|
||||
if (args.command === 'target-status') {
|
||||
const fs = require('fs-extra');
|
||||
|
||||
|
||||
95
packages/app-cli/app/command-testing.ts
Normal file
95
packages/app-cli/app/command-testing.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import populateDatabase from '@joplin/lib/services/debug/populateDatabase';
|
||||
|
||||
function randomElement(array: any[]): any {
|
||||
if (!array.length) return null;
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
function itemCount(args: any) {
|
||||
const count = Number(args.arg0);
|
||||
if (!count || isNaN(count)) throw new Error('Note count must be specified');
|
||||
return count;
|
||||
}
|
||||
|
||||
class Command extends BaseCommand {
|
||||
usage() {
|
||||
return 'testing <command> [arg0]';
|
||||
}
|
||||
|
||||
description() {
|
||||
return 'testing';
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
options(): any[] {
|
||||
return [
|
||||
['--folder-count <count>', 'Folders to create'],
|
||||
['--note-count <count>', 'Notes to create'],
|
||||
['--tag-count <count>', 'Tags to create'],
|
||||
['--tags-per-note <count>', 'Tags per note'],
|
||||
['--silent', 'Silent'],
|
||||
];
|
||||
}
|
||||
|
||||
async action(args: any) {
|
||||
const { command, options } = args;
|
||||
|
||||
if (command === 'populate') {
|
||||
await populateDatabase(reg.db(), {
|
||||
folderCount: options['folder-count'],
|
||||
noteCount: options['note-count'],
|
||||
tagCount: options['tag-count'],
|
||||
tagsPerNote: options['tags-per-note'],
|
||||
silent: options['silent'],
|
||||
});
|
||||
}
|
||||
|
||||
const promises: any[] = [];
|
||||
|
||||
if (command === 'createRandomNotes') {
|
||||
const noteCount = itemCount(args);
|
||||
|
||||
for (let i = 0; i < noteCount; i++) {
|
||||
promises.push(Note.save({
|
||||
title: `Note ${uuid.createNano()}`,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (command === 'updateRandomNotes') {
|
||||
const noteCount = itemCount(args);
|
||||
|
||||
const noteIds = await Note.allIds();
|
||||
|
||||
for (let i = 0; i < noteCount; i++) {
|
||||
const noteId = randomElement(noteIds);
|
||||
promises.push(Note.save({
|
||||
id: noteId,
|
||||
title: `Note ${uuid.createNano()}`,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (command === 'deleteRandomNotes') {
|
||||
const noteCount = itemCount(args);
|
||||
const noteIds = await Note.allIds();
|
||||
|
||||
for (let i = 0; i < noteCount; i++) {
|
||||
const noteId = randomElement(noteIds);
|
||||
promises.push(Note.delete(noteId));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
||||
52
packages/app-cli/createUsers.sh
Executable file
52
packages/app-cli/createUsers.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start the server with:
|
||||
#
|
||||
# JOPLIN_IS_TESTING=1 npm run start-dev
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
# curl --data '{"action": "clearDatabase"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
# SMALL
|
||||
|
||||
# curl --data '{"action": "createTestUsers", "count": 400, "fromNum": 1}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
NUM=398
|
||||
while [ "$NUM" -lt 400 ]; do
|
||||
NUM=$(( NUM + 1 ))
|
||||
|
||||
echo "User $NUM"
|
||||
|
||||
CMD_FILE="$SCRIPT_DIR/createUsers-$NUM.txt"
|
||||
PROFILE_DIR=~/.config/joplindev-testing-$NUM
|
||||
USER_EMAIL="user$NUM@example.com"
|
||||
|
||||
rm -rf "$CMD_FILE" "$PROFILE_DIR"
|
||||
touch "$CMD_FILE"
|
||||
|
||||
FLAG_FOLDER_COUNT=100
|
||||
FLAG_NOTE_COUNT=1000
|
||||
FLAG_TAG_COUNT=20
|
||||
|
||||
if [ "$NUM" -gt 300 ]; then
|
||||
FLAG_FOLDER_COUNT=2000
|
||||
FLAG_NOTE_COUNT=10000
|
||||
FLAG_TAG_COUNT=200
|
||||
fi
|
||||
|
||||
if [ "$NUM" -gt 399 ]; then
|
||||
FLAG_FOLDER_COUNT=10000
|
||||
FLAG_NOTE_COUNT=150000
|
||||
FLAG_TAG_COUNT=2000
|
||||
fi
|
||||
|
||||
echo "testing populate --silent --folder-count $FLAG_FOLDER_COUNT --note-count $FLAG_NOTE_COUNT --tag-count $FLAG_TAG_COUNT" >> "$CMD_FILE"
|
||||
echo "config keychain.supported 0" >> "$CMD_FILE"
|
||||
echo "config sync.target 10" >> "$CMD_FILE"
|
||||
echo "config sync.10.username $USER_EMAIL" >> "$CMD_FILE"
|
||||
echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE"
|
||||
echo "sync" >> "$CMD_FILE"
|
||||
|
||||
npm start -- --profile "$PROFILE_DIR" batch "$CMD_FILE"
|
||||
done
|
||||
@@ -10,6 +10,7 @@
|
||||
"test-ci": "jest --config=jest.config.js --forceExit",
|
||||
"build": "gulp build",
|
||||
"start": "gulp build -L && node \"build/main.js\" --stack-trace-enabled --log-level debug --env dev",
|
||||
"start-no-build": "node \"build/main.js\" --stack-trace-enabled --log-level debug --env dev",
|
||||
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
|
||||
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json"
|
||||
},
|
||||
|
||||
@@ -84,4 +84,10 @@ describe('HtmlToMd', function() {
|
||||
}
|
||||
}));
|
||||
|
||||
it('should allow disabling escape', async () => {
|
||||
const htmlToMd = new HtmlToMd();
|
||||
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: true })).toBe('https://test.com/1_2_3.pdf');
|
||||
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: false })).toBe('https://test.com/1\\_2\\_3.pdf');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <s>Strike</s>
|
||||
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <span style="text-decoration: underline;">Insert alt</span> <s>Strike</s>
|
||||
@@ -1 +1 @@
|
||||
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> ~~Strike~~
|
||||
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <ins>Insert alt</ins> ~~Strike~~
|
||||
108
packages/app-cli/tools/populateDatabase.ts
Normal file
108
packages/app-cli/tools/populateDatabase.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// This script can be used to simulate a running production environment, by
|
||||
// having multiple users in parallel changing notes and synchronising.
|
||||
//
|
||||
// To get it working:
|
||||
//
|
||||
// - Run the Postgres database -- `sudo docker-compose --file docker-compose.db-dev.yml up`
|
||||
// - Update the DB parameters in ~/joplin-credentials/server.env to use the dev
|
||||
// database
|
||||
// - Run the server - `JOPLIN_IS_TESTING=1 npm run start-dev`
|
||||
// - Then run this script - `node populateDatabase.js`
|
||||
//
|
||||
// Currently it doesn't actually create the users, so that should be done using:
|
||||
//
|
||||
// curl --data '{"action": "createTestUsers", "count": 400, "fromNum": 1}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
//
|
||||
// That will create n users with email `user<n>@example.com`
|
||||
|
||||
import * as fs from 'fs-extra';
|
||||
import { homedir } from 'os';
|
||||
import { execCommand2 } from '@joplin/tools/tool-utils';
|
||||
import { chdir } from 'process';
|
||||
|
||||
const minUserNum = 1;
|
||||
const maxUserNum = 400;
|
||||
|
||||
const cliDir = `${__dirname}/..`;
|
||||
const tempDir = `${__dirname}/temp`;
|
||||
|
||||
function randomInt(min: number, max: number) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
const processing_: Record<number, boolean> = {};
|
||||
|
||||
const processUser = async (userNum: number) => {
|
||||
if (processing_[userNum]) {
|
||||
console.info(`User already being processed: ${userNum} - skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
processing_[userNum] = true;
|
||||
|
||||
try {
|
||||
const userEmail = `user${userNum}@example.com`;
|
||||
const userPassword = 'hunter1hunter2hunter3';
|
||||
const commandFile = `${tempDir}/populateDatabase-${userNum}.txt`;
|
||||
const profileDir = `${homedir()}/.config/joplindev-populate/joplindev-testing-${userNum}`;
|
||||
|
||||
const commands: string[] = [];
|
||||
const jackpot = Math.random() >= 0.95 ? 100 : 1;
|
||||
|
||||
commands.push(`testing createRandomNotes ${randomInt(1, 500 * jackpot)}`);
|
||||
commands.push(`testing updateRandomNotes ${randomInt(1, 1500 * jackpot)}`);
|
||||
commands.push(`testing deleteRandomNotes ${randomInt(1, 200 * jackpot)}`);
|
||||
commands.push('config keychain.supported 0');
|
||||
commands.push('config sync.target 10');
|
||||
commands.push(`config sync.10.username ${userEmail}`);
|
||||
commands.push(`config sync.10.password ${userPassword}`);
|
||||
commands.push('sync');
|
||||
|
||||
await fs.writeFile(commandFile, commands.join('\n'), 'utf8');
|
||||
|
||||
await chdir(cliDir);
|
||||
|
||||
await execCommand2(['npm', 'run', 'start-no-build', '--', '--profile', profileDir, 'batch', commandFile]);
|
||||
} catch (error) {
|
||||
console.error(`Could not process user ${userNum}:`, error);
|
||||
} finally {
|
||||
delete processing_[userNum];
|
||||
}
|
||||
};
|
||||
|
||||
const waitForProcessing = (count: number) => {
|
||||
return new Promise((resolve) => {
|
||||
const iid = setInterval(() => {
|
||||
if (Object.keys(processing_).length <= count) {
|
||||
clearInterval(iid);
|
||||
resolve(null);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
await fs.mkdirp(tempDir);
|
||||
|
||||
// Build the app once before starting, because we'll use start-no-build to
|
||||
// run the scripts (faster)
|
||||
await execCommand2(['npm', 'run', 'build']);
|
||||
|
||||
const focusUserNum = 400;
|
||||
|
||||
while (true) {
|
||||
let userNum = randomInt(minUserNum, maxUserNum);
|
||||
|
||||
if (Math.random() >= .7) userNum = focusUserNum;
|
||||
|
||||
void processUser(userNum);
|
||||
await waitForProcessing(10);
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -535,12 +535,12 @@ class Application extends BaseApplication {
|
||||
// }, 2000);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
this.dispatch({
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'masterPassword',
|
||||
});
|
||||
}, 2000);
|
||||
// setTimeout(() => {
|
||||
// this.dispatch({
|
||||
// type: 'DIALOG_OPEN',
|
||||
// name: 'syncWizard',
|
||||
// });
|
||||
// }, 2000);
|
||||
|
||||
// setTimeout(() => {
|
||||
// this.dispatch({
|
||||
|
||||
|
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 441 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -6,7 +6,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../services/bridge';
|
||||
import Setting, { AppType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
||||
import control_PluginsStates from './controls/plugins/PluginsStates';
|
||||
import EncryptionConfigScreen from '../EncryptionConfigScreen';
|
||||
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
@@ -42,9 +42,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
this.sidebar_selectionChange = this.sidebar_selectionChange.bind(this);
|
||||
this.checkSyncConfig_ = this.checkSyncConfig_.bind(this);
|
||||
// this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this);
|
||||
this.showLogButton_click = this.showLogButton_click.bind(this);
|
||||
this.nextcloudAppHelpLink_click = this.nextcloudAppHelpLink_click.bind(this);
|
||||
this.onCancelClick = this.onCancelClick.bind(this);
|
||||
this.onSaveClick = this.onSaveClick.bind(this);
|
||||
this.onApplyClick = this.onApplyClick.bind(this);
|
||||
@@ -58,19 +55,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
}
|
||||
|
||||
// async checkNextcloudAppButton_click() {
|
||||
// this.setState({ showNextcloudAppLog: true });
|
||||
// await shared.checkNextcloudApp(this, this.state.settings);
|
||||
// }
|
||||
|
||||
showLogButton_click() {
|
||||
this.setState({ showNextcloudAppLog: true });
|
||||
}
|
||||
|
||||
nextcloudAppHelpLink_click() {
|
||||
bridge().openExternal('https://joplinapp.org/nextcloud_app');
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import PluginBox, { InstallState } from './PluginBox';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useOnInstallHandler from './useOnInstallHandler';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
const Root = styled.div`
|
||||
`;
|
||||
@@ -100,6 +101,13 @@ export default function(props: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
const renderContentSourceInfo = () => {
|
||||
if (props.repoApi().isUsingDefaultContentUrl) return null;
|
||||
const theme = themeStyle(props.themeId);
|
||||
const url = new URL(props.repoApi().contentBaseUrl);
|
||||
return <div style={{ ...theme.textStyleMinor, marginTop: 5, fontSize: theme.fontSize }}>{_('Content provided by %s', url.hostname)}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<div style={{ marginBottom: 10, width: props.maxWidth }}>
|
||||
@@ -112,6 +120,7 @@ export default function(props: Props) {
|
||||
placeholder={props.disabled ? _('Please wait...') : _('Search for plugins...')}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
{renderContentSourceInfo()}
|
||||
</div>
|
||||
|
||||
<ResultsRoot>
|
||||
|
||||
@@ -27,12 +27,11 @@ const DialogRoot = styled.div`
|
||||
|
||||
interface Props {
|
||||
renderContent: Function;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Dialog(props: Props) {
|
||||
return (
|
||||
<DialogModalLayer className={props.className}>
|
||||
<DialogModalLayer>
|
||||
<DialogRoot>
|
||||
{props.renderContent()}
|
||||
</DialogRoot>
|
||||
|
||||
@@ -17,13 +17,10 @@ export type ClickEventHandler = (event: ClickEvent)=> void;
|
||||
interface Props {
|
||||
themeId: number;
|
||||
onClick?: ClickEventHandler;
|
||||
okButtonShow?: boolean;
|
||||
cancelButtonShow?: boolean;
|
||||
cancelButtonLabel?: string;
|
||||
cancelButtonDisabled?: boolean;
|
||||
okButtonShow?: boolean;
|
||||
okButtonLabel?: string;
|
||||
okButtonRef?: any;
|
||||
okButtonDisabled?: boolean;
|
||||
customButtons?: ButtonSpec[];
|
||||
}
|
||||
|
||||
@@ -71,15 +68,15 @@ export default function DialogButtonRow(props: Props) {
|
||||
|
||||
if (props.okButtonShow !== false) {
|
||||
buttonComps.push(
|
||||
<button disabled={props.okButtonDisabled} key="ok" style={buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
||||
{props.okButtonLabel ? props.okButtonLabel : _('OK')}
|
||||
<button key="ok" style={buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
||||
{_('OK')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.cancelButtonShow !== false) {
|
||||
buttonComps.push(
|
||||
<button disabled={props.cancelButtonDisabled} key="cancel" style={Object.assign({}, buttonStyle)} onClick={cancelButton_click}>
|
||||
<button key="cancel" style={Object.assign({}, buttonStyle)} onClick={cancelButton_click}>
|
||||
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,13 @@ import styled from 'styled-components';
|
||||
|
||||
const Root = styled.div`
|
||||
display: flex;
|
||||
justify-content: ${props => props.justifyContent ? props.justifyContent : 'center'};
|
||||
justify-content: ${props => props.justifyContent ? props.justifyContent : 'flex-start'};
|
||||
font-family: ${props => props.theme.fontFamily};
|
||||
font-size: ${props => props.theme.fontSize * 1.5}px;
|
||||
line-height: 1.6em;
|
||||
color: ${props => props.theme.color};
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 1.2em;
|
||||
`;
|
||||
|
||||
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import dialogs from './dialogs';
|
||||
import bridge from '../services/bridge';
|
||||
import shared from '@joplin/lib/components/shared/encryption-config-shared';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, toggleAndSetupEncryption, getMasterPassword } from '@joplin/lib/services/e2ee/utils';
|
||||
import MasterKey from '@joplin/lib/models/MasterKey';
|
||||
import StyledInput from './style/StyledInput';
|
||||
import Button, { ButtonLevel } from './Button/Button';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const MasterPasswordInput = styled(StyledInput)`
|
||||
min-width: 300px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface Props {}
|
||||
|
||||
class EncryptionConfigScreenComponent extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
shared.initialize(this, props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted_ = false;
|
||||
shared.componentWillUnmount();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isMounted_ = true;
|
||||
shared.componentDidMount(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
shared.componentDidUpdate(this, prevProps);
|
||||
}
|
||||
|
||||
async checkPasswords() {
|
||||
return shared.checkPasswords(this);
|
||||
}
|
||||
|
||||
private renderMasterKey(mk: MasterKeyEntity, _isDefault: boolean) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const onToggleEnabledClick = () => {
|
||||
return shared.onToggleEnabledClick(this, mk);
|
||||
};
|
||||
|
||||
const passwordStyle = {
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
const onSaveClick = () => {
|
||||
return shared.onSavePasswordClick(this, mk);
|
||||
};
|
||||
|
||||
const onPasswordChange = (event: any) => {
|
||||
return shared.onPasswordChange(this, mk, event.target.value);
|
||||
};
|
||||
|
||||
const renderPasswordInput = (masterKeyId: string) => {
|
||||
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
|
||||
return (
|
||||
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
|
||||
({_('Master password')})
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<td style={theme.textStyle}>
|
||||
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
|
||||
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
|
||||
{_('Save')}
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
|
||||
const isActive = this.props.activeMasterKeyId === mk.id;
|
||||
const activeIcon = isActive ? '✔' : '';
|
||||
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
|
||||
return (
|
||||
<tr key={mk.id}>
|
||||
<td style={theme.textStyle}>{activeIcon}</td>
|
||||
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
|
||||
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
|
||||
{renderPasswordInput(mk.id)}
|
||||
<td style={theme.textStyle}>{passwordOk}</td>
|
||||
<td style={theme.textStyle}>
|
||||
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
renderNeedUpgradeSection() {
|
||||
if (!shim.isElectron()) return null;
|
||||
|
||||
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys);
|
||||
if (!needUpgradeMasterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const rows = [];
|
||||
const comp = this;
|
||||
|
||||
for (const mk of needUpgradeMasterKeys) {
|
||||
rows.push(
|
||||
<tr key={mk.id}>
|
||||
<td style={theme.textStyle}>{mk.id}</td>
|
||||
<td><button onClick={() => shared.upgradeMasterKey(comp, mk)} style={theme.buttonStyle}>Upgrade</button></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
|
||||
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
<th style={theme.textStyle}>{_('Upgrade')}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderReencryptData() {
|
||||
if (!shim.isElectron()) return null;
|
||||
if (!this.props.shouldReencrypt) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const buttonLabel = _('Re-encrypt data');
|
||||
|
||||
const intro = this.props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
|
||||
|
||||
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
|
||||
|
||||
t = t.replace(/\n\n/g, '</p><p>');
|
||||
t = t.replace(/\n/g, '<br>');
|
||||
t = `<p>${t}</p>`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
|
||||
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
||||
<span style={{ marginRight: 10 }}>
|
||||
<button onClick={() => shared.reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
|
||||
</span>
|
||||
|
||||
{ !this.props.shouldReencrypt ? null : <button onClick={() => shared.dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMasterKeySection(masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const mkComps = [];
|
||||
const showTable = isEnabledMasterKeys || this.state.showDisabledMasterKeys;
|
||||
const latestMasterKey = MasterKey.latest();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(this.renderMasterKey(mk, isEnabledMasterKeys && latestMasterKey && mk.id === latestMasterKey.id));
|
||||
}
|
||||
|
||||
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => shared.toggleShowDisabledMasterKeys(this) } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
|
||||
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
|
||||
const tableComp = !showTable ? null : (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('Active')}</th>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
<th style={theme.textStyle}>{_('Date')}</th>
|
||||
<th style={theme.textStyle}>{_('Password')}</th>
|
||||
<th style={theme.textStyle}>{_('Valid')}</th>
|
||||
<th style={theme.textStyle}>{_('Actions')}</th>
|
||||
</tr>
|
||||
{mkComps}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
if (mkComps.length) {
|
||||
return (
|
||||
<div>
|
||||
{headerComp}
|
||||
{tableComp}
|
||||
{infoComp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private renderMasterPassword() {
|
||||
// if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const onMasterPasswordSave = async () => {
|
||||
shared.onMasterPasswordSave(this);
|
||||
|
||||
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
|
||||
alert('Password is invalid. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
// const status = this.getMasterPasswordStatus();
|
||||
|
||||
// const statusMessages = {
|
||||
// [MasterPasswordStatus.NotSet]: 'Not set',
|
||||
// [MasterPasswordStatus.Valid]: '✓ ' + 'Valid',
|
||||
// [MasterPasswordStatus.Invalid]: '❌ ' + 'Invalid',
|
||||
// };
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<span style={theme.textStyle}>{_('Master password:')}</span>
|
||||
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}>{statusMessages[status]}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
// if (this.state.passwordChecks['master']) {
|
||||
// return (
|
||||
// <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
// <span style={theme.textStyle}>{_('Status:')}</span>
|
||||
// <span style={{ ...theme.textStyle, fontWeight: 'bold' }}>{statusMessages[status]}</span>
|
||||
// </div>
|
||||
// );
|
||||
// } else {
|
||||
|
||||
|
||||
// return (
|
||||
// <div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
// <span style={theme.textStyle}>❌ {'The master password is not set or is invalid. Please type it below:'}</span>
|
||||
// <div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
||||
// <MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
|
||||
// <Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
|
||||
|
||||
const containerStyle = Object.assign({}, theme.containerStyle, {
|
||||
padding: theme.configScreenPadding,
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
});
|
||||
|
||||
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
const isEnabled = getEncryptionEnabled();
|
||||
let masterKey = getDefaultMasterKey();
|
||||
|
||||
// If the user has explicitly disabled the master key, we generate a
|
||||
// new one. Needed for one the password has been forgotten.
|
||||
if (!masterKey.enabled) masterKey = null;
|
||||
|
||||
let answer = null;
|
||||
if (isEnabled) {
|
||||
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
} else {
|
||||
const msg = shared.enableEncryptionConfirmationMessages(masterKey);
|
||||
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
|
||||
}
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
try {
|
||||
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
|
||||
} catch (error) {
|
||||
await dialogs.alert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const decryptedItemsInfo = <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p>;
|
||||
const toggleButton = (
|
||||
<button
|
||||
style={theme.buttonStyle}
|
||||
onClick={() => {
|
||||
void onToggleButtonClick();
|
||||
}}
|
||||
>
|
||||
{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
|
||||
</button>
|
||||
);
|
||||
|
||||
const needUpgradeSection = this.renderNeedUpgradeSection();
|
||||
const reencryptDataSection = this.renderReencryptData();
|
||||
|
||||
const enabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => masterKeyEnabled(mk)), true);
|
||||
const disabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(
|
||||
<tr key={id}>
|
||||
<td style={theme.textStyle}>{id}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
|
||||
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={containerStyle}>
|
||||
{
|
||||
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
|
||||
<p style={theme.textStyle}>
|
||||
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
|
||||
<a
|
||||
onClick={() => {
|
||||
bridge().openExternal('https://joplinapp.org/e2ee/');
|
||||
}}
|
||||
href="#"
|
||||
style={theme.urlStyle}
|
||||
>
|
||||
https://joplinapp.org/e2ee/
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
<h1 style={theme.h1Style}>{_('Master password')}</h1>
|
||||
{this.renderMasterPassword()}
|
||||
|
||||
<h1 style={theme.h1Style}>{_('End-to-end encryption')}</h1>
|
||||
<p style={theme.textStyle}>
|
||||
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
|
||||
</p>
|
||||
{decryptedItemsInfo}
|
||||
{toggleButton}
|
||||
{needUpgradeSection}
|
||||
{this.props.shouldReencrypt ? reencryptDataSection : null}
|
||||
{enabledMasterKeySection}
|
||||
{disabledMasterKeySection}
|
||||
{nonExistingMasterKeySection}
|
||||
{!this.props.shouldReencrypt ? reencryptDataSection : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State) => {
|
||||
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
masterKeys: syncInfo.masterKeys,
|
||||
passwords: state.settings['encryption.passwordCache'],
|
||||
encryptionEnabled: syncInfo.e2ee,
|
||||
activeMasterKeyId: syncInfo.activeMasterKeyId,
|
||||
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
masterPassword: state.settings['encryption.masterPassword'],
|
||||
};
|
||||
};
|
||||
|
||||
const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent);
|
||||
|
||||
export default EncryptionConfigScreen;
|
||||
@@ -0,0 +1,366 @@
|
||||
const React = require('react');
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import dialogs from '../dialogs';
|
||||
import bridge from '../../services/bridge';
|
||||
import { decryptedStatText, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import StyledInput from '../style/StyledInput';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import styled from 'styled-components';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const MasterPasswordInput = styled(StyledInput)`
|
||||
min-width: 300px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
themeId: any;
|
||||
masterKeys: MasterKeyEntity[];
|
||||
passwords: Record<string, string>;
|
||||
notLoadedMasterKeys: string[];
|
||||
encryptionEnabled: boolean;
|
||||
shouldReencrypt: boolean;
|
||||
activeMasterKeyId: string;
|
||||
masterPassword: string;
|
||||
}
|
||||
|
||||
const EncryptionConfigScreen = (props: Props) => {
|
||||
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
|
||||
|
||||
const theme: any = useMemo(() => {
|
||||
return themeStyle(props.themeId);
|
||||
}, [props.themeId]);
|
||||
|
||||
const stats = useStats();
|
||||
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
||||
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
|
||||
|
||||
const onUpgradeMasterKey = useCallback((mk: MasterKeyEntity) => {
|
||||
void upgradeMasterKey(mk, passwordChecks, props.passwords);
|
||||
}, [passwordChecks, props.passwords]);
|
||||
|
||||
const renderNeedUpgradeSection = () => {
|
||||
if (!shim.isElectron()) return null;
|
||||
|
||||
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(props.masterKeys);
|
||||
if (!needUpgradeMasterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const rows = [];
|
||||
|
||||
for (const mk of needUpgradeMasterKeys) {
|
||||
rows.push(
|
||||
<tr key={mk.id}>
|
||||
<td style={theme.textStyle}>{mk.id}</td>
|
||||
<td><button onClick={() => onUpgradeMasterKey(mk)} style={theme.buttonStyle}>Upgrade</button></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
|
||||
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
<th style={theme.textStyle}>{_('Upgrade')}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderReencryptData = () => {
|
||||
if (!shim.isElectron()) return null;
|
||||
if (!props.shouldReencrypt) return null;
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
const buttonLabel = _('Re-encrypt data');
|
||||
|
||||
const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
|
||||
|
||||
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
|
||||
|
||||
t = t.replace(/\n\n/g, '</p><p>');
|
||||
t = t.replace(/\n/g, '<br>');
|
||||
t = `<p>${t}</p>`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
|
||||
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
||||
<span style={{ marginRight: 10 }}>
|
||||
<button onClick={() => void reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
|
||||
</span>
|
||||
|
||||
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMasterKey = (mk: MasterKeyEntity) => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const passwordStyle = {
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
|
||||
const isActive = props.activeMasterKeyId === mk.id;
|
||||
const activeIcon = isActive ? '✔' : '';
|
||||
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
|
||||
const renderPasswordInput = (masterKeyId: string) => {
|
||||
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
|
||||
return (
|
||||
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
|
||||
({_('Master password')})
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<td style={theme.textStyle}>
|
||||
<input type="password" style={passwordStyle} value={password} onChange={event => onInputPasswordChange(mk, event.target.value)} />{' '}
|
||||
<button style={theme.buttonStyle} onClick={() => onSavePasswordClick(mk, props.passwords)}>
|
||||
{_('Save')}
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<tr key={mk.id}>
|
||||
<td style={theme.textStyle}>{activeIcon}</td>
|
||||
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
|
||||
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
|
||||
{renderPasswordInput(mk.id)}
|
||||
<td style={theme.textStyle}>{passwordOk}</td>
|
||||
<td style={theme.textStyle}>
|
||||
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick(mk)}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMasterKeySection = (masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const mkComps = [];
|
||||
const showTable = isEnabledMasterKeys || showDisabledMasterKeys;
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(renderMasterKey(mk));
|
||||
}
|
||||
|
||||
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => toggleShowDisabledMasterKeys() } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
|
||||
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
|
||||
const tableComp = !showTable ? null : (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('Active')}</th>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
<th style={theme.textStyle}>{_('Date')}</th>
|
||||
<th style={theme.textStyle}>{_('Password')}</th>
|
||||
<th style={theme.textStyle}>{_('Valid')}</th>
|
||||
<th style={theme.textStyle}>{_('Actions')}</th>
|
||||
</tr>
|
||||
{mkComps}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
if (mkComps.length) {
|
||||
return (
|
||||
<div>
|
||||
{headerComp}
|
||||
{tableComp}
|
||||
{infoComp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
|
||||
|
||||
const renderMasterPassword = () => {
|
||||
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
if (passwordChecks['master']) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<span style={theme.textStyle}>{_('Master password:')}</span>
|
||||
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}>✔ {_('Loaded')}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={theme.textStyle}>❌ {'The master password is not set or is invalid. Please type it below:'}</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
||||
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={inputMasterPassword} onChange={(event: any) => onMasterPasswordChange(event.target.value)} />{' '}
|
||||
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const containerStyle = Object.assign({}, theme.containerStyle, {
|
||||
padding: theme.configScreenPadding,
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
});
|
||||
|
||||
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < props.masterKeys.length; i++) {
|
||||
const mk = props.masterKeys[i];
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
const isEnabled = getEncryptionEnabled();
|
||||
const masterKey = getDefaultMasterKey();
|
||||
|
||||
let answer = null;
|
||||
if (isEnabled) {
|
||||
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
} else {
|
||||
const msg = enableEncryptionConfirmationMessages(masterKey);
|
||||
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
|
||||
}
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
try {
|
||||
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
|
||||
} catch (error) {
|
||||
await dialogs.alert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const decryptedItemsInfo = <p style={theme.textStyle}>{decryptedStatText(stats)}</p>;
|
||||
const toggleButton = (
|
||||
<button
|
||||
style={theme.buttonStyle}
|
||||
onClick={() => {
|
||||
void onToggleButtonClick();
|
||||
}}
|
||||
>
|
||||
{props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
|
||||
</button>
|
||||
);
|
||||
|
||||
const needUpgradeSection = renderNeedUpgradeSection();
|
||||
const reencryptDataSection = renderReencryptData();
|
||||
|
||||
const enabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true);
|
||||
const disabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(
|
||||
<tr key={id}>
|
||||
<td style={theme.textStyle}>{id}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
|
||||
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={containerStyle}>
|
||||
{
|
||||
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
|
||||
<p style={theme.textStyle}>
|
||||
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
|
||||
<a
|
||||
onClick={() => {
|
||||
bridge().openExternal('https://joplinapp.org/e2ee/');
|
||||
}}
|
||||
href="#"
|
||||
style={theme.urlStyle}
|
||||
>
|
||||
https://joplinapp.org/e2ee/
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
<h1 style={theme.h1Style}>{_('Status')}</h1>
|
||||
<p style={theme.textStyle}>
|
||||
{_('Encryption is:')} <strong>{props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
|
||||
</p>
|
||||
{renderMasterPassword()}
|
||||
{decryptedItemsInfo}
|
||||
{toggleButton}
|
||||
{needUpgradeSection}
|
||||
{props.shouldReencrypt ? reencryptDataSection : null}
|
||||
{enabledMasterKeySection}
|
||||
{disabledMasterKeySection}
|
||||
{nonExistingMasterKeySection}
|
||||
{!props.shouldReencrypt ? reencryptDataSection : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
masterKeys: syncInfo.masterKeys,
|
||||
passwords: state.settings['encryption.passwordCache'],
|
||||
encryptionEnabled: syncInfo.e2ee,
|
||||
activeMasterKeyId: syncInfo.activeMasterKeyId,
|
||||
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
masterPassword: state.settings['encryption.masterPassword'],
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(EncryptionConfigScreen);
|
||||
@@ -0,0 +1,5 @@
|
||||
.encryption-config-test {
|
||||
& > .item {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { filename, basename } = require('@joplin/lib/path-utils');
|
||||
const { importEnex } = require('@joplin/lib/import-enex');
|
||||
const importEnex = require('@joplin/lib/import-enex').default;
|
||||
|
||||
class ImportScreenComponent extends React.Component {
|
||||
UNSAFE_componentWillMount() {
|
||||
|
||||
@@ -37,7 +37,6 @@ import { reg } from '@joplin/lib/registry';
|
||||
import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems';
|
||||
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
|
||||
import { MasterKeyEntity } from '../../../lib/services/e2ee/types';
|
||||
import commands from './commands/index';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
@@ -546,8 +545,8 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
bridge().restart();
|
||||
};
|
||||
|
||||
const onInvitationRespond = async (shareUserId: string, masterKey: MasterKeyEntity, accept: boolean) => {
|
||||
await ShareService.instance().respondInvitation(shareUserId, masterKey, accept);
|
||||
const onInvitationRespond = async (shareUserId: string, accept: boolean) => {
|
||||
await ShareService.instance().respondInvitation(shareUserId, accept);
|
||||
await ShareService.instance().refreshShareInvitations();
|
||||
void reg.scheduleSync(1000);
|
||||
};
|
||||
@@ -594,9 +593,9 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('%s (%s) would like to share a notebook with you.', sharer.full_name, sharer.email),
|
||||
_('Accept'),
|
||||
() => onInvitationRespond(invitation.id, invitation.master_key, true),
|
||||
() => onInvitationRespond(invitation.id, true),
|
||||
_('Reject'),
|
||||
() => onInvitationRespond(invitation.id, invitation.master_key, false)
|
||||
() => onInvitationRespond(invitation.id, false)
|
||||
);
|
||||
} else if (this.props.hasDisabledSyncItems) {
|
||||
msg = this.renderNotificationMessage(
|
||||
|
||||
@@ -19,6 +19,11 @@ export const runtime = (): CommandRuntime => {
|
||||
visible: !layoutItemProp(layout, 'noteList', 'visible'),
|
||||
});
|
||||
|
||||
// Toggling the sidebar will affect the size of most other on-screen components.
|
||||
// Dispatching a window resize event is a bit of a hack, but it ensures that any
|
||||
// component that watches for resizes will be accurately notified
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
context.dispatch({
|
||||
type: 'MAIN_LAYOUT_SET',
|
||||
value: newLayout,
|
||||
|
||||
@@ -19,6 +19,11 @@ export const runtime = (): CommandRuntime => {
|
||||
visible: !layoutItemProp(layout, 'sideBar', 'visible'),
|
||||
});
|
||||
|
||||
// Toggling the sidebar will affect the size of most other on-screen components.
|
||||
// Dispatching a window resize event is a bit of a hack, but it ensures that any
|
||||
// component that watches for resizes will be accurately notified
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
context.dispatch({
|
||||
type: 'MAIN_LAYOUT_SET',
|
||||
value: newLayout,
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
|
||||
import Dialog from '../Dialog';
|
||||
import DialogTitle from '../DialogTitle';
|
||||
import StyledInput from '../style/StyledInput';
|
||||
import { getMasterPasswordStatus, getMasterPasswordStatusMessage, masterPasswordIsValid, MasterPasswordStatus, updateMasterPassword } from '@joplin/lib/services/e2ee/utils';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
const [status, setStatus] = useState(MasterPasswordStatus.NotSet);
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [currentPasswordIsValid, setCurrentPasswordIsValid] = useState(false);
|
||||
const [password1, setPassword1] = useState('');
|
||||
const [password2, setPassword2] = useState('');
|
||||
const [saveButtonDisabled, setSaveButtonDisabled] = useState(true);
|
||||
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
||||
const [updatingPassword, setUpdatingPassword] = useState(false);
|
||||
|
||||
function closeDialog(dispatch: Function) {
|
||||
dispatch({
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'masterPassword',
|
||||
});
|
||||
}
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const newStatus = await getMasterPasswordStatus();
|
||||
if (event.cancelled) return;
|
||||
setStatus(newStatus);
|
||||
}, []);
|
||||
|
||||
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
|
||||
if (event.buttonName === 'cancel') {
|
||||
closeDialog(props.dispatch);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.buttonName === 'ok') {
|
||||
setUpdatingPassword(true);
|
||||
try {
|
||||
await updateMasterPassword(currentPassword, password1, () => reg.waitForSyncFinishedThenSync());
|
||||
closeDialog(props.dispatch);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
} finally {
|
||||
setUpdatingPassword(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [props.dispatch, currentPassword, password1]);
|
||||
|
||||
const onCurrentPasswordChange = useCallback((event: any) => {
|
||||
setCurrentPassword(event.target.value);
|
||||
}, []);
|
||||
|
||||
const onPasswordChange1 = useCallback((event: any) => {
|
||||
setPassword1(event.target.value);
|
||||
}, []);
|
||||
|
||||
const onPasswordChange2 = useCallback((event: any) => {
|
||||
setPassword2(event.target.value);
|
||||
}, []);
|
||||
|
||||
const onShowPasswordForm = useCallback(() => {
|
||||
setShowPasswordForm(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSaveButtonDisabled(updatingPassword || (!password1 || password1 !== password2));
|
||||
}, [password1, password2, updatingPassword]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowPasswordForm(status === MasterPasswordStatus.NotSet);
|
||||
}, [status]);
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const isValid = await masterPasswordIsValid(currentPassword);
|
||||
if (event.cancelled) return;
|
||||
setCurrentPasswordIsValid(isValid);
|
||||
}, [currentPassword]);
|
||||
|
||||
function renderCurrentPasswordIcon() {
|
||||
if (!currentPassword || status === MasterPasswordStatus.NotSet) return null;
|
||||
return currentPasswordIsValid ? <i className="fas fa-check"></i> : <i className="fas fa-times"></i>;
|
||||
}
|
||||
|
||||
function renderPasswordForm() {
|
||||
if (showPasswordForm) {
|
||||
return (
|
||||
<div>
|
||||
<div className="form">
|
||||
<div className="form-input-group">
|
||||
<label>{'Current password'}</label>
|
||||
<div className="current-password-wrapper">
|
||||
<StyledInput
|
||||
disabled={status === MasterPasswordStatus.NotSet}
|
||||
placeholder={status === MasterPasswordStatus.NotSet ? `(${_('Not set')})` : ''}
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={onCurrentPasswordChange}
|
||||
/>
|
||||
{renderCurrentPasswordIcon()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-input-group">
|
||||
<label>{'Enter new password'}</label>
|
||||
<StyledInput type="password" value={password1} onChange={onPasswordChange1}/>
|
||||
</div>
|
||||
<div className="form-input-group">
|
||||
<label>{'Re-enter password'}</label>
|
||||
<StyledInput type="password" value={password2} onChange={onPasswordChange2}/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="bold">Please make sure you remember your password. For security reasons, it is not possible to recover it if it is lost.</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<p>
|
||||
<a onClick={onShowPasswordForm} href="#">Change master password</a>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
return (
|
||||
<div className="dialog-content">
|
||||
<p>Your master password is used to protect sensitive information. In particular, it is used to encrypt your notes when end-to-end encryption (E2EE) is enabled, or to share and encrypt notes with someone who has E2EE enabled.</p>
|
||||
<p>
|
||||
<span>{'Master password status:'}</span> <span className="bold">{getMasterPasswordStatusMessage(status)}</span>
|
||||
</p>
|
||||
{renderPasswordForm()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDialogWrapper() {
|
||||
return (
|
||||
<div className="dialog-root">
|
||||
<DialogTitle title={_('Master password')}/>
|
||||
{renderContent()}
|
||||
<DialogButtonRow
|
||||
themeId={props.themeId}
|
||||
onClick={onButtonRowClick}
|
||||
okButtonLabel={_('Save')}
|
||||
okButtonDisabled={saveButtonDisabled}
|
||||
cancelButtonDisabled={updatingPassword}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog className="master-password-dialog" renderContent={renderDialogWrapper}/>
|
||||
);
|
||||
}
|
||||
@@ -752,7 +752,6 @@ function useMenu(props: Props) {
|
||||
|
||||
rootMenus.go.submenu.push(menuItemDic.gotoAnything);
|
||||
rootMenus.tools.submenu.push(menuItemDic.commandPalette);
|
||||
rootMenus.tools.submenu.push(menuItemDic.openMasterPasswordDialog);
|
||||
|
||||
for (const view of props.pluginMenuItems) {
|
||||
const location: MenuItemLocation = view.location;
|
||||
|
||||
@@ -6,7 +6,8 @@ import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
|
||||
import { CommandValue } from '../../utils/types';
|
||||
import { useScrollHandler, usePrevious, cursorPositionToTextOffset, useRootSize } from './utils';
|
||||
import { useScrollHandler, usePrevious, cursorPositionToTextOffset } from './utils';
|
||||
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
||||
import Toolbar from './Toolbar';
|
||||
import styles_ from './styles';
|
||||
import { RenderedBody, defaultRenderedBody } from './utils/types';
|
||||
@@ -59,7 +60,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
const props_onChangeRef = useRef<Function>(null);
|
||||
props_onChangeRef.current = props.onChange;
|
||||
|
||||
const rootSize = useRootSize({ rootRef });
|
||||
const rootSize = useElementSize(rootRef);
|
||||
|
||||
usePluginServiceRegistration(ref);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useCallback, useRef, useState } from 'react';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export function cursorPositionToTextOffset(cursorPos: any, body: string) {
|
||||
@@ -89,21 +89,3 @@ export function useScrollHandler(editorRef: any, webviewRef: any, onScroll: Func
|
||||
return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll };
|
||||
}
|
||||
|
||||
|
||||
export function useRootSize(dependencies: any) {
|
||||
const { rootRef } = dependencies;
|
||||
|
||||
const [rootSize, setRootSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootRef.current) return;
|
||||
|
||||
const { width, height } = rootRef.current.getBoundingClientRect();
|
||||
|
||||
if (rootSize.width !== width || rootSize.height !== height) {
|
||||
setRootSize({ width: width, height: height });
|
||||
}
|
||||
});
|
||||
|
||||
return rootSize;
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ export default function useKeymap(CodeMirror: any) {
|
||||
'Cmd-Right': 'goLineRightSmart',
|
||||
'Alt-Backspace': 'delGroupBefore',
|
||||
'Alt-Delete': 'delGroupAfter',
|
||||
'Cmd-Backspace': 'delWrappedLineLeft',
|
||||
|
||||
'fallthrough': 'basic',
|
||||
};
|
||||
|
||||
@@ -793,7 +793,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
};
|
||||
}
|
||||
|
||||
await loadDocumentAssets(editor, await props.allAssets(props.contentMarkupLanguage));
|
||||
await loadDocumentAssets(editor, await props.allAssets(props.contentMarkupLanguage, { contentMaxWidthTarget: '.mce-content-body' }));
|
||||
|
||||
dispatchDidUpdate(editor);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import useMarkupToHtml from './utils/useMarkupToHtml';
|
||||
import useFormNote, { OnLoadEvent } from './utils/useFormNote';
|
||||
import useFolder from './utils/useFolder';
|
||||
import styles_ from './styles';
|
||||
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types';
|
||||
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions } from './utils/types';
|
||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import ToolbarButton from '../ToolbarButton/ToolbarButton';
|
||||
@@ -151,7 +151,12 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
plugins: props.plugins,
|
||||
});
|
||||
|
||||
const allAssets = useCallback(async (markupLanguage: number): Promise<any[]> => {
|
||||
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {
|
||||
options = {
|
||||
contentMaxWidthTarget: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
|
||||
@@ -159,7 +164,10 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
customCss: props.customCss,
|
||||
});
|
||||
|
||||
return markupToHtml.allAssets(markupLanguage, theme, { contentMaxWidth: props.contentMaxWidth });
|
||||
return markupToHtml.allAssets(markupLanguage, theme, {
|
||||
contentMaxWidth: props.contentMaxWidth,
|
||||
contentMaxWidthTarget: options.contentMaxWidthTarget,
|
||||
});
|
||||
}, [props.themeId, props.customCss, props.contentMaxWidth]);
|
||||
|
||||
const handleProvisionalFlag = useCallback(() => {
|
||||
|
||||
@@ -69,8 +69,17 @@ export function htmlToClipboardData(html: string): ClipboardData {
|
||||
// In that case we need to set both HTML and Text context, otherwise it
|
||||
// won't be possible to paste the text in, for example, a text editor.
|
||||
// https://github.com/laurent22/joplin/issues/4788
|
||||
//
|
||||
// Also we don't escape the content produced in HTML to MD conversion
|
||||
// because it's not what would be expected. For example, if the content is
|
||||
// `* something`, strictly speaking it would be correct to escape to `\*
|
||||
// something`, however this is not what the user would expect when copying
|
||||
// text. Likewise for URLs that contain "_". So the resulting Markdown might
|
||||
// not be perfectly valid but would be closer to what a user would expect.
|
||||
// If they want accurate MArkdown they can switch to the MD editor.
|
||||
// https://github.com/laurent22/joplin/issues/5440
|
||||
return {
|
||||
text: htmlToMd().parse(copyableContent),
|
||||
text: htmlToMd().parse(copyableContent, { disableEscapeContent: true }),
|
||||
html: cleanUpCodeBlocks(copyableContent),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
|
||||
import { MarkupToHtmlOptions } from './useMarkupToHtml';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
}
|
||||
|
||||
export interface ToolbarButtonInfos {
|
||||
[key: string]: ToolbarButtonInfo;
|
||||
}
|
||||
@@ -55,7 +59,7 @@ export interface NoteBodyEditorProps {
|
||||
onScroll(event: any): void;
|
||||
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
||||
htmlToMarkdown: Function;
|
||||
allAssets: (markupLanguage: MarkupLanguage)=> Promise<RenderResultPluginAsset[]>;
|
||||
allAssets: (markupLanguage: MarkupLanguage, options: AllAssetsOptions)=> Promise<RenderResultPluginAsset[]>;
|
||||
disabled: boolean;
|
||||
dispatch: Function;
|
||||
noteToolbar: any;
|
||||
|
||||
@@ -139,9 +139,10 @@ class NoteSearchBarComponent extends React.Component {
|
||||
color: theme.colorFaded,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
});
|
||||
const matchesFoundString = (query.length > 0 && this.props.resultCount > 0) ? (
|
||||
|
||||
const matchesFoundString = (query.length > 0) ? (
|
||||
<div style={textStyle}>
|
||||
{`${this.props.selectedIndex + 1} / ${this.props.resultCount}`}
|
||||
{`${this.props.resultCount === 0 ? 0 : this.props.selectedIndex + 1} / ${this.props.resultCount}`}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import DialogTitle from './DialogTitle';
|
||||
import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow';
|
||||
import Dialog from './Dialog';
|
||||
import SyncWizardDialog from './SyncWizard/Dialog';
|
||||
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
@@ -62,12 +61,6 @@ const registeredDialogs: Record<string, RegisteredDialog> = {
|
||||
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId}/>;
|
||||
},
|
||||
},
|
||||
|
||||
masterPassword: {
|
||||
render: (props: RegisteredDialogProps) => {
|
||||
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId}/>;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
|
||||
@@ -171,7 +171,7 @@ function ShareFolderDialog(props: Props) {
|
||||
try {
|
||||
setLatestError(null);
|
||||
const share = await ShareService.instance().shareFolder(props.folderId);
|
||||
await ShareService.instance().addShareRecipient(share.id, share.master_key_id, recipientEmail);
|
||||
await ShareService.instance().addShareRecipient(share.id, recipientEmail);
|
||||
await Promise.all([
|
||||
ShareService.instance().refreshShares(),
|
||||
ShareService.instance().refreshShareUsers(share.id),
|
||||
|
||||
@@ -138,9 +138,9 @@ const syncTargetNames: string[] = [
|
||||
|
||||
|
||||
const logosImageNames: Record<string, string> = {
|
||||
'dropbox': 'Dropbox.svg',
|
||||
'joplinCloud': 'JoplinCloud.svg',
|
||||
'onedrive': 'OneDrive.svg',
|
||||
'dropbox': 'SyncTarget_Dropbox.svg',
|
||||
'joplinCloud': 'SyncTarget_JoplinCloud.svg',
|
||||
'onedrive': 'SyncTarget_OneDrive.svg',
|
||||
};
|
||||
|
||||
export default function(props: Props) {
|
||||
@@ -274,7 +274,7 @@ export default function(props: Props) {
|
||||
const height = info.name !== 'joplinCloud' ? descriptionHeight : null;
|
||||
|
||||
const logoImageName = logosImageNames[info.name];
|
||||
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/syncTargetLogos/${logoImageName}` : '';
|
||||
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/${logoImageName}` : '';
|
||||
const logo = logoImageSrc ? <SyncTargetLogo src={logoImageSrc}/> : null;
|
||||
const descriptionComp = <SyncTargetDescription height={height} ref={info.name === 'joplinCloud' ? joplinCloudDescriptionRef : null}>{info.description}</SyncTargetDescription>;
|
||||
const featuresComp = showJoplinCloudForm && info.name === 'joplinCloud' ? null : renderFeatures(info.name);
|
||||
|
||||
@@ -49,6 +49,5 @@ export default function() {
|
||||
'showShareFolderDialog',
|
||||
'gotoAnything',
|
||||
'commandPalette',
|
||||
'openMasterPasswordDialog',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const gulp = require('gulp');
|
||||
const utils = require('@joplin/tools/gulp/utils');
|
||||
const compileSass = require('@joplin/tools/compileSass');
|
||||
|
||||
const tasks = {
|
||||
compileScripts: {
|
||||
@@ -20,6 +21,14 @@ const tasks = {
|
||||
tsc: require('@joplin/tools/gulp/tasks/tsc'),
|
||||
updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
buildCommandIndex: require('@joplin/tools/gulp/tasks/buildCommandIndex'),
|
||||
compileSass: {
|
||||
fn: async () => {
|
||||
const guiDir = `${__dirname}/gui`;
|
||||
await compileSass([
|
||||
`${guiDir}/EncryptionConfigScreen/style.scss`,
|
||||
], `${__dirname}/style.min.css`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
@@ -31,6 +40,7 @@ const buildParallel = [
|
||||
'copyTinyMceLangs',
|
||||
'updateIgnoredTypeScriptBuild',
|
||||
'buildCommandIndex',
|
||||
'compileSass',
|
||||
];
|
||||
|
||||
gulp.task('build', gulp.parallel(...buildParallel));
|
||||
|
||||
4
packages/app-desktop/package-lock.json
generated
4
packages/app-desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.8",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
# Setup the sync parameters for user X and create a few folders and notes to
|
||||
# allow sharing. Also calls the API to create the test users and clear the data.
|
||||
|
||||
# For example, to setup a user for sharing, and another as recipient with E2EE
|
||||
# enabled:
|
||||
|
||||
# ./runForTesting.sh 1 createUsers,createData,reset,e2ee,sync && ./runForTesting.sh 2 reset,e2ee,sync && ./runForTesting.sh 1
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
@@ -55,21 +50,12 @@ do
|
||||
echo "config sync.target 10" >> "$CMD_FILE"
|
||||
# echo "config sync.10.path http://api.joplincloud.local:22300" >> "$CMD_FILE"
|
||||
echo "config sync.10.username $USER_EMAIL" >> "$CMD_FILE"
|
||||
echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE"
|
||||
echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE"
|
||||
|
||||
elif [[ $CMD == "e2ee" ]]; then
|
||||
|
||||
echo "e2ee enable --password 111111" >> "$CMD_FILE"
|
||||
|
||||
elif [[ $CMD == "sync" ]]; then
|
||||
|
||||
echo "sync" >> "$CMD_FILE"
|
||||
|
||||
# elif [[ $CMD == "generatePpk" ]]; then
|
||||
|
||||
# echo "e2ee generate-ppk --password 111111" >> "$CMD_FILE"
|
||||
# echo "sync" >> "$CMD_FILE"
|
||||
|
||||
else
|
||||
|
||||
echo "Unknown command: $CMD"
|
||||
|
||||
@@ -143,87 +143,4 @@ a {
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* =========================================================================================
|
||||
General classes
|
||||
========================================================================================= */
|
||||
|
||||
* {
|
||||
color: var(--joplin-color);
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form > .form-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form > .form-input-group > label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p,
|
||||
div.form,
|
||||
.form > .form-input-group {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form > .form-input-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--joplin-url-color);
|
||||
}
|
||||
|
||||
/* =========================================================================================
|
||||
Component-specific classes
|
||||
========================================================================================= */
|
||||
|
||||
.master-password-dialog .dialog-root {
|
||||
min-width: 500px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.master-password-dialog .dialog-content {
|
||||
background-color: var(--joplin-background-color3);
|
||||
padding: 1em;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.master-password-dialog .current-password-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.master-password-dialog .current-password-wrapper input {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.master-password-dialog .fa-check {
|
||||
color: var(--joplin-color-correct);
|
||||
}
|
||||
|
||||
.master-password-dialog .fa-times {
|
||||
color: var(--joplin-color-error);
|
||||
}
|
||||
5
packages/app-desktop/style.min.css
vendored
Normal file
5
packages/app-desktop/style.min.css
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.encryption-config-test > .item {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.min.css.map */
|
||||
@@ -141,8 +141,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097649
|
||||
versionName "2.4.1"
|
||||
versionCode 2097650
|
||||
versionName "2.4.2"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -2,68 +2,56 @@ const React = require('react');
|
||||
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('../screen-header.js');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import shared from '@joplin/lib/components/shared/encryption-config-shared';
|
||||
import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
interface Props {}
|
||||
interface Props {
|
||||
themeId: any;
|
||||
masterKeys: MasterKeyEntity[];
|
||||
passwords: Record<string, string>;
|
||||
notLoadedMasterKeys: string[];
|
||||
encryptionEnabled: boolean;
|
||||
shouldReencrypt: boolean;
|
||||
activeMasterKeyId: string;
|
||||
masterPassword: string;
|
||||
}
|
||||
|
||||
class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
static navigationOptions(): any {
|
||||
return { header: null };
|
||||
}
|
||||
const EncryptionConfigScreen = (props: Props) => {
|
||||
const [passwordPromptShow, setPasswordPromptShow] = useState(false);
|
||||
const [passwordPromptAnswer, setPasswordPromptAnswer] = useState('');
|
||||
const [passwordPromptConfirmAnswer, setPasswordPromptConfirmAnswer] = useState('');
|
||||
const stats = useStats();
|
||||
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
||||
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
|
||||
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
|
||||
const dialogBoxRef = useRef(null);
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const mkComps = [];
|
||||
|
||||
this.state = {
|
||||
passwordPromptShow: false,
|
||||
passwordPromptAnswer: '',
|
||||
passwordPromptConfirmAnswer: '',
|
||||
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
|
||||
|
||||
const theme: any = useMemo(() => {
|
||||
return themeStyle(props.themeId);
|
||||
}, [props.themeId]);
|
||||
|
||||
const rootStyle = useMemo(() => {
|
||||
return {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
shared.initialize(this, props);
|
||||
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
async refreshStats() {
|
||||
return shared.refreshStats(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isMounted_ = true;
|
||||
shared.componentDidMount(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
shared.componentDidUpdate(this, prevProps);
|
||||
}
|
||||
|
||||
async checkPasswords() {
|
||||
return shared.checkPasswords(this);
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
const styles = useMemo(() => {
|
||||
const styles = {
|
||||
titleText: {
|
||||
flex: 1,
|
||||
@@ -93,39 +81,32 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
},
|
||||
};
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
return StyleSheet.create(styles);
|
||||
}, [theme]);
|
||||
|
||||
renderMasterKey(_num: number, mk: MasterKeyEntity) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null;
|
||||
|
||||
const onSaveClick = () => {
|
||||
return shared.onSavePasswordClick(this, mk);
|
||||
};
|
||||
const renderMasterKey = (_num: number, mk: MasterKeyEntity) => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const onPasswordChange = (text: string) => {
|
||||
return shared.onPasswordChange(this, mk, text);
|
||||
};
|
||||
|
||||
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
|
||||
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
|
||||
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
|
||||
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
|
||||
inputStyle.borderBottomWidth = 1;
|
||||
inputStyle.borderBottomColor = theme.dividerColor;
|
||||
|
||||
const renderPasswordInput = (masterKeyId: string) => {
|
||||
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
|
||||
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
|
||||
return (
|
||||
<Text style={{ ...this.styles().normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
|
||||
<Text style={{ ...styles.normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onInputPasswordChange(mk, text)} style={inputStyle}></TextInput>
|
||||
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
|
||||
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
|
||||
<Button title={_('Save')} onPress={() => onSavePasswordClick(mk, props.passwords)}></Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -133,69 +114,65 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
|
||||
return (
|
||||
<View key={mk.id}>
|
||||
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
|
||||
<Text style={styles.titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
|
||||
<Text style={styles.normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
|
||||
{renderPasswordInput(mk.id)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
passwordPromptComponent() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
let masterKey = getDefaultMasterKey();
|
||||
|
||||
// If the user has explicitly disabled the master key, we generate a
|
||||
// new one. Needed for one the password has been forgotten.
|
||||
if (!masterKey.enabled) masterKey = null;
|
||||
const renderPasswordPrompt = () => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const masterKey = getDefaultMasterKey();
|
||||
|
||||
const onEnableClick = async () => {
|
||||
try {
|
||||
const password = this.state.passwordPromptAnswer;
|
||||
const password = passwordPromptAnswer;
|
||||
if (!password) throw new Error(_('Password cannot be empty'));
|
||||
const password2 = this.state.passwordPromptConfirmAnswer;
|
||||
const password2 = passwordPromptConfirmAnswer;
|
||||
if (!password2) throw new Error(_('Confirm password cannot be empty'));
|
||||
if (password !== password2) throw new Error(_('Passwords do not match!'));
|
||||
await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, password);
|
||||
// await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password);
|
||||
this.setState({ passwordPromptShow: false });
|
||||
setPasswordPromptShow(false);
|
||||
} catch (error) {
|
||||
await dialogs.error(this, error.message);
|
||||
alert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const messages = shared.enableEncryptionConfirmationMessages(masterKey);
|
||||
const messages = enableEncryptionConfirmationMessages(masterKey);
|
||||
|
||||
const messageComps = messages.map(msg => {
|
||||
const messageComps = messages.map((msg: string) => {
|
||||
return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>;
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
|
||||
<View>{messageComps}</View>
|
||||
<Text style={this.styles().normalText}>{_('Password:')}</Text>
|
||||
<Text style={styles.normalText}>{_('Password:')}</Text>
|
||||
<TextInput
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
style={this.styles().normalTextInput}
|
||||
style={styles.normalTextInput}
|
||||
secureTextEntry={true}
|
||||
value={this.state.passwordPromptAnswer}
|
||||
value={passwordPromptAnswer}
|
||||
onChangeText={(text: string) => {
|
||||
this.setState({ passwordPromptAnswer: text });
|
||||
setPasswordPromptAnswer(text);
|
||||
}}
|
||||
></TextInput>
|
||||
|
||||
<Text style={this.styles().normalText}>{_('Confirm password:')}</Text>
|
||||
<Text style={styles.normalText}>{_('Confirm password:')}</Text>
|
||||
<TextInput
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
style={this.styles().normalTextInput}
|
||||
style={styles.normalTextInput}
|
||||
secureTextEntry={true}
|
||||
value={this.state.passwordPromptConfirmAnswer}
|
||||
value={passwordPromptConfirmAnswer}
|
||||
onChangeText={(text: string) => {
|
||||
this.setState({ passwordPromptConfirmAnswer: text });
|
||||
setPasswordPromptConfirmAnswer(text);
|
||||
}}
|
||||
></TextInput>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
@@ -211,156 +188,132 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
<Button
|
||||
title={_('Cancel')}
|
||||
onPress={() => {
|
||||
this.setState({ passwordPromptShow: false });
|
||||
setPasswordPromptShow(false);
|
||||
}}
|
||||
></Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private renderMasterPassword() {
|
||||
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const onMasterPasswordSave = async () => {
|
||||
shared.onMasterPasswordSave(this);
|
||||
|
||||
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
|
||||
alert('Password is invalid. Please try again.');
|
||||
}
|
||||
};
|
||||
const renderMasterPassword = () => {
|
||||
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
|
||||
|
||||
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
|
||||
inputStyle.borderBottomWidth = 1;
|
||||
inputStyle.borderBottomColor = theme.dividerColor;
|
||||
|
||||
if (this.state.passwordChecks['master']) {
|
||||
if (passwordChecks['master']) {
|
||||
return (
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ ...this.styles().normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
|
||||
<Text style={{ ...this.styles().normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
|
||||
<Text style={{ ...styles.normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
|
||||
<Text style={{ ...styles.normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
|
||||
<Text style={this.styles().normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
|
||||
<Text style={styles.normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
|
||||
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={this.state.masterPasswordInput} onChangeText={(text: string) => shared.onMasterPasswordChange(this, text)} style={inputStyle}></TextInput>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={inputMasterPassword} onChangeText={(text: string) => onMasterPasswordChange(text)} style={inputStyle}></TextInput>
|
||||
<Button onPress={onMasterPasswordSave} title={_('Save')} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
for (let i = 0; i < props.masterKeys.length; i++) {
|
||||
const mk = props.masterKeys[i];
|
||||
mkComps.push(renderMasterKey(i + 1, mk));
|
||||
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const masterKeys = this.props.masterKeys;
|
||||
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
|
||||
const onToggleButtonClick = async () => {
|
||||
if (props.encryptionEnabled) {
|
||||
const ok = await dialogs.confirmRef(dialogBoxRef.current, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
if (!ok) return;
|
||||
|
||||
const mkComps = [];
|
||||
|
||||
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(this.renderMasterKey(i + 1, mk));
|
||||
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
try {
|
||||
await setupAndDisableEncryption(EncryptionService.instance());
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
} else {
|
||||
setPasswordPromptShow(true);
|
||||
setPasswordPromptAnswer('');
|
||||
setPasswordPromptConfirmAnswer('');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
if (this.props.encryptionEnabled) {
|
||||
const ok = await dialogs.confirm(this, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
if (!ok) return;
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
try {
|
||||
await setupAndDisableEncryption(EncryptionService.instance());
|
||||
} catch (error) {
|
||||
await dialogs.error(this, error.message);
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
passwordPromptShow: true,
|
||||
passwordPromptAnswer: '',
|
||||
passwordPromptConfirmAnswer: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(
|
||||
<Text style={this.styles().normalText} key={id}>
|
||||
{id}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<View>
|
||||
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
|
||||
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
|
||||
<View style={{ marginTop: 10 }}>{rows}</View>
|
||||
</View>
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(
|
||||
<Text style={styles.normalText} key={id}>
|
||||
{id}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
|
||||
const toggleButton = !this.state.passwordPromptShow ? (
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.themeId).root}>
|
||||
<ScreenHeader title={_('Encryption Config')} />
|
||||
<ScrollView style={this.styles().container}>
|
||||
{
|
||||
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
|
||||
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/e2ee/');
|
||||
}}
|
||||
>
|
||||
<Text>https://joplinapp.org/e2ee/</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
|
||||
<Text style={this.styles().titleText}>{_('Status')}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
|
||||
{decryptedItemsInfo}
|
||||
{this.renderMasterPassword()}
|
||||
{toggleButton}
|
||||
{passwordPromptComp}
|
||||
{mkComps}
|
||||
{nonExistingMasterKeySection}
|
||||
<View style={{ flex: 1, height: 20 }}></View>
|
||||
</ScrollView>
|
||||
<DialogBox
|
||||
ref={(dialogbox: any) => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
nonExistingMasterKeySection = (
|
||||
<View>
|
||||
<Text style={styles.titleText}>{_('Missing Master Keys')}</Text>
|
||||
<Text style={styles.normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
|
||||
<View style={{ marginTop: 10 }}>{rows}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const EncryptionConfigScreen = connect((state: State) => {
|
||||
const passwordPromptComp = passwordPromptShow ? renderPasswordPrompt() : null;
|
||||
const toggleButton = !passwordPromptShow ? (
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<Button title={props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader title={_('Encryption Config')} />
|
||||
<ScrollView style={styles.container}>
|
||||
{
|
||||
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
|
||||
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/e2ee/');
|
||||
}}
|
||||
>
|
||||
<Text>https://joplinapp.org/e2ee/</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
|
||||
<Text style={styles.titleText}>{_('Status')}</Text>
|
||||
<Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
|
||||
{decryptedItemsInfo}
|
||||
{renderMasterPassword()}
|
||||
{toggleButton}
|
||||
{passwordPromptComp}
|
||||
{mkComps}
|
||||
{nonExistingMasterKeySection}
|
||||
<View style={{ flex: 1, height: 20 }}></View>
|
||||
</ScrollView>
|
||||
<DialogBox ref={dialogBoxRef}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect((state: State) => {
|
||||
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
||||
|
||||
return {
|
||||
@@ -372,6 +325,4 @@ const EncryptionConfigScreen = connect((state: State) => {
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
masterPassword: state.settings['encryption.masterPassword'],
|
||||
};
|
||||
})(EncryptionConfigScreenComponent);
|
||||
|
||||
export default EncryptionConfigScreen;
|
||||
})(EncryptionConfigScreen);
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"postinstall": "jetify && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "^2.2.0",
|
||||
"@joplin/renderer": "^2.2.0",
|
||||
"@joplin/lib": "~2.4",
|
||||
"@joplin/renderer": "~2.4",
|
||||
"@react-native-community/clipboard": "^1.5.0",
|
||||
"@react-native-community/datetimepicker": "^3.0.3",
|
||||
"@react-native-community/geolocation": "^2.0.2",
|
||||
@@ -69,7 +69,7 @@
|
||||
"@codemirror/lang-markdown": "^0.18.4",
|
||||
"@codemirror/state": "^0.18.7",
|
||||
"@codemirror/view": "^0.18.19",
|
||||
"@joplin/tools": "^1.0.9",
|
||||
"@joplin/tools": "~2.4",
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@types/node": "^14.14.6",
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = `LyoKCkF0b20gT25lIERhcmsgV2l0aCBzdXBwb3J0IGZvciBSZWFzb25NTCBieSBHaWRpIE1vcnJpcywgYmFzZWQgb2ZmIHdvcmsgYnkgRGFuaWVsIEdhbWFnZQoKT3JpZ2luYWwgT25lIERhcmsgU3ludGF4IHRoZW1lIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL2F0b20vb25lLWRhcmstc3ludGF4CgoqLwouaGxqcyB7CiAgZGlzcGxheTogYmxvY2s7CiAgb3ZlcmZsb3cteDogYXV0bzsKICBwYWRkaW5nOiAwLjVlbTsKICBjb2xvcjogI2FiYjJiZjsKICBiYWNrZ3JvdW5kOiAjMjgyYzM0Owp9Ci5obGpzLWtleXdvcmQsIC5obGpzLW9wZXJhdG9yIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIC5obGpzLWNvbnN0cnVjdG9yIHsKICBjb2xvcjogIzYxYWVlZTsKfQouaGxqcy1mdW5jdGlvbiB7CiAgY29sb3I6ICM2MWFlZWU7Cn0KLmhsanMtZnVuY3Rpb24gLmhsanMtcGFyYW1zIHsKICBjb2xvcjogI0E2RTIyRTsKfQouaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5nIHsKICBjb2xvcjogI0ZEOTcxRjsKfQouaGxqcy1tb2R1bGUtYWNjZXNzIC5obGpzLW1vZHVsZSB7CiAgY29sb3I6ICM3ZTU3YzI7Cn0KLmhsanMtY29uc3RydWN0b3IgewogIGNvbG9yOiAjZTJiOTNkOwp9Ci5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZyB7CiAgY29sb3I6ICM5Q0NDNjU7Cn0KLmhsanMtY29tbWVudCwgLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYjE4ZWIxOwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQouaGxqcy1kb2N0YWcsIC5obGpzLWZvcm11bGEgewogIGNvbG9yOiAjYzY3OGRkOwp9Ci5obGpzLXNlY3Rpb24sIC5obGpzLW5hbWUsIC5obGpzLXNlbGVjdG9yLXRhZywgLmhsanMtZGVsZXRpb24sIC5obGpzLXN1YnN0IHsKICBjb2xvcjogI2UwNmM3NTsKfQouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzU2YjZjMjsKfQouaGxqcy1zdHJpbmcsIC5obGpzLXJlZ2V4cCwgLmhsanMtYWRkaXRpb24sIC5obGpzLWF0dHJpYnV0ZSwgLmhsanMtbWV0YS1zdHJpbmcgewogIGNvbG9yOiAjOThjMzc5Owp9Ci5obGpzLWJ1aWx0X2luLCAuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSB7CiAgY29sb3I6ICNlNmMwN2I7Cn0KLmhsanMtYXR0ciwgLmhsanMtdmFyaWFibGUsIC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLCAuaGxqcy10eXBlLCAuaGxqcy1zZWxlY3Rvci1jbGFzcywgLmhsanMtc2VsZWN0b3ItYXR0ciwgLmhsanMtc2VsZWN0b3ItcHNldWRvLCAuaGxqcy1udW1iZXIgewogIGNvbG9yOiAjZDE5YTY2Owp9Ci5obGpzLXN5bWJvbCwgLmhsanMtYnVsbGV0LCAuaGxqcy1saW5rLCAuaGxqcy1tZXRhLCAuaGxqcy1zZWxlY3Rvci1pZCwgLmhsanMtdGl0bGUgewogIGNvbG9yOiAjNjFhZWVlOwp9Ci5obGpzLWVtcGhhc2lzIHsKICBmb250LXN0eWxlOiBpdGFsaWM7Cn0KLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQouaGxqcy1saW5rIHsKICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsKfQo=`;
|
||||
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiNhYmIyYmY7YmFja2dyb3VuZDojMjgyYzM0fS5obGpzLWtleXdvcmQsLmhsanMtb3BlcmF0b3IsLmhsanMtcGF0dGVybi1tYXRjaHtjb2xvcjojZjkyNjcyfS5obGpzLWZ1bmN0aW9uLC5obGpzLXBhdHRlcm4tbWF0Y2ggLmhsanMtY29uc3RydWN0b3J7Y29sb3I6IzYxYWVlZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXN7Y29sb3I6I2E2ZTIyZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5ne2NvbG9yOiNmZDk3MWZ9LmhsanMtbW9kdWxlLWFjY2VzcyAuaGxqcy1tb2R1bGV7Y29sb3I6IzdlNTdjMn0uaGxqcy1jb25zdHJ1Y3Rvcntjb2xvcjojZTJiOTNkfS5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZ3tjb2xvcjojOWNjYzY1fS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2IxOGViMTtmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYXtjb2xvcjojYzY3OGRkfS5obGpzLWRlbGV0aW9uLC5obGpzLW5hbWUsLmhsanMtc2VjdGlvbiwuaGxqcy1zZWxlY3Rvci10YWcsLmhsanMtc3Vic3R7Y29sb3I6I2UwNmM3NX0uaGxqcy1saXRlcmFse2NvbG9yOiM1NmI2YzJ9LmhsanMtYWRkaXRpb24sLmhsanMtYXR0cmlidXRlLC5obGpzLW1ldGEgLmhsanMtc3RyaW5nLC5obGpzLXJlZ2V4cCwuaGxqcy1zdHJpbmd7Y29sb3I6Izk4YzM3OX0uaGxqcy1idWlsdF9pbiwuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSwuaGxqcy10aXRsZS5jbGFzc197Y29sb3I6I2U2YzA3Yn0uaGxqcy1hdHRyLC5obGpzLW51bWJlciwuaGxqcy1zZWxlY3Rvci1hdHRyLC5obGpzLXNlbGVjdG9yLWNsYXNzLC5obGpzLXNlbGVjdG9yLXBzZXVkbywuaGxqcy10ZW1wbGF0ZS12YXJpYWJsZSwuaGxqcy10eXBlLC5obGpzLXZhcmlhYmxle2NvbG9yOiNkMTlhNjZ9LmhsanMtYnVsbGV0LC5obGpzLWxpbmssLmhsanMtbWV0YSwuaGxqcy1zZWxlY3Rvci1pZCwuaGxqcy1zeW1ib2wsLmhsanMtdGl0bGV7Y29sb3I6IzYxYWVlZX0uaGxqcy1lbXBoYXNpc3tmb250LXN0eWxlOml0YWxpY30uaGxqcy1zdHJvbmd7Zm9udC13ZWlnaHQ6NzAwfS5obGpzLWxpbmt7dGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZX0=`;
|
||||
@@ -1 +1 @@
|
||||
module.exports = `LyoKCkF0b20gT25lIExpZ2h0IGJ5IERhbmllbCBHYW1hZ2UKT3JpZ2luYWwgT25lIExpZ2h0IFN5bnRheCB0aGVtZSBmcm9tIGh0dHBzOi8vZ2l0aHViLmNvbS9hdG9tL29uZS1saWdodC1zeW50YXgKCmJhc2U6ICAgICNmYWZhZmEKbW9uby0xOiAgIzM4M2E0Mgptb25vLTI6ICAjNjg2Yjc3Cm1vbm8tMzogICNhMGExYTcKaHVlLTE6ICAgIzAxODRiYgpodWUtMjogICAjNDA3OGYyCmh1ZS0zOiAgICNhNjI2YTQKaHVlLTQ6ICAgIzUwYTE0ZgpodWUtNTogICAjZTQ1NjQ5Cmh1ZS01LTI6ICNjOTEyNDMKaHVlLTY6ICAgIzk4NjgwMQpodWUtNi0yOiAjYzE4NDAxCgoqLwoKLmhsanMgewogIGRpc3BsYXk6IGJsb2NrOwogIG92ZXJmbG93LXg6IGF1dG87CiAgcGFkZGluZzogMC41ZW07CiAgY29sb3I6ICMzODNhNDI7CiAgYmFja2dyb3VuZDogI2ZhZmFmYTsKfQoKLmhsanMtY29tbWVudCwKLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYTBhMWE3OwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtZG9jdGFnLAouaGxqcy1rZXl3b3JkLAouaGxqcy1mb3JtdWxhIHsKICBjb2xvcjogI2E2MjZhNDsKfQoKLmhsanMtc2VjdGlvbiwKLmhsanMtbmFtZSwKLmhsanMtc2VsZWN0b3ItdGFnLAouaGxqcy1kZWxldGlvbiwKLmhsanMtc3Vic3QgewogIGNvbG9yOiAjZTQ1NjQ5Owp9CgouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzAxODRiYjsKfQoKLmhsanMtc3RyaW5nLAouaGxqcy1yZWdleHAsCi5obGpzLWFkZGl0aW9uLAouaGxqcy1hdHRyaWJ1dGUsCi5obGpzLW1ldGEtc3RyaW5nIHsKICBjb2xvcjogIzUwYTE0ZjsKfQoKLmhsanMtYnVpbHRfaW4sCi5obGpzLWNsYXNzIC5obGpzLXRpdGxlIHsKICBjb2xvcjogI2MxODQwMTsKfQoKLmhsanMtYXR0ciwKLmhsanMtdmFyaWFibGUsCi5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLAouaGxqcy10eXBlLAouaGxqcy1zZWxlY3Rvci1jbGFzcywKLmhsanMtc2VsZWN0b3ItYXR0ciwKLmhsanMtc2VsZWN0b3ItcHNldWRvLAouaGxqcy1udW1iZXIgewogIGNvbG9yOiAjOTg2ODAxOwp9CgouaGxqcy1zeW1ib2wsCi5obGpzLWJ1bGxldCwKLmhsanMtbGluaywKLmhsanMtbWV0YSwKLmhsanMtc2VsZWN0b3ItaWQsCi5obGpzLXRpdGxlIHsKICBjb2xvcjogIzQwNzhmMjsKfQoKLmhsanMtZW1waGFzaXMgewogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQoKLmhsanMtbGluayB7CiAgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmU7Cn0K`;
|
||||
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiMzODNhNDI7YmFja2dyb3VuZDojZmFmYWZhfS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2EwYTFhNztmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYSwuaGxqcy1rZXl3b3Jke2NvbG9yOiNhNjI2YTR9LmhsanMtZGVsZXRpb24sLmhsanMtbmFtZSwuaGxqcy1zZWN0aW9uLC5obGpzLXNlbGVjdG9yLXRhZywuaGxqcy1zdWJzdHtjb2xvcjojZTQ1NjQ5fS5obGpzLWxpdGVyYWx7Y29sb3I6IzAxODRiYn0uaGxqcy1hZGRpdGlvbiwuaGxqcy1hdHRyaWJ1dGUsLmhsanMtbWV0YSAuaGxqcy1zdHJpbmcsLmhsanMtcmVnZXhwLC5obGpzLXN0cmluZ3tjb2xvcjojNTBhMTRmfS5obGpzLWF0dHIsLmhsanMtbnVtYmVyLC5obGpzLXNlbGVjdG9yLWF0dHIsLmhsanMtc2VsZWN0b3ItY2xhc3MsLmhsanMtc2VsZWN0b3ItcHNldWRvLC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLC5obGpzLXR5cGUsLmhsanMtdmFyaWFibGV7Y29sb3I6Izk4NjgwMX0uaGxqcy1idWxsZXQsLmhsanMtbGluaywuaGxqcy1tZXRhLC5obGpzLXNlbGVjdG9yLWlkLC5obGpzLXN5bWJvbCwuaGxqcy10aXRsZXtjb2xvcjojNDA3OGYyfS5obGpzLWJ1aWx0X2luLC5obGpzLWNsYXNzIC5obGpzLXRpdGxlLC5obGpzLXRpdGxlLmNsYXNzX3tjb2xvcjojYzE4NDAxfS5obGpzLWVtcGhhc2lze2ZvbnQtc3R5bGU6aXRhbGljfS5obGpzLXN0cm9uZ3tmb250LXdlaWdodDo3MDB9LmhsanMtbGlua3t0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lfQ==`;
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
hash:"6608023b8053b48e0eec248644475e33", files: {
|
||||
hash:"de3871f000c87478973d7cd0913bd3ff", files: {
|
||||
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -552,7 +552,7 @@ async function initialize(dispatch: Function) {
|
||||
// / E2EE SETUP
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
await ShareService.instance().initialize(store, EncryptionService.instance());
|
||||
await ShareService.instance().initialize(store);
|
||||
|
||||
reg.logger().info('Loading folders...');
|
||||
|
||||
|
||||
@@ -7,14 +7,13 @@ const { Keyboard } = require('react-native');
|
||||
|
||||
const dialogs = {};
|
||||
|
||||
dialogs.confirm = (parentComponent, message) => {
|
||||
if (!parentComponent) throw new Error('parentComponent is required');
|
||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||
dialogs.confirmRef = (ref, message) => {
|
||||
if (!ref) throw new Error('ref is required');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Keyboard.dismiss();
|
||||
|
||||
parentComponent.dialogbox.confirm({
|
||||
ref.confirm({
|
||||
content: message,
|
||||
|
||||
ok: {
|
||||
@@ -32,6 +31,13 @@ dialogs.confirm = (parentComponent, message) => {
|
||||
});
|
||||
};
|
||||
|
||||
dialogs.confirm = (parentComponent, message) => {
|
||||
if (!parentComponent) throw new Error('parentComponent is required');
|
||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||
|
||||
return dialogs.confirmRef(parentComponent.dialogbox, message);
|
||||
};
|
||||
|
||||
dialogs.pop = (parentComponent, message, buttons, options = null) => {
|
||||
if (!parentComponent) throw new Error('parentComponent is required');
|
||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||
|
||||
@@ -251,6 +251,12 @@ export default class BaseApplication {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.indexOf('--user-data-dir=') === 0) {
|
||||
// Electron-specific flag. Allows users to run the app with chromedriver.
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.length && arg[0] == '-') {
|
||||
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
|
||||
} else {
|
||||
@@ -630,7 +636,7 @@ export default class BaseApplication {
|
||||
BaseSyncTarget.dispatch = this.store().dispatch;
|
||||
DecryptionWorker.instance().dispatch = this.store().dispatch;
|
||||
ResourceFetcher.instance().dispatch = this.store().dispatch;
|
||||
ShareService.instance().initialize(this.store(), EncryptionService.instance());
|
||||
ShareService.instance().initialize(this.store());
|
||||
}
|
||||
|
||||
deinitRedux() {
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ParseOptions {
|
||||
anchorNames?: string[];
|
||||
preserveImageTagsWithSize?: boolean;
|
||||
baseUrl?: string;
|
||||
disableEscapeContent?: boolean;
|
||||
}
|
||||
|
||||
export default class HtmlToMd {
|
||||
@@ -20,6 +21,7 @@ export default class HtmlToMd {
|
||||
emDelimiter: '*',
|
||||
strongDelimiter: '**',
|
||||
br: '',
|
||||
disableEscapeContent: 'disableEscapeContent' in options ? options.disableEscapeContent : false,
|
||||
});
|
||||
turndown.use(turndownPluginGfm);
|
||||
turndown.remove('script');
|
||||
|
||||
@@ -900,11 +900,6 @@ export default class JoplinDatabase extends Database {
|
||||
queries.push('ALTER TABLE `notes` ADD COLUMN conflict_original_id TEXT NOT NULL DEFAULT ""');
|
||||
}
|
||||
|
||||
// if (targetVersion == 40) {
|
||||
// queries.push('ALTER TABLE `folders` ADD COLUMN master_key_id TEXT NOT NULL DEFAULT ""');
|
||||
// queries.push('ALTER TABLE `notes` ADD COLUMN master_key_id TEXT NOT NULL DEFAULT ""');
|
||||
// }
|
||||
|
||||
const updateVersionQuery = { sql: 'UPDATE version SET version = ?', params: [targetVersion] };
|
||||
|
||||
queries.push(updateVersionQuery);
|
||||
|
||||
@@ -142,7 +142,7 @@ export default class JoplinServerApi {
|
||||
}
|
||||
|
||||
if (sessionId) headers['X-API-AUTH'] = sessionId;
|
||||
headers['X-API-MIN-VERSION'] = '2.5.0';
|
||||
headers['X-API-MIN-VERSION'] = '2.1.4';
|
||||
|
||||
const fetchOptions: any = {};
|
||||
fetchOptions.headers = headers;
|
||||
|
||||
@@ -24,7 +24,6 @@ import { FileApi } from './file-api';
|
||||
import JoplinDatabase from './JoplinDatabase';
|
||||
import { fetchSyncInfo, getActiveMasterKey, localSyncInfo, mergeSyncInfos, saveLocalSyncInfo, syncInfoEquals, uploadSyncInfo } from './services/synchronizer/syncInfoUtils';
|
||||
import { setupAndDisableEncryption, setupAndEnableEncryption } from './services/e2ee/utils';
|
||||
import { setPpkIfNotExist } from './services/e2ee/ppk';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { Dirnames } = require('./services/synchronizer/utils/types');
|
||||
|
||||
@@ -421,52 +420,49 @@ export default class Synchronizer {
|
||||
this.api().setTempDirName(Dirnames.Temp);
|
||||
|
||||
try {
|
||||
let remoteInfo = await fetchSyncInfo(this.api());
|
||||
const remoteInfo = await fetchSyncInfo(this.api());
|
||||
logger.info('Sync target remote info:', remoteInfo);
|
||||
|
||||
if (!remoteInfo.version) {
|
||||
logger.info('Sync target is new - setting it up...');
|
||||
await this.migrationHandler().upgrade(Setting.value('syncVersion'));
|
||||
remoteInfo = await fetchSyncInfo(this.api());
|
||||
}
|
||||
|
||||
logger.info('Sync target is already setup - checking it...');
|
||||
|
||||
await this.migrationHandler().checkCanSync(remoteInfo);
|
||||
|
||||
const localInfo = await localSyncInfo();
|
||||
|
||||
logger.info('Sync target local info:', localInfo);
|
||||
|
||||
await setPpkIfNotExist(this.encryptionService(), localInfo, remoteInfo);
|
||||
|
||||
// console.info('LOCAL', localInfo);
|
||||
// console.info('REMOTE', remoteInfo);
|
||||
|
||||
if (!syncInfoEquals(localInfo, remoteInfo)) {
|
||||
const newInfo = mergeSyncInfos(localInfo, remoteInfo);
|
||||
const previousE2EE = localInfo.e2ee;
|
||||
logger.info('Sync target info differs between local and remote - merging infos: ', newInfo.toObject());
|
||||
|
||||
await this.lockHandler().acquireLock(LockType.Exclusive, this.appType_, this.clientId_, { clearExistingSyncLocksFromTheSameClient: true });
|
||||
await uploadSyncInfo(this.api(), newInfo);
|
||||
await saveLocalSyncInfo(newInfo);
|
||||
await this.lockHandler().releaseLock(LockType.Exclusive, this.appType_, this.clientId_);
|
||||
|
||||
// console.info('NEW', newInfo);
|
||||
|
||||
if (newInfo.e2ee !== previousE2EE) {
|
||||
if (newInfo.e2ee) {
|
||||
const mk = getActiveMasterKey(newInfo);
|
||||
await setupAndEnableEncryption(this.encryptionService(), mk);
|
||||
} else {
|
||||
await setupAndDisableEncryption(this.encryptionService());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Set it to remote anyway so that timestamps are the same
|
||||
// Note: that's probably not needed anymore?
|
||||
// await uploadSyncInfo(this.api(), remoteInfo);
|
||||
logger.info('Sync target is already setup - checking it...');
|
||||
|
||||
await this.migrationHandler().checkCanSync(remoteInfo);
|
||||
|
||||
const localInfo = await localSyncInfo();
|
||||
|
||||
logger.info('Sync target local info:', localInfo);
|
||||
|
||||
// console.info('LOCAL', localInfo);
|
||||
// console.info('REMOTE', remoteInfo);
|
||||
|
||||
if (!syncInfoEquals(localInfo, remoteInfo)) {
|
||||
const newInfo = mergeSyncInfos(localInfo, remoteInfo);
|
||||
const previousE2EE = localInfo.e2ee;
|
||||
logger.info('Sync target info differs between local and remote - merging infos: ', newInfo.toObject());
|
||||
|
||||
await this.lockHandler().acquireLock(LockType.Exclusive, this.appType_, this.clientId_, { clearExistingSyncLocksFromTheSameClient: true });
|
||||
await uploadSyncInfo(this.api(), newInfo);
|
||||
await saveLocalSyncInfo(newInfo);
|
||||
await this.lockHandler().releaseLock(LockType.Exclusive, this.appType_, this.clientId_);
|
||||
|
||||
// console.info('NEW', newInfo);
|
||||
|
||||
if (newInfo.e2ee !== previousE2EE) {
|
||||
if (newInfo.e2ee) {
|
||||
const mk = getActiveMasterKey(newInfo);
|
||||
await setupAndEnableEncryption(this.encryptionService(), mk);
|
||||
} else {
|
||||
await setupAndDisableEncryption(this.encryptionService());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Set it to remote anyway so that timestamps are the same
|
||||
// Note: that's probably not needed anymore?
|
||||
// await uploadSyncInfo(this.api(), remoteInfo);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'outdatedSyncTarget') {
|
||||
@@ -483,6 +479,31 @@ export default class Synchronizer {
|
||||
void this.cancel();
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 2. DELETE_REMOTE
|
||||
// ------------------------------------------------------------------------
|
||||
// Delete the remote items that have been deleted locally.
|
||||
// ========================================================================
|
||||
|
||||
if (syncSteps.indexOf('delete_remote') >= 0) {
|
||||
const deletedItems = await BaseItem.deletedItems(syncTargetId);
|
||||
for (let i = 0; i < deletedItems.length; i++) {
|
||||
if (this.cancelling()) break;
|
||||
|
||||
const item = deletedItems[i];
|
||||
const path = BaseItem.systemPath(item.item_id);
|
||||
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
|
||||
await this.apiCall('delete', path);
|
||||
|
||||
if (item.item_type === BaseModel.TYPE_RESOURCE) {
|
||||
const remoteContentPath = resourceRemotePath(item.item_id);
|
||||
await this.apiCall('delete', remoteContentPath);
|
||||
}
|
||||
|
||||
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
|
||||
}
|
||||
} // DELETE_REMOTE STEP
|
||||
|
||||
// ========================================================================
|
||||
// 1. UPLOAD
|
||||
// ------------------------------------------------------------------------
|
||||
@@ -767,31 +788,6 @@ export default class Synchronizer {
|
||||
}
|
||||
} // UPLOAD STEP
|
||||
|
||||
// ========================================================================
|
||||
// 2. DELETE_REMOTE
|
||||
// ------------------------------------------------------------------------
|
||||
// Delete the remote items that have been deleted locally.
|
||||
// ========================================================================
|
||||
|
||||
if (syncSteps.indexOf('delete_remote') >= 0) {
|
||||
const deletedItems = await BaseItem.deletedItems(syncTargetId);
|
||||
for (let i = 0; i < deletedItems.length; i++) {
|
||||
if (this.cancelling()) break;
|
||||
|
||||
const item = deletedItems[i];
|
||||
const path = BaseItem.systemPath(item.item_id);
|
||||
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
|
||||
await this.apiCall('delete', path);
|
||||
|
||||
if (item.item_type === BaseModel.TYPE_RESOURCE) {
|
||||
const remoteContentPath = resourceRemotePath(item.item_id);
|
||||
await this.apiCall('delete', remoteContentPath);
|
||||
}
|
||||
|
||||
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
|
||||
}
|
||||
} // DELETE_REMOTE STEP
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 3. DELTA
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as historyBackward from './historyBackward';
|
||||
import * as historyForward from './historyForward';
|
||||
import * as openMasterPasswordDialog from './openMasterPasswordDialog';
|
||||
import * as synchronize from './synchronize';
|
||||
|
||||
const index:any[] = [
|
||||
historyBackward,
|
||||
historyForward,
|
||||
openMasterPasswordDialog,
|
||||
synchronize,
|
||||
];
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
|
||||
import { _ } from '../locale';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'openMasterPasswordDialog',
|
||||
label: () => _('Manage master password...'),
|
||||
iconName: 'fas fa-key',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, isOpen: boolean = true) => {
|
||||
context.dispatch({
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'masterPassword',
|
||||
isOpen: isOpen,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
183
packages/lib/components/EncryptionConfigScreen/utils.ts
Normal file
183
packages/lib/components/EncryptionConfigScreen/utils.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import shim from '../../shim';
|
||||
import { _ } from '../../locale';
|
||||
import BaseItem, { EncryptedItemsStats } from '../../models/BaseItem';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '../../hooks/useAsyncEffect';
|
||||
import { MasterKeyEntity } from '../../services/e2ee/types';
|
||||
import time from '../../time';
|
||||
import { findMasterKeyPassword } from '../../services/e2ee/utils';
|
||||
import EncryptionService from '../../services/e2ee/EncryptionService';
|
||||
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import { reg } from '../../registry';
|
||||
import Setting from '../../models/Setting';
|
||||
const { useCallback, useEffect, useState } = shim.react();
|
||||
|
||||
type PasswordChecks = Record<string, boolean>;
|
||||
|
||||
export const useStats = () => {
|
||||
const [stats, setStats] = useState<EncryptedItemsStats>({ encrypted: null, total: null });
|
||||
const [statsUpdateTime, setStatsUpdateTime] = useState<number>(0);
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const r = await BaseItem.encryptedItemsStats();
|
||||
if (event.cancelled) return;
|
||||
setStats(r);
|
||||
}, [statsUpdateTime]);
|
||||
|
||||
useEffect(() => {
|
||||
const iid = shim.setInterval(() => {
|
||||
setStatsUpdateTime(Date.now());
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearInterval(iid);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
export const decryptedStatText = (stats: EncryptedItemsStats) => {
|
||||
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
|
||||
const totalCount = stats.total !== null ? stats.total : '-';
|
||||
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const enableEncryptionConfirmationMessages = (masterKey: MasterKeyEntity) => {
|
||||
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
|
||||
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
|
||||
return msg;
|
||||
};
|
||||
|
||||
const masterPasswordIsValid = async (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string = null) => {
|
||||
const activeMasterKey = masterKeys.find((mk: MasterKeyEntity) => mk.id === activeMasterKeyId);
|
||||
masterPassword = masterPassword === null ? masterPassword : masterPassword;
|
||||
if (activeMasterKey && masterPassword) {
|
||||
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const reencryptData = async () => {
|
||||
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
|
||||
if (!ok) return;
|
||||
|
||||
await BaseItem.forceSyncAll();
|
||||
void reg.waitForSyncFinishedThenSync();
|
||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||
alert(_('Your data is going to be re-encrypted and synced again.'));
|
||||
};
|
||||
|
||||
export const dontReencryptData = () => {
|
||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||
};
|
||||
|
||||
export const useToggleShowDisabledMasterKeys = () => {
|
||||
const [showDisabledMasterKeys, setShowDisabledMasterKeys] = useState<boolean>(false);
|
||||
|
||||
const toggleShowDisabledMasterKeys = () => {
|
||||
setShowDisabledMasterKeys((current) => !current);
|
||||
};
|
||||
|
||||
return { showDisabledMasterKeys, toggleShowDisabledMasterKeys };
|
||||
};
|
||||
|
||||
export const onToggleEnabledClick = (mk: MasterKeyEntity) => {
|
||||
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
|
||||
};
|
||||
|
||||
export const onSavePasswordClick = (mk: MasterKeyEntity, passwords: Record<string, string>) => {
|
||||
const password = passwords[mk.id];
|
||||
if (!password) {
|
||||
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
|
||||
} else {
|
||||
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
|
||||
}
|
||||
};
|
||||
|
||||
export const onMasterPasswordSave = (masterPasswordInput: string) => {
|
||||
Setting.setValue('encryption.masterPassword', masterPasswordInput);
|
||||
};
|
||||
|
||||
export const useInputMasterPassword = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string) => {
|
||||
const [inputMasterPassword, setInputMasterPassword] = useState<string>('');
|
||||
|
||||
const onMasterPasswordSave = useCallback(async () => {
|
||||
Setting.setValue('encryption.masterPassword', inputMasterPassword);
|
||||
|
||||
if (!(await masterPasswordIsValid(masterKeys, activeMasterKeyId, inputMasterPassword))) {
|
||||
alert('Password is invalid. Please try again.');
|
||||
}
|
||||
}, [inputMasterPassword]);
|
||||
|
||||
const onMasterPasswordChange = useCallback((password: string) => {
|
||||
setInputMasterPassword(password);
|
||||
}, []);
|
||||
|
||||
return { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange };
|
||||
};
|
||||
|
||||
export const useInputPasswords = (propsPasswords: Record<string, string>) => {
|
||||
const [inputPasswords, setInputPasswords] = useState<Record<string, string>>(propsPasswords);
|
||||
|
||||
useEffect(() => {
|
||||
setInputPasswords(propsPasswords);
|
||||
}, [propsPasswords]);
|
||||
|
||||
const onInputPasswordChange = useCallback((mk: MasterKeyEntity, password: string) => {
|
||||
setInputPasswords(current => {
|
||||
return {
|
||||
...current,
|
||||
[mk.id]: password,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { inputPasswords, onInputPasswordChange };
|
||||
};
|
||||
|
||||
export const usePasswordChecker = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string, passwords: Record<string, string>) => {
|
||||
const [passwordChecks, setPasswordChecks] = useState<PasswordChecks>({});
|
||||
const [masterPasswordKeys, setMasterPasswordKeys] = useState<PasswordChecks>({});
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const newPasswordChecks: PasswordChecks = {};
|
||||
const newMasterPasswordKeys: PasswordChecks = {};
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
const password = await findMasterKeyPassword(EncryptionService.instance(), mk, passwords);
|
||||
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
|
||||
newPasswordChecks[mk.id] = ok;
|
||||
newMasterPasswordKeys[mk.id] = password === masterPassword;
|
||||
}
|
||||
|
||||
newPasswordChecks['master'] = await masterPasswordIsValid(masterKeys, activeMasterKeyId, masterPassword);
|
||||
|
||||
if (event.cancelled) return;
|
||||
|
||||
setPasswordChecks(newPasswordChecks);
|
||||
setMasterPasswordKeys(newMasterPasswordKeys);
|
||||
}, [masterKeys, masterPassword]);
|
||||
|
||||
return { passwordChecks, masterPasswordKeys };
|
||||
};
|
||||
|
||||
export const upgradeMasterKey = async (masterKey: MasterKeyEntity, passwordChecks: PasswordChecks, passwords: Record<string, string>): Promise<string> => {
|
||||
const passwordCheck = passwordChecks[masterKey.id];
|
||||
if (!passwordCheck) {
|
||||
return _('Please enter your password in the master key list below before upgrading the key.');
|
||||
}
|
||||
|
||||
try {
|
||||
const password = passwords[masterKey.id];
|
||||
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
|
||||
await MasterKey.save(newMasterKey);
|
||||
void reg.waitForSyncFinishedThenSync();
|
||||
return _('The master key has been upgraded successfully!');
|
||||
} catch (error) {
|
||||
return _('Could not upgrade master key: %s', error.message);
|
||||
}
|
||||
};
|
||||
@@ -9,10 +9,8 @@ const shared = {};
|
||||
shared.init = function(comp) {
|
||||
if (!comp.state) comp.state = {};
|
||||
comp.state.checkSyncConfigResult = null;
|
||||
comp.state.checkNextcloudAppResult = null;
|
||||
comp.state.settings = {};
|
||||
comp.state.changedSettingKeys = [];
|
||||
comp.state.showNextcloudAppLog = false;
|
||||
comp.state.showAdvancedSettings = false;
|
||||
};
|
||||
|
||||
@@ -35,7 +33,6 @@ shared.checkSyncConfig = async function(comp, settings) {
|
||||
comp.setState({ checkSyncConfigResult: result });
|
||||
|
||||
if (result.ok) {
|
||||
// await shared.checkNextcloudApp(comp, settings);
|
||||
// Users often expect config to be auto-saved at this point, if the config check was successful
|
||||
shared.saveSettings(comp);
|
||||
}
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
import EncryptionService from '../../services/e2ee/EncryptionService';
|
||||
import { _ } from '../../locale';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
import Setting from '../../models/Setting';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import { reg } from '../../registry.js';
|
||||
import shim from '../../shim';
|
||||
import { MasterKeyEntity } from '../../services/e2ee/types';
|
||||
import time from '../../time';
|
||||
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
|
||||
import { findMasterKeyPassword } from '../../services/e2ee/utils';
|
||||
|
||||
class Shared {
|
||||
|
||||
private refreshStatsIID_: any;
|
||||
|
||||
public initialize(comp: any, props: any) {
|
||||
comp.state = {
|
||||
passwordChecks: {},
|
||||
// Master keys that can be decrypted with the master password
|
||||
// (normally all of them, but for legacy support we need this).
|
||||
masterPasswordKeys: {},
|
||||
stats: {
|
||||
encrypted: null,
|
||||
total: null,
|
||||
},
|
||||
passwords: Object.assign({}, props.passwords),
|
||||
showDisabledMasterKeys: false,
|
||||
masterPasswordInput: '',
|
||||
};
|
||||
comp.isMounted_ = false;
|
||||
|
||||
this.refreshStatsIID_ = null;
|
||||
}
|
||||
|
||||
public async refreshStats(comp: any) {
|
||||
const stats = await BaseItem.encryptedItemsStats();
|
||||
comp.setState({
|
||||
stats: stats,
|
||||
});
|
||||
}
|
||||
|
||||
public async toggleShowDisabledMasterKeys(comp: any) {
|
||||
comp.setState({ showDisabledMasterKeys: !comp.state.showDisabledMasterKeys });
|
||||
}
|
||||
|
||||
public async reencryptData() {
|
||||
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
|
||||
if (!ok) return;
|
||||
|
||||
await BaseItem.forceSyncAll();
|
||||
void reg.waitForSyncFinishedThenSync();
|
||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||
alert(_('Your data is going to be re-encrypted and synced again.'));
|
||||
}
|
||||
|
||||
public dontReencryptData() {
|
||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||
}
|
||||
|
||||
public async upgradeMasterKey(comp: any, masterKey: MasterKeyEntity) {
|
||||
const passwordCheck = comp.state.passwordChecks[masterKey.id];
|
||||
if (!passwordCheck) {
|
||||
alert(_('Please enter your password in the master key list below before upgrading the key.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const password = comp.state.passwords[masterKey.id];
|
||||
const newMasterKey = await EncryptionService.instance().reencryptMasterKey(masterKey, password, password);
|
||||
await MasterKey.save(newMasterKey);
|
||||
void reg.waitForSyncFinishedThenSync();
|
||||
alert(_('The master key has been upgraded successfully!'));
|
||||
} catch (error) {
|
||||
alert(_('Could not upgrade master key: %s', error.message));
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount(comp: any) {
|
||||
this.componentDidUpdate(comp);
|
||||
|
||||
void this.refreshStats(comp);
|
||||
|
||||
if (this.refreshStatsIID_) {
|
||||
shim.clearInterval(this.refreshStatsIID_);
|
||||
this.refreshStatsIID_ = null;
|
||||
}
|
||||
|
||||
this.refreshStatsIID_ = shim.setInterval(() => {
|
||||
if (!comp.isMounted_) {
|
||||
shim.clearInterval(this.refreshStatsIID_);
|
||||
this.refreshStatsIID_ = null;
|
||||
return;
|
||||
}
|
||||
void this.refreshStats(comp);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
public componentDidUpdate(comp: any, prevProps: any = null) {
|
||||
if (prevProps && comp.props.passwords !== prevProps.passwords) {
|
||||
comp.setState({ passwords: Object.assign({}, comp.props.passwords) });
|
||||
}
|
||||
|
||||
if (!prevProps || comp.props.masterKeys !== prevProps.masterKeys || comp.props.passwords !== prevProps.passwords) {
|
||||
comp.checkPasswords();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.refreshStatsIID_) {
|
||||
shim.clearInterval(this.refreshStatsIID_);
|
||||
this.refreshStatsIID_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async masterPasswordIsValid(comp: any, masterPassword: string = null) {
|
||||
const activeMasterKey = comp.props.masterKeys.find((mk: MasterKeyEntity) => mk.id === comp.props.activeMasterKeyId);
|
||||
masterPassword = masterPassword === null ? comp.props.masterPassword : masterPassword;
|
||||
if (activeMasterKey && masterPassword) {
|
||||
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async checkPasswords(comp: any) {
|
||||
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
|
||||
const masterPasswordKeys = Object.assign({}, comp.state.masterPasswordKeys);
|
||||
for (let i = 0; i < comp.props.masterKeys.length; i++) {
|
||||
const mk = comp.props.masterKeys[i];
|
||||
const password = await findMasterKeyPassword(EncryptionService.instance(), mk);
|
||||
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
|
||||
passwordChecks[mk.id] = ok;
|
||||
masterPasswordKeys[mk.id] = password === comp.props.masterPassword;
|
||||
}
|
||||
|
||||
passwordChecks['master'] = await this.masterPasswordIsValid(comp);
|
||||
|
||||
comp.setState({ passwordChecks, masterPasswordKeys });
|
||||
}
|
||||
|
||||
public masterPasswordStatus(comp: any) {
|
||||
// Don't translate for now because that's temporary - later it should
|
||||
// always be set and the label should be replaced by a "Change master
|
||||
// password" button.
|
||||
return comp.props.masterPassword ? 'Master password is set' : 'Master password is not set';
|
||||
}
|
||||
|
||||
public decryptedStatText(comp: any) {
|
||||
const stats = comp.state.stats;
|
||||
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
|
||||
const totalCount = stats.total !== null ? stats.total : '-';
|
||||
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
public onSavePasswordClick(comp: any, mk: MasterKeyEntity) {
|
||||
const password = comp.state.passwords[mk.id];
|
||||
if (!password) {
|
||||
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
|
||||
} else {
|
||||
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
|
||||
}
|
||||
|
||||
comp.checkPasswords();
|
||||
}
|
||||
|
||||
public onMasterPasswordChange(comp: any, value: string) {
|
||||
comp.setState({ masterPasswordInput: value });
|
||||
}
|
||||
|
||||
public onMasterPasswordSave(comp: any) {
|
||||
Setting.setValue('encryption.masterPassword', comp.state.masterPasswordInput);
|
||||
}
|
||||
|
||||
public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) {
|
||||
const passwords = Object.assign({}, comp.state.passwords);
|
||||
passwords[mk.id] = password;
|
||||
comp.setState({ passwords: passwords });
|
||||
}
|
||||
|
||||
public onToggleEnabledClick(_comp: any, mk: MasterKeyEntity) {
|
||||
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
|
||||
}
|
||||
|
||||
public enableEncryptionConfirmationMessages(masterKey: MasterKeyEntity) {
|
||||
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
|
||||
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
|
||||
return msg;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const shared = new Shared();
|
||||
|
||||
export default shared;
|
||||
@@ -40,10 +40,6 @@ export default class FileApiDriverJoplinServer {
|
||||
return true;
|
||||
}
|
||||
|
||||
public get requiresPublicPrivateKeyPair() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public requestRepeatCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@@ -106,10 +106,6 @@ class FileApi {
|
||||
return !!this.driver().supportsAccurateTimestamp;
|
||||
}
|
||||
|
||||
public get requiresPublicPrivateKeyPair(): boolean {
|
||||
return !!this.driver().requiresPublicPrivateKeyPair;
|
||||
}
|
||||
|
||||
async fetchRemoteDateOffset_() {
|
||||
const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`;
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -25,6 +25,7 @@ function useEventListener(
|
||||
const eventListener = (event: Event) => {
|
||||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
if (!!savedHandler?.current) {
|
||||
// @ts-ignore
|
||||
savedHandler.current(event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ const os = require('os');
|
||||
const { filename } = require('./path-utils');
|
||||
import { setupDatabaseAndSynchronizer, switchClient, expectNotThrow, supportDir } from './testing/test-utils';
|
||||
const { enexXmlToMd } = require('./import-enex-md-gen.js');
|
||||
const { importEnex } = require('./import-enex');
|
||||
import importEnex from './import-enex';
|
||||
import Note from './models/Note';
|
||||
import Tag from './models/Tag';
|
||||
import Resource from './models/Resource';
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
const uuid = require('./uuid').default;
|
||||
import uuid from './uuid';
|
||||
import BaseModel from './BaseModel';
|
||||
import Note from './models/Note';
|
||||
import Tag from './models/Tag';
|
||||
import Resource from './models/Resource';
|
||||
import Setting from './models/Setting';
|
||||
import time from './time';
|
||||
import shim from './shim';
|
||||
import { NoteEntity } from './services/database/types';
|
||||
import { enexXmlToMd } from './import-enex-md-gen';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
const moment = require('moment');
|
||||
const BaseModel = require('./BaseModel').default;
|
||||
const Note = require('./models/Note').default;
|
||||
const Tag = require('./models/Tag').default;
|
||||
const Resource = require('./models/Resource').default;
|
||||
const Setting = require('./models/Setting').default;
|
||||
const { MarkupToHtml } = require('@joplin/renderer');
|
||||
const { wrapError } = require('./errorUtils');
|
||||
const { enexXmlToMd } = require('./import-enex-md-gen.js');
|
||||
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
|
||||
const time = require('./time').default;
|
||||
const Levenshtein = require('levenshtein');
|
||||
const md5 = require('md5');
|
||||
const { Base64Decode } = require('base64-stream');
|
||||
const md5File = require('md5-file');
|
||||
const shim = require('./shim').default;
|
||||
const { mime } = require('./mime-utils');
|
||||
|
||||
// const Promise = require('promise');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
function dateToTimestamp(s, defaultValue = null) {
|
||||
function dateToTimestamp(s: string, defaultValue: number = null): number {
|
||||
// Most dates seem to be in this format
|
||||
let m = moment(s, 'YYYYMMDDTHHmmssZ');
|
||||
|
||||
@@ -36,12 +37,12 @@ function dateToTimestamp(s, defaultValue = null) {
|
||||
return m.toDate().getTime();
|
||||
}
|
||||
|
||||
function extractRecognitionObjId(recognitionXml) {
|
||||
function extractRecognitionObjId(recognitionXml: string) {
|
||||
const r = recognitionXml.match(/objID="(.*?)"/);
|
||||
return r && r.length >= 2 ? r[1] : null;
|
||||
}
|
||||
|
||||
async function decodeBase64File(sourceFilePath, destFilePath) {
|
||||
async function decodeBase64File(sourceFilePath: string, destFilePath: string) {
|
||||
// When something goes wrong with streams you can get an error "EBADF, Bad file descriptor"
|
||||
// with no strack trace to tell where the error happened.
|
||||
|
||||
@@ -73,17 +74,17 @@ async function decodeBase64File(sourceFilePath, destFilePath) {
|
||||
destStream.on('finish', () => {
|
||||
fs.fdatasyncSync(destFile);
|
||||
fs.closeSync(destFile);
|
||||
resolve();
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
sourceStream.on('error', (error) => reject(error));
|
||||
destStream.on('error', (error) => reject(error));
|
||||
sourceStream.on('error', (error: any) => reject(error));
|
||||
destStream.on('error', (error: any) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
async function md5FileAsync(filePath) {
|
||||
async function md5FileAsync(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
md5File(filePath, (error, hash) => {
|
||||
md5File(filePath, (error: any, hash: string) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
@@ -94,24 +95,24 @@ async function md5FileAsync(filePath) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeUndefinedProperties(note) {
|
||||
const output = {};
|
||||
function removeUndefinedProperties(note: NoteEntity) {
|
||||
const output: any = {};
|
||||
for (const n in note) {
|
||||
if (!note.hasOwnProperty(n)) continue;
|
||||
const v = note[n];
|
||||
const v = (note as any)[n];
|
||||
if (v === undefined || v === null) continue;
|
||||
output[n] = v;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function levenshteinPercent(s1, s2) {
|
||||
function levenshteinPercent(s1: string, s2: string) {
|
||||
const l = new Levenshtein(s1, s2);
|
||||
if (!s1.length || !s2.length) return 1;
|
||||
return Math.abs(l.distance / s1.length);
|
||||
}
|
||||
|
||||
async function fuzzyMatch(note) {
|
||||
async function fuzzyMatch(note: ExtractedNote) {
|
||||
if (note.created_time < time.unixMs() - 1000 * 60 * 60 * 24 * 360) {
|
||||
const notes = await Note.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0 AND created_time = ? AND title = ?', [note.created_time, note.title]);
|
||||
return notes.length !== 1 ? null : notes[0];
|
||||
@@ -137,9 +138,30 @@ async function fuzzyMatch(note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
interface ExtractedResource {
|
||||
hasData?: boolean;
|
||||
id?: string;
|
||||
size?: number;
|
||||
dataFilePath?: string;
|
||||
dataEncoding?: string;
|
||||
data?: string;
|
||||
filename?: string;
|
||||
sourceUrl?: string;
|
||||
mime?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface ExtractedNote extends NoteEntity {
|
||||
resources?: ExtractedResource[];
|
||||
tags?: string[];
|
||||
title?: string;
|
||||
bodyXml?: string;
|
||||
// is_todo?: boolean;
|
||||
}
|
||||
|
||||
// At this point we have the resource has it's been parsed from the XML, but additional
|
||||
// processing needs to be done to get the final resource file, its size, MD5, etc.
|
||||
async function processNoteResource(resource) {
|
||||
async function processNoteResource(resource: ExtractedResource) {
|
||||
if (!resource.hasData) {
|
||||
// Some resources have no data, go figure, so we need a special case for this.
|
||||
resource.id = md5(Date.now() + Math.random());
|
||||
@@ -175,7 +197,7 @@ async function processNoteResource(resource) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
async function saveNoteResources(note) {
|
||||
async function saveNoteResources(note: ExtractedNote) {
|
||||
let resourcesCreated = 0;
|
||||
for (let i = 0; i < note.resources.length; i++) {
|
||||
const resource = note.resources[i];
|
||||
@@ -198,7 +220,7 @@ async function saveNoteResources(note) {
|
||||
return resourcesCreated;
|
||||
}
|
||||
|
||||
async function saveNoteTags(note) {
|
||||
async function saveNoteTags(note: ExtractedNote) {
|
||||
let notesTagged = 0;
|
||||
for (let i = 0; i < note.tags.length; i++) {
|
||||
const tagTitle = note.tags[i];
|
||||
@@ -213,12 +235,19 @@ async function saveNoteTags(note) {
|
||||
return notesTagged;
|
||||
}
|
||||
|
||||
async function saveNoteToStorage(note, importOptions) {
|
||||
interface ImportOptions {
|
||||
fuzzyMatching?: boolean;
|
||||
onProgress?: Function;
|
||||
onError?: Function;
|
||||
outputFormat?: string;
|
||||
}
|
||||
|
||||
async function saveNoteToStorage(note: ExtractedNote, importOptions: ImportOptions) {
|
||||
importOptions = Object.assign({}, {
|
||||
fuzzyMatching: false,
|
||||
}, importOptions);
|
||||
|
||||
note = Note.filter(note);
|
||||
note = Note.filter(note as any);
|
||||
|
||||
const existingNote = importOptions.fuzzyMatching ? await fuzzyMatch(note) : null;
|
||||
|
||||
@@ -230,7 +259,7 @@ async function saveNoteToStorage(note, importOptions) {
|
||||
notesTagged: 0,
|
||||
};
|
||||
|
||||
const resourcesCreated = await saveNoteResources(note, importOptions);
|
||||
const resourcesCreated = await saveNoteResources(note);
|
||||
result.resourcesCreated += resourcesCreated;
|
||||
|
||||
const notesTagged = await saveNoteTags(note);
|
||||
@@ -262,16 +291,50 @@ async function saveNoteToStorage(note, importOptions) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
interface Node {
|
||||
name: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
interface NoteResourceRecognition {
|
||||
objID?: string;
|
||||
}
|
||||
|
||||
const preProcessFile = async (filePath: string): Promise<string> => {
|
||||
const content: string = await shim.fsDriver().readFile(filePath, 'utf8');
|
||||
|
||||
// The note content in an ENEX file is wrapped in a CDATA block so it means
|
||||
// that any "]]>" inside the note must be somehow escaped, or else the CDATA
|
||||
// block would be closed at the wrong point.
|
||||
//
|
||||
// The problem is that Evernote appears to encode "]]>" as "]]<![CDATA[>]]>"
|
||||
// instead of the more sensible "]]>", or perhaps they have nothing in
|
||||
// place to properly escape data imported from their web clipper. In any
|
||||
// case it results in invalid XML that Evernote cannot even import back.
|
||||
//
|
||||
// Handling that invalid XML with SAX would also be very tricky, so instead
|
||||
// we add a pre-processing step that converts this tags to just ">". It
|
||||
// should be safe to do so because such content can only be within the body
|
||||
// of a note - and ">" or ">" is equivalent.
|
||||
//
|
||||
// Ref: https://discourse.joplinapp.org/t/20470/4
|
||||
const newContent = content.replace(/<!\[CDATA\[>\]\]>/g, '>');
|
||||
if (content === newContent) return filePath;
|
||||
const newFilePath = `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}.enex`;
|
||||
await shim.fsDriver().writeFile(newFilePath, newContent, 'utf8');
|
||||
return newFilePath;
|
||||
};
|
||||
|
||||
export default async function importEnex(parentFolderId: string, filePath: string, importOptions: ImportOptions = null) {
|
||||
if (!importOptions) importOptions = {};
|
||||
if (!('fuzzyMatching' in importOptions)) importOptions.fuzzyMatching = false;
|
||||
if (!('onProgress' in importOptions)) importOptions.onProgress = function() {};
|
||||
if (!('onError' in importOptions)) importOptions.onError = function() {};
|
||||
|
||||
function handleSaxStreamEvent(fn) {
|
||||
return function(...args) {
|
||||
function handleSaxStreamEvent(fn: Function) {
|
||||
return function(...args: any[]) {
|
||||
// Pass the parser to the wrapped function for debugging purposes
|
||||
if (this._parser) fn._parser = this._parser;
|
||||
if (this._parser) (fn as any)._parser = this._parser;
|
||||
|
||||
try {
|
||||
fn.call(this, ...args);
|
||||
@@ -285,6 +348,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
};
|
||||
}
|
||||
|
||||
const fileToProcess = await preProcessFile(filePath);
|
||||
const needToDeleteFileToProcess = fileToProcess !== filePath;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const progressState = {
|
||||
loaded: 0,
|
||||
@@ -295,22 +361,22 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
notesTagged: 0,
|
||||
};
|
||||
|
||||
const stream = fs.createReadStream(filePath);
|
||||
const stream = fs.createReadStream(fileToProcess);
|
||||
|
||||
const options = {};
|
||||
const strict = true;
|
||||
const saxStream = require('@joplin/fork-sax').createStream(strict, options);
|
||||
|
||||
const nodes = []; // LIFO list of nodes so that we know in which node we are in the onText event
|
||||
let note = null;
|
||||
let noteAttributes = null;
|
||||
let noteResource = null;
|
||||
let noteResourceAttributes = null;
|
||||
let noteResourceRecognition = null;
|
||||
const notes = [];
|
||||
const nodes: Node[] = []; // LIFO list of nodes so that we know in which node we are in the onText event
|
||||
let note: ExtractedNote = null;
|
||||
let noteAttributes: Record<string, any> = null;
|
||||
let noteResource: ExtractedResource = null;
|
||||
let noteResourceAttributes: Record<string, any> = null;
|
||||
let noteResourceRecognition: NoteResourceRecognition = null;
|
||||
const notes: ExtractedNote[] = [];
|
||||
let processingNotes = false;
|
||||
|
||||
const createErrorWithNoteTitle = (fnThis, error) => {
|
||||
const createErrorWithNoteTitle = (fnThis: any, error: any) => {
|
||||
const line = [];
|
||||
|
||||
const parser = fnThis ? fnThis._parser : null;
|
||||
@@ -329,7 +395,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
return error;
|
||||
};
|
||||
|
||||
stream.on('error', function(error) {
|
||||
stream.on('error', function(error: any) {
|
||||
importOptions.onError(createErrorWithNoteTitle(this, error));
|
||||
});
|
||||
|
||||
@@ -417,11 +483,11 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
saxStream.on('error', function(error) {
|
||||
saxStream.on('error', function(error: any) {
|
||||
importOptions.onError(createErrorWithNoteTitle(this, error));
|
||||
});
|
||||
|
||||
saxStream.on('text', handleSaxStreamEvent(function(text) {
|
||||
saxStream.on('text', handleSaxStreamEvent(function(text: string) {
|
||||
const n = currentNodeName();
|
||||
|
||||
if (noteAttributes) {
|
||||
@@ -443,8 +509,8 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
|
||||
fs.appendFileSync(noteResource.dataFilePath, text);
|
||||
} else {
|
||||
if (!(n in noteResource)) noteResource[n] = '';
|
||||
noteResource[n] += text;
|
||||
if (!(n in noteResource)) (noteResource as any)[n] = '';
|
||||
(noteResource as any)[n] += text;
|
||||
}
|
||||
} else if (note) {
|
||||
if (n == 'title') {
|
||||
@@ -465,7 +531,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
}
|
||||
}));
|
||||
|
||||
saxStream.on('opentag', handleSaxStreamEvent(function(node) {
|
||||
saxStream.on('opentag', handleSaxStreamEvent(function(node: Node) {
|
||||
const n = node.name.toLowerCase();
|
||||
nodes.push(node);
|
||||
|
||||
@@ -488,7 +554,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
}
|
||||
}));
|
||||
|
||||
saxStream.on('cdata', handleSaxStreamEvent(function(data) {
|
||||
saxStream.on('cdata', handleSaxStreamEvent(function(data: any) {
|
||||
const n = currentNodeName();
|
||||
|
||||
if (noteResourceRecognition) {
|
||||
@@ -500,7 +566,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
}
|
||||
}));
|
||||
|
||||
saxStream.on('closetag', handleSaxStreamEvent(function(n) {
|
||||
saxStream.on('closetag', handleSaxStreamEvent(function(n: string) {
|
||||
nodes.pop();
|
||||
|
||||
if (n == 'note') {
|
||||
@@ -529,7 +595,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
note.longitude = noteAttributes.longitude;
|
||||
note.altitude = noteAttributes.altitude;
|
||||
note.author = noteAttributes.author ? noteAttributes.author.trim() : '';
|
||||
note.is_todo = noteAttributes['reminder-order'] !== '0' && !!noteAttributes['reminder-order'];
|
||||
note.is_todo = noteAttributes['reminder-order'] !== '0' && !!noteAttributes['reminder-order'] as any;
|
||||
note.todo_due = dateToTimestamp(noteAttributes['reminder-time'], 0);
|
||||
note.todo_completed = dateToTimestamp(noteAttributes['reminder-done-time'], 0);
|
||||
note.order = dateToTimestamp(noteAttributes['reminder-order'], 0);
|
||||
@@ -572,10 +638,11 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
saxStream.on('end', handleSaxStreamEvent(function() {
|
||||
// Wait till there is no more notes to process.
|
||||
const iid = shim.setInterval(() => {
|
||||
processNotes().then(allDone => {
|
||||
void processNotes().then(allDone => {
|
||||
if (allDone) {
|
||||
shim.clearTimeout(iid);
|
||||
resolve();
|
||||
if (needToDeleteFileToProcess) void shim.fsDriver().remove(fileToProcess);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
@@ -584,5 +651,3 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
stream.pipe(saxStream);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { importEnex };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -41,45 +41,45 @@ locales['uk_UA'] = require('./uk_UA.json');
|
||||
locales['vi'] = require('./vi.json');
|
||||
locales['zh_CN'] = require('./zh_CN.json');
|
||||
locales['zh_TW'] = require('./zh_TW.json');
|
||||
stats['ar'] = {"percentDone":91};
|
||||
stats['ar'] = {"percentDone":99};
|
||||
stats['eu'] = {"percentDone":28};
|
||||
stats['bs_BA'] = {"percentDone":71};
|
||||
stats['bg_BG'] = {"percentDone":56};
|
||||
stats['ca'] = {"percentDone":95};
|
||||
stats['hr_HR'] = {"percentDone":96};
|
||||
stats['cs_CZ'] = {"percentDone":95};
|
||||
stats['bg_BG'] = {"percentDone":55};
|
||||
stats['ca'] = {"percentDone":99};
|
||||
stats['hr_HR'] = {"percentDone":95};
|
||||
stats['cs_CZ'] = {"percentDone":94};
|
||||
stats['da_DK'] = {"percentDone":99};
|
||||
stats['de_DE'] = {"percentDone":95};
|
||||
stats['de_DE'] = {"percentDone":99};
|
||||
stats['et_EE'] = {"percentDone":54};
|
||||
stats['en_GB'] = {"percentDone":100};
|
||||
stats['en_US'] = {"percentDone":100};
|
||||
stats['es_ES'] = {"percentDone":95};
|
||||
stats['eo'] = {"percentDone":31};
|
||||
stats['fi_FI'] = {"percentDone":90};
|
||||
stats['fi_FI'] = {"percentDone":99};
|
||||
stats['fr_FR'] = {"percentDone":96};
|
||||
stats['gl_ES'] = {"percentDone":36};
|
||||
stats['id_ID'] = {"percentDone":96};
|
||||
stats['it_IT'] = {"percentDone":96};
|
||||
stats['hu_HU'] = {"percentDone":84};
|
||||
stats['nl_BE'] = {"percentDone":87};
|
||||
stats['id_ID'] = {"percentDone":95};
|
||||
stats['it_IT'] = {"percentDone":95};
|
||||
stats['hu_HU'] = {"percentDone":83};
|
||||
stats['nl_BE'] = {"percentDone":86};
|
||||
stats['nl_NL'] = {"percentDone":90};
|
||||
stats['nb_NO'] = {"percentDone":96};
|
||||
stats['fa'] = {"percentDone":67};
|
||||
stats['pl_PL'] = {"percentDone":90};
|
||||
stats['pl_PL'] = {"percentDone":89};
|
||||
stats['pt_BR'] = {"percentDone":96};
|
||||
stats['pt_PT'] = {"percentDone":90};
|
||||
stats['ro'] = {"percentDone":63};
|
||||
stats['sl_SI'] = {"percentDone":91};
|
||||
stats['sv'] = {"percentDone":96};
|
||||
stats['pt_PT'] = {"percentDone":89};
|
||||
stats['ro'] = {"percentDone":62};
|
||||
stats['sl_SI'] = {"percentDone":90};
|
||||
stats['sv'] = {"percentDone":99};
|
||||
stats['th_TH'] = {"percentDone":42};
|
||||
stats['vi'] = {"percentDone":96};
|
||||
stats['tr_TR'] = {"percentDone":99};
|
||||
stats['uk_UA'] = {"percentDone":89};
|
||||
stats['el_GR'] = {"percentDone":92};
|
||||
stats['ru_RU'] = {"percentDone":89};
|
||||
stats['sr_RS'] = {"percentDone":81};
|
||||
stats['sr_RS'] = {"percentDone":80};
|
||||
stats['zh_CN'] = {"percentDone":99};
|
||||
stats['zh_TW'] = {"percentDone":94};
|
||||
stats['zh_TW'] = {"percentDone":95};
|
||||
stats['ja_JP'] = {"percentDone":95};
|
||||
stats['ko'] = {"percentDone":95};
|
||||
stats['ko'] = {"percentDone":94};
|
||||
module.exports = { locales: locales, stats: stats };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user