1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-01-02 00:08:04 +02:00

Compare commits

..

7 Commits

Author SHA1 Message Date
Laurent Cozic
8411e1270e fix 2021-08-22 11:25:51 +01:00
Laurent Cozic
e9d4a777fd refactor 2021-08-22 11:23:07 +01:00
Laurent Cozic
85984f1f39 comment 2021-08-21 18:17:17 +01:00
Laurent Cozic
3c0524c6e9 update 2021-08-21 17:50:40 +01:00
Laurent Cozic
769d47a768 flags 2021-08-21 16:59:50 +01:00
Laurent Cozic
87fe0e4dcf update 2021-08-21 15:14:54 +01:00
Laurent Cozic
2513e0aaab update 2021-08-21 15:06:41 +01:00
210 changed files with 2810 additions and 3810 deletions

View File

@@ -84,6 +84,9 @@ packages/app-cli/tests/HtmlToMd.js.map
packages/app-cli/tests/MdToHtml.d.ts
packages/app-cli/tests/MdToHtml.js
packages/app-cli/tests/MdToHtml.js.map
packages/app-cli/tests/MdToMd.d.ts
packages/app-cli/tests/MdToMd.js
packages/app-cli/tests/MdToMd.js.map
packages/app-cli/tests/services/keychain/KeychainService.d.ts
packages/app-cli/tests/services/keychain/KeychainService.js
packages/app-cli/tests/services/keychain/KeychainService.js.map
@@ -1092,6 +1095,12 @@ packages/lib/services/CommandService.test.js.map
packages/lib/services/DecryptionWorker.d.ts
packages/lib/services/DecryptionWorker.js
packages/lib/services/DecryptionWorker.js.map
packages/lib/services/EncryptionService.d.ts
packages/lib/services/EncryptionService.js
packages/lib/services/EncryptionService.js.map
packages/lib/services/EncryptionService.test.d.ts
packages/lib/services/EncryptionService.test.js
packages/lib/services/EncryptionService.test.js.map
packages/lib/services/ExternalEditWatcher.d.ts
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher.js.map
@@ -1185,12 +1194,6 @@ packages/lib/services/database/types.js.map
packages/lib/services/debug/populateDatabase.d.ts
packages/lib/services/debug/populateDatabase.js
packages/lib/services/debug/populateDatabase.js.map
packages/lib/services/e2ee/EncryptionService.d.ts
packages/lib/services/e2ee/EncryptionService.js
packages/lib/services/e2ee/EncryptionService.js.map
packages/lib/services/e2ee/EncryptionService.test.d.ts
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.test.js.map
packages/lib/services/e2ee/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
@@ -1245,9 +1248,6 @@ packages/lib/services/interop/InteropService_Importer_Jex.js.map
packages/lib/services/interop/InteropService_Importer_Md.d.ts
packages/lib/services/interop/InteropService_Importer_Md.js
packages/lib/services/interop/InteropService_Importer_Md.js.map
packages/lib/services/interop/InteropService_Importer_Md.test.d.ts
packages/lib/services/interop/InteropService_Importer_Md.test.js
packages/lib/services/interop/InteropService_Importer_Md.test.js.map
packages/lib/services/interop/InteropService_Importer_Raw.d.ts
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/InteropService_Importer_Raw.js.map
@@ -1740,9 +1740,6 @@ packages/tools/generate-database-types.js.map
packages/tools/generate-images.d.ts
packages/tools/generate-images.js
packages/tools/generate-images.js.map
packages/tools/git-changelog.d.ts
packages/tools/git-changelog.js
packages/tools/git-changelog.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map

21
.gitignore vendored
View File

@@ -69,6 +69,9 @@ packages/app-cli/tests/HtmlToMd.js.map
packages/app-cli/tests/MdToHtml.d.ts
packages/app-cli/tests/MdToHtml.js
packages/app-cli/tests/MdToHtml.js.map
packages/app-cli/tests/MdToMd.d.ts
packages/app-cli/tests/MdToMd.js
packages/app-cli/tests/MdToMd.js.map
packages/app-cli/tests/services/keychain/KeychainService.d.ts
packages/app-cli/tests/services/keychain/KeychainService.js
packages/app-cli/tests/services/keychain/KeychainService.js.map
@@ -1077,6 +1080,12 @@ packages/lib/services/CommandService.test.js.map
packages/lib/services/DecryptionWorker.d.ts
packages/lib/services/DecryptionWorker.js
packages/lib/services/DecryptionWorker.js.map
packages/lib/services/EncryptionService.d.ts
packages/lib/services/EncryptionService.js
packages/lib/services/EncryptionService.js.map
packages/lib/services/EncryptionService.test.d.ts
packages/lib/services/EncryptionService.test.js
packages/lib/services/EncryptionService.test.js.map
packages/lib/services/ExternalEditWatcher.d.ts
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher.js.map
@@ -1170,12 +1179,6 @@ packages/lib/services/database/types.js.map
packages/lib/services/debug/populateDatabase.d.ts
packages/lib/services/debug/populateDatabase.js
packages/lib/services/debug/populateDatabase.js.map
packages/lib/services/e2ee/EncryptionService.d.ts
packages/lib/services/e2ee/EncryptionService.js
packages/lib/services/e2ee/EncryptionService.js.map
packages/lib/services/e2ee/EncryptionService.test.d.ts
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.test.js.map
packages/lib/services/e2ee/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
@@ -1230,9 +1233,6 @@ packages/lib/services/interop/InteropService_Importer_Jex.js.map
packages/lib/services/interop/InteropService_Importer_Md.d.ts
packages/lib/services/interop/InteropService_Importer_Md.js
packages/lib/services/interop/InteropService_Importer_Md.js.map
packages/lib/services/interop/InteropService_Importer_Md.test.d.ts
packages/lib/services/interop/InteropService_Importer_Md.test.js
packages/lib/services/interop/InteropService_Importer_Md.test.js.map
packages/lib/services/interop/InteropService_Importer_Raw.d.ts
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/InteropService_Importer_Raw.js.map
@@ -1725,9 +1725,6 @@ packages/tools/generate-database-types.js.map
packages/tools/generate-images.d.ts
packages/tools/generate-images.js
packages/tools/generate-images.js.map
packages/tools/git-changelog.d.ts
packages/tools/git-changelog.js
packages/tools/git-changelog.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map

View File

