1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

...

43 Commits

Author SHA1 Message Date
Laurent Cozic
8c56cf98be Server v2.0.13 2021-06-16 15:28:41 +01:00
Laurent Cozic
18965494d9 Server: Allow creating a new user with no password, which must be set via email confirmation 2021-06-16 15:24:15 +01:00
Laurent Cozic
ecd1602658 Server: Allow creating a user with a specific account type from admin UI 2021-06-16 15:02:26 +01:00
Laurent Cozic
3c181906c2 Server: Fixed issue with user not being able to modify own profile 2021-06-16 14:34:58 +01:00
Laurent Cozic
9e1e144311 Android 2.0.4 2021-06-16 13:22:51 +01:00
Laurent Cozic
757c125bd3 Android release v2.0.4 2021-06-16 13:15:09 +01:00
Laurent Cozic
2867b66cf1 Tools: Fixed tests 2021-06-16 13:10:42 +01:00
Laurent Cozic
5c6fd93753 All: Prevent sync process from being stuck when the download state of a resource is invalid 2021-06-16 13:03:10 +01:00
Laurent Cozic
ea65313bdb Server: Fixed error message when item is over the limit 2021-06-16 11:07:21 +01:00
Laurent Cozic
1711f7ec88 Android 2.0.3 2021-06-16 10:49:12 +01:00
Laurent Cozic
e0b5ef6630 Android release v2.0.3 2021-06-16 10:48:10 +01:00
Laurent Cozic
4bbb3d1d58 Android: Verbose mode for synchronizer 2021-06-16 10:43:39 +01:00
Laurent Cozic
fd769945b1 ios-v12.0.2 2021-06-16 09:26:21 +01:00
Laurent Cozic
6e91d2784f Tools: Fixed iOS versions 2021-06-16 09:26:10 +01:00
Laurent Cozic
881b2f17b1 ios-v20.0.1 2021-06-16 09:06:37 +01:00
Laurent Cozic
e83cc58ea6 Tools: Fix iOS version number 2021-06-16 09:04:41 +01:00
Laurent Cozic
77def9f782 Tools: Publish full changelog for Android app 2021-06-15 21:08:55 +01:00
Laurent Cozic
b23cc5d30a Android 2.0.2 2021-06-15 21:08:28 +01:00
Laurent Cozic
d8119bcf07 Android release v2.0.2 2021-06-15 21:02:32 +01:00
Laurent Cozic
8bce259dc9 Desktop release v2.0.10 2021-06-15 20:56:38 +01:00
Laurent Cozic
8a00eef901 Server v2.0.12 2021-06-15 17:24:56 +01:00
Laurent Cozic
31121c86d5 Server: Fixed handling of user content URL 2021-06-15 17:24:04 +01:00
Laurent Cozic
a4a156c7a5 Desktop: Fixes #5080: Ensure resources are decrypted when sharing a notebook with Joplin Server 2021-06-15 17:17:12 +01:00
Laurent Cozic
c5b0529968 Cli: Allow setting up E2EE without having to confirm the password 2021-06-15 17:15:00 +01:00
Laurent Cozic
ba322b1f9b Tools: Only notarize macOS app when building a desktop app tag 2021-06-15 16:05:26 +01:00
Laurent Cozic
6f27eae7dd Server v2.0.11 2021-06-15 12:41:59 +01:00
Laurent Cozic
85cc08c0d4 typo 2021-06-15 12:41:15 +01:00
Laurent Cozic
ba38bf3490 Server v2.0.10 2021-06-15 12:28:05 +01:00
Laurent Cozic
2cf70675dc All: Fixed user content URLs when sharing note via Joplin Server 2021-06-15 12:25:55 +01:00
Laurent Cozic
58f8d7e1b4 Merge branch 'dev' into release-2.0 2021-06-15 11:53:51 +01:00
Helmut K. C. Tessarek
b55b35e53f Update translations 2021-06-14 11:38:21 -04:00
Michal Stanke
c7194bf243 All: Translation: Update cs_CZ.po (#5074) 2021-06-14 10:55:23 -04:00
milotype
48abe2316e All: Translation: Update hr_HR.po (#5073) 2021-06-13 18:02:08 -04:00
Laurent Cozic
7aca380cfa Desktop release v2.0.9 2021-06-12 10:00:27 +02:00
Laurent Cozic
551033f8ba Merge branch 'dev' into release-2.0 2021-06-12 10:00:04 +02:00
Laurent Cozic
3b6a66a016 Merge branch 'dev' of github.com:laurent22/joplin into dev 2021-06-12 09:57:54 +02:00
Laurent Cozic
5d7d1be363 Tools: Only install Docker Engine when server image needs to be built 2021-06-12 09:57:42 +02:00
Ahmad Mamdouh
2af3bf61ea All: Conflict notes will now populate a new field with the ID of the conflict note. (#5049) 2021-06-12 08:46:49 +01:00
Laurent Cozic
6803f1c6a7 Tools: Fixed Docker login for CI 2021-06-12 08:52:09 +02:00
Laurent Cozic
1aa70dd6b4 Generator: Include JSON files in webpack config 2021-06-12 00:18:33 +02:00
Laurent Cozic
feaecf7653 Desktop, Mobile: Filter out form elements from note body to prevent potential XSS (thanks to Dmytro Vdovychinskiy for the PoC) 2021-06-11 20:17:45 +02:00
Laurent Cozic
af9f3eedd3 Server v2.0.9 2021-06-11 18:49:29 +02:00
Laurent Cozic
815800827b Tools: Fixed Docker image version number 2021-06-11 18:34:16 +02:00
97 changed files with 10413 additions and 10030 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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%

View File

@@ -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;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.0.8",
"version": "2.0.10",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -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,

View File

@@ -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"

View 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"

View File

@@ -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;
}

View File

@@ -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"
}

View File

@@ -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)";

