You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-01-02 00:08:04 +02:00
Compare commits
7 Commits
master_pas
...
server_use
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8411e1270e | ||
|
|
e9d4a777fd | ||
|
|
85984f1f39 | ||
|
|
3c0524c6e9 | ||
|
|
769d47a768 | ||
|
|
87fe0e4dcf | ||
|
|
2513e0aaab |
@@ -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
21
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)"
|
||||
|
||||
12
README.md
12
README.md
@@ -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 -->
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
50
packages/app-cli/tests/MdToMd.ts
Normal file
50
packages/app-cli/tests/MdToMd.ts
Normal 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 = '';
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||

|
||||

|
||||
9
packages/app-cli/tests/md_to_md/sample-files.md
Normal file
9
packages/app-cli/tests/md_to_md/sample-files.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Markdown file test
|
||||
|
||||

|
||||
|
||||
[welcome.pdf](../support/welcome.pdf)
|
||||
|
||||
[sample.md](sample.md)
|
||||
|
||||
[sample2.md](./sample.md)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Markdown
|
||||
 should put resource link inside () not []
|
||||
 this case (spaces before/after link but within parens) is not currently covered
|
||||
1
packages/app-cli/tests/md_to_md/sample-special-chars.md
Normal file
1
packages/app-cli/tests/md_to_md/sample-special-chars.md
Normal file
@@ -0,0 +1 @@
|
||||

|
||||
13
packages/app-cli/tests/md_to_md/sample.md
Normal file
13
packages/app-cli/tests/md_to_md/sample.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Markdown
|
||||
|
||||
lorem ipsum 
|
||||
- [ ] check!
|
||||
- [ ] boxes!
|
||||
|
||||
ipsum lorem
|
||||
|
||||
**strong text**
|
||||
 lorem ipsum
|
||||
|
||||
**some directory**
|
||||
 lorem ipsum
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,3 +0,0 @@
|
||||
# Test Spaces
|
||||
|
||||
I hope this get's imported correctly!
|
||||
@@ -1 +0,0 @@
|
||||
[Section 1](./sample-no-links.md#markdown)
|
||||
@@ -1,3 +0,0 @@
|
||||
# Markdown file test
|
||||
|
||||
[sample.md](sample-cycles-b.md)
|
||||
@@ -1,4 +0,0 @@
|
||||
# Markdown file test
|
||||
|
||||
|
||||
[sample.md](./sample-cycles-a.md)
|
||||
@@ -1,2 +0,0 @@
|
||||

|
||||

|
||||
@@ -1 +0,0 @@
|
||||

|
||||
@@ -1,9 +0,0 @@
|
||||
# Markdown file test
|
||||
|
||||

|
||||
|
||||
[welcome.pdf](../../welcome.pdf)
|
||||
|
||||
[sample.md](sample.md)
|
||||
|
||||
[sample2.md](./sample.md)
|
||||
@@ -1,3 +0,0 @@
|
||||
# Markdown
|
||||
 should put resource link inside () not []
|
||||
 this case (spaces before/after link but within parens) is not currently covered
|
||||
@@ -1,3 +0,0 @@
|
||||

|
||||

|
||||
[Worst Case](<./sample spaces.md> "title")
|
||||
@@ -1 +0,0 @@
|
||||
I am here, but am I alive?
|
||||
@@ -1,3 +0,0 @@
|
||||
# Some Title
|
||||
|
||||
[link](./sample-md)
|
||||
@@ -1,4 +0,0 @@
|
||||

|
||||
[sample photo](../../photo%20sample.jpg)
|
||||
[sample.md](./sample%20spaces.md)
|
||||
[sample special syntax](<../../photo sample.jpg>)
|
||||
@@ -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">
|
||||
@@ -1,13 +0,0 @@
|
||||
# Markdown
|
||||
|
||||
lorem ipsum 
|
||||
- [ ] check!
|
||||
- [ ] boxes!
|
||||
|
||||
ipsum lorem
|
||||
|
||||
**strong text**
|
||||
 lorem ipsum
|
||||
|
||||
**some directory**
|
||||
 lorem ipsum
|
||||
@@ -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'",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
<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'],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
4
packages/app-desktop/package-lock.json
generated
4
packages/app-desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 %>",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
4
packages/lib/package-lock.json
generated
4
packages/lib/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: `` });
|
||||
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 () => {
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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 = '';
|
||||
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('');
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
4
packages/renderer/package-lock.json
generated
4
packages/renderer/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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.
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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> {
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user