@@ -31,14 +31,12 @@ Joplin is available in multiple languages thanks to the help of its users. You c
If you want to start contributing to the project's code, please follow these guidelines before creating a pull request:
- Explain WHY you want to add this change. Explain it inside the pull request and you may link to an issue for additional information, but the PR should gives a clear overview of why you want to add this.
- Bug fixes are always welcome. Start by reviewing the [list of bugs](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
- A good way to easily start contributing is to pick and work on a [good first issue](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). We try to make these issues as clear as possible and provide basic info on how the code should be changed, and if something is unclear feel free to ask for more information on the issue.
- Before adding a new feature, ask about it in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue) or the [Joplin Forum](https://discourse.joplinapp.org/), or check if existing discussions exist to make sure the new functionality is desired.
- **Changes that will consist in more than 50 lines of code should be discussed the [Joplin Forum](https://discourse.joplinapp.org/)**, so that you don't spend too much time implementing something that might not be accepted.
- All the applications share the same backend (database, synchronisation, settings, models, business logic, etc.) so if you change something in the backend in one app, makes sure it still work in the other apps. Usually it does, but keep this in mind.
- Pull requests that make many changes using an automated tool, like for spell fixing, styling, etc. will not be accepted. An exception would be if the changes have been discussed in the forum and someone has agreed to review **and test** the pull request.
- Pull requests that make address multiple issues will most likely stall and eventually be closed. This is because we might be fine with one of the changes but not with others and untangling that kind of pull request is too much hassle both for maintainers and the person who submitted it. So most of the time someone gives up and the PR gets closed. So please keep the pull request focused on one issue.
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/dev/BUILD.md) for more details.

View File

@@ -1,11 +1,6 @@
# https://versatile.nl/blog/deploying-lerna-web-apps-with-docker
FROM node:16-bullseye
RUN apt-get update \
&& apt-get install -y \
python \
&& rm -rf /var/lib/apt/lists/*
FROM node:16
RUN echo "Node: $(node --version)"
RUN echo "Npm: $(npm --version)"

View File

@@ -511,7 +511,7 @@ Current translations:
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/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/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: | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 54%
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
@@ -527,7 +527,7 @@ Current translations:
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 87%
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | Alexander Dawson | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 67%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 68%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Nicolas Suzuki](mailto:nicolas.suzuki@pm.me) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 90%
@@ -536,13 +536,13 @@ Current translations:
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 42%
<img src="https://joplinapp.org/images/flags/country-4x3/vi.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 81%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Po-Chiang Chao](mailto:BobChao%29%20%28bobchao@gmail.com) | 94%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Po-Chiang Chao](mailto:BobChao%29%20%28bobchao@gmail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 95%
<!-- LOCALE-TABLE-AUTO-GENERATED -->

View File

@@ -1,6 +1,6 @@
const { BaseCommand } = require('./base-command.js');
import { _ } from '@joplin/lib/locale';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import BaseItem from '@joplin/lib/models/BaseItem';
import Setting from '@joplin/lib/models/Setting';

View File

@@ -27,7 +27,7 @@ const { shimInit } = require('@joplin/lib/shim-init-node.js');
const shim = require('@joplin/lib/shim').default;
const { _ } = require('@joplin/lib/locale');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const EncryptionService = require('@joplin/lib/services/EncryptionService').default;
const envFromArgs = require('@joplin/lib/envFromArgs');
const env = envFromArgs(process.argv);

View File

@@ -32,7 +32,7 @@
],
"owner": "Laurent Cozic"
},
"version": "2.4.0",
"version": "2.3.2",
"bin": {
"joplin": "./main.js"
},
@@ -40,8 +40,8 @@
"node": ">=10.0.0"
},
"dependencies": {
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@joplin/lib": "~2.3",
"@joplin/renderer": "~2.3",
"aws-sdk": "^2.588.0",
"chalk": "^4.1.0",
"compare-version": "^0.1.2",
@@ -65,7 +65,7 @@
"yargs-parser": "^7.0.0"
},
"devDependencies": {
"@joplin/tools": "~2.4",
"@joplin/tools": "~2.3",
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",

View File

@@ -0,0 +1,50 @@
const mdImporterService = require('@joplin/lib/services/interop/InteropService_Importer_Md').default;
const Note = require('@joplin/lib/models/Note').default;
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
const importer = new mdImporterService();
describe('InteropService_Importer_Md: importLocalImages', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should import linked files and modify tags appropriately', async function() {
const tagNonExistentFile = '![does not exist](does_not_exist.png)';
const note = await importer.importFile(`${__dirname}/md_to_md/sample.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(2);
const inexistentLinkUnchanged = note.body.includes(tagNonExistentFile);
expect(inexistentLinkUnchanged).toBe(true);
});
it('should only create 1 resource for duplicate links, all tags should be updated', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-duplicate-links.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
const reg = new RegExp(items[0].id, 'g');
const matched = note.body.match(reg);
expect(matched.length).toBe(2);
});
it('should import linked files and modify tags appropriately when link is also in alt text', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-link-in-alt-text.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
});
it('should passthrough unchanged if no links present', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-no-links.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(0);
expect(note.body).toContain('Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.');
});
it('should import linked image with special characters in name', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-special-chars.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
});
it('should import resources for files', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-files.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(4);
});
});

View File

@@ -0,0 +1,2 @@
![link 1](../support/photo.jpg)
![link 2](../support/photo.jpg)

View File

@@ -0,0 +1,9 @@
# Markdown file test
![../support/photo.jpg](../support/photo.jpg)
[welcome.pdf](../support/welcome.pdf)
[sample.md](sample.md)
[sample2.md](./sample.md)

View File

@@ -0,0 +1,3 @@
# Markdown
![../support/photo.jpg](../support/photo.jpg) should put resource link inside () not []
![../support/photo.jpg]( ../support/photo.jpg ) this case (spaces before/after link but within parens) is not currently covered

View File

@@ -0,0 +1 @@
![link special chars](../support/photo-åäö.jpg)

View File

@@ -0,0 +1,13 @@
# Markdown
lorem ipsum ![alt text here](../support/photo.jpg)
- [ ] check!
- [ ] boxes!
![alt text here](../support/photo-two.jpg)ipsum lorem
**strong text**
![does not exist](does_not_exist.png) lorem ipsum
**some directory**
![a directory](../support) lorem ipsum

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,3 +0,0 @@
# Test Spaces
I hope this get's imported correctly!

View File

@@ -1 +0,0 @@
[Section 1](./sample-no-links.md#markdown)

View File

@@ -1,3 +0,0 @@
# Markdown file test
[sample.md](sample-cycles-b.md)

View File

@@ -1,4 +0,0 @@
# Markdown file test
[sample.md](./sample-cycles-a.md)

View File

@@ -1,2 +0,0 @@
![link 1](../../photo.jpg)
![link 2](../../photo.jpg)

View File

@@ -1 +0,0 @@
![sample](file://../../photo.jpg)

View File

@@ -1,9 +0,0 @@
# Markdown file test
![../../photo.jpg](../../photo.jpg)
[welcome.pdf](../../welcome.pdf)
[sample.md](sample.md)
[sample2.md](./sample.md)

View File

@@ -1,3 +0,0 @@
# Markdown
![../../photo.jpg](../../photo.jpg) should put resource link inside () not []
![../../photo.jpg]( ../../photo.jpg ) this case (spaces before/after link but within parens) is not currently covered

View File

@@ -1,3 +0,0 @@
![Alt text](../../photo.jpg "Scott Joplin")
![Worst Case](<../../photo sample.jpg> "title")
[Worst Case](<./sample spaces.md> "title")

View File

@@ -1 +0,0 @@
I am here, but am I alive?

View File

@@ -1,3 +0,0 @@
# Some Title
[link](./sample-md)

View File

@@ -1,4 +0,0 @@
![link special chars](../../photo-åäö.jpg)
[sample photo](../../photo%20sample.jpg)
[sample.md](./sample%20spaces.md)
[sample special syntax](<../../photo sample.jpg>)

View File

@@ -1,4 +0,0 @@
<img src="../../photo.jpg">
<img src='../../photo-two.jpg'>
<img src='does-not-exist' alt="../../photo.jpg">
<a href="./sample-no-links.md">

View File

@@ -1,13 +0,0 @@
# Markdown
lorem ipsum ![alt text here](../../photo.jpg)
- [ ] check!
- [ ] boxes!
![alt text here](../../photo-two.jpg)ipsum lorem
**strong text**
![does not exist](does_not_exist.png) lorem ipsum
**some directory**
![a directory](../..) lorem ipsum

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "2.4.0",
"version": "2.3.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'",

View File

@@ -7,7 +7,7 @@ import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import ClipperServer from '@joplin/lib/ClipperServer';
import Setting from '@joplin/lib/models/Setting';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import { AppState } from '../app';
class ClipperConfigScreenComponent extends React.Component {

View File

@@ -299,7 +299,7 @@ export default function(props: Props) {
function renderRepoApiError() {
if (!repoApiError) return null;
return <RepoApiErrorMessage maxWidth={maxWidth} type="error">{_('Could not connect to plugin repository.')}<br/><br/>- <StyledLink href="#" onClick={() => { setFetchManifestTime(Date.now()); }}>{_('Try again')}</StyledLink><br/><br/>- <StyledLink href="#" onClick={onBrowsePlugins}>{_('Browse all plugins')}</StyledLink></RepoApiErrorMessage>;
return <RepoApiErrorMessage maxWidth={maxWidth} type="error">{_('Could not connect to plugin repository')} - <StyledLink href="#" onClick={() => { setFetchManifestTime(Date.now()); }}>{_('Try again')}</StyledLink></RepoApiErrorMessage>;
}
function renderBottomArea() {

View File

@@ -1,7 +1,7 @@
const React = require('react');
const { connect } = require('react-redux');
import Setting from '@joplin/lib/models/Setting';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
@@ -12,16 +12,8 @@ import bridge from '../services/bridge';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import { toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import MasterKey from '@joplin/lib/models/MasterKey';
import StyledInput from './style/StyledInput';
import Button, { ButtonLevel } from './Button/Button';
import styled from 'styled-components';
const MasterPasswordInput = styled(StyledInput)`
min-width: 300px;
align-items: center;
`;
interface Props {}
@@ -53,10 +45,6 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
private renderMasterKey(mk: MasterKeyEntity, isDefault: boolean) {
const theme = themeStyle(this.props.themeId);
const onToggleEnabledClick = () => {
return shared.onToggleEnabledClick(this, mk);
};
const passwordStyle = {
color: theme.color,
backgroundColor: theme.backgroundColor,
@@ -72,23 +60,8 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
return shared.onPasswordChange(this, mk, event.target.value);
};
const renderPasswordInput = (masterKeyId: string) => {
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
return (
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
({_('Master password')})
</td>
);
} else {
return (
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
{_('Save')}
</button>
</td>
);
}
const onToggleEnabledClick = () => {
return shared.onToggleEnabledClick(this, mk);
};
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
@@ -101,7 +74,12 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
<td style={theme.textStyle}>{activeIcon}</td>
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
{renderPasswordInput(mk.id)}
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
{_('Save')}
</button>
</td>
<td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}>
<button disabled={isActive || isDefault} style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
@@ -187,7 +165,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
}
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => shared.toggleShowDisabledMasterKeys(this) } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{_('Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.')}</p> : null;
const tableComp = !showTable ? null : (
<table>
<tbody>
@@ -217,39 +195,6 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
return null;
}
private renderMasterPassword() {
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const onMasterPasswordSave = async () => {
shared.onMasterPasswordSave(this);
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
alert('Password is invalid. Please try again.');
}
};
if (this.state.passwordChecks['master']) {
return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span style={theme.textStyle}>{_('Master password:')}</span>&nbsp;&nbsp;
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}> {_('Loaded')}</span>
</div>
);
} else {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={theme.textStyle}> {'The master password is not set or is invalid. Please type it below:'}</span>
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
</div>
</div>
);
}
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
@@ -270,7 +215,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
const onToggleButtonClick = async () => {
const isEnabled = getEncryptionEnabled();
const masterKey = getDefaultMasterKey();
const masterKey = MasterKey.latest();
let answer = null;
if (isEnabled) {
@@ -359,7 +304,6 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
<p style={theme.textStyle}>
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p>
{this.renderMasterPassword()}
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
@@ -385,7 +329,6 @@ const mapStateToProps = (state: State) => {
activeMasterKeyId: syncInfo.activeMasterKeyId,
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
};

View File

@@ -29,7 +29,7 @@ import { themeStyle } from '@joplin/lib/theme';
import validateLayout from '../ResizableLayout/utils/validateLayout';
import iterateItems from '../ResizableLayout/utils/iterateItems';
import removeItem from '../ResizableLayout/utils/removeItem';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import ShareFolderDialog from '../ShareFolderDialog/ShareFolderDialog';
import { ShareInvitation } from '@joplin/lib/services/share/reducer';
import ShareService from '@joplin/lib/services/share/ShareService';

View File

@@ -34,7 +34,7 @@ export const runtime = (comp: any): CommandRuntime => {
value: '',
autocomplete: startFolders,
onClose: async (answer: any) => {
if (answer) {
if (answer != null) {
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], answer.value);
}

View File

@@ -381,9 +381,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
`.CodeMirror-selected {
background: #6b6b6b !important;
}` : '';
// Vim mode draws a fat cursor in the background, we don't want to add background colors
// to the inline code in this case (it would hide the cursor)
const codeBackgroundColor = Setting.value('editor.keyboardMode') !== 'vim' ? theme.codeBackgroundColor : 'inherit';
const monospaceFonts = [];
if (Setting.value('style.editor.monospaceFontFamily')) monospaceFonts.push(`"${Setting.value('style.editor.monospaceFontFamily')}"`);
monospaceFonts.push('monospace');
@@ -479,9 +476,9 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
}
/* Negative margins are needed to componsate for the border */
div.CodeMirror span.cm-comment.cm-jn-inline-code:not(.cm-search-marker):not(.cm-fat-cursor-mark):not(.cm-search-marker-selected):not(.CodeMirror-selectedtext) {
div.CodeMirror span.cm-comment.cm-jn-inline-code {
border: 1px solid ${theme.codeBorderColor};
background-color: ${codeBackgroundColor};
background-color: ${theme.codeBackgroundColor};
margin-left: -1px;
margin-right: -1px;
border-radius: .25em;

View File

@@ -1,11 +1,6 @@
import { useState, useCallback } from 'react';
import Logger from '@joplin/lib/Logger';
import { SearchMarkers } from './useSearchMarkers';
const logger = Logger.create('useNoteSearchBar');
const queryMaxLength = 1000;
interface LocalSearch {
query: string;
selectedIndex: number;
@@ -29,14 +24,6 @@ export default function useNoteSearchBar() {
const [localSearch, setLocalSearch] = useState<LocalSearch>(defaultLocalSearch());
const onChange = useCallback((query: string) => {
// A query that's too long would make CodeMirror throw an exception
// which would crash the app.
// https://github.com/laurent22/joplin/issues/5380
if (query.length > queryMaxLength) {
logger.warn(`Query is longer than ${queryMaxLength} characters - it is going to be trimmed`);
query = query.substr(0, queryMaxLength);
}
setLocalSearch((prev: LocalSearch) => {
return {
query: query,

View File

@@ -24,7 +24,7 @@ const Logger = require('@joplin/lib/Logger').default;
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
const shim = require('@joplin/lib/shim').default;
const { shimInit } = require('@joplin/lib/shim-init-node.js');
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const EncryptionService = require('@joplin/lib/services/EncryptionService').default;
const bridge = require('electron').remote.require('./bridge').default;
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const React = require('react');

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.3",
"version": "2.3.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/app-desktop",
"version": "2.4.3",
"version": "2.3.5",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.3",
"version": "2.3.5",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -93,7 +93,7 @@
},
"homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": {
"@joplin/tools": "~2.4",
"@joplin/tools": "~2.3",
"@testing-library/react-hooks": "^3.4.2",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
@@ -122,8 +122,8 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@joplin/lib": "~2.3",
"@joplin/renderer": "~2.3",
"async-mutex": "^0.1.3",
"codemirror": "^5.56.0",
"color": "^3.1.2",

View File

@@ -142,7 +142,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097648
versionName "2.4.0"
versionName "2.3.4"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -6,7 +6,7 @@ const { BaseScreenComponent } = require('../base-screen.js');
const { themeStyle } = require('../global-style.js');
const DialogBox = require('react-native-dialogbox').default;
const { dialogs } = require('../../utils/dialogs.js');
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
@@ -116,29 +116,15 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;
const renderPasswordInput = (masterKeyId: string) => {
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
return (
<Text style={{ ...this.styles().normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
);
} else {
return (
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
</View>
);
}
};
return (
<View key={mk.id}>
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
{renderPasswordInput(mk.id)}
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
</View>
</View>
);
@@ -217,43 +203,6 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
);
}
private renderMasterPassword() {
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const onMasterPasswordSave = async () => {
shared.onMasterPasswordSave(this);
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
alert('Password is invalid. Please try again.');
}
};
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;
if (this.state.passwordChecks['master']) {
return (
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ ...this.styles().normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
<Text style={{ ...this.styles().normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
</View>
);
} else {
return (
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
<Text style={this.styles().normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={this.state.masterPasswordInput} onChangeText={(text: string) => shared.onMasterPasswordChange(this, text)} style={inputStyle}></TextInput>
<Button onPress={onMasterPasswordSave} title={_('Save')} />
</View>
</View>
);
}
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys = this.props.masterKeys;
@@ -340,7 +289,6 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
<Text style={this.styles().titleText}>{_('Status')}</Text>
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo}
{this.renderMasterPassword()}
{toggleButton}
{passwordPromptComp}
{mkComps}
@@ -367,7 +315,6 @@ const EncryptionConfigScreen = connect((state: State) => {
encryptionEnabled: syncInfo.e2ee,
activeMasterKeyId: syncInfo.activeMasterKeyId,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
})(EncryptionConfigScreenComponent);

View File

@@ -492,7 +492,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -519,7 +519,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -666,7 +666,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
@@ -697,7 +697,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -46,11 +46,6 @@
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>api.joplincloud.local</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSCameraUsageDescription</key>

View File

@@ -14,7 +14,7 @@ import ResourceService from '@joplin/lib/services/ResourceService';
import KvStore from '@joplin/lib/services/KvStore';
import NoteScreen from './components/screens/Note';
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
import Setting, { Env } from '@joplin/lib/models/Setting';
import Setting from '@joplin/lib/models/Setting';
import RNFetchBlob from 'rn-fetch-blob';
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import reducer from '@joplin/lib/reducer';
@@ -97,13 +97,13 @@ SyncTargetRegistry.addClass(SyncTargetJoplinCloud);
import FsDriverRN from './utils/fs-driver-rn';
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import MigrationService from '@joplin/lib/services/MigrationService';
import { clearSharedFilesCache } from './utils/ShareUtils';
import setIgnoreTlsErrors from './utils/TlsUtils';
import ShareService from '@joplin/lib/services/share/ShareService';
import setupNotifications from './utils/setupNotifications';
import { loadMasterKeysFromSettings, migrateMasterPassword } from '@joplin/lib/services/e2ee/utils';
import { loadMasterKeysFromSettings } from '@joplin/lib/services/e2ee/utils';
import SyncTargetNone from '../lib/SyncTargetNone';
let storeDispatch = function(_action: any) {};
@@ -474,7 +474,7 @@ async function initialize(dispatch: Function) {
if (Setting.value('env') == 'prod') {
await db.open({ name: 'joplin.sqlite' });
} else {
await db.open({ name: 'joplin-104.sqlite' });
await db.open({ name: 'joplin-101.sqlite' });
// await db.clearForTesting();
}
@@ -483,14 +483,6 @@ async function initialize(dispatch: Function) {
reg.logger().info('Loading settings...');
await loadKeychainServiceAndSettings(KeychainServiceDriverMobile);
await migrateMasterPassword();
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');
}
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
@@ -538,6 +530,7 @@ async function initialize(dispatch: Function) {
// ----------------------------------------------------------------
EncryptionService.fsDriver_ = fsDriver;
EncryptionService.instance().setLogger(mainLogger);
// eslint-disable-next-line require-atomic-updates
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
@@ -732,9 +725,6 @@ class AppComponent extends React.Component {
setupQuickActions(this.props.dispatch, this.props.selectedFolderId);
await setupNotifications(this.props.dispatch);
// Setting.setValue('encryption.masterPassword', 'WRONG');
// setTimeout(() => NavService.go('EncryptionConfig'), 2000);
}
componentWillUnmount() {

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"id": "<%= pluginId %>",
"app_min_version": "2.4",
"app_min_version": "2.3",
"version": "1.0.0",
"name": "<%= pluginName %>",
"description": "<%= pluginDescription %>",

View File

@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "2.4.0",
"version": "2.3.0",
"description": "Scaffolds out a new Joplin plugin",
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
"author": {

View File

@@ -36,14 +36,14 @@ const SyncTargetNextcloud = require('./SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('./SyncTargetWebDAV.js');
const SyncTargetDropbox = require('./SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('./SyncTargetAmazonS3.js');
import EncryptionService from './services/e2ee/EncryptionService';
import EncryptionService from './services/EncryptionService';
import ResourceFetcher from './services/ResourceFetcher';
import SearchEngineUtils from './services/searchengine/SearchEngineUtils';
import SearchEngine from './services/searchengine/SearchEngine';
import RevisionService from './services/RevisionService';
import ResourceService from './services/ResourceService';
import DecryptionWorker from './services/DecryptionWorker';
import { loadKeychainServiceAndSettings } from './services/SettingUtils';
const { loadKeychainServiceAndSettings } = require('./services/SettingUtils');
import MigrationService from './services/MigrationService';
import ShareService from './services/share/ShareService';
import handleSyncStartupOperation from './services/synchronizer/utils/handleSyncStartupOperation';
@@ -51,7 +51,7 @@ import SyncTargetJoplinCloud from './SyncTargetJoplinCloud';
const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer');
import { getEncryptionEnabled } from './services/synchronizer/syncInfoUtils';
import { loadMasterKeysFromSettings, migrateMasterPassword } from './services/e2ee/utils';
import { loadMasterKeysFromSettings } from './services/e2ee/utils';
import SyncTargetNone from './SyncTargetNone';
const appLogger: LoggerWrapper = Logger.create('App');
@@ -465,7 +465,6 @@ export default class BaseApplication {
sideEffects['timeFormat'] = sideEffects['dateFormat'];
sideEffects['locale'] = sideEffects['dateFormat'];
sideEffects['encryption.passwordCache'] = sideEffects['syncInfoCache'];
sideEffects['encryption.masterPassword'] = sideEffects['syncInfoCache'];
if (action) {
const effect = sideEffects[action.key];
@@ -769,7 +768,6 @@ export default class BaseApplication {
BaseModel.setDb(this.database_);
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
await migrateMasterPassword();
await handleSyncStartupOperation();
appLogger.info(`Client ID: ${Setting.value('clientId')}`);
@@ -827,6 +825,7 @@ export default class BaseApplication {
KvStore.instance().setDb(reg.db());
EncryptionService.instance().setLogger(globalLogger);
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
DecryptionWorker.instance().setLogger(globalLogger);

View File

@@ -1,6 +1,6 @@
import Logger from './Logger';
import Synchronizer from './Synchronizer';
import EncryptionService from './services/e2ee/EncryptionService';
import EncryptionService from './services/EncryptionService';
import shim from './shim';
import ResourceService from './services/ResourceService';
import ShareService from './services/share/ShareService';

View File

@@ -15,7 +15,7 @@ import MasterKey from './models/MasterKey';
import BaseModel, { ModelType } from './BaseModel';
import time from './time';
import ResourceService from './services/ResourceService';
import EncryptionService from './services/e2ee/EncryptionService';
import EncryptionService from './services/EncryptionService';
import JoplinError from './JoplinError';
import ShareService from './services/share/ShareService';
import TaskQueue from './TaskQueue';

View File

@@ -1,4 +1,4 @@
import EncryptionService from '../../services/e2ee/EncryptionService';
import EncryptionService from '../../services/EncryptionService';
import { _ } from '../../locale';
import BaseItem from '../../models/BaseItem';
import Setting from '../../models/Setting';
@@ -8,7 +8,6 @@ import shim from '../../shim';
import { MasterKeyEntity } from '../../services/e2ee/types';
import time from '../../time';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
import { findMasterKeyPassword } from '../../services/e2ee/utils';
class Shared {
@@ -17,16 +16,12 @@ class Shared {
public initialize(comp: any, props: any) {
comp.state = {
passwordChecks: {},
// Master keys that can be decrypted with the master password
// (normally all of them, but for legacy support we need this).
masterPasswordKeys: {},
stats: {
encrypted: null,
total: null,
},
passwords: Object.assign({}, props.passwords),
showDisabledMasterKeys: false,
masterPasswordInput: '',
};
comp.isMounted_ = false;
@@ -113,37 +108,15 @@ class Shared {
}
}
public async masterPasswordIsValid(comp: any, masterPassword: string = null) {
const activeMasterKey = comp.props.masterKeys.find((mk: MasterKeyEntity) => mk.id === comp.props.activeMasterKeyId);
masterPassword = masterPassword === null ? comp.props.masterPassword : masterPassword;
if (activeMasterKey && masterPassword) {
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
}
return false;
}
public async checkPasswords(comp: any) {
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
const masterPasswordKeys = Object.assign({}, comp.state.masterPasswordKeys);
for (let i = 0; i < comp.props.masterKeys.length; i++) {
const mk = comp.props.masterKeys[i];
const password = await findMasterKeyPassword(EncryptionService.instance(), mk);
const password = comp.state.passwords[mk.id];
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
passwordChecks[mk.id] = ok;
masterPasswordKeys[mk.id] = password === comp.props.masterPassword;
}
passwordChecks['master'] = await this.masterPasswordIsValid(comp);
comp.setState({ passwordChecks, masterPasswordKeys });
}
public masterPasswordStatus(comp: any) {
// Don't translate for now because that's temporary - later it should
// always be set and the label should be replaced by a "Change master
// password" button.
return comp.props.masterPassword ? 'Master password is set' : 'Master password is not set';
comp.setState({ passwordChecks: passwordChecks });
}
public decryptedStatText(comp: any) {
@@ -165,14 +138,6 @@ class Shared {
comp.checkPasswords();
}
public onMasterPasswordChange(comp: any, value: string) {
comp.setState({ masterPasswordInput: value });
}
public onMasterPasswordSave(comp: any) {
Setting.setValue('encryption.masterPassword', comp.state.masterPasswordInput);
}
public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) {
const passwords = Object.assign({}, comp.state.passwords);
passwords[mk.id] = password;

View File

@@ -56,7 +56,7 @@ export default class FsDriverBase {
return output;
}
public async findUniqueFilename(name: string, reservedNames: string[] = null, markdownSafe: boolean = false): Promise<string> {
public async findUniqueFilename(name: string, reservedNames: string[] = null): Promise<string> {
if (reservedNames === null) {
reservedNames = [];
}
@@ -70,11 +70,7 @@ export default class FsDriverBase {
// Check if the filename does not exist in the filesystem and is not reserved
const exists = await this.exists(nameToTry) || reservedNames.includes(nameToTry);
if (!exists) return nameToTry;
if (!markdownSafe) {
nameToTry = `${nameNoExt} (${counter})${extension}`;
} else {
nameToTry = `${nameNoExt}-${counter}${extension}`;
}
nameToTry = `${nameNoExt} (${counter})${extension}`;
counter++;
if (counter >= 1000) {
nameToTry = `${nameNoExt} (${new Date().getTime()})${extension}`;

View File

@@ -45,39 +45,18 @@ class HtmlUtils {
}
// Returns the **encoded** URLs, so to be useful they should be decoded again before use.
private extractUrls(regex: RegExp, html: string) {
public extractImageUrls(html: string) {
if (!html) return [];
const output = [];
let matches;
while ((matches = regex.exec(html))) {
while ((matches = imageRegex.exec(html))) {
output.push(matches[2]);
}
return output.filter(url => !!url);
}
// Returns the **encoded** URLs, so to be useful they should be decoded again before use.
public extractImageUrls(html: string) {
return this.extractUrls(imageRegex, html);
}
// Returns the **encoded** URLs, so to be useful they should be decoded again before use.
public extractAnchorUrls(html: string) {
return this.extractUrls(anchorRegex, html);
}
// Returns the **encoded** URLs, so to be useful they should be decoded again before use.
public extractFileUrls(html: string) {
return this.extractImageUrls(html).concat(this.extractAnchorUrls(html));
}
public replaceResourceUrl(html: string, urlToReplace: string, id: string) {
const htmlLinkRegex = `(?<=(?:src|href)=["'])${urlToReplace}(?=["'])`;
const htmlReg = new RegExp(htmlLinkRegex, 'g');
return html.replace(htmlReg, `:/${id}`);
}
public replaceImageUrls(html: string, callback: Function) {
return this.processImageTags(html, (data: any) => {
const newSrc = callback(data.src);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -48,7 +48,7 @@ stats['bg_BG'] = {"percentDone":56};
stats['ca'] = {"percentDone":95};
stats['hr_HR'] = {"percentDone":96};
stats['cs_CZ'] = {"percentDone":95};
stats['da_DK'] = {"percentDone":99};
stats['da_DK'] = {"percentDone":96};
stats['de_DE'] = {"percentDone":95};
stats['et_EE'] = {"percentDone":54};
stats['en_GB'] = {"percentDone":100};
@@ -64,7 +64,7 @@ stats['hu_HU'] = {"percentDone":84};
stats['nl_BE'] = {"percentDone":87};
stats['nl_NL'] = {"percentDone":90};
stats['nb_NO'] = {"percentDone":96};
stats['fa'] = {"percentDone":67};
stats['fa'] = {"percentDone":68};
stats['pl_PL'] = {"percentDone":90};
stats['pt_BR'] = {"percentDone":96};
stats['pt_PT'] = {"percentDone":90};
@@ -73,13 +73,13 @@ stats['sl_SI'] = {"percentDone":91};
stats['sv'] = {"percentDone":96};
stats['th_TH'] = {"percentDone":42};
stats['vi'] = {"percentDone":96};
stats['tr_TR'] = {"percentDone":99};
stats['tr_TR'] = {"percentDone":95};
stats['uk_UA'] = {"percentDone":89};
stats['el_GR'] = {"percentDone":92};
stats['ru_RU'] = {"percentDone":89};
stats['ru_RU'] = {"percentDone":90};
stats['sr_RS'] = {"percentDone":81};
stats['zh_CN'] = {"percentDone":99};
stats['zh_TW'] = {"percentDone":94};
stats['zh_CN'] = {"percentDone":96};
stats['zh_TW'] = {"percentDone":95};
stats['ja_JP'] = {"percentDone":95};
stats['ko'] = {"percentDone":95};
module.exports = { locales: locales, stats: stats };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -100,12 +100,6 @@ const markdownUtils = {
return output;
},
replaceResourceUrl(md: string, urlToReplace: string, id: string) {
const linkRegex = `(?<=\\]\\()\\<?${urlToReplace}\\>?(?=.*\\))`;
const reg = new RegExp(linkRegex, 'g');
return md.replace(reg, `:/${id}`);
},
extractImageUrls(md: string) {
return markdownUtils.extractFileUrls(md,true);
},

View File

@@ -26,6 +26,7 @@ export default class MasterKey extends BaseItem {
}
}
return output;
// return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)');
}
static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], method: number) {

View File

@@ -919,7 +919,6 @@ class Setting extends BaseModel {
'encryption.enabled': { value: false, type: SettingItemType.Bool, public: false },
'encryption.activeMasterKeyId': { value: '', type: SettingItemType.String, public: false },
'encryption.passwordCache': { value: {}, type: SettingItemType.Object, public: false, secure: true },
'encryption.masterPassword': { value: '', type: SettingItemType.String, public: false, secure: true },
'encryption.shouldReencrypt': {
value: -1, // will be set on app startup
type: SettingItemType.Int,

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.3.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.3.1",
"license": "ISC",
"dependencies": {
"async-mutex": "^0.1.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.3.1",
"description": "Joplin Core library",
"author": "Laurent Cozic",
"homepage": "",
@@ -27,7 +27,7 @@
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.33",
"@joplin/fork-sax": "^1.2.37",
"@joplin/renderer": "~2.4",
"@joplin/renderer": "^2.3.1",
"@joplin/turndown": "^4.0.55",
"@joplin/turndown-plugin-gfm": "^1.0.37",
"async-mutex": "^0.1.3",

View File

@@ -6,7 +6,6 @@ import ResourceService from './ResourceService';
import Logger from '../Logger';
import shim from '../shim';
import KvStore from './KvStore';
import EncryptionService from './e2ee/EncryptionService';
const EventEmitter = require('events');
@@ -29,7 +28,7 @@ export default class DecryptionWorker {
private kvStore_: KvStore = null;
private maxDecryptionAttempts_ = 2;
private startCalls_: boolean[] = [];
private encryptionService_: EncryptionService = null;
private encryptionService_: any = null;
constructor() {
this.state_ = 'idle';
@@ -135,11 +134,6 @@ export default class DecryptionWorker {
this.logger().info(msg);
const ids = await MasterKey.allIds();
// Note that the current implementation means that a warning will be
// displayed even if the user has no encrypted note. Just having
// encrypted master key is sufficient. Not great but good enough for
// now.
if (ids.length) {
if (options.masterKeyNotLoadedHandler === 'throw') {
// By trying to load the master key here, we throw the "masterKeyNotLoaded" error

View File

@@ -1,11 +1,11 @@
import { fileContentEqual, setupDatabaseAndSynchronizer, supportDir, switchClient, objectsEqual, checkThrowAsync, msleep } from '../../testing/test-utils';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import Setting from '../../models/Setting';
import BaseItem from '../../models/BaseItem';
import MasterKey from '../../models/MasterKey';
import EncryptionService from './EncryptionService';
import { setEncryptionEnabled } from '../synchronizer/syncInfoUtils';
import { fileContentEqual, setupDatabaseAndSynchronizer, supportDir, switchClient, objectsEqual, checkThrowAsync, msleep } from '../testing/test-utils';
import Folder from '../models/Folder';
import Note from '../models/Note';
import Setting from '../models/Setting';
import BaseItem from '../models/BaseItem';
import MasterKey from '../models/MasterKey';
import EncryptionService from '../services/EncryptionService';
import { setEncryptionEnabled } from '../services/synchronizer/syncInfoUtils';
let service: EncryptionService = null;

View File

@@ -1,14 +1,12 @@
import { MasterKeyEntity } from './types';
import Logger from '../../Logger';
import shim from '../../shim';
import Setting from '../../models/Setting';
import MasterKey from '../../models/MasterKey';
import BaseItem from '../../models/BaseItem';
import JoplinError from '../../JoplinError';
import { getActiveMasterKeyId, setActiveMasterKeyId } from '../synchronizer/syncInfoUtils';
const { padLeft } = require('../../string-utils.js');
const logger = Logger.create('EncryptionService');
import { MasterKeyEntity } from './e2ee/types';
import Logger from '../Logger';
import shim from '../shim';
import Setting from '../models/Setting';
import MasterKey from '../models/MasterKey';
import BaseItem from '../models/BaseItem';
import JoplinError from '../JoplinError';
import { getActiveMasterKeyId, setActiveMasterKeyId } from './synchronizer/syncInfoUtils';
const { padLeft } = require('../string-utils.js');
function hexPad(s: string, length: number) {
return padLeft(s, length, '0');
@@ -54,6 +52,7 @@ export default class EncryptionService {
private decryptedMasterKeys_: Record<string, DecryptedMasterKey> = {};
public defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; // public because used in tests
private defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
private logger_ = new Logger();
private headerTemplates_ = {
// Template version 1
@@ -81,6 +80,7 @@ export default class EncryptionService {
this.decryptedMasterKeys_ = {};
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
this.logger_ = new Logger();
this.headerTemplates_ = {
// Template version 1
@@ -97,6 +97,14 @@ export default class EncryptionService {
return this.instance_;
}
setLogger(l: Logger) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
loadedMasterKeysCount() {
return Object.keys(this.decryptedMasterKeys_).length;
}
@@ -131,14 +139,10 @@ export default class EncryptionService {
public async loadMasterKey(model: MasterKeyEntity, password: string, makeActive = false) {
if (!model.id) throw new Error('Master key does not have an ID - save it first');
logger.info(`Loading master key: ${model.id}. Make active:`, makeActive);
this.decryptedMasterKeys_[model.id] = {
plainText: await this.decryptMasterKey_(model, password),
updatedTime: model.updated_time,
};
if (makeActive) this.setActiveMasterKeyId(model.id);
}
@@ -185,9 +189,7 @@ export default class EncryptionService {
}
masterKeysThatNeedUpgrading(masterKeys: MasterKeyEntity[]) {
const output = MasterKey.allWithoutEncryptionMethod(masterKeys, this.defaultMasterKeyEncryptionMethod_);
// Anything below 5 is a new encryption method and doesn't need an upgrade
return output.filter(mk => mk.encryption_method <= 5);
return MasterKey.allWithoutEncryptionMethod(masterKeys, this.defaultMasterKeyEncryptionMethod_);
}
async upgradeMasterKey(model: MasterKeyEntity, decryptionPassword: string) {
@@ -241,7 +243,7 @@ export default class EncryptionService {
return plainText;
}
public async checkMasterKeyPassword(model: MasterKeyEntity, password: string) {
async checkMasterKeyPassword(model: MasterKeyEntity, password: string) {
try {
await this.decryptMasterKey_(model, password);
} catch (error) {
@@ -251,7 +253,7 @@ export default class EncryptionService {
return true;
}
private wrapSjclError(sjclError: any) {
wrapSjclError(sjclError: any) {
const error = new Error(sjclError.message);
error.stack = sjclError.stack;
return error;

View File

@@ -1,8 +1,7 @@
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow } from '../../testing/test-utils';
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService } from '../../testing/test-utils';
import MasterKey from '../../models/MasterKey';
import { migrateMasterPassword, showMissingMasterKeyMessage } from './utils';
import { localSyncInfo, setActiveMasterKeyId, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
import Setting from '../../models/Setting';
import { showMissingMasterKeyMessage } from './utils';
import { localSyncInfo, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
describe('e2ee/utils', function() {
@@ -35,40 +34,9 @@ describe('e2ee/utils', function() {
setMasterKeyEnabled(mk2.id, true);
expect(showMissingMasterKeyMessage(localSyncInfo(), [mk1.id, mk2.id])).toBe(true);
await expectNotThrow(async () => showMissingMasterKeyMessage(localSyncInfo(), ['not_downloaded_yet']));
const syncInfo = localSyncInfo();
syncInfo.masterKeys = [];
expect(showMissingMasterKeyMessage(syncInfo, [mk1.id, mk2.id])).toBe(false);
});
it('should do the master password migration', async () => {
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey('111111'));
const mk2 = await MasterKey.save(await encryptionService().generateMasterKey('222222'));
Setting.setValue('encryption.passwordCache', {
[mk1.id]: '111111',
[mk2.id]: '222222',
});
await migrateMasterPassword();
{
expect(Setting.value('encryption.masterPassword')).toBe('');
const newCache = Setting.value('encryption.passwordCache');
expect(newCache[mk1.id]).toBe('111111');
expect(newCache[mk2.id]).toBe('222222');
}
setActiveMasterKeyId(mk1.id);
await migrateMasterPassword();
{
expect(Setting.value('encryption.masterPassword')).toBe('111111');
const newCache = Setting.value('encryption.passwordCache');
expect(newCache[mk1.id]).toBe(undefined);
expect(newCache[mk2.id]).toBe('222222');
}
});
});

View File

@@ -3,12 +3,12 @@ import BaseItem from '../../models/BaseItem';
import MasterKey from '../../models/MasterKey';
import Setting from '../../models/Setting';
import { MasterKeyEntity } from './types';
import EncryptionService from './EncryptionService';
import { getActiveMasterKey, getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
import EncryptionService from '../EncryptionService';
import { getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
const logger = Logger.create('e2ee/utils');
export async function setupAndEnableEncryption(service: EncryptionService, masterKey: MasterKeyEntity = null, masterPassword: string = null) {
export async function setupAndEnableEncryption(service: EncryptionService, masterKey: MasterKeyEntity = null, password: string = null) {
if (!masterKey) {
// May happen for example if there are master keys in info.json but none
// of them is set as active. But in fact, unless there is a bug in the
@@ -18,8 +18,10 @@ export async function setupAndEnableEncryption(service: EncryptionService, maste
setEncryptionEnabled(true, masterKey ? masterKey.id : null);
if (masterPassword) {
Setting.setValue('encryption.masterPassword', masterPassword);
if (masterKey && password) {
const passwordCache = Setting.value('encryption.passwordCache');
passwordCache[masterKey.id] = password;
Setting.setValue('encryption.passwordCache', passwordCache);
}
// Mark only the non-encrypted ones for sync since, if there are encrypted ones,
@@ -45,8 +47,6 @@ export async function setupAndDisableEncryption(service: EncryptionService) {
}
export async function toggleAndSetupEncryption(service: EncryptionService, enabled: boolean, masterKey: MasterKeyEntity, password: string) {
logger.info('toggleAndSetupEncryption: enabled:', enabled, ' Master key', masterKey);
if (!enabled) {
await setupAndDisableEncryption(service);
} else {
@@ -68,65 +68,17 @@ export async function generateMasterKeyAndEnableEncryption(service: EncryptionSe
return masterKey;
}
// Migration function to initialise the master password. Normally it is set when
// enabling E2EE, but previously it wasn't. So here we check if the password is
// set. If it is not, we set it from the active master key. It needs to be
// called after the settings have been initialized.
export async function migrateMasterPassword() {
if (Setting.value('encryption.masterPassword')) return; // Already migrated
logger.info('Master password is not set - trying to get it from the active master key...');
const mk = getActiveMasterKey();
if (!mk) return;
const masterPassword = Setting.value('encryption.passwordCache')[mk.id];
if (masterPassword) {
Setting.setValue('encryption.masterPassword', masterPassword);
logger.info('Master password is now set.');
// Also clear the key passwords that match the master password to avoid
// any confusion.
const cache = Setting.value('encryption.passwordCache');
const newCache = { ...cache };
for (const [mkId, password] of Object.entries(cache)) {
if (password === masterPassword) {
delete newCache[mkId];
}
}
Setting.setValue('encryption.passwordCache', newCache);
await Setting.saveAll();
}
}
// All master keys normally should be decryped with the master password, however
// previously any master key could be encrypted with any password, so to support
// this legacy case, we first check if the MK decrypts with the master password.
// If not, try with the master key specific password, if any is defined.
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity): Promise<string> {
const masterPassword = Setting.value('encryption.masterPassword');
if (masterPassword && await service.checkMasterKeyPassword(masterKey, masterPassword)) {
logger.info('findMasterKeyPassword: Using master password');
return masterPassword;
}
logger.warn('findMasterKeyPassword: No master password is defined - trying to get master key specific password');
const passwords = Setting.value('encryption.passwordCache');
return passwords[masterKey.id];
}
export async function loadMasterKeysFromSettings(service: EncryptionService) {
const masterKeys = await MasterKey.all();
const passwords = Setting.value('encryption.passwordCache');
const activeMasterKeyId = getActiveMasterKeyId();
logger.info(`Trying to load ${masterKeys.length} master keys...`);
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
const password = passwords[mk.id];
if (service.isMasterKeyLoaded(mk)) continue;
const password = await findMasterKeyPassword(service, mk);
if (!password) continue;
try {
@@ -146,22 +98,8 @@ export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterK
for (let i = notLoadedMasterKeys.length - 1; i >= 0; i--) {
const mk = syncInfo.masterKeys.find(mk => mk.id === notLoadedMasterKeys[i]);
// A "notLoadedMasterKey" is a key that either doesn't exist or for
// which a password hasn't been set yet. For the purpose of this
// function, we only want to notify the user about unset passwords.
// Master keys that haven't been downloaded yet should normally be
// downloaded at some point.
// https://github.com/laurent22/joplin/issues/5391
if (!mk) continue;
if (!masterKeyEnabled(mk)) notLoadedMasterKeys.pop();
}
return !!notLoadedMasterKeys.length;
}
export function getDefaultMasterKey(): MasterKeyEntity {
const mk = getActiveMasterKey();
if (mk) return mk;
return MasterKey.latest();
}

View File

@@ -442,10 +442,10 @@ describe('services_InteropService', function() {
await service.export({ path: outDir, format: 'md' });
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活-1.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活-2.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活 (1).md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活 (2).md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/Untitled.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/Untitled-1.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/Untitled (1).md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/salut, ça roule _.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/ジョプリン/ジョプリン.md`)).toBe(true);
}));

View File

@@ -53,7 +53,7 @@ export default class InteropService {
{
...defaultImportExportModule(ModuleType.Importer),
format: 'md',
fileExtensions: ['md', 'markdown', 'txt', 'html'],
fileExtensions: ['md', 'markdown', 'txt'],
sources: [FileSystemItem.File, FileSystemItem.Directory],
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
description: _('Markdown'),
@@ -401,16 +401,10 @@ export default class InteropService {
resourcePaths: {},
};
// Prepare to process each type before starting any
// This will allow exporters to operate on the full context
for (let typeOrderIndex = 0; typeOrderIndex < typeOrder.length; typeOrderIndex++) {
const type = typeOrder[typeOrderIndex];
await exporter.prepareForProcessingItemType(type, itemsToExport);
}
for (let typeOrderIndex = 0; typeOrderIndex < typeOrder.length; typeOrderIndex++) {
const type = typeOrder[typeOrderIndex];
for (let i = 0; i < itemsToExport.length; i++) {
const itemType = itemsToExport[i].type;

View File

@@ -9,7 +9,6 @@ const Folder = require('../../models/Folder').default;
const Resource = require('../../models/Resource').default;
const Note = require('../../models/Note').default;
const shim = require('../../shim').default;
const { MarkupToHtml } = require('@joplin/renderer');
describe('interop/InteropService_Exporter_Md', function() {
@@ -52,7 +51,7 @@ describe('interop/InteropService_Exporter_Md', function() {
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
const folder2 = await Folder.save({ title: 'folder2' });
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id, markup_language: MarkupToHtml.MARKUP_LANGUAGE_HTML });
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
await shim.attachFileToNote(note3, `${supportDir}/photo.jpg`);
note3 = await Note.load(note3.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
@@ -68,53 +67,7 @@ describe('interop/InteropService_Exporter_Md', function() {
expect(Object.keys(exporter.context().notePaths).length).toBe(3, 'There should be 3 note paths in the context.');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
expect(exporter.context().notePaths[note2.id]).toBe('folder1/note2.md');
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.html');
}));
it('should create resource paths and add them to context', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir());
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
const folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
const note2 = await Note.save({ title: 'note2', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`);
note1 = await Note.load(note1.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
queueExportItem(BaseModel.TYPE_NOTE, note1);
queueExportItem(BaseModel.TYPE_NOTE, note2);
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
const resource1 = await Resource.load(itemsToExport[3].itemOrId);
const folder2 = await Folder.save({ title: 'folder2' });
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
await shim.attachFileToNote(note3, `${supportDir}/photo.jpg`);
note3 = await Note.load(note3.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
queueExportItem(BaseModel.TYPE_NOTE, note3);
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note3.body))[0]);
const resource2 = await Resource.load(itemsToExport[6].itemOrId);
await exporter.processItem(Folder.modelType(), folder1);
await exporter.processItem(Folder.modelType(), folder2);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
await exporter.processResource(resource1, Resource.fullPath(resource1));
await exporter.processResource(resource2, Resource.fullPath(resource2));
expect(!exporter.context() && !(exporter.context().destResourcePaths || Object.keys(exporter.context().destResourcePaths).length)).toBe(false, 'Context should be empty before processing.');
expect(Object.keys(exporter.context().destResourcePaths).length).toBe(2, 'There should be 2 resource paths in the context.');
expect(exporter.context().destResourcePaths[resource1.id]).toBe(`${exportDir()}/_resources/photo.jpg`);
expect(exporter.context().destResourcePaths[resource2.id]).toBe(`${exportDir()}/_resources/photo-1.jpg`);
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.md');
}));
it('should handle duplicate note names', (async () => {
@@ -141,7 +94,7 @@ describe('interop/InteropService_Exporter_Md', function() {
expect(Object.keys(exporter.context().notePaths).length).toBe(2, 'There should be 2 note paths in the context.');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1-1.md');
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1 (1).md');
}));
it('should not override existing files', (async () => {
@@ -168,7 +121,7 @@ describe('interop/InteropService_Exporter_Md', function() {
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
expect(Object.keys(exporter.context().notePaths).length).toBe(1, 'There should be 1 note paths in the context.');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1-1.md');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1 (1).md');
}));
it('should save resource files in _resource directory', (async () => {
@@ -204,8 +157,8 @@ describe('interop/InteropService_Exporter_Md', function() {
await exporter.processResource(resource1, Resource.fullPath(resource1));
await exporter.processResource(resource2, Resource.fullPath(resource2));
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/photo.jpg`)).toBe(true, 'Resource file should be copied to _resources directory.');
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/photo-1.jpg`)).toBe(true, 'Resource file should be copied to _resources directory.');
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/${Resource.filename(resource1)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/${Resource.filename(resource2)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
}));
it('should create folders in fs', (async () => {
@@ -302,51 +255,23 @@ describe('interop/InteropService_Exporter_Md', function() {
queueExportItem(BaseModel.TYPE_NOTE, note2);
const resource2 = await Resource.load((await Note.linkedResourceIds(note2.body))[0]);
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
await shim.attachFileToNote(note3, `${supportDir}/photo.jpg`);
note3 = await Note.load(note3.id);
queueExportItem(BaseModel.TYPE_NOTE, note3);
const resource3 = await Resource.load((await Note.linkedResourceIds(note3.body))[0]);
note3 = await Note.save({ ...note3, body: `<img src=":/${resource3.id}" alt="alt">` });
note3 = await Note.load(note3.id);
let note4 = await Note.save({ title: 'note4', parent_id: folder2.id });
await shim.attachFileToNote(note4, `${supportDir}/photo.jpg`);
note4 = await Note.load(note4.id);
queueExportItem(BaseModel.TYPE_NOTE, note4);
const resource4 = await Resource.load((await Note.linkedResourceIds(note4.body))[0]);
note4 = await Note.save({ ...note4, body: `![](:/${resource4.id} "title")` });
note4 = await Note.load(note4.id);
await exporter.processItem(Folder.modelType(), folder1);
await exporter.processItem(Folder.modelType(), folder2);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
await exporter.processResource(resource1, Resource.fullPath(resource1));
await exporter.processResource(resource2, Resource.fullPath(resource2));
await exporter.processResource(resource3, Resource.fullPath(resource3));
await exporter.processResource(resource4, Resource.fullPath(resource3));
const context = {
resourcePaths: {},
};
context.resourcePaths[resource1.id] = 'resource1.jpg';
context.resourcePaths[resource2.id] = 'resource2.jpg';
context.resourcePaths[resource3.id] = 'resource3.jpg';
context.resourcePaths[resource4.id] = 'resource3.jpg';
exporter.updateContext(context);
await exporter.processItem(Note.modelType(), note1);
await exporter.processItem(Note.modelType(), note2);
await exporter.processItem(Note.modelType(), note3);
await exporter.processItem(Note.modelType(), note4);
const note1_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note1.id]}`);
const note2_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note2.id]}`);
const note3_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note3.id]}`);
const note4_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note4.id]}`);
expect(note1_body).toContain('](../_resources/photo.jpg)', 'Resource id should be replaced with a relative path.');
expect(note2_body).toContain('](../../_resources/photo-1.jpg)', 'Resource id should be replaced with a relative path.');
expect(note3_body).toContain('<img src="../../_resources/photo-2.jpg" alt="alt">', 'Resource id should be replaced with a relative path.');
expect(note4_body).toContain('](../../_resources/photo-3.jpg "title")', 'Resource id should be replaced with a relative path.');
expect(note1_body).toContain('](../_resources/resource1.jpg)', 'Resource id should be replaced with a relative path.');
expect(note2_body).toContain('](../../_resources/resource2.jpg)', 'Resource id should be replaced with a relative path.');
}));
it('should replace note ids with relative paths', (async () => {

View File

@@ -4,9 +4,7 @@ import shim from '../../shim';
import markdownUtils from '../../markdownUtils';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import { NoteEntity, ResourceEntity } from '../database/types';
import { basename, dirname, friendlySafeFilename } from '../../path-utils';
import { MarkupToHtml } from '@joplin/renderer';
export default class InteropService_Exporter_Md extends InteropService_Exporter_Base {
@@ -31,7 +29,7 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
output = `${pathPart}/${output}`;
} else {
output = `${friendlySafeFilename(item.title, null)}/${output}`;
if (findUniqueFilename) output = await shim.fsDriver().findUniqueFilename(output, null, true);
if (findUniqueFilename) output = await shim.fsDriver().findUniqueFilename(output);
}
}
if (!item.parent_id) return output;
@@ -48,7 +46,7 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
async replaceResourceIdsByRelativePaths_(noteBody: string, relativePathToRoot: string) {
const linkedResourceIds = await Note.linkedResourceIds(noteBody);
const resourcePaths = this.context() && this.context().destResourcePaths ? this.context().destResourcePaths : {};
const resourcePaths = this.context() && this.context().resourcePaths ? this.context().resourcePaths : {};
const createRelativePath = function(resourcePath: string) {
return `${relativePathToRoot}_resources/${basename(resourcePath)}`;
@@ -85,18 +83,17 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
notePaths: {},
};
for (let i = 0; i < itemsToExport.length; i++) {
const it = itemsToExport[i].type;
const itemType = itemsToExport[i].type;
if (it !== itemType) continue;
if (itemType !== itemType) continue;
const itemOrId = itemsToExport[i].itemOrId;
const note = typeof itemOrId === 'object' ? itemOrId : await Note.load(itemOrId);
if (!note) continue;
const ext = note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML ? 'html' : 'md';
let notePath = `${await this.makeDirPath_(note, null, false)}${friendlySafeFilename(note.title, null)}.${ext}`;
notePath = await shim.fsDriver().findUniqueFilename(`${this.destDir_}/${notePath}`, Object.values(context.notePaths), true);
let notePath = `${await this.makeDirPath_(note, null, false)}${friendlySafeFilename(note.title, null)}.md`;
notePath = await shim.fsDriver().findUniqueFilename(`${this.destDir_}/${notePath}`, Object.values(context.notePaths));
context.notePaths[note.id] = notePath;
}
@@ -110,10 +107,6 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
}
}
private async getNoteExportContent_(modNote: NoteEntity) {
return await Note.replaceResourceInternalToExternalLinks(await Note.serialize(modNote, ['body']));
}
async processItem(_itemType: number, item: any) {
if ([BaseModel.TYPE_NOTE, BaseModel.TYPE_FOLDER].indexOf(item.type_) < 0) return;
@@ -131,36 +124,15 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
const noteBody = await this.relaceLinkedItemIdsByRelativePaths_(item);
const modNote = Object.assign({}, item, { body: noteBody });
const noteContent = await this.getNoteExportContent_(modNote);
const noteContent = await Note.serializeForEdit(modNote);
await shim.fsDriver().mkdir(dirname(noteFilePath));
await shim.fsDriver().writeFile(noteFilePath, noteContent, 'utf-8');
}
}
private async findReasonableFilename(resource: ResourceEntity, filePath: string) {
let fileName = basename(filePath);
if (resource.filename) {
fileName = resource.filename;
} else if (resource.title) {
fileName = friendlySafeFilename(resource.title);
}
// Fall back on the resource filename saved in the users resource folder
return fileName;
}
async processResource(resource: ResourceEntity, filePath: string) {
const context = this.context();
if (!context.destResourcePaths) context.destResourcePaths = {};
const fileName = await this.findReasonableFilename(resource, filePath);
let destResourcePath = `${this.resourceDir_}/${fileName}`;
destResourcePath = await shim.fsDriver().findUniqueFilename(destResourcePath, Object.values(context.destResourcePaths), true);
async processResource(_resource: any, filePath: string) {
const destResourcePath = `${this.resourceDir_}/${basename(filePath)}`;
await shim.fsDriver().copy(filePath, destResourcePath);
context.destResourcePaths[resource.id] = destResourcePath;
this.updateContext(context);
}
async close() {}

View File

@@ -1,120 +0,0 @@
import InteropService_Importer_Md from '../../services/interop/InteropService_Importer_Md';
import Note from '../../models/Note';
import { setupDatabaseAndSynchronizer, supportDir, switchClient } from '../../testing/test-utils';
import { MarkupToHtml } from '@joplin/renderer';
describe('InteropService_Importer_Md: importLocalImages', function() {
async function importNote(path: string) {
const importer = new InteropService_Importer_Md();
importer.setMetadata({ fileExtensions: ['md', 'html'] });
return await importer.importFile(path, 'notebook');
}
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should import linked files and modify tags appropriately', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample.md`);
const tagNonExistentFile = '![does not exist](does_not_exist.png)';
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(2);
const inexistentLinkUnchanged = note.body.includes(tagNonExistentFile);
expect(inexistentLinkUnchanged).toBe(true);
});
it('should only create 1 resource for duplicate links, all tags should be updated', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-duplicate-links.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
const reg = new RegExp(items[0].id, 'g');
const matched = note.body.match(reg);
expect(matched.length).toBe(2);
});
it('should import linked files and modify tags appropriately when link is also in alt text', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-link-in-alt-text.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
});
it('should passthrough unchanged if no links present', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-no-links.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(0);
expect(note.body).toContain('Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.');
});
it('should import linked image with special characters in name', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-special-chars.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(3);
const noteIds = await Note.linkedNoteIds(note.body);
expect(noteIds.length).toBe(1);
const spaceSyntaxLeft = note.body.includes('<../../photo sample.jpg>');
expect(spaceSyntaxLeft).toBe(false);
});
it('should import resources and notes for files', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-files.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(3);
const noteIds = await Note.linkedNoteIds(note.body);
expect(noteIds.length).toBe(1);
});
it('should gracefully handle reference cycles in notes', async function() {
const importer = new InteropService_Importer_Md();
importer.setMetadata({ fileExtensions: ['md'] });
const noteA = await importer.importFile(`${supportDir}/test_notes/md/sample-cycles-a.md`, 'notebook');
const noteB = await importer.importFile(`${supportDir}/test_notes/md/sample-cycles-b.md`, 'notebook');
const noteAIds = await Note.linkedNoteIds(noteA.body);
expect(noteAIds.length).toBe(1);
const noteBIds = await Note.linkedNoteIds(noteB.body);
expect(noteBIds.length).toBe(1);
expect(noteAIds[0]).toEqual(noteB.id);
expect(noteBIds[0]).toEqual(noteA.id);
});
it('should not import resources from file:// links', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-file-links.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(0);
expect(note.body).toContain('![sample](file://../../photo.jpg)');
});
it('should attach resources that are missing the file extension', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-no-extension.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
});
it('should attach resources that include anchor links', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-anchor-link.md`);
const itemIds = await Note.linkedItemIds(note.body);
expect(itemIds.length).toBe(1);
expect(note.body).toContain(`[Section 1](:/${itemIds[0]}#markdown)`);
});
it('should attach resources that include a title', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-link-title.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(3);
const noteIds = await Note.linkedNoteIds(note.body);
expect(noteIds.length).toBe(1);
});
it('should import notes with html file extension as html', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample.html`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(3);
const noteIds = await Note.linkedNoteIds(note.body);
expect(noteIds.length).toBe(1);
expect(note.markup_language).toBe(MarkupToHtml.MARKUP_LANGUAGE_HTML);
const preservedAlt = note.body.includes('alt="../../photo.jpg"');
expect(preservedAlt).toBe(true);
});
});

View File

@@ -4,18 +4,14 @@ import { _ } from '../../locale';
import InteropService_Importer_Base from './InteropService_Importer_Base';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import { NoteEntity } from '../database/types';
import { basename, filename, rtrimSlashes, fileExtension, dirname } from '../../path-utils';
const { basename, filename, rtrimSlashes, fileExtension, dirname } = require('../../path-utils');
import shim from '../../shim';
import markdownUtils from '../../markdownUtils';
import htmlUtils from '../../htmlUtils';
const { unique } = require('../../ArrayUtils');
const { pregQuote } = require('../../string-utils-common');
import { MarkupToHtml } from '@joplin/renderer';
const { MarkupToHtml } = require('@joplin/renderer');
export default class InteropService_Importer_Md extends InteropService_Importer_Base {
private importedNotes: Record<string, NoteEntity> = {};
async exec(result: ImportExportResult) {
let parentFolderId = null;
@@ -63,100 +59,52 @@ export default class InteropService_Importer_Md extends InteropService_Importer_
}
}
private trimAnchorLink(link: string) {
if (link.indexOf('#') <= 0) return link;
const splitted = link.split('#');
splitted.pop();
return splitted.join('#');
}
/**
* Parse text for links, attempt to find local file, if found create Joplin resource
* and update link accordingly.
*/
async importLocalFiles(filePath: string, md: string, parentFolderId: string) {
async importLocalFiles(filePath: string, md: string) {
let updated = md;
const markdownLinks = markdownUtils.extractFileUrls(md);
const htmlLinks = htmlUtils.extractFileUrls(md);
const fileLinks = unique(markdownLinks.concat(htmlLinks));
const fileLinks = unique(markdownUtils.extractFileUrls(md));
await Promise.all(fileLinks.map(async (encodedLink: string) => {
const link = decodeURI(encodedLink);
// Handle anchor links appropriately
const trimmedLink = this.trimAnchorLink(link);
const attachmentPath = filename(`${dirname(filePath)}/${trimmedLink}`, true);
const pathWithExtension = `${attachmentPath}.${fileExtension(trimmedLink)}`;
const attachmentPath = filename(`${dirname(filePath)}/${link}`, true);
const pathWithExtension = `${attachmentPath}.${fileExtension(link)}`;
const stat = await shim.fsDriver().stat(pathWithExtension);
const isDir = stat ? stat.isDirectory() : false;
if (stat && !isDir) {
const supportedFileExtension = this.metadata().fileExtensions;
const resolvedPath = shim.fsDriver().resolve(pathWithExtension);
let id: string = '';
// If the link looks like a note, then import it
if (supportedFileExtension.indexOf(fileExtension(trimmedLink).toLowerCase()) >= 0) {
// If the note hasn't been imported yet, do so now
if (!this.importedNotes[resolvedPath]) {
await this.importFile(resolvedPath, parentFolderId);
}
id = this.importedNotes[resolvedPath].id;
} else {
const resource = await shim.createResourceFromPath(pathWithExtension);
id = resource.id;
}
// The first is a normal link, the second is supports the <link> and [](<link with spaces>) syntax
// Only opening patterns are consider in order to cover all occurances
// We need to use the encoded link as well because some links (link's with spaces)
// will appear encoded in the source. Other links (unicode chars) will not
const linksToReplace = [this.trimAnchorLink(link), this.trimAnchorLink(encodedLink)];
for (let j = 0; j < linksToReplace.length; j++) {
const linkToReplace = pregQuote(linksToReplace[j]);
// Markdown links
updated = markdownUtils.replaceResourceUrl(updated, linkToReplace, id);
// HTML links
updated = htmlUtils.replaceResourceUrl(updated, linkToReplace, id);
}
const resource = await shim.createResourceFromPath(pathWithExtension);
// NOTE: use ](link) in case the link also appears elsewhere, such as in alt text
const linkPatternEscaped = pregQuote(`](${link})`);
const reg = new RegExp(linkPatternEscaped, 'g');
updated = updated.replace(reg, `](:/${resource.id})`);
}
}));
return updated;
}
async importFile(filePath: string, parentFolderId: string) {
const resolvedPath = shim.fsDriver().resolve(filePath);
if (this.importedNotes[resolvedPath]) return this.importedNotes[resolvedPath];
const stat = await shim.fsDriver().stat(resolvedPath);
if (!stat) throw new Error(`Cannot read ${resolvedPath}`);
const ext = fileExtension(resolvedPath);
const title = filename(resolvedPath);
const body = await shim.fsDriver().readFile(resolvedPath);
const stat = await shim.fsDriver().stat(filePath);
if (!stat) throw new Error(`Cannot read ${filePath}`);
const title = filename(filePath);
const body = await shim.fsDriver().readFile(filePath);
let updatedBody;
try {
updatedBody = await this.importLocalFiles(filePath, body);
} catch (error) {
// console.error(`Problem importing links for file ${filePath}, error:\n ${error}`);
}
const note = {
parent_id: parentFolderId,
title: title,
body: body,
body: updatedBody || body,
updated_time: stat.mtime.getTime(),
created_time: stat.birthtime.getTime(),
user_updated_time: stat.mtime.getTime(),
user_created_time: stat.birthtime.getTime(),
markup_language: ext === 'html' ? MarkupToHtml.MARKUP_LANGUAGE_HTML : MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN,
markup_language: MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN,
};
this.importedNotes[resolvedPath] = await Note.save(note, { autoTimestamp: false });
try {
const updatedBody = await this.importLocalFiles(resolvedPath, body, parentFolderId);
const updatedNote = {
...this.importedNotes[resolvedPath],
body: updatedBody || body,
};
this.importedNotes[resolvedPath] = await Note.save(updatedNote, { isNew: false });
} catch (error) {
// console.error(`Problem importing links for file ${resolvedPath}, error:\n ${error}`);
}
return this.importedNotes[resolvedPath];
return Note.save(note, { autoTimestamp: false });
}
}

View File

@@ -3,62 +3,6 @@ import Setting, { SettingItem as InternalSettingItem, SettingSectionSource } fro
import Plugin from '../Plugin';
import { SettingItem, SettingSection } from './types';
// That's all the plugin as of 27/08/21 - any new plugin after that will not be
// able to use the registerSetting API. Fixes in particular all the ambrt
// plugins. Some of them don't need this hack but it's easier that way.
const registerSettingAllowedPluginIds: string[] = [
'b53da1f6-868c-468d-b60c-2897a27166ac',
'com.andrejilderda.macOSTheme',
'com.export-to-ssg.aman-d-1-n-only',
'com.github.BeatLink.joplin-plugin-untagged',
'com.github.joplin.kanban',
'com.github.marc0l92.joplin-plugin-jira-issue',
'com.github.uphy.PlantUmlPlugin',
'com.gitlab.BeatLink.joplin-plugin-repeating-todos',
'com.joplin_plugin.nlr',
'com.lki.homenote',
'com.plugin.randomNotePlugin',
'com.shantanugoel.JoplinCMLineNumbersPlugin',
'com.whatever.inline-tags',
'com.whatever.quick-links',
'com.xUser5000.bibtex',
'cx.evermeet.tessus.menu-shortcut-toolbar',
'fd117a99-b165-4824-893c-5825439a842d',
'io.github.jackgruber.backup',
'io.github.jackgruber.combine-notes',
'io.github.jackgruber.copytags',
'io.github.jackgruber.hotfolder',
'io.github.jackgruber.note-overview',
'io.treymo.LinkGraph',
'joplin-insert-date',
'joplin-plugin-conflict-resolution',
'joplin.plugin.ambrt.backlinksToNote',
'joplin.plugin.ambrt.convertToNewNote',
'joplin.plugin.ambrt.copyNoteLink',
'joplin.plugin.ambrt.embedSearch',
'joplin.plugin.ambrt.fold-cm',
'joplin.plugin.ambrt.goToItem',
'joplin.plugin.anki-sync',
'joplin.plugin.benji.favorites',
'joplin.plugin.benji.persistentLayout',
'joplin.plugin.benji.quick-move',
'joplin.plugin.forcewake.tags-generator',
'joplin.plugin.note.tabs',
'joplin.plugin.quick.html.tags',
'joplin.plugin.spoiler.cards',
'joplin.plugin.templates',
'net.rmusin.joplin-table-formatter',
'net.rmusin.resource-search',
'org.joplinapp.plugins.AbcSheetMusic',
'org.joplinapp.plugins.admonition',
'org.joplinapp.plugins.ToggleSidebars',
'osw.joplin.markdowncalc',
'outline',
'plugin.azamahJunior.note-statistics',
'plugin.calebjohn.MathMode',
'plugin.calebjohn.rich-markdown',
];
export interface ChangeEvent {
/**
* Setting keys that have been changed
@@ -133,13 +77,7 @@ export default class JoplinSettings {
* Registers a new setting.
*/
public async registerSetting(key: string, settingItem: SettingItem) {
// It's a warning for older plugins and an error for new ones.
this.plugin_.deprecationNotice(
'1.8',
'joplin.settings.registerSetting() is deprecated in favour of joplin.settings.registerSettings()',
!registerSettingAllowedPluginIds.includes(this.plugin_.id)
);
this.plugin_.deprecationNotice('1.8', 'joplin.settings.registerSetting() is deprecated in favour of joplin.settings.registerSettings()', true);
await this.registerSettings({ [key]: settingItem });
}

View File

@@ -42,7 +42,7 @@ const SyncTargetNextcloud = require('../SyncTargetNextcloud.js');
const SyncTargetDropbox = require('../SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('../SyncTargetAmazonS3.js');
import SyncTargetJoplinServer from '../SyncTargetJoplinServer';
import EncryptionService from '../services/e2ee/EncryptionService';
import EncryptionService from '../services/EncryptionService';
import DecryptionWorker from '../services/DecryptionWorker';
import RevisionService from '../services/RevisionService';
import ResourceFetcher from '../services/ResourceFetcher';

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/plugin-repo-cli",
"version": "2.4.0",
"version": "2.3.1",
"description": "",
"main": "index.js",
"bin": {
@@ -18,8 +18,8 @@
"author": "",
"license": "MIT",
"dependencies": {
"@joplin/lib": "~2.4",
"@joplin/tools": "~2.4",
"@joplin/lib": "^2.3.1",
"@joplin/tools": "^2.3.1",
"fs-extra": "^9.0.1",
"gh-release-assets": "^2.0.0",
"node-fetch": "^2.6.1",

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.3.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.3.1",
"license": "MIT",
"dependencies": {
"font-awesome-filetypes": "^2.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.3.1",
"description": "The Joplin note renderer, used the mobile and desktop application",
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
"main": "index.js",

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/server",
"version": "2.4.2",
"version": "2.3.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/server",
"version": "2.4.2",
"version": "2.3.7",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@koa/cors": "^3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.4.2",
"version": "2.3.7",
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
@@ -19,8 +19,8 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@joplin/lib": "~2.3",
"@joplin/renderer": "~2.3",
"@koa/cors": "^3.1.0",
"bcryptjs": "^2.4.3",
"bulma": "^0.9.1",
@@ -51,7 +51,7 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@joplin/tools": "~2.4",
"@joplin/tools": "~2.3",
"@rmp135/sql-ts": "^1.7.0",
"@types/fs-extra": "^8.0.0",
"@types/jest": "^26.0.15",

Binary file not shown.

View File

@@ -5,7 +5,7 @@ it('should pass', async function() {
// import { afterAllTests, beforeAllDb, beforeEachDb, db } from "./utils/testing/testUtils";
// import sqlts from '@rmp135/sql-ts';
// import config from "./config";
// import { connectDb, DbConnection, disconnectDb, migrateDown, migrateList, migrateUp, nextMigration } from "./services/database/types";
// import { connectDb, DbConnection, disconnectDb, migrateDown, migrateList, migrateUp, nextMigration } from "./db";
// async function dbSchemaSnapshot(db:DbConnection):Promise<any> {
// return sqlts.toObject({

View File

@@ -3,7 +3,6 @@ import { DatabaseConfig } from './utils/types';
import * as pathUtils from 'path';
import time from '@joplin/lib/time';
import Logger from '@joplin/lib/Logger';
import { databaseSchema } from './services/database/types';
// Make sure bigInteger values are numbers and not strings
//
@@ -306,3 +305,401 @@ export async function connectionCheck(db: DbConnection): Promise<ConnectionCheck
};
}
}
export type Uuid = string;
export enum ItemAddressingType {
Id = 1,
Path,
}
export enum NotificationLevel {
Important = 10,
Normal = 20,
}
export enum ItemType {
Item = 1,
UserItem = 2,
User,
}
export enum EmailSender {
NoReply = 1,
Support = 2,
}
export enum ChangeType {
Create = 1,
Update = 2,
Delete = 3,
}
export enum UserFlagType {
FailedPaymentWarning = 1,
FailedPaymentFinal = 2,
AccountOverLimit = 3,
AccountWithoutSubscription = 4,
SubscriptionCancelled = 5,
ManuallyDisabled = 6,
}
export enum FileContentType {
Any = 1,
JoplinItem = 2,
}
export function changeTypeToString(t: ChangeType): string {
if (t === ChangeType.Create) return 'create';
if (t === ChangeType.Update) return 'update';
if (t === ChangeType.Delete) return 'delete';
throw new Error(`Unkown type: ${t}`);
}
export enum ShareType {
Note = 1, // When a note is shared via a public link
Folder = 3, // When a complete folder is shared with another Joplin Server user
}
export enum ShareUserStatus {
Waiting = 0,
Accepted = 1,
Rejected = 2,
}
export interface WithDates {
updated_time?: number;
created_time?: number;
}
export interface WithUuid {
id?: Uuid;
}
interface DatabaseTableColumn {
type: string;
}
interface DatabaseTable {
[key: string]: DatabaseTableColumn;
}
interface DatabaseTables {
[key: string]: DatabaseTable;
}
// AUTO-GENERATED-TYPES
// Auto-generated using `npm run generate-types`
export interface Session extends WithDates, WithUuid {
user_id?: Uuid;
auth_code?: string;
}
export interface File {
id?: Uuid;
owner_id?: Uuid;
name?: string;
content?: any;
mime_type?: string;
size?: number;
is_directory?: number;
is_root?: number;
parent_id?: Uuid;
updated_time?: string;
created_time?: string;
source_file_id?: Uuid;
content_type?: number;
content_id?: Uuid;
}
export interface ApiClient extends WithDates, WithUuid {
name?: string;
secret?: string;
}
export interface Notification extends WithDates, WithUuid {
owner_id?: Uuid;
level?: NotificationLevel;
key?: string;
message?: string;
read?: number;
canBeDismissed?: number;
}
export interface ShareUser extends WithDates, WithUuid {
share_id?: Uuid;
user_id?: Uuid;
status?: ShareUserStatus;
}
export interface Item extends WithDates, WithUuid {
name?: string;
mime_type?: string;
content?: Buffer;
content_size?: number;
jop_id?: Uuid;
jop_parent_id?: Uuid;
jop_share_id?: Uuid;
jop_type?: number;
jop_encryption_applied?: number;
jop_updated_time?: number;
}
export interface UserItem extends WithDates {
id?: number;
user_id?: Uuid;
item_id?: Uuid;
}
export interface ItemResource {
id?: number;
item_id?: Uuid;
resource_id?: Uuid;
}
export interface KeyValue {
id?: number;
key?: string;
type?: number;
value?: string;
}
export interface Share extends WithDates, WithUuid {
owner_id?: Uuid;
item_id?: Uuid;
type?: ShareType;
folder_id?: Uuid;
note_id?: Uuid;
}
export interface Change extends WithDates, WithUuid {
counter?: number;
item_type?: ItemType;
item_id?: Uuid;
item_name?: string;
type?: ChangeType;
previous_item?: string;
user_id?: Uuid;
}
export interface Email extends WithDates {
id?: number;
recipient_name?: string;
recipient_email?: string;
recipient_id?: Uuid;
sender_id?: EmailSender;
subject?: string;
body?: string;
sent_time?: number;
sent_success?: number;
error?: string;
key?: string;
}
export interface Token extends WithDates {
id?: number;
value?: string;
user_id?: Uuid;
}
export interface Subscription {
id?: number;
user_id?: Uuid;
stripe_user_id?: Uuid;
stripe_subscription_id?: Uuid;
last_payment_time?: number;
last_payment_failed_time?: number;
updated_time?: string;
created_time?: string;
is_deleted?: number;
}
export interface User extends WithDates, WithUuid {
email?: string;
password?: string;
full_name?: string;
is_admin?: number;
email_confirmed?: number;
must_set_password?: number;
account_type?: number;
can_upload?: number;
max_item_size?: number | null;
can_share_folder?: number | null;
can_share_note?: number | null;
max_total_item_size?: number | null;
total_item_size?: number;
enabled?: number;
}
export interface UserFlag extends WithDates {
id?: number;
user_id?: Uuid;
type?: UserFlagType;
}
export const databaseSchema: DatabaseTables = {
sessions: {
id: { type: 'string' },
user_id: { type: 'string' },
auth_code: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
files: {
id: { type: 'string' },
owner_id: { type: 'string' },
name: { type: 'string' },
content: { type: 'any' },
mime_type: { type: 'string' },
size: { type: 'number' },
is_directory: { type: 'number' },
is_root: { type: 'number' },
parent_id: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
source_file_id: { type: 'string' },
content_type: { type: 'number' },
content_id: { type: 'string' },
},
api_clients: {
id: { type: 'string' },
name: { type: 'string' },
secret: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
notifications: {
id: { type: 'string' },
owner_id: { type: 'string' },
level: { type: 'number' },
key: { type: 'string' },
message: { type: 'string' },
read: { type: 'number' },
canBeDismissed: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
share_users: {
id: { type: 'string' },
share_id: { type: 'string' },
user_id: { type: 'string' },
status: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
items: {
id: { type: 'string' },
name: { type: 'string' },
mime_type: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
content: { type: 'any' },
content_size: { type: 'number' },
jop_id: { type: 'string' },
jop_parent_id: { type: 'string' },
jop_share_id: { type: 'string' },
jop_type: { type: 'number' },
jop_encryption_applied: { type: 'number' },
jop_updated_time: { type: 'string' },
},
user_items: {
id: { type: 'number' },
user_id: { type: 'string' },
item_id: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
item_resources: {
id: { type: 'number' },
item_id: { type: 'string' },
resource_id: { type: 'string' },
},
key_values: {
id: { type: 'number' },
key: { type: 'string' },
type: { type: 'number' },
value: { type: 'string' },
},
shares: {
id: { type: 'string' },
owner_id: { type: 'string' },
item_id: { type: 'string' },
type: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
folder_id: { type: 'string' },
note_id: { type: 'string' },
},
changes: {
counter: { type: 'number' },
id: { type: 'string' },
item_type: { type: 'number' },
item_id: { type: 'string' },
item_name: { type: 'string' },
type: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
previous_item: { type: 'string' },
user_id: { type: 'string' },
},
emails: {
id: { type: 'number' },
recipient_name: { type: 'string' },
recipient_email: { type: 'string' },
recipient_id: { type: 'string' },
sender_id: { type: 'number' },
subject: { type: 'string' },
body: { type: 'string' },
sent_time: { type: 'string' },
sent_success: { type: 'number' },
error: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
key: { type: 'string' },
},
tokens: {
id: { type: 'number' },
value: { type: 'string' },
user_id: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
subscriptions: {
id: { type: 'number' },
user_id: { type: 'string' },
stripe_user_id: { type: 'string' },
stripe_subscription_id: { type: 'string' },
last_payment_time: { type: 'string' },
last_payment_failed_time: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
is_deleted: { type: 'number' },
},
users: {
id: { type: 'string' },
email: { type: 'string' },
password: { type: 'string' },
full_name: { type: 'string' },
is_admin: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
email_confirmed: { type: 'number' },
must_set_password: { type: 'number' },
account_type: { type: 'number' },
can_upload: { type: 'number' },
max_item_size: { type: 'number' },
can_share_folder: { type: 'number' },
can_share_note: { type: 'number' },
max_total_item_size: { type: 'string' },
total_item_size: { type: 'string' },
enabled: { type: 'number' },
},
user_flags: {
id: { type: 'number' },
user_id: { type: 'string' },
type: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
};
// AUTO-GENERATED-TYPES

View File

@@ -1,6 +1,5 @@
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, koaAppContext, koaNext } from '../utils/testing/testUtils';
import { Notification } from '../services/database/types';
import { defaultAdminEmail, defaultAdminPassword } from '../db';
import { defaultAdminEmail, defaultAdminPassword, Notification } from '../db';
import notificationHandler from './notificationHandler';
describe('notificationHandler', function() {

View File

@@ -1,7 +1,6 @@
import { AppContext, KoaNext, NotificationView } from '../utils/types';
import { isApiRequest } from '../utils/requestUtils';
import { NotificationLevel } from '../services/database/types';
import { defaultAdminEmail, defaultAdminPassword } from '../db';
import { defaultAdminEmail, defaultAdminPassword, NotificationLevel } from '../db';
import { _ } from '@joplin/lib/locale';
import Logger from '@joplin/lib/Logger';
import * as MarkdownIt from 'markdown-it';

View File

@@ -1,5 +1,5 @@
import BaseModel from './BaseModel';
import { ApiClient } from '../services/database/types';
import { ApiClient } from '../db';
export default class ApiClientModel extends BaseModel<ApiClient> {

View File

@@ -1,5 +1,4 @@
import { WithDates, WithUuid, databaseSchema, ItemType, Uuid, User } from '../services/database/types';
import { DbConnection } from '../db';
import { WithDates, WithUuid, databaseSchema, DbConnection, ItemType, Uuid, User } from '../db';
import TransactionHandler from '../utils/TransactionHandler';
import uuidgen from '../utils/uuidgen';
import { ErrorUnprocessableEntity, ErrorBadRequest } from '../utils/errors';

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