View File

@@ -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'],
},
});

View File

@@ -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:

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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

View File

@@ -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};

View File

@@ -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;

View File

@@ -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('');
}));
});

View File

@@ -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 });
}
}

View File

@@ -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 };

View File

@@ -0,0 +1,5 @@
import { BaseItemEntity } from '../../services/database/types';
export default function(resource: BaseItemEntity): boolean {
return !resource.is_shared && !resource.share_id;
}

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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() {

View File

@@ -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]);
}

View File

@@ -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 () => {

View File

@@ -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);
}));

View File

@@ -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({

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.0.8",
"version": "2.0.13",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.0.8",
"version": "2.0.13",
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",

View File

@@ -38,6 +38,7 @@ export interface EnvVariables {
SIGNUP_ENABLED?: string;
TERMS_ENABLED?: string;
ACCOUNT_TYPES_ENABLED?: string;
ERROR_STACK_TRACES?: string;
}
@@ -152,6 +153,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
signupEnabled: env.SIGNUP_ENABLED === '1',
termsEnabled: env.TERMS_ENABLED === '1',
accountTypesEnabled: env.ACCOUNT_TYPES_ENABLED === '1',
...overrides,
};
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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> {

View File

@@ -8,7 +8,7 @@ import { formatBytes, MB } from '../utils/bytes';
export enum AccountType {
Default = 0,
Free = 1,
Basic = 1,
Pro = 2,
}
@@ -18,6 +18,11 @@ interface AccountTypeProperties {
max_item_size: number;
}
interface AccountTypeSelectOptions {
value: number;
label: string;
}
export function accountTypeProperties(accountType: AccountType): AccountTypeProperties {
const types: AccountTypeProperties[] = [
{
@@ -26,7 +31,7 @@ export function accountTypeProperties(accountType: AccountType): AccountTypeProp
max_item_size: 0,
},
{
account_type: AccountType.Free,
account_type: AccountType.Basic,
can_share: 0,
max_item_size: 10 * MB,
},
@@ -37,7 +42,26 @@ export function accountTypeProperties(accountType: AccountType): AccountTypeProp
},
];
return types.find(a => a.account_type === accountType);
const type = types.find(a => a.account_type === accountType);
if (!type) throw new Error(`Invalid account type: ${accountType}`);
return type;
}
export function accountTypeOptions(): AccountTypeSelectOptions[] {
return [
{
value: AccountType.Default,
label: 'Default',
},
{
value: AccountType.Basic,
label: 'Basic',
},
{
value: AccountType.Pro,
label: 'Pro',
},
];
}
export default class UserModel extends BaseModel<User> {
@@ -68,6 +92,8 @@ export default class UserModel extends BaseModel<User> {
if ('full_name' in object) user.full_name = object.full_name;
if ('max_item_size' in object) user.max_item_size = object.max_item_size;
if ('can_share' in object) user.can_share = object.can_share;
if ('account_type' in object) user.account_type = object.account_type;
if ('must_set_password' in object) user.must_set_password = object.must_set_password;
return user;
}
@@ -94,7 +120,10 @@ export default class UserModel extends BaseModel<User> {
if (!user.is_admin && resource.id !== user.id) throw new ErrorForbidden('non-admin user cannot modify another user');
if (!user.is_admin && 'is_admin' in resource) throw new ErrorForbidden('non-admin user cannot make themselves an admin');
if (user.is_admin && user.id === resource.id && 'is_admin' in resource && !resource.is_admin) throw new ErrorForbidden('admin user cannot make themselves a non-admin');
if (!user.is_admin && resource.max_item_size !== previousResource.max_item_size) throw new ErrorForbidden('non-admin user cannot change max_item_size');
if ('max_item_size' in resource && !user.is_admin && resource.max_item_size !== previousResource.max_item_size) throw new ErrorForbidden('non-admin user cannot change max_item_size');
if ('can_share' in resource && !user.is_admin && resource.can_share !== previousResource.can_share) throw new ErrorForbidden('non-admin user cannot change can_share');
if ('account_type' in resource && !user.is_admin && resource.account_type !== previousResource.account_type) throw new ErrorForbidden('non-admin user cannot change account_type');
if ('must_set_password' in resource && !user.is_admin && resource.must_set_password !== previousResource.must_set_password) throw new ErrorForbidden('non-admin user cannot change must_set_password');
}
if (action === AclAction.Delete) {
@@ -108,17 +137,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)
));
}
@@ -147,7 +176,7 @@ export default class UserModel extends BaseModel<User> {
if (options.isNew) {
if (!user.email) throw new ErrorUnprocessableEntity('email must be set');
if (!user.password) throw new ErrorUnprocessableEntity('password must be set');
if (!user.password && !user.must_set_password) throw new ErrorUnprocessableEntity('password must be set');
} else {
if ('email' in user && !user.email) throw new ErrorUnprocessableEntity('email must be set');
if ('password' in user && !user.password) throw new ErrorUnprocessableEntity('password must be set');
@@ -196,6 +225,17 @@ export default class UserModel extends BaseModel<User> {
await this.save({ id: user.id, email_confirmed: 1 });
}
// public async saveWithAccountType(accountType:AccountType, user: User, options: SaveOptions = {}): Promise<User> {
// if (accountType !== AccountType.Default) {
// user = {
// ...user,
// ...accountTypeProperties(accountType),
// };
// }
// return this.save(user, options);
// }
// Note that when the "password" property is provided, it is going to be
// hashed automatically. It means that it is not safe to do:
//

View File

@@ -41,7 +41,7 @@ describe('index_signup', function() {
// Check that the user has been created
const user = await models().user().loadByEmail('toto@example.com');
expect(user).toBeTruthy();
expect(user.account_type).toBe(AccountType.Free);
expect(user.account_type).toBe(AccountType.Basic);
expect(user.email_confirmed).toBe(0);
expect(user.can_share).toBe(0);
expect(user.max_item_size).toBe(10 * MB);

View File

@@ -44,7 +44,7 @@ router.post('signup', async (_path: SubPath, ctx: AppContext) => {
const password = checkPassword(formUser, true);
const user = await ctx.models.user().save({
...accountTypeProperties(AccountType.Free),
...accountTypeProperties(AccountType.Basic),
email: formUser.email,
full_name: formUser.full_name,
password,

View File

@@ -85,6 +85,7 @@ describe('index_users', function() {
expect(!!newUser.is_admin).toBe(false);
expect(!!newUser.email).toBe(true);
expect(newUser.max_item_size).toBe(0);
expect(newUser.must_set_password).toBe(0);
const userModel = models().user();
const userFromModel: User = await userModel.load(newUser.id);
@@ -93,6 +94,18 @@ describe('index_users', function() {
expect(userFromModel.password === '123456').toBe(false); // Password has been hashed
});
test('should ask user to set password if not set on creation', async function() {
const { session } = await createUserAndSession(1, true);
await postUser(session.id, 'test@example.com', '', {
max_item_size: '',
});
const newUser = await models().user().loadByEmail('test@example.com');
expect(newUser.must_set_password).toBe(1);
expect(!!newUser.password).toBe(true);
});
test('new user should be able to login', async function() {
const { session } = await createUserAndSession(1, true);
@@ -123,7 +136,7 @@ describe('index_users', function() {
});
test('should change user properties', async function() {
const { user, session } = await createUserAndSession(1, true);
const { user, session } = await createUserAndSession(1, false);
const userModel = models().user();
@@ -298,7 +311,11 @@ describe('index_users', function() {
await expectHttpError(async () => execRequest(adminSession.id, 'POST', `users/${admin.id}`, { delete_button: true }), ErrorForbidden.httpCode);
// non-admin cannot change max_item_size
await expectHttpError(async () => patchUser(session1.id, { id: admin.id, max_item_size: 1000 }), ErrorForbidden.httpCode);
await expectHttpError(async () => patchUser(session1.id, { id: user1.id, max_item_size: 1000 }), ErrorForbidden.httpCode);
// non-admin cannot change can_share
await models().user().save({ id: user1.id, can_share: 0 });
await expectHttpError(async () => patchUser(session1.id, { id: user1.id, can_share: 1 }), ErrorForbidden.httpCode);
});

View File

@@ -11,6 +11,8 @@ import defaultView from '../../utils/defaultView';
import { AclAction } from '../../models/BaseModel';
import { NotificationKey } from '../../models/NotificationModel';
import { formatBytes } from '../../utils/bytes';
import { accountTypeOptions, accountTypeProperties } from '../../models/UserModel';
import uuidgen from '../../utils/uuidgen';
interface CheckPasswordInput {
password: string;
@@ -29,25 +31,39 @@ export function checkPassword(fields: CheckPasswordInput, required: boolean): st
}
function makeUser(isNew: boolean, fields: any): User {
const user: User = {};
let user: User = {};
if ('email' in fields) user.email = fields.email;
if ('full_name' in fields) user.full_name = fields.full_name;
if ('is_admin' in fields) user.is_admin = fields.is_admin;
if ('max_item_size' in fields) user.max_item_size = fields.max_item_size || 0;
user.can_share = fields.can_share ? 1 : 0;
if ('can_share' in fields) user.can_share = fields.can_share ? 1 : 0;
if ('account_type' in fields) {
user.account_type = Number(fields.account_type);
user = {
...user,
...accountTypeProperties(user.account_type),
};
}
const password = checkPassword(fields, false);
if (password) user.password = password;
if (!isNew) user.id = fields.id;
if (isNew) {
user.must_set_password = user.password ? 0 : 1;
user.password = user.password ? user.password : uuidgen();
}
return user;
}
function defaultUser(): User {
return {
can_share: 1,
max_item_size: 0,
};
}
@@ -107,6 +123,14 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
view.content.postUrl = postUrl;
view.content.showDeleteButton = !isNew && !!owner.is_admin && owner.id !== user.id;
if (config().accountTypesEnabled) {
view.content.showAccountTypes = true;
view.content.accountTypes = accountTypeOptions().map((o: any) => {
o.selected = user.account_type === o.value;
return o;
});
}
return view;
});

View File

@@ -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_}`);
}

View File

@@ -477,6 +477,7 @@ encryption_applied: 0
markup_language: 1
is_shared: 1
share_id: ${note.share_id || ''}
conflict_original_id:
type_: 1`;
}

View File

@@ -80,6 +80,7 @@ export interface Config {
userContentBaseUrl: string;
signupEnabled: boolean;
termsEnabled: boolean;
accountTypesEnabled: boolean;
showErrorStackTraces: boolean;
database: DatabaseConfig;
mailer: MailerConfig;

View File

@@ -15,6 +15,20 @@
</div>
</div>
{{#global.owner.is_admin}}
{{#showAccountTypes}}
<div class="field">
<label class="label">Account type</label>
<div class="select">
<select name="account_type">
{{#accountTypes}}
<option value="{{value}}" {{#selected}}selected{{/selected}}>{{label}}</option>
{{/accountTypes}}
</select>
</div>
<p class="help">If the account type is anything other than Default, the account-specific properties will be applied.</p>
</div>
{{/showAccountTypes}}
<div class="field">
<label class="label">Max item size</label>
<div class="control">
@@ -39,7 +53,10 @@
<div class="control">
<input class="input" type="password" name="password2" autocomplete="new-password"/>
</div>
</div>
{{#global.owner.is_admin}}
<p class="help">When creating a new user, if no password is specified the user will have to set it by following the link in their email.</p>
{{/global.owner.is_admin}}
</div>
<div class="control">
<input type="submit" name="post_button" class="button is-primary" value="{{buttonTitle}}" />
{{#showDeleteButton}}

View File

@@ -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 {

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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('.');
}

View File

@@ -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);

View File

@@ -1 +1,22 @@
# Joplin Android app changelog
## [android-v2.0.4](https://github.com/laurent22/joplin/releases/tag/android-v2.0.4) - 2021-06-16T12:15:56Z
- Improved: Prevent sync process from being stuck when the download state of a resource is invalid (5c6fd93)
## [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))

View File

@@ -1,6 +1,24 @@
# 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.13](https://github.com/laurent22/joplin/releases/tag/server-v2.0.13) - 2021-06-16T14:28:20Z
- Improved: Allow creating a new user with no password, which must be set via email confirmation (1896549)
- Improved: Allow creating a user with a specific account type from admin UI (ecd1602)
- Fixed: Fixed error message when item is over the limit (ea65313)
- Fixed: Fixed issue with user not being able to modify own profile (3c18190)
## [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)