You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-30 20:39:46 +02:00
Compare commits
38 Commits
server-v2.
...
android-v2
Author | SHA1 | Date | |
---|---|---|---|
|
757c125bd3 | ||
|
2867b66cf1 | ||
|
5c6fd93753 | ||
|
ea65313bdb | ||
|
1711f7ec88 | ||
|
e0b5ef6630 | ||
|
4bbb3d1d58 | ||
|
fd769945b1 | ||
|
6e91d2784f | ||
|
881b2f17b1 | ||
|
e83cc58ea6 | ||
|
77def9f782 | ||
|
b23cc5d30a | ||
|
d8119bcf07 | ||
|
8bce259dc9 | ||
|
8a00eef901 | ||
|
31121c86d5 | ||
|
a4a156c7a5 | ||
|
c5b0529968 | ||
|
ba322b1f9b | ||
|
6f27eae7dd | ||
|
85cc08c0d4 | ||
|
ba38bf3490 | ||
|
2cf70675dc | ||
|
58f8d7e1b4 | ||
|
b55b35e53f | ||
|
c7194bf243 | ||
|
48abe2316e | ||
|
7aca380cfa | ||
|
551033f8ba | ||
|
3b6a66a016 | ||
|
5d7d1be363 | ||
|
2af3bf61ea | ||
|
6803f1c6a7 | ||
|
1aa70dd6b4 | ||
|
feaecf7653 | ||
|
af9f3eedd3 | ||
|
815800827b |
@@ -980,6 +980,9 @@ packages/lib/models/dateTimeFormats.test.js.map
|
||||
packages/lib/models/settings/FileHandler.d.ts
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
packages/lib/models/settings/FileHandler.js.map
|
||||
packages/lib/models/utils/itemCanBeEncrypted.d.ts
|
||||
packages/lib/models/utils/itemCanBeEncrypted.js
|
||||
packages/lib/models/utils/itemCanBeEncrypted.js.map
|
||||
packages/lib/models/utils/paginatedFeed.d.ts
|
||||
packages/lib/models/utils/paginatedFeed.js
|
||||
packages/lib/models/utils/paginatedFeed.js.map
|
||||
@@ -1169,6 +1172,9 @@ packages/lib/services/interop/InteropService_Importer_Raw.js.map
|
||||
packages/lib/services/interop/types.d.ts
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/interop/types.js.map
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.d.ts
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js.map
|
||||
packages/lib/services/keychain/KeychainService.d.ts
|
||||
packages/lib/services/keychain/KeychainService.js
|
||||
packages/lib/services/keychain/KeychainService.js.map
|
||||
|
7
.github/workflows/github-actions-main.yml
vendored
7
.github/workflows/github-actions-main.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
|
||||
- name: Install Docker Engine
|
||||
if: runner.os == 'Linux'
|
||||
if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
|
||||
run: |
|
||||
sudo apt-get install -y apt-transport-https
|
||||
sudo apt-get install -y ca-certificates
|
||||
@@ -41,8 +41,11 @@ jobs:
|
||||
with:
|
||||
node-version: '12'
|
||||
|
||||
# Login to Docker only if we're on a server release tag. If we run this on
|
||||
# a pull request it will fail because the PR doesn't have access to
|
||||
# secrets
|
||||
- uses: docker/login-action@v1
|
||||
if: runner.os == 'Linux'
|
||||
if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -966,6 +966,9 @@ packages/lib/models/dateTimeFormats.test.js.map
|
||||
packages/lib/models/settings/FileHandler.d.ts
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
packages/lib/models/settings/FileHandler.js.map
|
||||
packages/lib/models/utils/itemCanBeEncrypted.d.ts
|
||||
packages/lib/models/utils/itemCanBeEncrypted.js
|
||||
packages/lib/models/utils/itemCanBeEncrypted.js.map
|
||||
packages/lib/models/utils/paginatedFeed.d.ts
|
||||
packages/lib/models/utils/paginatedFeed.js
|
||||
packages/lib/models/utils/paginatedFeed.js.map
|
||||
@@ -1155,6 +1158,9 @@ packages/lib/services/interop/InteropService_Importer_Raw.js.map
|
||||
packages/lib/services/interop/types.d.ts
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/interop/types.js.map
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.d.ts
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js.map
|
||||
packages/lib/services/keychain/KeychainService.d.ts
|
||||
packages/lib/services/keychain/KeychainService.js
|
||||
packages/lib/services/keychain/KeychainService.js.map
|
||||
|
@@ -36,7 +36,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/Jo
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
---|---|---
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.7.5/joplin-v1.7.5.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.7.5/joplin-v1.7.5-32bit.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.0.4/joplin-v2.0.4.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.0.4/joplin-v2.0.4-32bit.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
@@ -524,9 +524,9 @@ Current translations:
|
||||
<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) | 74%
|
||||
<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) | | 57%
|
||||
<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 | 82%
|
||||
<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) | [Lukas Helebrandt](mailto:lukas@aiya.cz) | 85%
|
||||
<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: | 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) | 100%
|
||||
<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) | 100%
|
||||
<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) | 94%
|
||||
<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) | | 56%
|
||||
<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%
|
||||
|
@@ -71,20 +71,28 @@ class Command extends BaseCommand {
|
||||
};
|
||||
|
||||
if (args.command === 'enable') {
|
||||
const password = options.password ? options.password.toString() : await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
||||
const argPassword = options.password ? options.password.toString() : '';
|
||||
const password = argPassword ? argPassword : await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
||||
if (!password) {
|
||||
this.stdout(_('Operation cancelled'));
|
||||
return;
|
||||
}
|
||||
const password2 = await this.prompt(_('Confirm password:'), { type: 'string', secure: true });
|
||||
if (!password2) {
|
||||
this.stdout(_('Operation cancelled'));
|
||||
return;
|
||||
}
|
||||
if (password !== password2) {
|
||||
this.stdout(_('Passwords do not match!'));
|
||||
return;
|
||||
|
||||
// If the password was passed via command line, we don't ask for
|
||||
// confirmation. This is to allow setting up E2EE entirely from the
|
||||
// command line.
|
||||
if (!argPassword) {
|
||||
const password2 = await this.prompt(_('Confirm password:'), { type: 'string', secure: true });
|
||||
if (!password2) {
|
||||
this.stdout(_('Operation cancelled'));
|
||||
return;
|
||||
}
|
||||
if (password !== password2) {
|
||||
this.stdout(_('Passwords do not match!'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password);
|
||||
return;
|
||||
}
|
||||
|
2
packages/app-desktop/package-lock.json
generated
2
packages/app-desktop/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.10",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.10",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
@@ -1,48 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
ROOT_DIR="$SCRIPT_DIR/../.."
|
||||
|
||||
if [ "$1" == "" ]; then
|
||||
echo "User number is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USER_NUM=$1
|
||||
RESET_ALL=$2
|
||||
PROFILE_DIR=~/.config/joplindev-desktop-$USER_NUM
|
||||
|
||||
if [ "$RESET_ALL" == "1" ]; then
|
||||
CMD_FILE="$SCRIPT_DIR/runForSharingCommands-$USER_NUM.txt"
|
||||
rm -f "$CMD_FILE"
|
||||
|
||||
USER_EMAIL="user$USER_NUM@example.com"
|
||||
rm -rf "$PROFILE_DIR"
|
||||
|
||||
echo "config keychain.supported 0" >> "$CMD_FILE"
|
||||
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 123456" >> "$CMD_FILE"
|
||||
|
||||
if [ "$USER_NUM" == "1" ]; then
|
||||
curl --data '{"action": "createTestUsers"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
echo 'mkbook "shared"' >> "$CMD_FILE"
|
||||
echo 'mkbook "other"' >> "$CMD_FILE"
|
||||
echo 'use "shared"' >> "$CMD_FILE"
|
||||
echo 'mknote "note 1"' >> "$CMD_FILE"
|
||||
echo 'mknote "note 2"' >> "$CMD_FILE"
|
||||
fi
|
||||
|
||||
cd "$ROOT_DIR/packages/app-cli"
|
||||
npm start -- --profile "$PROFILE_DIR" batch "$CMD_FILE"
|
||||
fi
|
||||
|
||||
cd "$ROOT_DIR/packages/app-desktop"
|
||||
npm start -- --profile "$PROFILE_DIR"
|
68
packages/app-desktop/runForTesting.sh
Executable file
68
packages/app-desktop/runForTesting.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
ROOT_DIR="$SCRIPT_DIR/../.."
|
||||
|
||||
if [ "$1" == "" ]; then
|
||||
echo "User number is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USER_NUM=$1
|
||||
COMMANDS=($(echo $2 | tr "," "\n"))
|
||||
PROFILE_DIR=~/.config/joplindev-desktop-$USER_NUM
|
||||
|
||||
CMD_FILE="$SCRIPT_DIR/runForSharingCommands-$USER_NUM.txt"
|
||||
rm -f "$CMD_FILE"
|
||||
touch "$CMD_FILE"
|
||||
|
||||
for CMD in "${COMMANDS[@]}"
|
||||
do
|
||||
if [[ $CMD == "createUsers" ]]; then
|
||||
|
||||
curl --data '{"action": "createTestUsers"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
elif [[ $CMD == "createData" ]]; then
|
||||
|
||||
echo 'mkbook "shared"' >> "$CMD_FILE"
|
||||
echo 'mkbook "other"' >> "$CMD_FILE"
|
||||
echo 'use "shared"' >> "$CMD_FILE"
|
||||
echo 'mknote "note 1"' >> "$CMD_FILE"
|
||||
echo 'mknote "note 2"' >> "$CMD_FILE"
|
||||
|
||||
elif [[ $CMD == "reset" ]]; then
|
||||
|
||||
USER_EMAIL="user$USER_NUM@example.com"
|
||||
rm -rf "$PROFILE_DIR"
|
||||
echo "config keychain.supported 0" >> "$CMD_FILE"
|
||||
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 123456" >> "$CMD_FILE"
|
||||
|
||||
elif [[ $CMD == "e2ee" ]]; then
|
||||
|
||||
echo "e2ee enable --password 111111" >> "$CMD_FILE"
|
||||
|
||||
else
|
||||
|
||||
echo "Unknown command: $CMD"
|
||||
exit 1
|
||||
|
||||
fi
|
||||
done
|
||||
|
||||
cd "$ROOT_DIR/packages/app-cli"
|
||||
npm start -- --profile "$PROFILE_DIR" batch "$CMD_FILE"
|
||||
|
||||
if [[ $COMMANDS != "" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$ROOT_DIR/packages/app-desktop"
|
||||
npm start -- --profile "$PROFILE_DIR"
|
@@ -20,13 +20,18 @@ function execCommand(command) {
|
||||
});
|
||||
}
|
||||
|
||||
function isDesktopAppTag(tagName) {
|
||||
if (!tagName) return false;
|
||||
return tagName[0] === 'v';
|
||||
}
|
||||
|
||||
module.exports = async function(params) {
|
||||
if (process.platform !== 'darwin') return;
|
||||
|
||||
console.info('Checking if notarization should be done...');
|
||||
|
||||
if (!process.env.IS_CONTINUOUS_INTEGRATION || !process.env.GIT_TAG_NAME) {
|
||||
console.info(`Either not running in CI or not processing a tag - skipping notarization. process.env.IS_CONTINUOUS_INTEGRATION = ${process.env.IS_CONTINUOUS_INTEGRATION}; process.env.GIT_TAG_NAME = ${process.env.GIT_TAG_NAME}`);
|
||||
if (!process.env.IS_CONTINUOUS_INTEGRATION || !isDesktopAppTag(process.env.GIT_TAG_NAME)) {
|
||||
console.info(`Either not running in CI or not processing a desktop app tag - skipping notarization. process.env.IS_CONTINUOUS_INTEGRATION = ${process.env.IS_CONTINUOUS_INTEGRATION}; process.env.GIT_TAG_NAME = ${process.env.GIT_TAG_NAME}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -141,8 +141,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097632
|
||||
versionName "2.0.1"
|
||||
versionCode 2097635
|
||||
versionName "2.0.4"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -486,13 +486,13 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MARKETING_VERSION = 12.0.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -514,12 +514,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MARKETING_VERSION = 12.0.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -659,14 +659,14 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MARKETING_VERSION = 12.0.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -690,14 +690,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MARKETING_VERSION = 12.0.2;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@@ -135,7 +135,9 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
},
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
// JSON files can also be required from scripts so we include this.
|
||||
// https://github.com/joplin/plugin-bibtex/pull/2
|
||||
extensions: ['.tsx', '.ts', '.js', '.json'],
|
||||
},
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
@@ -167,7 +169,7 @@ const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
},
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
extensions: ['.tsx', '.ts', '.js', '.json'],
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -766,10 +766,10 @@ export default class BaseApplication {
|
||||
}
|
||||
|
||||
if (Setting.value('env') === Env.Dev) {
|
||||
Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
|
||||
Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
|
||||
// Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
|
||||
// Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
|
||||
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
|
||||
// Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
|
||||
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
|
||||
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
|
||||
}
|
||||
|
||||
// For now always disable fuzzy search due to performance issues:
|
||||
|
@@ -343,7 +343,7 @@ export default class JoplinDatabase extends Database {
|
||||
// must be set in the synchronizer too.
|
||||
|
||||
// Note: v16 and v17 don't do anything. They were used to debug an issue.
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38];
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39];
|
||||
|
||||
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
|
||||
|
||||
@@ -888,6 +888,10 @@ export default class JoplinDatabase extends Database {
|
||||
GROUP BY tags.id`);
|
||||
}
|
||||
|
||||
if (targetVersion == 39) {
|
||||
queries.push('ALTER TABLE `notes` ADD COLUMN conflict_original_id TEXT NOT NULL DEFAULT ""');
|
||||
}
|
||||
|
||||
const updateVersionQuery = { sql: 'UPDATE version SET version = ?', params: [targetVersion] };
|
||||
|
||||
queries.push(updateVersionQuery);
|
||||
|
@@ -4,6 +4,7 @@ const { rtrimSlashes } = require('./path-utils.js');
|
||||
import JoplinError from './JoplinError';
|
||||
import { Env } from './models/Setting';
|
||||
import Logger from './Logger';
|
||||
import personalizedUserContentBaseUrl from './services/joplinServer/personalizedUserContentBaseUrl';
|
||||
const { stringify } = require('query-string');
|
||||
|
||||
const logger = Logger.create('JoplinServerApi');
|
||||
@@ -56,14 +57,8 @@ export default class JoplinServerApi {
|
||||
return rtrimSlashes(this.options_.baseUrl());
|
||||
}
|
||||
|
||||
public userContentBaseUrl(userId: string) {
|
||||
if (this.options_.userContentBaseUrl()) {
|
||||
if (!userId) throw new Error('User ID must be specified');
|
||||
const url = new URL(this.options_.userContentBaseUrl());
|
||||
return `${url.protocol}//${userId.substr(0, 10).toLowerCase()}.${url.host}`;
|
||||
} else {
|
||||
return this.baseUrl();
|
||||
}
|
||||
public personalizedUserContentBaseUrl(userId: string) {
|
||||
return personalizedUserContentBaseUrl(userId, this.baseUrl(), this.options_.userContentBaseUrl());
|
||||
}
|
||||
|
||||
private async session() {
|
||||
|
@@ -46,6 +46,8 @@ function isCannotSyncError(error: any): boolean {
|
||||
|
||||
export default class Synchronizer {
|
||||
|
||||
public static verboseMode: boolean = true;
|
||||
|
||||
private db_: any;
|
||||
private api_: any;
|
||||
private appType_: string;
|
||||
@@ -195,7 +197,11 @@ export default class Synchronizer {
|
||||
line.push(`(Remote ${s.join(', ')})`);
|
||||
}
|
||||
|
||||
this.logger().debug(line.join(': '));
|
||||
if (Synchronizer.verboseMode) {
|
||||
this.logger().info(line.join(': '));
|
||||
} else {
|
||||
this.logger().debug(line.join(': '));
|
||||
}
|
||||
|
||||
if (!this.progressReport_[action]) this.progressReport_[action] = 0;
|
||||
this.progressReport_[action] += actionCount;
|
||||
@@ -516,6 +522,40 @@ export default class Synchronizer {
|
||||
if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || action === 'updateRemote' || (action == 'itemConflict' && remote))) {
|
||||
const localState = await Resource.localState(local.id);
|
||||
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE) {
|
||||
// This condition normally shouldn't happen
|
||||
// because the normal cases are as follow:
|
||||
//
|
||||
// - User creates a resource locally - in that
|
||||
// case the fetch status is DONE, so we cannot
|
||||
// end up here.
|
||||
//
|
||||
// - User fetches a new resource metadata, but
|
||||
// not the blob - in that case fetch status is
|
||||
// IDLE. However in that case, we cannot end
|
||||
// up in this place either, because the action
|
||||
// cannot be createRemote (because the
|
||||
// resource has not been created locally) or
|
||||
// updateRemote (because a resouce cannot be
|
||||
// modified locally unless the blob is present
|
||||
// too).
|
||||
//
|
||||
// Possibly the only case we can end up here is
|
||||
// if a resource metadata has been downloaded,
|
||||
// but not the blob yet. Then the sync target is
|
||||
// switched to a different one. In that case, we
|
||||
// can have a fetch status IDLE, with an
|
||||
// "updateRemote" action, if the timestamp of
|
||||
// the server resource is before the timestamp
|
||||
// of the local resource.
|
||||
//
|
||||
// In that case we can't do much so we mark the
|
||||
// resource as "cannot sync". Otherwise it will
|
||||
// throw the error "Processing a path that has
|
||||
// already been done" on the next loop, and sync
|
||||
// will never finish because we'll always end up
|
||||
// here.
|
||||
this.logger().info(`Need to upload a resource, but blob is not present: ${path}`);
|
||||
await handleCannotSyncItem(ItemClass, syncTargetId, local, 'Trying to upload resource, but only metadata is present.');
|
||||
action = null;
|
||||
} else {
|
||||
try {
|
||||
@@ -610,10 +650,7 @@ export default class Synchronizer {
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
if (mustHandleConflict) {
|
||||
const conflictedNote = Object.assign({}, local);
|
||||
delete conflictedNote.id;
|
||||
conflictedNote.is_conflict = 1;
|
||||
await Note.save(conflictedNote, { autoTimestamp: false, changeSource: ItemChange.SOURCE_SYNC });
|
||||
await Note.createConflictNote(local, ItemChange.SOURCE_SYNC);
|
||||
}
|
||||
} else if (action == 'resourceConflict') {
|
||||
// ------------------------------------------------------------------------------
|
||||
|
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
@@ -46,9 +46,9 @@ stats['eu'] = {"percentDone":30};
|
||||
stats['bs_BA'] = {"percentDone":74};
|
||||
stats['bg_BG'] = {"percentDone":57};
|
||||
stats['ca'] = {"percentDone":82};
|
||||
stats['hr_HR'] = {"percentDone":96};
|
||||
stats['cs_CZ'] = {"percentDone":85};
|
||||
stats['da_DK'] = {"percentDone":95};
|
||||
stats['hr_HR'] = {"percentDone":100};
|
||||
stats['cs_CZ'] = {"percentDone":100};
|
||||
stats['da_DK'] = {"percentDone":99};
|
||||
stats['de_DE'] = {"percentDone":94};
|
||||
stats['et_EE'] = {"percentDone":56};
|
||||
stats['en_GB'] = {"percentDone":100};
|
||||
|
@@ -1,26 +1,18 @@
|
||||
import { ModelType } from '../BaseModel';
|
||||
import { NoteEntity } from '../services/database/types';
|
||||
import { BaseItemEntity, NoteEntity } from '../services/database/types';
|
||||
import Setting from './Setting';
|
||||
import BaseModel from '../BaseModel';
|
||||
import time from '../time';
|
||||
import markdownUtils from '../markdownUtils';
|
||||
import { _ } from '../locale';
|
||||
|
||||
import Database from '../database';
|
||||
import ItemChange from './ItemChange';
|
||||
import ShareService from '../services/share/ShareService';
|
||||
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
|
||||
const JoplinError = require('../JoplinError.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const moment = require('moment');
|
||||
|
||||
export interface BaseItemEntity {
|
||||
id?: string;
|
||||
encryption_applied?: boolean;
|
||||
is_shared?: number;
|
||||
share_id?: string;
|
||||
type_?: ModelType;
|
||||
}
|
||||
|
||||
export interface ItemsThatNeedDecryptionResult {
|
||||
hasMore: boolean;
|
||||
items: any[];
|
||||
@@ -404,7 +396,7 @@ export default class BaseItem extends BaseModel {
|
||||
|
||||
const serialized = await ItemClass.serialize(item, shownKeys);
|
||||
|
||||
if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported() || item.is_shared || item.share_id) {
|
||||
if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported() || !itemCanBeEncrypted(item)) {
|
||||
// Normally not possible since itemsThatNeedSync should only return decrypted items
|
||||
if (item.encryption_applied) throw new JoplinError('Item is encrypted but encryption is currently disabled', 'cannotSyncEncrypted');
|
||||
return serialized;
|
||||
|
@@ -6,6 +6,7 @@ import { sortedIds, createNTestNotes, setupDatabaseAndSynchronizer, switchClient
|
||||
import Folder from './Folder';
|
||||
import Note from './Note';
|
||||
import Tag from './Tag';
|
||||
import ItemChange from './ItemChange';
|
||||
const ArrayUtils = require('../ArrayUtils.js');
|
||||
|
||||
async function allItems() {
|
||||
@@ -344,4 +345,44 @@ describe('models_Note', function() {
|
||||
expect(sortedNotes3[4].id).toBe(note2.id);
|
||||
}));
|
||||
|
||||
it('should create a conflict note', async () => {
|
||||
const folder = await Folder.save({ title: 'Source Folder' });
|
||||
const origNote = await Note.save({ title: 'note', parent_id: folder.id });
|
||||
const conflictedNote = await Note.createConflictNote(origNote, ItemChange.SOURCE_SYNC);
|
||||
|
||||
expect(conflictedNote.is_conflict).toBe(1);
|
||||
expect(conflictedNote.conflict_original_id).toBe(origNote.id);
|
||||
expect(conflictedNote.parent_id).toBe(folder.id);
|
||||
});
|
||||
|
||||
it('should copy conflicted note to target folder and cancel conflict', (async () => {
|
||||
const srcfolder = await Folder.save({ title: 'Source Folder' });
|
||||
const targetfolder = await Folder.save({ title: 'Target Folder' });
|
||||
|
||||
const note1 = await Note.save({ title: 'note', parent_id: srcfolder.id });
|
||||
const conflictedNote = await Note.createConflictNote(note1, ItemChange.SOURCE_SYNC);
|
||||
|
||||
const note2 = await Note.copyToFolder(conflictedNote.id, targetfolder.id);
|
||||
|
||||
expect(note2.id === conflictedNote.id).toBe(false);
|
||||
expect(note2.title).toBe(conflictedNote.title);
|
||||
expect(note2.is_conflict).toBe(0);
|
||||
expect(note2.conflict_original_id).toBe('');
|
||||
expect(note2.parent_id).toBe(targetfolder.id);
|
||||
}));
|
||||
|
||||
it('should move conflicted note to target folder and cancel conflict', (async () => {
|
||||
const srcFolder = await Folder.save({ title: 'Source Folder' });
|
||||
const targetFolder = await Folder.save({ title: 'Target Folder' });
|
||||
const note1 = await Note.save({ title: 'note', parent_id: srcFolder.id });
|
||||
|
||||
const conflictedNote = await Note.createConflictNote(note1, ItemChange.SOURCE_SYNC);
|
||||
|
||||
const movedNote = await Note.moveToFolder(conflictedNote.id, targetFolder.id);
|
||||
|
||||
expect(movedNote.parent_id).toBe(targetFolder.id);
|
||||
expect(movedNote.is_conflict).toBe(0);
|
||||
expect(movedNote.conflict_original_id).toBe('');
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -127,7 +127,7 @@ export default class Note extends BaseItem {
|
||||
|
||||
static async linkedItemIdsByType(type: ModelType, body: string) {
|
||||
const items = await this.linkedItems(body);
|
||||
const output = [];
|
||||
const output: string[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
@@ -523,6 +523,7 @@ export default class Note extends BaseItem {
|
||||
changes: {
|
||||
parent_id: folderId,
|
||||
is_conflict: 0, // Also reset the conflict flag in case we're moving the note out of the conflict folder
|
||||
conflict_original_id: '', // Reset parent id as well.
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -537,6 +538,7 @@ export default class Note extends BaseItem {
|
||||
id: noteId,
|
||||
parent_id: folderId,
|
||||
is_conflict: 0,
|
||||
conflict_original_id: '',
|
||||
updated_time: time.unixMs(),
|
||||
};
|
||||
|
||||
@@ -911,4 +913,12 @@ export default class Note extends BaseItem {
|
||||
return new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
||||
}
|
||||
|
||||
|
||||
static async createConflictNote(sourceNote: NoteEntity, changeSource: number): Promise<NoteEntity> {
|
||||
const conflictNote = Object.assign({}, sourceNote);
|
||||
delete conflictNote.id;
|
||||
conflictNote.is_conflict = 1;
|
||||
conflictNote.conflict_original_id = sourceNote.id;
|
||||
return await Note.save(conflictNote, { autoTimestamp: false, changeSource: changeSource });
|
||||
}
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ const { mime } = require('../mime-utils.js');
|
||||
const { filename, safeFilename } = require('../path-utils');
|
||||
const { FsDriverDummy } = require('../fs-driver-dummy.js');
|
||||
import JoplinError from '../JoplinError';
|
||||
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
|
||||
|
||||
export default class Resource extends BaseItem {
|
||||
|
||||
@@ -192,10 +193,10 @@ export default class Resource extends BaseItem {
|
||||
// as it should be uploaded to the sync target. Note that this may be different from what is stored
|
||||
// in the database. In particular, the flag encryption_blob_encrypted might be 1 on the sync target
|
||||
// if the resource is encrypted, but will be 0 locally because the device has the decrypted resource.
|
||||
static async fullPathForSyncUpload(resource: ResourceEntity) {
|
||||
public static async fullPathForSyncUpload(resource: ResourceEntity) {
|
||||
const plainTextPath = this.fullPath(resource);
|
||||
|
||||
if (!Setting.value('encryption.enabled')) {
|
||||
if (!Setting.value('encryption.enabled') || !itemCanBeEncrypted(resource as any)) {
|
||||
// Normally not possible since itemsThatNeedSync should only return decrypted items
|
||||
if (resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled');
|
||||
return { path: plainTextPath, resource: resource };
|
||||
|
5
packages/lib/models/utils/itemCanBeEncrypted.ts
Normal file
5
packages/lib/models/utils/itemCanBeEncrypted.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { BaseItemEntity } from '../../services/database/types';
|
||||
|
||||
export default function(resource: BaseItemEntity): boolean {
|
||||
return !resource.is_shared && !resource.share_id;
|
||||
}
|
@@ -1,3 +1,17 @@
|
||||
import { ModelType } from "../../BaseModel";
|
||||
|
||||
export interface BaseItemEntity {
|
||||
id?: string;
|
||||
encryption_applied?: boolean;
|
||||
is_shared?: number;
|
||||
share_id?: string;
|
||||
type_?: ModelType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// AUTO-GENERATED BY packages/tools/generate-database-types.js
|
||||
|
||||
/*
|
||||
@@ -29,8 +43,6 @@ export interface FolderEntity {
|
||||
"encryption_applied"?: number
|
||||
"parent_id"?: string
|
||||
"is_shared"?: number
|
||||
"is_linked_folder"?: number
|
||||
"source_folder_owner_id"?: string
|
||||
"share_id"?: string
|
||||
"type_"?: number
|
||||
}
|
||||
@@ -117,6 +129,7 @@ export interface NoteEntity {
|
||||
"markup_language"?: number
|
||||
"is_shared"?: number
|
||||
"share_id"?: string
|
||||
"conflict_original_id"?: string
|
||||
"type_"?: number
|
||||
}
|
||||
export interface NotesNormalizedEntity {
|
||||
@@ -226,6 +239,7 @@ export interface TagsWithNoteCountEntity {
|
||||
"created_time"?: number | null
|
||||
"updated_time"?: number | null
|
||||
"note_count"?: any | null
|
||||
"todo_completed_count"?: any | null
|
||||
"type_"?: number
|
||||
}
|
||||
export interface VersionEntity {
|
||||
|
@@ -0,0 +1,18 @@
|
||||
// For this:
|
||||
//
|
||||
// userId: d67VzcrHs6zGzROagnzwhOZJI0vKbezc
|
||||
// baseUrl: http://example.com
|
||||
// userContentBaseUrl: http://usercontent.com
|
||||
//
|
||||
// => Returns http://d67Vzcrhs6.usercontent.com
|
||||
//
|
||||
// If the userContentBaseUrl is an empty string, the baseUrl is returned instead.
|
||||
export default function(userId: string, baseUrl: string, userContentBaseUrl: string) {
|
||||
if (userContentBaseUrl && baseUrl !== userContentBaseUrl) {
|
||||
if (!userId) throw new Error('User ID must be specified');
|
||||
const url = new URL(userContentBaseUrl);
|
||||
return `${url.protocol}//${userId.substr(0, 10).toLowerCase()}.${url.host}`;
|
||||
} else {
|
||||
return baseUrl;
|
||||
}
|
||||
}
|
@@ -141,7 +141,7 @@ export default class ShareService {
|
||||
}
|
||||
|
||||
public shareUrl(userId: string, share: StateShare): string {
|
||||
return `${this.api().userContentBaseUrl(userId)}/shares/${share.id}`;
|
||||
return `${this.api().personalizedUserContentBaseUrl(userId)}/shares/${share.id}`;
|
||||
}
|
||||
|
||||
public get shares() {
|
||||
|
@@ -44,9 +44,10 @@ describe('Synchronizer.conflicts', function() {
|
||||
// the conflicted and original note must be the same in every way, to make sure no data has been lost.
|
||||
const conflictedNote = conflictedNotes[0];
|
||||
expect(conflictedNote.id == note2conf.id).toBe(false);
|
||||
expect(conflictedNote.conflict_original_id).toBe(note2conf.id);
|
||||
for (const n in conflictedNote) {
|
||||
if (!conflictedNote.hasOwnProperty(n)) continue;
|
||||
if (n == 'id' || n == 'is_conflict') continue;
|
||||
if (n == 'id' || n == 'is_conflict' || n == 'conflict_original_id') continue;
|
||||
expect(conflictedNote[n]).toBe(note2conf[n]);
|
||||
}
|
||||
|
||||
|
@@ -8,9 +8,15 @@ import Resource from '../../models/Resource';
|
||||
import ResourceFetcher from '../../services/ResourceFetcher';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
import { ResourceEntity } from '../database/types';
|
||||
import Synchronizer from '../../Synchronizer';
|
||||
|
||||
let insideBeforeEach = false;
|
||||
|
||||
function newResourceFetcher(synchronizer: Synchronizer) {
|
||||
return new ResourceFetcher(() => { return synchronizer.api(); });
|
||||
}
|
||||
|
||||
describe('Synchronizer.e2ee', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
@@ -223,7 +229,7 @@ describe('Synchronizer.e2ee', function() {
|
||||
Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
|
||||
await encryptionService().loadMasterKeysFromSettings();
|
||||
|
||||
const fetcher = new ResourceFetcher(() => { return synchronizer().api(); });
|
||||
const fetcher = newResourceFetcher(synchronizer());
|
||||
fetcher.queueDownload_(resource1.id);
|
||||
await fetcher.waitForAllFinished();
|
||||
await decryptionWorker().start();
|
||||
@@ -287,7 +293,7 @@ describe('Synchronizer.e2ee', function() {
|
||||
expect(!!resource.encryption_applied).toBe(false);
|
||||
expect(!!resource.encryption_blob_encrypted).toBe(true);
|
||||
|
||||
const resourceFetcher = new ResourceFetcher(() => { return synchronizer().api(); });
|
||||
const resourceFetcher = newResourceFetcher(synchronizer());
|
||||
await resourceFetcher.start();
|
||||
await resourceFetcher.waitForAllFinished();
|
||||
|
||||
@@ -378,8 +384,15 @@ describe('Synchronizer.e2ee', function() {
|
||||
},
|
||||
]);
|
||||
|
||||
const note1 = await Note.loadByTitle('un');
|
||||
let note1 = await Note.loadByTitle('un');
|
||||
let note2 = await Note.loadByTitle('deux');
|
||||
await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`);
|
||||
await shim.attachFileToNote(note2, `${supportDir}/photo.jpg`);
|
||||
note1 = await Note.loadByTitle('un');
|
||||
note2 = await Note.loadByTitle('deux');
|
||||
const resourceId1 = (await Note.linkedResourceIds(note1.body))[0];
|
||||
const resourceId2 = (await Note.linkedResourceIds(note2.body))[0];
|
||||
|
||||
await synchronizerStart();
|
||||
|
||||
await switchClient(2);
|
||||
@@ -405,11 +418,37 @@ describe('Synchronizer.e2ee', function() {
|
||||
// The shared note should be decrypted
|
||||
const note2_2 = await Note.load(note2.id);
|
||||
expect(note2_2.title).toBe('deux');
|
||||
expect(note2_2.encryption_applied).toBe(0);
|
||||
expect(note2_2.is_shared).toBe(1);
|
||||
|
||||
// The resource linked to the shared note should also be decrypted
|
||||
const resource2: ResourceEntity = await Resource.load(resourceId2);
|
||||
expect(resource2.is_shared).toBe(1);
|
||||
expect(resource2.encryption_applied).toBe(0);
|
||||
|
||||
const fetcher = newResourceFetcher(synchronizer());
|
||||
await fetcher.start();
|
||||
await fetcher.waitForAllFinished();
|
||||
|
||||
// Because the resource is decrypted, the encrypted blob file should not
|
||||
// exist, but the plain text one should.
|
||||
expect(await shim.fsDriver().exists(Resource.fullPath(resource2, true))).toBe(false);
|
||||
expect(await shim.fsDriver().exists(Resource.fullPath(resource2))).toBe(true);
|
||||
|
||||
// The non-shared note should be encrypted
|
||||
const note1_2 = await Note.load(note1.id);
|
||||
expect(note1_2.title).toBe('');
|
||||
|
||||
// The linked resource should also be encrypted
|
||||
const resource1: ResourceEntity = await Resource.load(resourceId1);
|
||||
expect(resource1.is_shared).toBe(0);
|
||||
expect(resource1.encryption_applied).toBe(1);
|
||||
|
||||
// And the plain text blob should not be present. The encrypted one
|
||||
// shouldn't either because it can only be downloaded once the metadata
|
||||
// has been decrypted.
|
||||
expect(await shim.fsDriver().exists(Resource.fullPath(resource1, true))).toBe(false);
|
||||
expect(await shim.fsDriver().exists(Resource.fullPath(resource1))).toBe(false);
|
||||
}));
|
||||
|
||||
it('should not encrypt items that are shared by folder', (async () => {
|
||||
|
@@ -10,6 +10,7 @@ import Note from '../../models/Note';
|
||||
import Resource from '../../models/Resource';
|
||||
import ResourceFetcher from '../../services/ResourceFetcher';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
import { ModelType } from '../../BaseModel';
|
||||
|
||||
let insideBeforeEach = false;
|
||||
|
||||
@@ -333,6 +334,13 @@ describe('Synchronizer.resources', function() {
|
||||
await Resource.setLocalState(resource.id, { fetch_status: Resource.FETCH_STATUS_DONE });
|
||||
await synchronizerStart();
|
||||
|
||||
// At first, the resource is marked as cannot sync, so even after
|
||||
// synchronisation, nothing should happen.
|
||||
expect((await remoteResources()).length).toBe(0);
|
||||
|
||||
// The user can retry the item, in which case sync should happen.
|
||||
await BaseItem.saveSyncEnabled(ModelType.Resource, resource.id);
|
||||
await synchronizerStart();
|
||||
expect((await remoteResources()).length).toBe(1);
|
||||
}));
|
||||
|
||||
|
@@ -158,7 +158,7 @@ class HtmlUtils {
|
||||
// "link" can be used to escape the parser and inject JavaScript.
|
||||
// Adding "meta" too for the same reason as it shouldn't be used in
|
||||
// notes anyway.
|
||||
const disallowedTags = ['script', 'iframe', 'frameset', 'frame', 'object', 'base', 'embed', 'link', 'meta', 'noscript'];
|
||||
const disallowedTags = ['script', 'iframe', 'frameset', 'frame', 'object', 'base', 'embed', 'link', 'meta', 'noscript', 'button', 'form', 'input', 'select', 'textarea', 'option', 'optgroup'];
|
||||
|
||||
const parser = new htmlparser2.Parser({
|
||||
|
||||
|
2
packages/server/package-lock.json
generated
2
packages/server/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.12",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
|
@@ -5,6 +5,7 @@ import { ErrorUnprocessableEntity, ErrorBadRequest } from '../utils/errors';
|
||||
import { Models } from './factory';
|
||||
import * as EventEmitter from 'events';
|
||||
import { Config } from '../utils/types';
|
||||
import personalizedUserContentBaseUrl from '@joplin/lib/services/joplinServer/personalizedUserContentBaseUrl';
|
||||
|
||||
export interface SaveOptions {
|
||||
isNew?: boolean;
|
||||
@@ -64,10 +65,14 @@ export default abstract class BaseModel<T> {
|
||||
return this.config_.baseUrl;
|
||||
}
|
||||
|
||||
protected get userContentUrl(): string {
|
||||
protected get userContentBaseUrl(): string {
|
||||
return this.config_.userContentBaseUrl;
|
||||
}
|
||||
|
||||
protected personalizedUserContentBaseUrl(userId: Uuid): string {
|
||||
return personalizedUserContentBaseUrl(userId, this.baseUrl, this.userContentBaseUrl);
|
||||
}
|
||||
|
||||
protected get appName(): string {
|
||||
return this.config_.appName;
|
||||
}
|
||||
|
@@ -309,13 +309,15 @@ export default class ItemModel extends BaseModel<Item> {
|
||||
item.jop_encryption_applied = joplinItem.encryption_applied || 0;
|
||||
item.jop_share_id = joplinItem.share_id || '';
|
||||
|
||||
delete joplinItem.id;
|
||||
delete joplinItem.parent_id;
|
||||
delete joplinItem.share_id;
|
||||
delete joplinItem.type_;
|
||||
delete joplinItem.encryption_applied;
|
||||
const joplinItemToSave = { ...joplinItem };
|
||||
|
||||
item.content = Buffer.from(JSON.stringify(joplinItem));
|
||||
delete joplinItemToSave.id;
|
||||
delete joplinItemToSave.parent_id;
|
||||
delete joplinItemToSave.share_id;
|
||||
delete joplinItemToSave.type_;
|
||||
delete joplinItemToSave.encryption_applied;
|
||||
|
||||
item.content = Buffer.from(JSON.stringify(joplinItemToSave));
|
||||
} else {
|
||||
item.content = buffer;
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ export default class ShareModel extends BaseModel<Share> {
|
||||
}
|
||||
|
||||
public checkShareUrl(share: Share, shareUrl: string) {
|
||||
if (this.baseUrl === this.userContentUrl) return; // OK
|
||||
if (this.baseUrl === this.userContentBaseUrl) return; // OK
|
||||
|
||||
const userId = userIdFromUserContentUrl(shareUrl);
|
||||
const shareUserId = share.owner_id.toLowerCase();
|
||||
@@ -93,8 +93,8 @@ export default class ShareModel extends BaseModel<Share> {
|
||||
return !!r;
|
||||
}
|
||||
|
||||
public shareUrl(id: Uuid, query: any = null): string {
|
||||
return setQueryParameters(`${this.userContentUrl}/shares/${id}`, query);
|
||||
public shareUrl(shareOwnerId: Uuid, id: Uuid, query: any = null): string {
|
||||
return setQueryParameters(`${this.personalizedUserContentBaseUrl(shareOwnerId)}/shares/${id}`, query);
|
||||
}
|
||||
|
||||
public async byItemId(itemId: Uuid): Promise<Share | null> {
|
||||
|
@@ -108,17 +108,17 @@ export default class UserModel extends BaseModel<User> {
|
||||
}
|
||||
|
||||
public async checkMaxItemSizeLimit(user: User, buffer: Buffer, item: Item, joplinItem: any) {
|
||||
const itemTitle = joplinItem ? joplinItem.title || '' : '';
|
||||
const isNote = joplinItem && joplinItem.type_ === ModelType.Note;
|
||||
|
||||
// If the item is encrypted, we apply a multipler because encrypted
|
||||
// items can be much larger (seems to be up to twice the size but for
|
||||
// safety let's go with 2.2).
|
||||
const maxSize = user.max_item_size * (item.jop_encryption_applied ? 2.2 : 1);
|
||||
if (maxSize && buffer.byteLength > maxSize) {
|
||||
const itemTitle = joplinItem ? joplinItem.title || '' : '';
|
||||
const isNote = joplinItem && joplinItem.type_ === ModelType.Note;
|
||||
|
||||
throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it is larger than than the allowed limit (%s)',
|
||||
isNote ? _('note') : _('attachment'),
|
||||
itemTitle ? itemTitle : name,
|
||||
itemTitle ? itemTitle : item.name,
|
||||
formatBytes(user.max_item_size)
|
||||
));
|
||||
}
|
||||
|
@@ -177,7 +177,7 @@ async function renderNote(share: Share, note: NoteEntity, resourceInfos: Resourc
|
||||
if (item.type_ === ModelType.Note) {
|
||||
return '#';
|
||||
} else if (item.type_ === ModelType.Resource) {
|
||||
return `${models_.share().shareUrl(share.id)}?resource_id=${item.id}&t=${item.updated_time}`;
|
||||
return `${models_.share().shareUrl(share.owner_id, share.id)}?resource_id=${item.id}&t=${item.updated_time}`;
|
||||
} else {
|
||||
throw new Error(`Unsupported item type: ${item.type_}`);
|
||||
}
|
||||
|
@@ -477,6 +477,7 @@ encryption_applied: 0
|
||||
markup_language: 1
|
||||
is_shared: 1
|
||||
share_id: ${note.share_id || ''}
|
||||
conflict_original_id:
|
||||
type_: 1`;
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import { execCommand2, rootDir } from './tool-utils';
|
||||
function getVersionFromTag(tagName: string): string {
|
||||
if (tagName.indexOf('server-') !== 0) throw new Error(`Invalid tag: ${tagName}`);
|
||||
const s = tagName.split('-');
|
||||
return s[1];
|
||||
return s[1].substr(1);
|
||||
}
|
||||
|
||||
function getIsPreRelease(tagName: string): boolean {
|
||||
|
@@ -47,7 +47,12 @@ async function main() {
|
||||
|
||||
const targetFile = `${rootDir}/packages/lib/services/database/types.ts`;
|
||||
console.info(`Writing type definitions to ${targetFile}...`);
|
||||
await fs.writeFile(targetFile, `${header}\n\n${tsString}`, 'utf8');
|
||||
|
||||
const existingContent = (await fs.pathExists(targetFile)) ? await fs.readFile(targetFile, 'utf8') : '';
|
||||
const splitted = existingContent.split('// AUTO-GENERATED BY');
|
||||
const staticContent = splitted[0];
|
||||
|
||||
await fs.writeFile(targetFile, `${staticContent}\n\n${header}\n\n${tsString}`, 'utf8');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
@@ -351,6 +351,7 @@ async function main() {
|
||||
|
||||
let publishFormat = 'full';
|
||||
if (['android', 'ios'].indexOf(platform) >= 0) publishFormat = 'simple';
|
||||
if (argv.publishFormat) publishFormat = argv.publishFormat;
|
||||
let changelog = createChangeLog(filteredLogs, { publishFormat: publishFormat });
|
||||
|
||||
const changelogFixes = [];
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -58,12 +58,12 @@ async function updatePluginGeneratorTemplateVersion(manifestPath, majorMinorVers
|
||||
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
|
||||
}
|
||||
|
||||
// Need this hack to transform 1.x.x into 10.x.x due to some mistake
|
||||
// Need this hack to transform x.y.z into 1x.y.z due to some mistake
|
||||
// on one of the release and the App Store won't allow decreasing
|
||||
// the major version number.
|
||||
function iosVersionHack(majorMinorVersion) {
|
||||
const p = majorMinorVersion.split('.');
|
||||
p[0] = `${p[0]}0`;
|
||||
p[0] = `1${p[0]}`;
|
||||
return p.join('.');
|
||||
}
|
||||
|
||||
|
@@ -65,7 +65,7 @@ async function insertChangelog(tag: string, changelogPath: string, changelog: st
|
||||
}
|
||||
|
||||
export async function completeReleaseWithChangelog(changelogPath: string, newVersion: string, newTag: string, appName: string, isPreRelease: boolean) {
|
||||
const changelog = (await execCommand2(`node ${rootDir}/packages/tools/git-changelog ${newTag}`, { })).trim();
|
||||
const changelog = (await execCommand2(`node ${rootDir}/packages/tools/git-changelog ${newTag} --publish-format full`, { })).trim();
|
||||
|
||||
const newChangelog = await insertChangelog(newTag, changelogPath, changelog, isPreRelease);
|
||||
|
||||
|
@@ -1 +1,18 @@
|
||||
# Joplin Android app changelog
|
||||
|
||||
## [android-v2.0.3](https://github.com/laurent22/joplin/releases/tag/android-v2.0.3) (Pre-release) - 2021-06-16T09:48:58Z
|
||||
|
||||
- Improved: Verbose mode for synchronizer (4bbb3d1)
|
||||
|
||||
## [android-v2.0.2](https://github.com/laurent22/joplin/releases/tag/android-v2.0.2) - 2021-06-15T20:03:21Z
|
||||
|
||||
- Improved: Conflict notes will now populate a new field with the ID of the conflict note. (#5049 by [@Ahmad45123](https://github.com/Ahmad45123))
|
||||
- Improved: Filter out form elements from note body to prevent potential XSS (thanks to Dmytro Vdovychinskiy for the PoC) (feaecf7)
|
||||
- Improved: Focus note editor where tapped instead of scrolling to the end (#4998) (#4216 by Roman Musin)
|
||||
- Improved: Improve search with Asian scripts (#5018) (#4613 by [@mablin7](https://github.com/mablin7))
|
||||
- Fixed: Fixed and improved alarm notifications (#4984) (#4912 by Roman Musin)
|
||||
- Fixed: Fixed opening URLs that contain non-alphabetical characters (#4494)
|
||||
- Fixed: Fixed user content URLs when sharing note via Joplin Server (2cf7067)
|
||||
- Fixed: Inline Katex gets broken when editing in Rich Text editor (#5052) (#5025 by [@Subhra264](https://github.com/Subhra264))
|
||||
- Fixed: Items are filtered in the API search (#5017) (#5007 by [@JackGruber](https://github.com/JackGruber))
|
||||
- Fixed: Wrong field removed in API search (#5066 by [@JackGruber](https://github.com/JackGruber))
|
||||
|
@@ -1,6 +1,17 @@
|
||||
# Joplin Server Changelog
|
||||
|
||||
## [server-v2.0.8-beta](https://github.com/laurent22/joplin/releases/tag/server-v2.0.8-beta) (Pre-release) - 2021-06-11T16:29:18Z
|
||||
## [server-v2.0.12](https://github.com/laurent22/joplin/releases/tag/server-v2.0.12) - 2021-06-15T16:24:42Z
|
||||
|
||||
- Fixed: Fixed handling of user content URL (31121c8)
|
||||
|
||||
## [server-v2.0.11](https://github.com/laurent22/joplin/releases/tag/server-v2.0.11) - 2021-06-15T11:41:41Z
|
||||
|
||||
- New: Add navbar on login and sign up page (7a3a208)
|
||||
- New: Added option to enable or disable stack traces (5614eb9)
|
||||
- Improved: Handle custom user content URLs (a36b13d)
|
||||
- Fixed: Fixed error when creating user (594084e)
|
||||
|
||||
## [server-v2.0.9-beta](https://github.com/laurent22/joplin/releases/tag/server-v2.0.9-beta) (Pre-release) - 2021-06-11T16:49:05Z
|
||||
|
||||
- New: Add navbar on login and sign up page (7a3a208)
|
||||
- New: Added option to enable or disable stack traces (5614eb9)
|
||||
|
Reference in New Issue
Block a user