1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-30 20:39:46 +02:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Laurent Cozic
8801e82cab pagination 2020-12-29 14:35:51 +00:00
Laurent Cozic
c4757d6c60 pagination 2020-12-29 00:13:04 +00:00
Laurent Cozic
af6c79b844 file manager 2020-12-28 17:24:06 +00:00
Laurent Cozic
0e0de1207f Server: Adding basic file manager 2020-12-28 16:37:12 +00:00
Laurent Cozic
2fda067034 Server: Improved logging and error handling 2020-12-28 15:15:30 +00:00
Laurent Cozic
29177330b0 Merge branch 'release-1.5' into dev 2020-12-28 14:31:37 +00:00
Laurent Cozic
66a5490b54 Desktop release v1.5.12 2020-12-28 14:29:37 +00:00
Laurent Cozic
469cd19ec1 Desktop: Fix issue when importing ENEX file that contains invalid list elements 2020-12-28 14:29:02 +00:00
Laurent
41684a64ef Server: Add Joplin Server package (#1872) 2020-12-28 11:48:47 +00:00
Laurent Cozic
2cd7839552 Increase version num to 1.6 2020-12-27 22:46:27 +00:00
Laurent Cozic
c3d4617612 Update website 2020-12-27 22:41:47 +00:00
Laurent Cozic
158fafc4a0 Merge branch 'release-1.5' into dev 2020-12-27 22:40:26 +00:00
Laurent Cozic
0f59731c06 Plugin Generator release v1.5.3 2020-12-27 22:28:16 +00:00
Laurent Cozic
d0f1a67d96 Plugins: Updated types 2020-12-27 22:26:17 +00:00
Laurent Cozic
2a1434f987 Generator: Fixed publish logic 2020-12-27 22:25:17 +00:00
Laurent Cozic
a97f25fd61 Update website 2020-12-27 19:55:34 +00:00
172 changed files with 16092 additions and 650 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
**/node_modules
Assets/
.git/
_releases/
packages/app-desktop
packages/app-cli
packages/app-mobile
packages/app-clipper
packages/generator-joplin

9
.env-sample Normal file
View File

@@ -0,0 +1,9 @@
# Example of local config, for development:
#
# JOPLIN_BASE_URL=http://localhost:22300
# JOPLIN_PORT=22300
# Example of config for production:
#
# JOPLIN_BASE_URL=https://example.com/joplin
# JOPLIN_PORT=22300

View File

@@ -6,6 +6,7 @@ _releases/
**/node_modules/
Assets/
docs/
packages/server/dist/
highlight.pack.js
Modules/TinyMCE/IconPack/postinstall.js
Modules/TinyMCE/JoplinLists/
@@ -889,12 +890,18 @@ packages/lib/InMemoryCache.js.map
packages/lib/JoplinServerApi.d.ts
packages/lib/JoplinServerApi.js
packages/lib/JoplinServerApi.js.map
packages/lib/JoplinServerApi2.d.ts
packages/lib/JoplinServerApi2.js
packages/lib/JoplinServerApi2.js.map
packages/lib/Logger.d.ts
packages/lib/Logger.js
packages/lib/Logger.js.map
packages/lib/PoorManIntervals.d.ts
packages/lib/PoorManIntervals.js
packages/lib/PoorManIntervals.js.map
packages/lib/SyncTargetJoplinServer.d.ts
packages/lib/SyncTargetJoplinServer.js
packages/lib/SyncTargetJoplinServer.js.map
packages/lib/Synchronizer.d.ts
packages/lib/Synchronizer.js
packages/lib/Synchronizer.js.map
@@ -916,6 +923,9 @@ packages/lib/errorUtils.js.map
packages/lib/eventManager.d.ts
packages/lib/eventManager.js
packages/lib/eventManager.js.map
packages/lib/file-api-driver-joplinServer.d.ts
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-joplinServer.js.map
packages/lib/fs-driver-base.d.ts
packages/lib/fs-driver-base.js
packages/lib/fs-driver-base.js.map
@@ -1387,4 +1397,211 @@ packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
packages/server/src/app.d.ts
packages/server/src/app.js
packages/server/src/app.js.map
packages/server/src/config-base.d.ts
packages/server/src/config-base.js
packages/server/src/config-base.js.map
packages/server/src/config-buildTypes.d.ts
packages/server/src/config-buildTypes.js
packages/server/src/config-buildTypes.js.map
packages/server/src/config-dev.d.ts
packages/server/src/config-dev.js
packages/server/src/config-dev.js.map
packages/server/src/config-prod.d.ts
packages/server/src/config-prod.js
packages/server/src/config-prod.js.map
packages/server/src/config-tests.d.ts
packages/server/src/config-tests.js
packages/server/src/config-tests.js.map
packages/server/src/config.d.ts
packages/server/src/config.js
packages/server/src/config.js.map
packages/server/src/controllers/BaseController.d.ts
packages/server/src/controllers/BaseController.js
packages/server/src/controllers/BaseController.js.map
packages/server/src/controllers/api/FileController.d.ts
packages/server/src/controllers/api/FileController.js
packages/server/src/controllers/api/FileController.js.map
packages/server/src/controllers/api/FileController.test.d.ts
packages/server/src/controllers/api/FileController.test.js
packages/server/src/controllers/api/FileController.test.js.map
packages/server/src/controllers/api/OAuthController.d.ts
packages/server/src/controllers/api/OAuthController.js
packages/server/src/controllers/api/OAuthController.js.map
packages/server/src/controllers/api/SessionController.d.ts
packages/server/src/controllers/api/SessionController.js
packages/server/src/controllers/api/SessionController.js.map
packages/server/src/controllers/api/SessionController.test.d.ts
packages/server/src/controllers/api/SessionController.test.js
packages/server/src/controllers/api/SessionController.test.js.map
packages/server/src/controllers/api/UserController.d.ts
packages/server/src/controllers/api/UserController.js
packages/server/src/controllers/api/UserController.js.map
packages/server/src/controllers/api/UserController.test.d.ts
packages/server/src/controllers/api/UserController.test.js
packages/server/src/controllers/api/UserController.test.js.map
packages/server/src/controllers/factory.d.ts
packages/server/src/controllers/factory.js
packages/server/src/controllers/factory.js.map
packages/server/src/controllers/index/FileController.d.ts
packages/server/src/controllers/index/FileController.js
packages/server/src/controllers/index/FileController.js.map
packages/server/src/controllers/index/HomeController.d.ts
packages/server/src/controllers/index/HomeController.js
packages/server/src/controllers/index/HomeController.js.map
packages/server/src/controllers/index/LoginController.d.ts
packages/server/src/controllers/index/LoginController.js
packages/server/src/controllers/index/LoginController.js.map
packages/server/src/controllers/index/ProfileController.d.ts
packages/server/src/controllers/index/ProfileController.js
packages/server/src/controllers/index/ProfileController.js.map
packages/server/src/controllers/index/UserController.d.ts
packages/server/src/controllers/index/UserController.js
packages/server/src/controllers/index/UserController.js.map
packages/server/src/db.d.ts
packages/server/src/db.js
packages/server/src/db.js.map
packages/server/src/migrations/20190913171451_create.d.ts
packages/server/src/migrations/20190913171451_create.js
packages/server/src/migrations/20190913171451_create.js.map
packages/server/src/models/ApiClientModel.d.ts
packages/server/src/models/ApiClientModel.js
packages/server/src/models/ApiClientModel.js.map
packages/server/src/models/BaseModel.d.ts
packages/server/src/models/BaseModel.js
packages/server/src/models/BaseModel.js.map
packages/server/src/models/ChangeModel.d.ts
packages/server/src/models/ChangeModel.js
packages/server/src/models/ChangeModel.js.map
packages/server/src/models/ChangeModel.test.d.ts
packages/server/src/models/ChangeModel.test.js
packages/server/src/models/ChangeModel.test.js.map
packages/server/src/models/FileModel.d.ts
packages/server/src/models/FileModel.js
packages/server/src/models/FileModel.js.map
packages/server/src/models/FileModel.test.d.ts
packages/server/src/models/FileModel.test.js
packages/server/src/models/FileModel.test.js.map
packages/server/src/models/PermissionModel.d.ts
packages/server/src/models/PermissionModel.js
packages/server/src/models/PermissionModel.js.map
packages/server/src/models/SessionModel.d.ts
packages/server/src/models/SessionModel.js
packages/server/src/models/SessionModel.js.map
packages/server/src/models/UserModel.d.ts
packages/server/src/models/UserModel.js
packages/server/src/models/UserModel.js.map
packages/server/src/models/factory.d.ts
packages/server/src/models/factory.js
packages/server/src/models/factory.js.map
packages/server/src/models/utils/pagination.d.ts
packages/server/src/models/utils/pagination.js
packages/server/src/models/utils/pagination.js.map
packages/server/src/models/utils/pagination.test.d.ts
packages/server/src/models/utils/pagination.test.js
packages/server/src/models/utils/pagination.test.js.map
packages/server/src/routes/api/files.d.ts
packages/server/src/routes/api/files.js
packages/server/src/routes/api/files.js.map
packages/server/src/routes/api/index.d.ts
packages/server/src/routes/api/index.js
packages/server/src/routes/api/index.js.map
packages/server/src/routes/api/ping.d.ts
packages/server/src/routes/api/ping.js
packages/server/src/routes/api/ping.js.map
packages/server/src/routes/api/sessions.d.ts
packages/server/src/routes/api/sessions.js
packages/server/src/routes/api/sessions.js.map
packages/server/src/routes/default.d.ts
packages/server/src/routes/default.js
packages/server/src/routes/default.js.map
packages/server/src/routes/index/files.d.ts
packages/server/src/routes/index/files.js
packages/server/src/routes/index/files.js.map
packages/server/src/routes/index/home.d.ts
packages/server/src/routes/index/home.js
packages/server/src/routes/index/home.js.map
packages/server/src/routes/index/login.d.ts
packages/server/src/routes/index/login.js
packages/server/src/routes/index/login.js.map
packages/server/src/routes/index/logout.d.ts
packages/server/src/routes/index/logout.js
packages/server/src/routes/index/logout.js.map
packages/server/src/routes/index/profile.d.ts
packages/server/src/routes/index/profile.js
packages/server/src/routes/index/profile.js.map
packages/server/src/routes/index/user.d.ts
packages/server/src/routes/index/user.js
packages/server/src/routes/index/user.js.map
packages/server/src/routes/index/users.d.ts
packages/server/src/routes/index/users.js
packages/server/src/routes/index/users.js.map
packages/server/src/routes/oauth2/authorize.d.ts
packages/server/src/routes/oauth2/authorize.js
packages/server/src/routes/oauth2/authorize.js.map
packages/server/src/routes/routes.d.ts
packages/server/src/routes/routes.js
packages/server/src/routes/routes.js.map
packages/server/src/services/MustacheService.d.ts
packages/server/src/services/MustacheService.js
packages/server/src/services/MustacheService.js.map
packages/server/src/tools/db-migrate.d.ts
packages/server/src/tools/db-migrate.js
packages/server/src/tools/db-migrate.js.map
packages/server/src/tools/dbTools.d.ts
packages/server/src/tools/dbTools.js
packages/server/src/tools/dbTools.js.map
packages/server/src/tools/generate-types.d.ts
packages/server/src/tools/generate-types.js
packages/server/src/tools/generate-types.js.map
packages/server/src/utils/TransactionHandler.d.ts
packages/server/src/utils/TransactionHandler.js
packages/server/src/utils/TransactionHandler.js.map
packages/server/src/utils/auth.d.ts
packages/server/src/utils/auth.js
packages/server/src/utils/auth.js.map
packages/server/src/utils/base64.d.ts
packages/server/src/utils/base64.js
packages/server/src/utils/base64.js.map
packages/server/src/utils/cache.d.ts
packages/server/src/utils/cache.js
packages/server/src/utils/cache.js.map
packages/server/src/utils/defaultView.d.ts
packages/server/src/utils/defaultView.js
packages/server/src/utils/defaultView.js.map
packages/server/src/utils/errors.d.ts
packages/server/src/utils/errors.js
packages/server/src/utils/errors.js.map
packages/server/src/utils/htmlUtils.d.ts
packages/server/src/utils/htmlUtils.js
packages/server/src/utils/htmlUtils.js.map
packages/server/src/utils/koaIf.d.ts
packages/server/src/utils/koaIf.js
packages/server/src/utils/koaIf.js.map
packages/server/src/utils/requestUtils.d.ts
packages/server/src/utils/requestUtils.js
packages/server/src/utils/requestUtils.js.map
packages/server/src/utils/routeUtils.d.ts
packages/server/src/utils/routeUtils.js
packages/server/src/utils/routeUtils.js.map
packages/server/src/utils/routeUtils.test.d.ts
packages/server/src/utils/routeUtils.test.js
packages/server/src/utils/routeUtils.test.js.map
packages/server/src/utils/testUtils.d.ts
packages/server/src/utils/testUtils.js
packages/server/src/utils/testUtils.js.map
packages/server/src/utils/testing/testRouters.d.ts
packages/server/src/utils/testing/testRouters.js
packages/server/src/utils/testing/testRouters.js.map
packages/server/src/utils/time.d.ts
packages/server/src/utils/time.js
packages/server/src/utils/time.js.map
packages/server/src/utils/types.d.ts
packages/server/src/utils/types.js
packages/server/src/utils/types.js.map
packages/server/src/utils/uuidgen.d.ts
packages/server/src/utils/uuidgen.js
packages/server/src/utils/uuidgen.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

@@ -1,4 +1,5 @@
module.exports = {
'root': true,
'env': {
'browser': true,
'es6': true,
@@ -34,6 +35,9 @@ module.exports = {
'chrome': 'readonly',
'browser': 'readonly',
// Server admin UI global variables
'onDocumentReady': 'readonly',
'tinymce': 'readonly',
},
'parserOptions': {

217
.gitignore vendored
View File

@@ -48,6 +48,7 @@ TODO.md
packages/tools/commit_hook.txt
packages/tools/github_oauth_token.txt
lerna-debug.log
.env
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
packages/app-cli/app/LinkSelector.d.ts
@@ -878,12 +879,18 @@ packages/lib/InMemoryCache.js.map
packages/lib/JoplinServerApi.d.ts
packages/lib/JoplinServerApi.js
packages/lib/JoplinServerApi.js.map
packages/lib/JoplinServerApi2.d.ts
packages/lib/JoplinServerApi2.js
packages/lib/JoplinServerApi2.js.map
packages/lib/Logger.d.ts
packages/lib/Logger.js
packages/lib/Logger.js.map
packages/lib/PoorManIntervals.d.ts
packages/lib/PoorManIntervals.js
packages/lib/PoorManIntervals.js.map
packages/lib/SyncTargetJoplinServer.d.ts
packages/lib/SyncTargetJoplinServer.js
packages/lib/SyncTargetJoplinServer.js.map
packages/lib/Synchronizer.d.ts
packages/lib/Synchronizer.js
packages/lib/Synchronizer.js.map
@@ -905,6 +912,9 @@ packages/lib/errorUtils.js.map
packages/lib/eventManager.d.ts
packages/lib/eventManager.js
packages/lib/eventManager.js.map
packages/lib/file-api-driver-joplinServer.d.ts
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-joplinServer.js.map
packages/lib/fs-driver-base.d.ts
packages/lib/fs-driver-base.js
packages/lib/fs-driver-base.js.map
@@ -1376,4 +1386,211 @@ packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
packages/server/src/app.d.ts
packages/server/src/app.js
packages/server/src/app.js.map
packages/server/src/config-base.d.ts
packages/server/src/config-base.js
packages/server/src/config-base.js.map
packages/server/src/config-buildTypes.d.ts
packages/server/src/config-buildTypes.js
packages/server/src/config-buildTypes.js.map
packages/server/src/config-dev.d.ts
packages/server/src/config-dev.js
packages/server/src/config-dev.js.map
packages/server/src/config-prod.d.ts
packages/server/src/config-prod.js
packages/server/src/config-prod.js.map
packages/server/src/config-tests.d.ts
packages/server/src/config-tests.js
packages/server/src/config-tests.js.map
packages/server/src/config.d.ts
packages/server/src/config.js
packages/server/src/config.js.map
packages/server/src/controllers/BaseController.d.ts
packages/server/src/controllers/BaseController.js
packages/server/src/controllers/BaseController.js.map
packages/server/src/controllers/api/FileController.d.ts
packages/server/src/controllers/api/FileController.js
packages/server/src/controllers/api/FileController.js.map
packages/server/src/controllers/api/FileController.test.d.ts
packages/server/src/controllers/api/FileController.test.js
packages/server/src/controllers/api/FileController.test.js.map
packages/server/src/controllers/api/OAuthController.d.ts
packages/server/src/controllers/api/OAuthController.js
packages/server/src/controllers/api/OAuthController.js.map
packages/server/src/controllers/api/SessionController.d.ts
packages/server/src/controllers/api/SessionController.js
packages/server/src/controllers/api/SessionController.js.map
packages/server/src/controllers/api/SessionController.test.d.ts
packages/server/src/controllers/api/SessionController.test.js
packages/server/src/controllers/api/SessionController.test.js.map
packages/server/src/controllers/api/UserController.d.ts
packages/server/src/controllers/api/UserController.js
packages/server/src/controllers/api/UserController.js.map
packages/server/src/controllers/api/UserController.test.d.ts
packages/server/src/controllers/api/UserController.test.js
packages/server/src/controllers/api/UserController.test.js.map
packages/server/src/controllers/factory.d.ts
packages/server/src/controllers/factory.js
packages/server/src/controllers/factory.js.map
packages/server/src/controllers/index/FileController.d.ts
packages/server/src/controllers/index/FileController.js
packages/server/src/controllers/index/FileController.js.map
packages/server/src/controllers/index/HomeController.d.ts
packages/server/src/controllers/index/HomeController.js
packages/server/src/controllers/index/HomeController.js.map
packages/server/src/controllers/index/LoginController.d.ts
packages/server/src/controllers/index/LoginController.js
packages/server/src/controllers/index/LoginController.js.map
packages/server/src/controllers/index/ProfileController.d.ts
packages/server/src/controllers/index/ProfileController.js
packages/server/src/controllers/index/ProfileController.js.map
packages/server/src/controllers/index/UserController.d.ts
packages/server/src/controllers/index/UserController.js
packages/server/src/controllers/index/UserController.js.map
packages/server/src/db.d.ts
packages/server/src/db.js
packages/server/src/db.js.map
packages/server/src/migrations/20190913171451_create.d.ts
packages/server/src/migrations/20190913171451_create.js
packages/server/src/migrations/20190913171451_create.js.map
packages/server/src/models/ApiClientModel.d.ts
packages/server/src/models/ApiClientModel.js
packages/server/src/models/ApiClientModel.js.map
packages/server/src/models/BaseModel.d.ts
packages/server/src/models/BaseModel.js
packages/server/src/models/BaseModel.js.map
packages/server/src/models/ChangeModel.d.ts
packages/server/src/models/ChangeModel.js
packages/server/src/models/ChangeModel.js.map
packages/server/src/models/ChangeModel.test.d.ts
packages/server/src/models/ChangeModel.test.js
packages/server/src/models/ChangeModel.test.js.map
packages/server/src/models/FileModel.d.ts
packages/server/src/models/FileModel.js
packages/server/src/models/FileModel.js.map
packages/server/src/models/FileModel.test.d.ts
packages/server/src/models/FileModel.test.js
packages/server/src/models/FileModel.test.js.map
packages/server/src/models/PermissionModel.d.ts
packages/server/src/models/PermissionModel.js
packages/server/src/models/PermissionModel.js.map
packages/server/src/models/SessionModel.d.ts
packages/server/src/models/SessionModel.js
packages/server/src/models/SessionModel.js.map
packages/server/src/models/UserModel.d.ts
packages/server/src/models/UserModel.js
packages/server/src/models/UserModel.js.map
packages/server/src/models/factory.d.ts
packages/server/src/models/factory.js
packages/server/src/models/factory.js.map
packages/server/src/models/utils/pagination.d.ts
packages/server/src/models/utils/pagination.js
packages/server/src/models/utils/pagination.js.map
packages/server/src/models/utils/pagination.test.d.ts
packages/server/src/models/utils/pagination.test.js
packages/server/src/models/utils/pagination.test.js.map
packages/server/src/routes/api/files.d.ts
packages/server/src/routes/api/files.js
packages/server/src/routes/api/files.js.map
packages/server/src/routes/api/index.d.ts
packages/server/src/routes/api/index.js
packages/server/src/routes/api/index.js.map
packages/server/src/routes/api/ping.d.ts
packages/server/src/routes/api/ping.js
packages/server/src/routes/api/ping.js.map
packages/server/src/routes/api/sessions.d.ts
packages/server/src/routes/api/sessions.js
packages/server/src/routes/api/sessions.js.map
packages/server/src/routes/default.d.ts
packages/server/src/routes/default.js
packages/server/src/routes/default.js.map
packages/server/src/routes/index/files.d.ts
packages/server/src/routes/index/files.js
packages/server/src/routes/index/files.js.map
packages/server/src/routes/index/home.d.ts
packages/server/src/routes/index/home.js
packages/server/src/routes/index/home.js.map
packages/server/src/routes/index/login.d.ts
packages/server/src/routes/index/login.js
packages/server/src/routes/index/login.js.map
packages/server/src/routes/index/logout.d.ts
packages/server/src/routes/index/logout.js
packages/server/src/routes/index/logout.js.map
packages/server/src/routes/index/profile.d.ts
packages/server/src/routes/index/profile.js
packages/server/src/routes/index/profile.js.map
packages/server/src/routes/index/user.d.ts
packages/server/src/routes/index/user.js
packages/server/src/routes/index/user.js.map
packages/server/src/routes/index/users.d.ts
packages/server/src/routes/index/users.js
packages/server/src/routes/index/users.js.map
packages/server/src/routes/oauth2/authorize.d.ts
packages/server/src/routes/oauth2/authorize.js
packages/server/src/routes/oauth2/authorize.js.map
packages/server/src/routes/routes.d.ts
packages/server/src/routes/routes.js
packages/server/src/routes/routes.js.map
packages/server/src/services/MustacheService.d.ts
packages/server/src/services/MustacheService.js
packages/server/src/services/MustacheService.js.map
packages/server/src/tools/db-migrate.d.ts
packages/server/src/tools/db-migrate.js
packages/server/src/tools/db-migrate.js.map
packages/server/src/tools/dbTools.d.ts
packages/server/src/tools/dbTools.js
packages/server/src/tools/dbTools.js.map
packages/server/src/tools/generate-types.d.ts
packages/server/src/tools/generate-types.js
packages/server/src/tools/generate-types.js.map
packages/server/src/utils/TransactionHandler.d.ts
packages/server/src/utils/TransactionHandler.js
packages/server/src/utils/TransactionHandler.js.map
packages/server/src/utils/auth.d.ts
packages/server/src/utils/auth.js
packages/server/src/utils/auth.js.map
packages/server/src/utils/base64.d.ts
packages/server/src/utils/base64.js
packages/server/src/utils/base64.js.map
packages/server/src/utils/cache.d.ts
packages/server/src/utils/cache.js
packages/server/src/utils/cache.js.map
packages/server/src/utils/defaultView.d.ts
packages/server/src/utils/defaultView.js
packages/server/src/utils/defaultView.js.map
packages/server/src/utils/errors.d.ts
packages/server/src/utils/errors.js
packages/server/src/utils/errors.js.map
packages/server/src/utils/htmlUtils.d.ts
packages/server/src/utils/htmlUtils.js
packages/server/src/utils/htmlUtils.js.map
packages/server/src/utils/koaIf.d.ts
packages/server/src/utils/koaIf.js
packages/server/src/utils/koaIf.js.map
packages/server/src/utils/requestUtils.d.ts
packages/server/src/utils/requestUtils.js
packages/server/src/utils/requestUtils.js.map
packages/server/src/utils/routeUtils.d.ts
packages/server/src/utils/routeUtils.js
packages/server/src/utils/routeUtils.js.map
packages/server/src/utils/routeUtils.test.d.ts
packages/server/src/utils/routeUtils.test.js
packages/server/src/utils/routeUtils.test.js.map
packages/server/src/utils/testUtils.d.ts
packages/server/src/utils/testUtils.js
packages/server/src/utils/testUtils.js.map
packages/server/src/utils/testing/testRouters.d.ts
packages/server/src/utils/testing/testRouters.js
packages/server/src/utils/testing/testRouters.js.map
packages/server/src/utils/time.d.ts
packages/server/src/utils/time.js
packages/server/src/utils/time.js.map
packages/server/src/utils/types.d.ts
packages/server/src/utils/types.js
packages/server/src/utils/types.js.map
packages/server/src/utils/uuidgen.d.ts
packages/server/src/utils/uuidgen.js
packages/server/src/utils/uuidgen.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

3
Dockerfile.db Normal file
View File

@@ -0,0 +1,3 @@
FROM postgres:13.1
EXPOSE 5432

71
Dockerfile.server Normal file
View File

@@ -0,0 +1,71 @@
# https://versatile.nl/blog/deploying-lerna-web-apps-with-docker
FROM node:12
RUN apt-get update
RUN apt-get --yes install vim
ARG user=joplin
RUN useradd --create-home --shell /bin/bash $user
USER $user
ENV NODE_ENV development
WORKDIR /home/$user
RUN mkdir /home/$user/logs
# To take advantage of the Docker cache, we first copy all the package.json
# and package-lock.json files, as they rarely change? and then bootstrap
# all the packages.
#
# Note that bootstrapping the packages will run all the postinstall
# scripts, which means that for packages that have such scripts, we need to
# copy all the files.
#
# We can't run boostrap with "--ignore-scripts" because that would
# prevent certain sub-packages, such as sqlite3, from being built
COPY --chown=$user:$user package*.json ./
# Install the root scripts but don't run postinstall (which would bootstrap
# and build TypeScript files, but we don't have the TypeScript files at
# this point)
RUN npm install --ignore-scripts
COPY --chown=$user:$user packages/fork-sax/package*.json ./packages/fork-sax/
COPY --chown=$user:$user packages/lib/package*.json ./packages/lib/
COPY --chown=$user:$user packages/renderer/package*.json ./packages/renderer/
COPY --chown=$user:$user packages/tools/package*.json ./packages/tools/
COPY --chown=$user:$user packages/server/package*.json ./packages/server/
COPY --chown=$user:$user lerna.json .
COPY --chown=$user:$user tsconfig.json .
# The following have postinstall scripts so we need to copy all the files.
# Since they should rarely change this is not an issue
COPY --chown=$user:$user packages/turndown ./packages/turndown
COPY --chown=$user:$user packages/turndown-plugin-gfm ./packages/turndown-plugin-gfm
COPY --chown=$user:$user packages/fork-htmlparser2 ./packages/fork-htmlparser2
RUN ls -la /home/$user
# Then bootstrap only, without compiling the TypeScript files
RUN npm run bootstrap
COPY --chown=$user:$user packages/fork-sax ./packages/fork-sax
COPY --chown=$user:$user packages/lib ./packages/lib
COPY --chown=$user:$user packages/renderer ./packages/renderer
COPY --chown=$user:$user packages/tools ./packages/tools
COPY --chown=$user:$user packages/server ./packages/server
# Finally build everything, in particular the TypeScript files.
RUN npm run build
EXPOSE ${JOPLIN_PORT}
CMD [ "npm", "--prefix", "packages/server", "start" ]

View File

@@ -20,9 +20,9 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
Operating System | Download | Alternative
---|---|---
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.4.19/Joplin-Setup-1.4.19.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.4.19/JoplinPortable.exe'>Portable version</a><br><br>The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.4.19/Joplin-1.4.19.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> | -
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.4.19/Joplin-1.4.19.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | The recommended way is to use the following installation script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh \| bash`
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.5.11/Joplin-Setup-1.5.11.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.5.11/JoplinPortable.exe'>Portable version</a><br><br>The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.5.11/Joplin-1.5.11.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> | -
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.5.11/Joplin-1.5.11.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | The recommended way is to use the following installation script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh \| bash`
## Mobile applications

View File

@@ -0,0 +1,28 @@
# For development, the easiest might be to only start the Postgres container and
# run the app directly with `npm start`. Or use sqlite3.
version: '3'
services:
# app:
# build:
# context: .
# dockerfile: Dockerfile.server-dev
# ports:
# - "22300:22300"
# # volumes:
# # - ./packages/server/:/var/www/joplin/packages/server/
# # - /var/www/joplin/packages/server/node_modules/
db:
build:
context: .
dockerfile: Dockerfile.db
ports:
- "5432:5432"
environment:
# TODO: Considering the database is only exposed to the
# application, and not to the outside world, is there a need to
# pick a secure password?
- POSTGRES_PASSWORD=joplin
- POSTGRES_USER=joplin
- POSTGRES_DB=joplin

40
docker-compose.server.yml Normal file
View File

@@ -0,0 +1,40 @@
version: '3'
services:
app:
environment:
- JOPLIN_BASE_URL=${JOPLIN_BASE_URL}
- JOPLIN_PORT=${JOPLIN_PORT}
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile.server
ports:
- "${JOPLIN_PORT}:${JOPLIN_PORT}"
# volumes:
# # Mount the server directory so that it's possible to edit file
# # while the container is running. However don't mount the
# # node_modules directory which will be specific to the Docker
# # image (eg native modules will be built for Ubuntu, while the
# # container might be running in Windows)
# # https://stackoverflow.com/a/37898591/561309
# - ./packages/server:/home/joplin/packages/server
# - /home/joplin/packages/server/node_modules/
db:
restart: unless-stopped
# By default, the Postgres image saves the data to a Docker volume,
# so it persists whenever the server is restarted using
# `docker-compose up`. Note that it would however be deleted when
# running `docker-compose down`.
build:
context: .
dockerfile: Dockerfile.db
ports:
- "5432:5432"
environment:
# TODO: Considering the database is only exposed to the
# application, and not to the outside world, is there a need to
# pick a secure password?
- POSTGRES_PASSWORD=joplin
- POSTGRES_USER=joplin
- POSTGRES_DB=joplin

File diff suppressed because one or more lines are too long

View File

@@ -150,13 +150,21 @@
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Registers a new content script. Unlike regular plugin code, which runs in a separate process, content scripts run within the main process code
and thus allow improved performances and more customisations in specific cases. It can be used for example to load a Markdown or editor plugin.</p>
<p>Registers a new content script. Unlike regular plugin code, which
runs in a separate process, content scripts run within the main
process code and thus allow improved performances and more
customisations in specific cases. It can be used for example to load
a Markdown or editor plugin.</p>
</div>
<p>Note that registering a content script in itself will do nothing - it will only be loaded in specific cases by the relevant app modules
(eg. the Markdown renderer or the code editor). So it is not a way to inject and run arbitrary code in the app, which for safety and performance reasons is not supported.</p>
<p><a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script">View the renderer demo plugin</a>
<a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script">View the editor demo plugin</a></p>
<p>Note that registering a content script in itself will do nothing -
it will only be loaded in specific cases by the relevant app modules
(eg. the Markdown renderer or the code editor). So it is not a way
to inject and run arbitrary code in the app, which for safety and
performance reasons is not supported.</p>
<ul>
<li><a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script">View the renderer demo plugin</a></li>
<li><a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script">View the editor demo plugin</a></li>
</ul>
</div>
<h4 class="tsd-parameters-title">Parameters</h4>
<ul class="tsd-parameters">

View File

@@ -124,7 +124,7 @@
<p>Gets a global setting value, including app-specific settings and those set by other plugins.</p>
</div>
<p>The list of available settings is not documented yet, but can be found by looking at the source code:</p>
<p><a href="https://github.com/laurent22/joplin/blob/3539a452a359162c461d2849829d2d42973eab50/packages/app-mobile/lib/models/Setting.ts#L142">https://github.com/laurent22/joplin/blob/3539a452a359162c461d2849829d2d42973eab50/packages/app-mobile/lib/models/Setting.ts#L142</a></p>
<p><a href="https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142">https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142</a></p>
</div>
<h4 class="tsd-parameters-title">Parameters</h4>
<ul class="tsd-parameters">

View File

@@ -88,7 +88,8 @@
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Registers a new CodeMirror plugin, which should follow the template below.</p>
<p>Registers a new CodeMirror plugin, which should follow the template
below.</p>
</div>
<pre><code class="language-javascript"><span class="hljs-built_in">module</span>.exports = {
<span class="hljs-attr">default</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">context</span>) </span>{
@@ -98,8 +99,8 @@
},
<span class="hljs-attr">codeMirrorResources</span>: [],
<span class="hljs-attr">codeMirrorOptions</span>: {
<span class="hljs-comment">// ...</span>
},
<span class="hljs-comment">// ...</span>
},
<span class="hljs-attr">assets</span>: {
<span class="hljs-comment">// ...</span>
},
@@ -107,19 +108,42 @@
}
}</code></pre>
<ul>
<li><p>The <code>context</code> argument is currently unused but could be used later on to provide access to your own plugin so that the content script and plugin can communicate.</p>
<li><p>The <code>context</code> argument is currently unused but could be used later
on to provide access to your own plugin so that the content script
and plugin can communicate.</p>
</li>
<li><p>The <code>plugin</code> key is your CodeMirror plugin. This is where you can register new commands with CodeMirror or interact with the CodeMirror instance as needed.</p>
<li><p>The <code>plugin</code> key is your CodeMirror plugin. This is where you can
register new commands with CodeMirror or interact with the
CodeMirror instance as needed.</p>
</li>
<li><p>The <code>codeMirrorResources</code> key is an array of CodeMirror resources that will be loaded and attached to the CodeMirror module. These are made up of addons, keymaps, and modes. For example, for a plugin that want&#39;s to enable clojure highlighting in code blocks. <code>codeMirrorResources</code> would be set to <code>[&#39;mode/clojure/clojure&#39;]</code>.</p>
<li><p>The <code>codeMirrorResources</code> key is an array of CodeMirror resources
that will be loaded and attached to the CodeMirror module. These
are made up of addons, keymaps, and modes. For example, for a
plugin that want&#39;s to enable clojure highlighting in code blocks.
<code>codeMirrorResources</code> would be set to <code>[&#39;mode/clojure/clojure&#39;]</code>.</p>
</li>
<li><p>The <code>codeMirrorOptions</code> key contains all the <a href="https://codemirror.net/doc/manual.html#config">CodeMirror</a> options that will be set or changed by this plugin. New options can alse be declared via <a href="https://codemirror.net/doc/manual.html#defineOption"><code>CodeMirror.defineOption</code></a>, and then have their value set here. For example, a plugin that enables line numbers would set <code>codeMirrorOptions</code> to <code>{&#39;lineNumbers&#39;: true}</code>.</p>
<li><p>The <code>codeMirrorOptions</code> key contains all the
<a href="https://codemirror.net/doc/manual.html#config">CodeMirror</a>
options that will be set or changed by this plugin. New options
can alse be declared via
<a href="https://codemirror.net/doc/manual.html#defineOption"><code>CodeMirror.defineOption</code></a>,
and then have their value set here. For example, a plugin that
enables line numbers would set <code>codeMirrorOptions</code> to
<code>{&#39;lineNumbers&#39;: true}</code>.</p>
</li>
<li><p>Using the <strong>optional</strong> <code>assets</code> key you may specify <strong>only</strong> CSS assets that should be loaded in the rendered HTML document. Check for example the Joplin <a href="https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/lib/joplin-renderer/MdToHtml/rules/mermaid.ts">Mermaid plugin</a> to see how the data should be structured.</p>
<li><p>Using the <strong>optional</strong> <code>assets</code> key you may specify <strong>only</strong> CSS
assets that should be loaded in the rendered HTML document. Check
for example the Joplin [Mermaid
plugin](<a href="https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts">https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts</a>)
to see how the data should be structured.</p>
</li>
</ul>
<p>One of the <code>plugin</code>, <code>codeMirrorResources</code>, or <code>codeMirrorOptions</code> keys must be provided for the plugin to be valid. Having multiple or all provided is also okay.</p>
<p>See the <a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script">demo plugin</a> for an example of all these keys being used in one plugin.</p>
<p>One of the <code>plugin</code>, <code>codeMirrorResources</code>, or <code>codeMirrorOptions</code>
keys must be provided for the plugin to be valid. Having multiple or
all provided is also okay.</p>
<p>See the <a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script">demo
plugin</a>
for an example of all these keys being used in one plugin.</p>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
@@ -130,7 +154,8 @@
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Registers a new Markdown-It plugin, which should follow the template below.</p>
<p>Registers a new Markdown-It plugin, which should follow the template
below.</p>
</div>
<pre><code class="language-javascript"><span class="hljs-built_in">module</span>.exports = {
<span class="hljs-attr">default</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">context</span>) </span>{
@@ -144,15 +169,50 @@
}
}
}</code></pre>
<p>See <a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script">the
demo</a>
for a simple Markdown-it plugin example.</p>
<a href="#exported-members" id="exported-members" style="color: inherit; text-decoration: none;">
<h2>Exported members</h2>
</a>
<ul>
<li><p>The <code>context</code> argument is currently unused but could be used later on to provide access to your own plugin so that the content script and plugin can communicate.</p>
<li><p>The <code>context</code> argument is currently unused but could be used later
on to provide access to your own plugin so that the content script
and plugin can communicate.</p>
</li>
<li><p>The <strong>required</strong> <code>plugin</code> key is the actual Markdown-It plugin - check the <a href="https://github.com/markdown-it/markdown-it">official doc</a> for more information. The <code>options</code> parameter is of type <a href="https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/lib/joplin-renderer/MdToHtml.ts">RuleOptions</a>, which contains a number of options, mostly useful for Joplin&#39;s internal code.</p>
<li><p>The <strong>required</strong> <code>plugin</code> key is the actual Markdown-It plugin -
check the [official
doc](<a href="https://github.com/markdown-it/markdown-it">https://github.com/markdown-it/markdown-it</a>) for more
information. The <code>options</code> parameter is of type
<a href="https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts">RuleOptions</a>,
which contains a number of options, mostly useful for Joplin&#39;s
internal code.</p>
</li>
<li><p>Using the <strong>optional</strong> <code>assets</code> key you may specify assets such as JS or CSS that should be loaded in the rendered HTML document. Check for example the Joplin <a href="https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/lib/joplin-renderer/MdToHtml/rules/mermaid.ts">Mermaid plugin</a> to see how the data should be structured.</p>
<li><p>Using the <strong>optional</strong> <code>assets</code> key you may specify assets such as
JS or CSS that should be loaded in the rendered HTML document.
Check for example the Joplin [Mermaid
plugin](<a href="https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts">https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts</a>)
to see how the data should be structured.</p>
</li>
</ul>
<p>To include a regular Markdown-It plugin, that doesn&#39;t make use of any Joplin-specific features, you would simply create a file such as this:</p>
<a href="#passing-messages-from-the-content-script-to-your-plugin" id="passing-messages-from-the-content-script-to-your-plugin" style="color: inherit; text-decoration: none;">
<h2>Passing messages from the content script to your plugin</h2>
</a>
<p>The application provides the following function to allow executing
commands from the rendered HTML code:</p>
<p><code>webviewApi.executeCommand(commandName, ...args)</code></p>
<p>So you can use this mechanism to pass messages from the note viewer
to your own plugin. To do so you would define a command, using
<code>joplin.commands.register</code>, then you would call this command using
the <code>webviewApi</code> object. See again <a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script">the
demo</a>
to see how this can be done.</p>
<a href="#registering-an-existing-markdown-it-plugin" id="registering-an-existing-markdown-it-plugin" style="color: inherit; text-decoration: none;">
<h2>Registering an existing Markdown-it plugin</h2>
</a>
<p>To include a regular Markdown-It plugin, that doesn&#39;t make use of
any Joplin-specific features, you would simply create a file such as
this:</p>
<pre><code class="language-javascript"><span class="hljs-built_in">module</span>.exports = {
<span class="hljs-attr">default</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">context</span>) </span>{
<span class="hljs-keyword">return</span> {

View File

@@ -75,9 +75,11 @@
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#edit" class="tsd-kind-icon">Edit</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#editorcontextmenu" class="tsd-kind-icon">Editor<wbr>Context<wbr>Menu</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#file" class="tsd-kind-icon">File</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#foldercontextmenu" class="tsd-kind-icon">Folder<wbr>Context<wbr>Menu</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#help" class="tsd-kind-icon">Help</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#note" class="tsd-kind-icon">Note</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#notelistcontextmenu" class="tsd-kind-icon">Note<wbr>List<wbr>Context<wbr>Menu</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#tagcontextmenu" class="tsd-kind-icon">Tag<wbr>Context<wbr>Menu</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#tools" class="tsd-kind-icon">Tools</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="menuitemlocation.html#view" class="tsd-kind-icon">View</a></li>
</ul>
@@ -122,6 +124,22 @@
<aside class="tsd-sources">
</aside>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="foldercontextmenu" class="tsd-anchor"></a>
<h3>Folder<wbr>Context<wbr>Menu</h3>
<div class="tsd-signature tsd-kind-icon">Folder<wbr>Context<wbr>Menu<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;folderContextMenu&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>When a command is called from a folder context menu, the
command will receive the following arguments:</p>
</div>
<ul>
<li><code>folderId:string</code>: ID of the folder that was right-clicked on</li>
</ul>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="help" class="tsd-anchor"></a>
<h3>Help</h3>
@@ -142,6 +160,31 @@
<div class="tsd-signature tsd-kind-icon">Note<wbr>List<wbr>Context<wbr>Menu<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;noteListContextMenu&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>When a command is called from the note list context menu, the
command will receive the following arguments:</p>
</div>
<ul>
<li><code>noteIds:string[]</code>: IDs of the notes that were right-clicked on.</li>
</ul>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="tagcontextmenu" class="tsd-anchor"></a>
<h3>Tag<wbr>Context<wbr>Menu</h3>
<div class="tsd-signature tsd-kind-icon">Tag<wbr>Context<wbr>Menu<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;tagContextMenu&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>When a command is called from a tag context menu, the
command will receive the following arguments:</p>
</div>
<ul>
<li><code>tagId:string</code>: ID of the tag that was right-clicked on</li>
</ul>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="tools" class="tsd-anchor"></a>
@@ -188,6 +231,9 @@
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="menuitemlocation.html#file" class="tsd-kind-icon">File</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="menuitemlocation.html#foldercontextmenu" class="tsd-kind-icon">Folder<wbr>Context<wbr>Menu</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="menuitemlocation.html#help" class="tsd-kind-icon">Help</a>
</li>
@@ -197,6 +243,9 @@
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="menuitemlocation.html#notelistcontextmenu" class="tsd-kind-icon">Note<wbr>List<wbr>Context<wbr>Menu</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="menuitemlocation.html#tagcontextmenu" class="tsd-kind-icon">Tag<wbr>Context<wbr>Menu</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="menuitemlocation.html#tools" class="tsd-kind-icon">Tools</a>
</li>

View File

@@ -133,6 +133,12 @@
<li class="tsd-kind-variable"><a href="globals.html#logger" class="tsd-kind-icon">logger</a></li>
</ul>
</section>
<section class="tsd-index-section ">
<h3>Functions</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-function"><a href="globals.html#iscontextmenuitemlocation" class="tsd-kind-icon">is<wbr>Context<wbr>Menu<wbr>Item<wbr>Location</a></li>
</ul>
</section>
</div>
</section>
</section>
@@ -187,9 +193,11 @@
<div class="lead">
<p>An array of at least one element and at most three elements.</p>
</div>
<p>[0]: Resource name (eg. &quot;notes&quot;, &quot;folders&quot;, &quot;tags&quot;, etc.)
[1]: (Optional) Resource ID.
[2]: (Optional) Resource link.</p>
<ul>
<li><strong>[0]</strong>: Resource name (eg. &quot;notes&quot;, &quot;folders&quot;, &quot;tags&quot;, etc.)</li>
<li><strong>[1]</strong>: (Optional) Resource ID.</li>
<li><strong>[2]</strong>: (Optional) Resource link.</li>
</ul>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-type-alias">
@@ -242,6 +250,33 @@
</aside>
</section>
</section>
<section class="tsd-panel-group tsd-member-group ">
<h2>Functions</h2>
<section class="tsd-panel tsd-member tsd-kind-function">
<a name="iscontextmenuitemlocation" class="tsd-anchor"></a>
<h3>is<wbr>Context<wbr>Menu<wbr>Item<wbr>Location</h3>
<ul class="tsd-signatures tsd-kind-function">
<li class="tsd-signature tsd-kind-icon">is<wbr>Context<wbr>Menu<wbr>Item<wbr>Location<span class="tsd-signature-symbol">(</span>location<span class="tsd-signature-symbol">: </span><a href="enums/menuitemlocation.html" class="tsd-signature-type">MenuItemLocation</a><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">boolean</span></li>
</ul>
<ul class="tsd-descriptions">
<li class="tsd-description">
<aside class="tsd-sources">
</aside>
<h4 class="tsd-parameters-title">Parameters</h4>
<ul class="tsd-parameters">
<li>
<h5>location: <a href="enums/menuitemlocation.html" class="tsd-signature-type">MenuItemLocation</a></h5>
</li>
</ul>
<!-- JOPLINCHANGE
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">boolean</span></h4>
-->
</li>
</ul>
</section>
</section>
</div>
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
<!--
@@ -381,6 +416,9 @@
<li class=" tsd-kind-type-alias">
<a href="globals.html#viewhandle" class="tsd-kind-icon">ViewHandle</a>
</li>
<li class=" tsd-kind-function">
<a href="globals.html#iscontextmenuitemlocation" class="tsd-kind-icon">isContextMenuItemLocation</a>
</li>
</ul>
</nav>
</div>

View File

@@ -206,6 +206,9 @@
<li class=" tsd-kind-type-alias">
<a href="globals.html#viewhandle" class="tsd-kind-icon">ViewHandle</a>
</li>
<li class=" tsd-kind-function">
<a href="globals.html#iscontextmenuitemlocation" class="tsd-kind-icon">isContextMenuItemLocation</a>
</li>
</ul>
</nav>
</div>

View File

@@ -142,7 +142,7 @@
<td>&quot;oneNoteSelected &amp;&amp; !inConflictFolder&quot;</td>
</tr>
</tbody></table>
<p>Currently the supported context variables aren&#39;t documented, but you can <a href="https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/lib/services/commands/stateToWhenClauseContext.ts">find the list here</a>.</p>
<p>Currently the supported context variables aren&#39;t documented, but you can <a href="https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts">find the list here</a>.</p>
<p>Note: Commands are enabled by default unless you use this property.</p>
</div>
</section>

View File

@@ -400,6 +400,46 @@ https://github.com/laurent22/joplin/blob/dev/readme/changelog.md
<div class="main">
<h1>Joplin changelog<a name="joplin-changelog" href="#joplin-changelog" class="heading-anchor">🔗</a></h1>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.5.11">v1.5.11</a> - 2020-12-27T19:54:07Z<a name="v1-5-11-https-github-com-laurent22-joplin-releases-tag-v1-5-11-2020-12-27t19-54-07z" href="#v1-5-11-https-github-com-laurent22-joplin-releases-tag-v1-5-11-2020-12-27t19-54-07z" class="heading-anchor">🔗</a></h2>
<ul>
<li>New: Add support for media players (video, audio and PDF)</li>
<li>New: Add table captions when importing ENEX files</li>
<li>New: Added doc about Rich Text editor and added way to dismiss warning banner</li>
<li>New: MacOS: Notarize application</li>
<li>New: Plugins: Add support for content script asset files, for Markdown-it plugins</li>
<li>New: Plugins: Add support for context menu items on notebooks and tags</li>
<li>New: Plugins: Add support for workspace.onSyncStart event</li>
<li>New: Plugins: Added a way to execute commands from Markdown-it content scripts</li>
<li>Fixed: Fix End key behavior with Codemirror spellcheck (<a href="https://github.com/laurent22/joplin/issues/4215">#4215</a> by Caleb John)</li>
<li>Fixed: Fixed basic search when executing a query in Chinese (<a href="https://github.com/laurent22/joplin/issues/4034">#4034</a> by Naveen M V)</li>
<li>Fixed: Fixed context menu when the UI is zoomed in or out (<a href="https://github.com/laurent22/joplin/issues/4201">#4201</a>)</li>
<li>Fixed: Fixed importing certain code blocks from ENEX</li>
<li>Fixed: Fixed importing ENEX files that contain empty resources</li>
<li>Fixed: Fixed importing ENEX files that contain resources with invalid mime type</li>
<li>Fixed: Fixed issue when searching for text that contains diacritic (<a href="https://github.com/laurent22/joplin/issues/4152">#4152</a>) (<a href="https://github.com/laurent22/joplin/issues/4025">#4025</a> by Roman Musin)</li>
<li>Fixed: Fixed issue with attachment paths being invalid when user has spaces in home directory path.</li>
<li>Fixed: Fixed issue with note not being saved when a column is added or remove from Rich Text editor</li>
<li>Fixed: Fixed issues when importing hidden tables within hidden sections in Enex files</li>
<li>Fixed: Fixed numbered list bug in markdown editor (<a href="https://github.com/laurent22/joplin/issues/4116">#4116</a>) (<a href="https://github.com/laurent22/joplin/issues/3917">#3917</a> by <a href="https://github.com/MichBoi">@MichBoi</a>)</li>
<li>Fixed: Fixed potential crash when watching note files or resources</li>
<li>Fixed: Fixed title input field width on small windows</li>
<li>Fixed: Focus editor after pressing toolbar buttons (<a href="https://github.com/laurent22/joplin/issues/4037">#4037</a>) (<a href="https://github.com/laurent22/joplin/issues/4036">#4036</a> by <a href="https://github.com/CalebJohn">@CalebJohn</a>)</li>
<li>Fixed: Plugins: Fixed disabling plugin files that start with &quot;_&quot;</li>
<li>Fixed: Prevent double paste when using Shift+Ctrl+V (<a href="https://github.com/laurent22/joplin/issues/4243">#4243</a>)</li>
<li>Fixed: Prevents crash when invalid spell checker language is selected, and provide fallback for invalid language codes (<a href="https://github.com/laurent22/joplin/issues/4146">#4146</a>)</li>
<li>Fixed: Register Markdown editor commands with the Keyboard Shortcut editor (<a href="https://github.com/laurent22/joplin/issues/4136">#4136</a>) (<a href="https://github.com/laurent22/joplin/issues/4130">#4130</a> by Caleb John)</li>
<li>Improved: Display Katex parsing errors</li>
<li>Improved: Improved warning banner colors</li>
<li>Improved: Plugins: Commands would not show up in keymap editor when no shortcut was associated with them</li>
<li>Improved: Plugins: Improved note change event handling.</li>
<li>Improved: Removed warning for Markdown editor spell checking</li>
<li>Improved: Restrict auto-detection of links, and added option to toggle linkify (<a href="https://github.com/laurent22/joplin/issues/4205">#4205</a>)</li>
<li>Improved: Rich Text: Do not converts to markdown links URLs that would be linkified</li>
<li>Improved: Translation: Update zh_CN (<a href="https://github.com/laurent22/joplin/issues/4195">#4195</a> by Zhang YANG)</li>
<li>Improved: Update macOS icon for macOS Big Sur</li>
<li>Improved: Update Mermaid: 8.8.1 -&gt; 8.8.4 (<a href="https://github.com/laurent22/joplin/issues/4193">#4193</a> by Helmut K. C. Tessarek)</li>
<li>Improved: Use plugins whenever printing or exporting notes</li>
</ul>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.4.19">v1.4.19</a> - 2020-12-01T11:11:16Z<a name="v1-4-19-https-github-com-laurent22-joplin-releases-tag-v1-4-19-2020-12-01t11-11-16z" href="#v1-4-19-https-github-com-laurent22-joplin-releases-tag-v1-4-19-2020-12-01t11-11-16z" class="heading-anchor">🔗</a></h2>
<ul>
<li>Improved: Disable soft-break by default in Markdown rendering</li>

View File

@@ -400,6 +400,22 @@ https://github.com/laurent22/joplin/blob/dev/readme/changelog_cli.md
<div class="main">
<h1>Joplin terminal app changelog<a name="joplin-terminal-app-changelog" href="#joplin-terminal-app-changelog" class="heading-anchor">🔗</a></h1>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/cli-v1.5.1">cli-v1.5.1</a> - 2020-12-26T00:46:31Z<a name="cli-v1-5-1-https-github-com-laurent22-joplin-releases-tag-cli-v1-5-1-2020-12-26t00-46-31z" href="#cli-v1-5-1-https-github-com-laurent22-joplin-releases-tag-cli-v1-5-1-2020-12-26t00-46-31z" class="heading-anchor">🔗</a></h2>
<ul>
<li>New: Add table captions when importing ENEX files</li>
<li>Improved: Allow exporting conflict notes (#4095)</li>
<li>Improved: Allow lowercase filters when doing search</li>
<li>Improved: Improved error handling when importing ENEX files</li>
<li>Improved: Partially reverts #3975 (link rendering)</li>
<li>Fixed: Fix sorting by title in a case insensitive way</li>
<li>Fixed: Fixed basic search when executing a query in Chinese (#4034 by Naveen M V)</li>
<li>Fixed: Fixed importing ENEX files that contain empty resources</li>
<li>Fixed: Fixed importing ENEX files that contain resources with invalid mime type</li>
<li>Fixed: Fixed importing certain ENEX files that contain invalid dates</li>
<li>Fixed: Fixed importing certain code blocks from ENEX</li>
<li>Fixed: Fixed issue when searching for text that contains diacritic (#4152) (#4025 by Roman Musin)</li>
<li>Fixed: Fixed issues when importing hidden tables within hidden sections in Enex files</li>
</ul>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/cli-v1.4.9">cli-v1.4.9</a> - 2020-11-26T15:00:37Z<a name="cli-v1-4-9-https-github-com-laurent22-joplin-releases-tag-cli-v1-4-9-2020-11-26t15-00-37z" href="#cli-v1-4-9-https-github-com-laurent22-joplin-releases-tag-cli-v1-4-9-2020-11-26t15-00-37z" class="heading-anchor">🔗</a></h2>
<ul>
<li>Improved: Allow exporting conflict notes (#4095)</li>

View File

@@ -411,6 +411,9 @@ https://github.com/laurent22/joplin/blob/dev/readme/faq.md
</ul>
<p>Now try to install again and it should work.</p>
<p>More info there: <a href="https://github.com/electron-userland/electron-builder/issues/4057">https://github.com/electron-userland/electron-builder/issues/4057</a></p>
<h2>How can I pass arguments to the Linux installation script?<a name="how-can-i-pass-arguments-to-the-linux-installation-script" href="#how-can-i-pass-arguments-to-the-linux-installation-script" class="heading-anchor">🔗</a></h2>
<p>You can pass <a href="https://github.com/laurent22/joplin/blob/dev/Joplin_install_and_update.sh#L37">arguments</a> to the installation script by using this command.</p>
<p><code>wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh \| bash -s -- --argument1 --argument2</code></p>
<h2>How can I edit my note in an external text editor?<a name="how-can-i-edit-my-note-in-an-external-text-editor" href="#how-can-i-edit-my-note-in-an-external-text-editor" class="heading-anchor">🔗</a></h2>
<p>The editor command (may include arguments) defines which editor will be used to open a note. If none is provided it will try to auto-detect the default editor. If this does nothing or you want to change it for Joplin, you need to configure it in the Preferences -&gt; Text editor command.</p>
<p>Some example configurations are: (comments after #)</p>

View File

@@ -420,17 +420,17 @@ https://github.com/laurent22/joplin/blob/dev/README.md
<tbody>
<tr>
<td>Windows (32 and 64-bit)</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.4.19/Joplin-Setup-1.4.19.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a></td>
<td>Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.4.19/JoplinPortable.exe'>Portable version</a><br><br>The <a href="https://en.wikipedia.org/wiki/Portable_application">portable application</a> allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called &quot;JoplinProfile&quot; next to the executable file.</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.5.11/Joplin-Setup-1.5.11.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a></td>
<td>Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.5.11/JoplinPortable.exe'>Portable version</a><br><br>The <a href="https://en.wikipedia.org/wiki/Portable_application">portable application</a> allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called &quot;JoplinProfile&quot; next to the executable file.</td>
</tr>
<tr>
<td>macOS</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.4.19/Joplin-1.4.19.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a></td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.5.11/Joplin-1.5.11.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a></td>
<td>-</td>
</tr>
<tr>
<td>Linux</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.4.19/Joplin-1.4.19.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a></td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.5.11/Joplin-1.5.11.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a></td>
<td>The recommended way is to use the following installation script as it will handle the desktop icon too:<br><br> <code>wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh | bash</code></td>
</tr>
</tbody>

View File

@@ -400,6 +400,10 @@ https://github.com/laurent22/joplin/blob/dev/readme/nextcloud_app.md
<div class="main">
<h1>Joplin Web API for Nextcloud<a name="joplin-web-api-for-nextcloud" href="#joplin-web-api-for-nextcloud" class="heading-anchor">🔗</a></h1>
<hr>
<p><strong>IMPORTANT: THIS APPLICATION IS DEPRECATED AND WILL NO LONGER BE SUPPORTED FROM JOPLIN 1.6.x</strong></p>
<p>It is deprecated in favour of <a href="https://discourse.joplinapp.org/t/joplin-web-api-for-nextcloud/4491/72?u=laurent">Joplin Server</a>, so if you are relying on it please do not upgrade to Joplin 1.6.x till you are ready to migrate to Joplin Server or some other alternative.</p>
<hr>
<p><strong>This is a beta feature, not yet completed. More info coming soon!</strong></p>
<p>The app can be downloaded from there: <a href="https://apps.nextcloud.com/apps/joplin">https://apps.nextcloud.com/apps/joplin</a></p>
<p>The Joplin Web API for Nextcloud is a helper application that enables certain features that are not possible otherwise. In particular:</p>

File diff suppressed because it is too large Load Diff

View File

@@ -116,6 +116,7 @@
"**/_vieux/": true,
"**/.DS_Store": true,
"**/*.base64": true,
"**/*~": true,
"**/*.bundle.js": true,
"**/*.eot": true,
"**/*.icns": true,
@@ -309,6 +310,10 @@
"packages/renderer/MdToHtml/rules/sanitize_html.js": true,
"packages/app-mobile/lib/rnInjectedJs/": true,
"packages/app-mobile/lib/sql-extensions/spellfix.so": true,
"packages/server/dist/": true,
"packages/server/db-*.sqlite": true,
"packages/server/test.pid": true,
"packages/server/temp": true,
"packages/generator-joplin/generators/app/templates/api/": true,
"packages/app-mobile/node_modules/": true,
"phpunit.xml": true,

146
package-lock.json generated
View File

@@ -289,6 +289,75 @@
"dedent": "^0.7.0",
"npmlog": "^4.1.2",
"yargs": "^14.2.2"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yargs": {
"version": "14.2.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
"integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"decamelize": "^1.2.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^15.0.1"
}
},
"yargs-parser": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
"integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"@lerna/collect-uncommitted": {
@@ -2183,6 +2252,14 @@
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
},
"dependencies": {
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
}
}
},
"cache-base": {
@@ -10578,80 +10655,11 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"yargs": {
"version": "14.2.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
"integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"decamelize": "^1.2.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^15.0.1"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
}
}
},
"yargs-parser": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
"integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
}

View File

@@ -20,12 +20,16 @@
"buildTranslationsNoTsc": "node packages/tools/build-translation.js",
"buildWebsite": "npm run buildApiDoc && node ./packages/tools/build-website.js && npm run buildPluginDoc",
"clean": "lerna clean -y && lerna run clean",
"circularDependencyCheck": "npx madge --warning --circular --extensions js ./",
"generateDatabaseTypes": "node packages/tools/generate-database-types",
"linkChecker": "linkchecker https://joplinapp.org",
"linter-ci": "./node_modules/.bin/eslint --resolve-plugins-relative-to . --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter-precommit": "./node_modules/.bin/eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter": "./node_modules/.bin/eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"postinstall": "lerna bootstrap --no-ci && npm run tsc",
"bootstrap": "lerna bootstrap --no-ci",
"bootstrapIgnoreScripts": "lerna bootstrap --ignore-scripts --no-ci",
"postinstall": "npm run bootstrap --no-ci && npm run build",
"build": "lerna run build && npm run tsc",
"publishAll": "git pull && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"releaseAndroid": "node packages/tools/release-android.js",
"releaseCli": "node packages/tools/release-cli.js",
@@ -39,7 +43,8 @@
"updateIgnored": "gulp updateIgnoredTypeScriptBuild",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"watch": "lerna run watch --stream --parallel",
"i": "lerna add --no-bootstrap --scope"
"i": "lerna add --no-bootstrap --scope",
"server-start-dev": "docker-compose --file docker-compose.server-dev.yml up"
},
"husky": {
"hooks": {

View File

@@ -31,7 +31,7 @@
],
"owner": "Laurent Cozic"
},
"version": "1.5.1",
"version": "1.6.0",
"bin": {
"joplin": "./main.js"
},

View File

@@ -1,7 +1,7 @@
import Setting from '@joplin/lib/models/Setting';
import { allNotesFolders, remoteNotesAndFolders, localNotesFoldersSameAsRemote } from './test-utils-synchronizer';
const { syncTargetName, synchronizerStart, setupDatabaseAndSynchronizer, synchronizer, sleep, switchClient, syncTargetId, fileApi } = require('./test-utils.js');
const { syncTargetName, afterAllCleanUp, synchronizerStart, setupDatabaseAndSynchronizer, synchronizer, sleep, switchClient, syncTargetId, fileApi } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const BaseItem = require('@joplin/lib/models/BaseItem.js');
@@ -16,6 +16,10 @@ describe('Synchronizer.basics', function() {
done();
});
afterAll(async () => {
await afterAllCleanUp();
});
it('should create remote items', (async () => {
const folder = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'un', parent_id: folder.id });
@@ -123,10 +127,6 @@ describe('Synchronizer.basics', function() {
}));
it('should delete local notes', (async () => {
// For these tests we pass the context around for each user. This is to make sure that the "deletedItemsProcessed"
// property of the basicDelta() function is cleared properly at the end of a sync operation. If it is not cleared
// it means items will no longer be deleted locally via sync.
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', parent_id: folder1.id });
const note2 = await Note.save({ title: 'deux', parent_id: folder1.id });

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -1,2 +1,3 @@
dist/*
node_modules/
*.jpl

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -20,4 +20,4 @@
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
}
}
}

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -208,7 +208,7 @@ export enum MenuItemLocation {
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
@@ -223,7 +223,7 @@ export enum MenuItemLocation {
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
@@ -243,7 +243,7 @@ export enum MenuItemLocation {
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location:MenuItemLocation):boolean {
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,

View File

@@ -15,6 +15,7 @@ import KeychainServiceDriver from '@joplin/lib/services/keychain/KeychainService
import KeychainServiceDriverDummy from '@joplin/lib/services/keychain/KeychainServiceDriver.dummy';
import PluginRunner from '../app/services/plugins/PluginRunner';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import FileApiDriverJoplinServer from '@joplin/lib/file-api-driver-joplinServer';
const fs = require('fs-extra');
const { JoplinDatabase } = require('@joplin/lib/joplin-database.js');
@@ -43,12 +44,14 @@ const SyncTargetOneDrive = require('@joplin/lib/SyncTargetOneDrive.js');
const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js');
const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
const SyncTargetJoplinServer = require('@joplin/lib/SyncTargetJoplinServer').default;
const EncryptionService = require('@joplin/lib/services/EncryptionService.js');
const DecryptionWorker = require('@joplin/lib/services/DecryptionWorker.js');
const RevisionService = require('@joplin/lib/services/RevisionService.js');
const ResourceFetcher = require('@joplin/lib/services/ResourceFetcher.js');
const WebDavApi = require('@joplin/lib/WebDavApi');
const DropboxApi = require('@joplin/lib/DropboxApi');
const JoplinServerApi = require('@joplin/lib/JoplinServerApi2').default;
const { OneDriveApi } = require('@joplin/lib/onedrive-api');
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
const md5 = require('md5');
@@ -116,6 +119,7 @@ SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
SyncTargetRegistry.addClass(SyncTargetDropbox);
SyncTargetRegistry.addClass(SyncTargetAmazonS3);
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
let syncTargetName_ = '';
let syncTargetId_: number = null;
@@ -132,7 +136,7 @@ function setSyncTargetName(name: string) {
syncTargetName_ = name;
syncTargetId_ = SyncTargetRegistry.nameToId(syncTargetName_);
sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 100;// 400;
isNetworkSyncTarget_ = ['nextcloud', 'dropbox', 'onedrive', 'amazon_s3'].includes(syncTargetName_);
isNetworkSyncTarget_ = ['nextcloud', 'dropbox', 'onedrive', 'amazon_s3', 'joplinServer'].includes(syncTargetName_);
synchronizers_ = [];
return previousName;
}
@@ -142,6 +146,7 @@ setSyncTargetName('memory');
// setSyncTargetName('dropbox');
// setSyncTargetName('onedrive');
// setSyncTargetName('amazon_s3');
// setSyncTargetName('joplinServer');
// console.info(`Testing with sync target: ${syncTargetName_}`);
@@ -214,6 +219,16 @@ async function afterEachCleanUp() {
KeymapService.destroyInstance();
}
async function afterAllCleanUp() {
if (fileApi()) {
try {
await fileApi().clearRoot();
} catch (error) {
console.warn('Could not clear sync target root:', error);
}
}
}
async function switchClient(id: number, options: any = null) {
options = Object.assign({}, { keychainEnabled: false }, options);
@@ -346,7 +361,7 @@ async function setupDatabaseAndSynchronizer(id: number, options: any = null) {
if (!synchronizers_[id]) {
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
const syncTarget = new SyncTargetClass(db(id));
await initFileApi();
await initFileApi(suiteName_);
syncTarget.setFileApi(fileApi());
syncTarget.setLogger(logger);
synchronizers_[id] = await syncTarget.synchronizer();
@@ -361,6 +376,7 @@ async function setupDatabaseAndSynchronizer(id: number, options: any = null) {
resourceFetchers_[id] = new ResourceFetcher(() => { return synchronizers_[id].api(); });
kvStores_[id] = new KvStore();
await fileApi().initialize();
await fileApi().clearRoot();
}
@@ -440,7 +456,7 @@ async function loadEncryptionMasterKey(id: number = null, useExisting = false) {
return masterKey;
}
async function initFileApi() {
async function initFileApi(suiteName: string) {
if (fileApis_[syncTargetId_]) return;
let fileApi = null;
@@ -482,7 +498,6 @@ async function initFileApi() {
if (!process.argv.includes('--runInBand')) {
throw new Error('OneDrive tests must be run sequentially, with the --runInBand arg. eg `npm test -- --runInBand`');
}
const { parameters, setEnvOverride } = require('@joplin/lib/parameters.js');
@@ -506,6 +521,16 @@ async function initFileApi() {
if (!amazonS3Creds || !amazonS3Creds.accessKeyId) throw new Error(`AWS auth JSON missing in ${amazonS3CredsPath} format should be: { "accessKeyId": "", "secretAccessKey": "", "bucket": "mybucket"}`);
const api = new S3({ accessKeyId: amazonS3Creds.accessKeyId, secretAccessKey: amazonS3Creds.secretAccessKey, s3UseArnRegion: true });
fileApi = new FileApi('', new FileApiDriverAmazonS3(api, amazonS3Creds.bucket));
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('joplinServer')) {
// Note that to test the API in parallel mode, you need to use Postgres
// as database, as the SQLite database is not reliable when being
// read/write from multiple processes at the same time.
const api = new JoplinServerApi({
baseUrl: () => 'http://localhost:22300',
username: () => 'admin@localhost',
password: () => 'admin',
});
fileApi = new FileApi(`root:/Apps/Joplin-${suiteName}`, new FileApiDriverJoplinServer(api));
}
fileApi.setLogger(logger);
@@ -743,18 +768,18 @@ class TestApp extends BaseApplication {
private middlewareCalls_: any[];
private logger_: LoggerWrapper;
constructor(hasGui = true) {
public constructor(hasGui = true) {
super();
this.hasGui_ = hasGui;
this.middlewareCalls_ = [];
this.logger_ = super.logger();
}
hasGui() {
public hasGui() {
return this.hasGui_;
}
async start(argv: any[]) {
public async start(argv: any[]) {
this.logger_.info('Test app starting...');
if (!argv.includes('--profile')) {
@@ -775,7 +800,7 @@ class TestApp extends BaseApplication {
this.logger_.info('Test app started...');
}
async generalMiddleware(store: any, next: any, action: any) {
public async generalMiddleware(store: any, next: any, action: any) {
this.middlewareCalls_.push(true);
try {
await super.generalMiddleware(store, next, action);
@@ -784,7 +809,7 @@ class TestApp extends BaseApplication {
}
}
async wait() {
public async wait() {
return new Promise((resolve) => {
const iid = shim.setInterval(() => {
if (!this.middlewareCalls_.length) {
@@ -795,11 +820,11 @@ class TestApp extends BaseApplication {
});
}
async profileDir() {
public async profileDir() {
return Setting.value('profileDir');
}
async destroy() {
public async destroy() {
this.logger_.info('Test app stopping...');
await this.wait();
await ItemChange.waitForAllSaved();
@@ -809,4 +834,4 @@ class TestApp extends BaseApplication {
}
}
module.exports = { exportDir, newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
module.exports = { afterAllCleanUp, exportDir, newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.5.11",
"version": "1.6.0",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

@@ -139,7 +139,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097614
versionName "1.5.1"
versionName "1.6.0"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -344,7 +344,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 10.5.1;
MARKETING_VERSION = 10.6.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -370,7 +370,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 10.5.1;
MARKETING_VERSION = 10.6.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "1.5.2",
"version": "1.5.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "1.5.2",
"version": "1.6.0",
"description": "Scaffolds out a new Joplin plugin",
"homepage": "https://joplinapp.org",
"author": {
@@ -24,5 +24,6 @@
"yosay": "^2.0.1"
},
"repository": "https://github.com/laurent22/generator-joplin",
"license": "MIT"
}
"license": "MIT",
"private": true
}

View File

@@ -6,6 +6,7 @@ import reducer from './reducer';
import KeychainServiceDriver from './services/keychain/KeychainServiceDriver.node';
import { _, setLocale } from './locale';
import KvStore from './services/KvStore';
import SyncTargetJoplinServer from './SyncTargetJoplinServer';
const { createStore, applyMiddleware } = require('redux');
const { defaultState, stateUtils } = require('./reducer');
@@ -681,6 +682,7 @@ export default class BaseApplication {
SyncTargetRegistry.addClass(SyncTargetWebDAV);
SyncTargetRegistry.addClass(SyncTargetDropbox);
SyncTargetRegistry.addClass(SyncTargetAmazonS3);
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
try {
await shim.fsDriver().remove(tempDir);

View File

@@ -0,0 +1,174 @@
import shim from './shim';
const { rtrimSlashes } = require('./path-utils.js');
const JoplinError = require('./JoplinError');
const { stringify } = require('query-string');
interface Options {
baseUrl(): string;
username(): string;
password(): string;
}
enum ExecOptionsResponseFormat {
Json = 'json',
Text = 'text',
}
enum ExecOptionsTarget {
String = 'string',
File = 'file',
}
interface ExecOptions {
responseFormat?: ExecOptionsResponseFormat;
target?: ExecOptionsTarget;
path?: string;
source?: string;
}
export default class JoplinServerApi {
private options_: Options;
private session_: any;
public constructor(options: Options) {
this.options_ = options;
}
private baseUrl() {
return rtrimSlashes(this.options_.baseUrl());
}
private async session() {
// TODO: handle invalid session
if (this.session_) return this.session_;
this.session_ = await this.exec('POST', 'api/sessions', null, {
email: this.options_.username(),
password: this.options_.password(),
});
return this.session_;
}
private async sessionId() {
const session = await this.session();
return session ? session.id : '';
}
// private requestToCurl_(url: string, options: any) {
// const output = [];
// output.push('curl');
// output.push('-v');
// if (options.method) output.push(`-X ${options.method}`);
// if (options.headers) {
// for (const n in options.headers) {
// if (!options.headers.hasOwnProperty(n)) continue;
// output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
// }
// }
// if (options.body) output.push(`${'--data ' + '\''}${JSON.stringify(options.body)}'`);
// output.push(url);
// return output.join(' ');
// }
public async exec(method: string, path: string = '', query: Record<string, any> = null, body: any = null, headers: any = null, options: ExecOptions = null) {
if (headers === null) headers = {};
if (options === null) options = {};
if (!options.responseFormat) options.responseFormat = ExecOptionsResponseFormat.Json;
if (!options.target) options.target = ExecOptionsTarget.String;
let sessionId = '';
if (path !== 'api/sessions' && !sessionId) {
sessionId = await this.sessionId();
}
if (sessionId) headers['X-API-AUTH'] = sessionId;
const fetchOptions: any = {};
fetchOptions.headers = headers;
fetchOptions.method = method;
if (options.path) fetchOptions.path = options.path;
if (body) {
if (typeof body === 'object') {
fetchOptions.body = JSON.stringify(body);
fetchOptions.headers['Content-Type'] = 'application/json';
} else {
fetchOptions.body = body;
}
fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(fetchOptions.body)}`;
}
let url = `${this.baseUrl()}/${path}`;
if (query) {
url += url.indexOf('?') < 0 ? '?' : '&';
url += stringify(query);
}
let response: any = null;
// console.info('Joplin API Call', `${method} ${url}`, headers, options);
// console.info(this.requestToCurl_(url, fetchOptions));
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
if (fetchOptions.path) {
const fileStat = await shim.fsDriver().stat(fetchOptions.path);
if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`;
}
response = await shim.uploadBlob(url, fetchOptions);
} else if (options.target == 'string') {
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
response = await shim.fetch(url, fetchOptions);
} else {
// file
response = await shim.fetchBlob(url, fetchOptions);
}
const responseText = await response.text();
// console.info('Joplin API Response', responseText);
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
const newError = (message: string, code: number = 0) => {
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
const shortResponseText = (`${responseText}`).substr(0, 1024);
return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
};
let responseJson_: any = null;
const loadResponseJson = async () => {
if (!responseText) return null;
if (responseJson_) return responseJson_;
responseJson_ = JSON.parse(responseText);
if (!responseJson_) throw newError('Cannot parse JSON response', response.status);
return responseJson_;
};
if (!response.ok) {
if (options.target === 'file') throw newError('fetchBlob error', response.status);
let json = null;
try {
json = await loadResponseJson();
} catch (error) {
// Just send back the plain text in newErro()
}
if (json && json.message) {
throw newError(`${json.message}`, response.status);
}
throw newError('Unknown error', response.status);
}
if (options.responseFormat === 'text') return responseText;
const output = await loadResponseJson();
return output;
}
}

View File

@@ -1,6 +1,7 @@
const moment = require('moment');
const time = require('./time').default;
const { FsDriverDummy } = require('./fs-driver-dummy.js');
const { sprintf } = require('sprintf-js');
export enum TargetType {
Database = 'database',
@@ -24,6 +25,12 @@ interface Target {
prefix?: string;
path?: string;
source?: string;
// Default message format
format?: string;
// If specified, will use this as format if it's an info message
formatInfo?: string;
}
export interface LoggerWrapper {
@@ -173,9 +180,25 @@ class Logger {
if (level == LogLevel.Warn) fn = 'warn';
if (level == LogLevel.Info) fn = 'info';
const consoleObj = target.console ? target.console : console;
const prefixItems = [moment().format('HH:mm:ss')];
if (targetPrefix) prefixItems.push(targetPrefix);
const items = [`${prefixItems.join(': ')}:`].concat(...object);
let items:any[] = [];
if (target.format) {
const format = level === LogLevel.Info && target.formatInfo ? target.formatInfo : target.format;
const s = sprintf(format, {
date_time: moment().format('YYYY-MM-DD HH:mm:ss'),
level: Logger.levelIdToString(level),
prefix: targetPrefix || '',
message: '',
});
items = [s.trim()].concat(...object);
} else {
const prefixItems = [moment().format('HH:mm:ss')];
if (targetPrefix) prefixItems.push(targetPrefix);
items = [`${prefixItems.join(': ')}:`].concat(...object);
}
consoleObj[fn](...items);
} else if (target.type == 'file') {
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');

View File

@@ -0,0 +1,91 @@
import FileApiDriverJoplinServer from './file-api-driver-joplinServer';
import Setting from './models/Setting';
import Synchronizer from './Synchronizer';
import { _ } from './locale.js';
import JoplinServerApi from './JoplinServerApi2';
const BaseSyncTarget = require('./BaseSyncTarget.js');
const { FileApi } = require('./file-api.js');
interface FileApiOptions {
path(): string;
username(): string;
password(): string;
directory(): string;
}
export default class SyncTargetJoplinServer extends BaseSyncTarget {
static id() {
return 9;
}
static supportsConfigCheck() {
return true;
}
static targetName() {
return 'joplinServer';
}
static label() {
return _('Joplin Server');
}
async isAuthenticated() {
return true;
}
static async newFileApi_(options: FileApiOptions) {
const apiOptions = {
baseUrl: () => options.path(),
username: () => options.username(),
password: () => options.password(),
};
const api = new JoplinServerApi(apiOptions);
const driver = new FileApiDriverJoplinServer(api);
const fileApi = new FileApi(() => `root:/${options.directory()}`, driver);
fileApi.setSyncTargetId(this.id());
await fileApi.initialize();
return fileApi;
}
static async checkConfig(options: FileApiOptions) {
const fileApi = await SyncTargetJoplinServer.newFileApi_(options);
fileApi.requestRepeatCount_ = 0;
const output = {
ok: false,
errorMessage: '',
};
try {
const result = await fileApi.stat('');
if (!result) throw new Error(`Sync directory not found: "${options.directory()}" on server "${options.path()}"`);
output.ok = true;
} catch (error) {
output.errorMessage = error.message;
if (error.code) output.errorMessage += ` (Code ${error.code})`;
}
return output;
}
async initFileApi() {
const fileApi = await SyncTargetJoplinServer.newFileApi_({
path: () => Setting.value('sync.9.path'),
username: () => Setting.value('sync.9.username'),
password: () => Setting.value('sync.9.password'),
directory: () => Setting.value('sync.9.directory'),
});
fileApi.setLogger(this.logger());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}

View File

@@ -336,6 +336,7 @@ export default class Synchronizer {
let syncLock = null;
try {
await this.api().initialize();
this.api().setTempDirName(Dirnames.Temp);
try {

View File

@@ -0,0 +1,196 @@
import JoplinServerApi from './JoplinServerApi2';
const { dirname, basename } = require('./path-utils');
function removeTrailingColon(path: string) {
if (!path || !path.length) return '';
if (path[path.length - 1] === ':') return path.substr(0, path.length - 1);
return path;
}
// All input paths should be in the format: "SPECIAL_DIR:/path/to/file"
// The trailing colon must not be included as it's automatically added
// when doing the API call.
// Only supported special dir at the moment is "root"
export default class FileApiDriverJoplinServer {
private api_: JoplinServerApi;
public constructor(api: JoplinServerApi) {
this.api_ = api;
}
public async initialize(basePath: string) {
const pieces = removeTrailingColon(basePath).split('/');
if (!pieces.length) return;
let parent = pieces.splice(0, 1)[0];
for (const p of pieces) {
// Syncing with the root, which is ok, and in that
// case there's no sub-dir to create.
if (!p && pieces.length === 1) return;
const subPath = `${parent}/${p}`;
await this.mkdir(subPath);
parent = subPath;
}
}
public api() {
return this.api_;
}
public requestRepeatCount() {
return 3;
}
private metadataToStat_(md: any, path: string, isDeleted: boolean = false) {
const output = {
path: path,
updated_time: md.updated_time,
isDir: !!md.is_directory,
isDeleted: isDeleted,
};
// TODO - HANDLE DELETED
// if (md['.tag'] === 'deleted') output.isDeleted = true;
return output;
}
private metadataToStats_(mds: any[]) {
const output = [];
for (let i = 0; i < mds.length; i++) {
output.push(this.metadataToStat_(mds[i], mds[i].name));
}
return output;
}
private apiFilePath_(p: string) {
if (p !== 'root') p += ':';
return `api/files/${p}`;
}
public async stat(path: string) {
try {
const response = await this.api().exec('GET', this.apiFilePath_(path));
return this.metadataToStat_(response, path);
} catch (error) {
if (error.code === 404) return null;
throw error;
}
}
public async delta(path: string, options: any) {
const context = options ? options.context : null;
let cursor = context ? context.cursor : null;
while (true) {
try {
const query = cursor ? { cursor } : {};
const response = await this.api().exec('GET', `${this.apiFilePath_(path)}/delta`, query);
const stats = response.items.map((item: any) => {
return this.metadataToStat_(item.item, item.item.name, item.type === 3);
});
const output = {
items: stats,
hasMore: response.has_more,
context: { cursor: response.cursor },
};
return output;
} catch (error) {
// If there's an error related to an invalid cursor, clear the cursor and retry.
if (cursor && error.code === 'resyncRequired') {
cursor = null;
continue;
}
throw error;
}
}
}
public async list(path: string, options: any = null) {
options = {
context: null,
...options,
};
const query = options.context?.cursor ? { cursor: options.context.cursor } : null;
const results = await this.api().exec('GET', `${this.apiFilePath_(path)}/children`, query);
const newContext: any = {};
if (results.cursor) newContext.cursor = results.cursor;
return {
items: this.metadataToStats_(results.items),
hasMore: results.has_more,
context: newContext,
} as any;
}
public async get(path: string, options: any) {
if (!options) options = {};
if (!options.responseFormat) options.responseFormat = 'text';
try {
const response = await this.api().exec('GET', `${this.apiFilePath_(path)}/content`, null, null, null, options);
return response;
} catch (error) {
if (error.code !== 404) throw error;
return null;
}
}
private parentPath_(path: string) {
let output = dirname(path);
// This is the root or a special folder
if (output.split('/').length === 1) {
output = output.substr(0, output.length - 1);
}
return output;
}
private basename_(path: string) {
return basename(path);
}
public async mkdir(path: string) {
const parentPath = this.parentPath_(path);
const filename = this.basename_(path);
try {
const response = await this.api().exec('POST', `${this.apiFilePath_(parentPath)}/children`, null, {
name: filename,
is_directory: 1,
});
return response;
} catch (error) {
// 409 is OK - directory already exists
if (error.code !== 409) throw error;
}
}
public async put(path: string, content: any, options: any = null) {
return this.api().exec('PUT', `${this.apiFilePath_(path)}/content`, null, content, {
'Content-Type': 'application/octet-stream',
}, options);
}
public async delete(path: string) {
return this.api().exec('DELETE', this.apiFilePath_(path));
}
public format() {
throw new Error('Not supported');
}
public async clearRoot(path: string) {
await this.delete(path);
await this.mkdir(path);
}
}

View File

@@ -8,6 +8,8 @@ const time = require('./time').default;
const { sprintf } = require('sprintf-js');
const Mutex = require('async-mutex').Mutex;
const logger = Logger.create('FileApi');
function requestCanBeRepeated(error) {
const errorCode = typeof error === 'object' && error.code ? error.code : null;
@@ -61,8 +63,14 @@ class FileApi {
this.remoteDateOffset_ = 0;
this.remoteDateNextCheckTime_ = 0;
this.remoteDateMutex_ = new Mutex();
this.initialized_ = false;
}
async initialize() {
if (this.initialized_) return;
this.initialized_ = true;
if (this.driver_.initialize) return this.driver_.initialize(this.fullPath_(''));
}
async fetchRemoteDateOffset_() {
const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`;
@@ -108,7 +116,7 @@ class FileApi {
this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000;
}
} catch (error) {
this.logger().warn('Could not retrieve remote date - defaulting to device date:', error);
logger.warn('Could not retrieve remote date - defaulting to device date:', error);
this.remoteDateOffset_ = 0;
this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000;
} finally {
@@ -137,7 +145,7 @@ class FileApi {
}
baseDir() {
return this.baseDir_;
return typeof this.baseDir_ === 'function' ? this.baseDir_() : this.baseDir_;
}
tempDirName() {
@@ -191,7 +199,7 @@ class FileApi {
if (!('includeDirs' in options)) options.includeDirs = true;
if (!('syncItemsOnly' in options)) options.syncItemsOnly = false;
this.logger().debug(`list ${this.baseDir()}`);
logger.debug(`list ${this.baseDir()}`);
const result = await tryAndRepeat(() => this.driver_.list(this.fullPath_(path), options), this.requestRepeatCount());
@@ -216,18 +224,18 @@ class FileApi {
// Deprectated
setTimestamp(path, timestampMs) {
this.logger().debug(`setTimestamp ${this.fullPath_(path)}`);
logger.debug(`setTimestamp ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath_(path), timestampMs), this.requestRepeatCount());
// return this.driver_.setTimestamp(this.fullPath_(path), timestampMs);
}
mkdir(path) {
this.logger().debug(`mkdir ${this.fullPath_(path)}`);
logger.debug(`mkdir ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.mkdir(this.fullPath_(path)), this.requestRepeatCount());
}
async stat(path) {
this.logger().debug(`stat ${this.fullPath_(path)}`);
logger.debug(`stat ${this.fullPath_(path)}`);
const output = await tryAndRepeat(() => this.driver_.stat(this.fullPath_(path)), this.requestRepeatCount());
@@ -246,12 +254,12 @@ class FileApi {
get(path, options = null) {
if (!options) options = {};
if (!options.encoding) options.encoding = 'utf8';
this.logger().debug(`get ${this.fullPath_(path)}`);
logger.debug(`get ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.get(this.fullPath_(path), options), this.requestRepeatCount());
}
async put(path, content, options = null) {
this.logger().debug(`put ${this.fullPath_(path)}`, options);
logger.debug(`put ${this.fullPath_(path)}`, options);
if (options && options.source === 'file') {
if (!(await this.fsDriver().exists(options.path))) throw new JoplinError(`File not found: ${options.path}`, 'fileNotFound');
@@ -261,13 +269,13 @@ class FileApi {
}
delete(path) {
this.logger().debug(`delete ${this.fullPath_(path)}`);
logger.debug(`delete ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.delete(this.fullPath_(path)), this.requestRepeatCount());
}
// Deprectated
move(oldPath, newPath) {
this.logger().debug(`move ${this.fullPath_(oldPath)} => ${this.fullPath_(newPath)}`);
logger.debug(`move ${this.fullPath_(oldPath)} => ${this.fullPath_(newPath)}`);
return tryAndRepeat(() => this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath)), this.requestRepeatCount());
}
@@ -281,7 +289,7 @@ class FileApi {
}
delta(path, options = null) {
this.logger().debug(`delta ${this.fullPath_(path)}`);
logger.debug(`delta ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.delta(this.fullPath_(path), options), this.requestRepeatCount());
}
}

View File

@@ -624,7 +624,7 @@ function enexXmlToMdArray(stream, resources) {
section.lines.push(BLOCK_OPEN);
if (!state.lists.length) {
displaySaxWarning(this, 'Found <li> tag without being inside a list');
// return;
return;
}
const container = state.lists[state.lists.length - 1];

View File

@@ -1,5 +1,6 @@
import shim from '../shim';
import { _, supportedLocalesToLanguages, defaultLocale } from '../locale';
import { ltrimSlashes } from '../path-utils';
const BaseModel = require('../BaseModel').default;
const { Database } = require('../database.js');
const SyncTargetRegistry = require('../SyncTargetRegistry.js');
@@ -309,6 +310,52 @@ class Setting extends BaseModel {
secure: true,
},
'sync.9.path': {
value: '',
type: SettingItemType.String,
section: 'sync',
show: (settings: any) => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('joplinServer');
},
public: true,
label: () => _('Joplin Server URL'),
description: () => emptyDirWarning,
},
'sync.9.directory': {
value: 'Apps/Joplin',
type: SettingItemType.String,
section: 'sync',
show: (settings: any) => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('joplinServer');
},
filter: value => {
return value ? ltrimSlashes(rtrimSlashes(value)) : '';
},
public: true,
label: () => _('Joplin Server Directory'),
},
'sync.9.username': {
value: '',
type: SettingItemType.String,
section: 'sync',
show: (settings: any) => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('joplinServer');
},
public: true,
label: () => _('Joplin Server username'),
},
'sync.9.password': {
value: '',
type: SettingItemType.String,
section: 'sync',
show: (settings: any) => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('joplinServer');
},
public: true,
label: () => _('Joplin Server password'),
secure: true,
},
'sync.5.syncTargets': { value: {}, type: SettingItemType.Object, public: false },
'sync.resourceDownloadMode': {
@@ -333,6 +380,7 @@ class Setting extends BaseModel {
'sync.3.auth': { value: '', type: SettingItemType.String, public: false },
'sync.4.auth': { value: '', type: SettingItemType.String, public: false },
'sync.7.auth': { value: '', type: SettingItemType.String, public: false },
'sync.9.auth': { value: '', type: SettingItemType.String, public: false },
'sync.1.context': { value: '', type: SettingItemType.String, public: false },
'sync.2.context': { value: '', type: SettingItemType.String, public: false },
'sync.3.context': { value: '', type: SettingItemType.String, public: false },
@@ -341,6 +389,7 @@ class Setting extends BaseModel {
'sync.6.context': { value: '', type: SettingItemType.String, public: false },
'sync.7.context': { value: '', type: SettingItemType.String, public: false },
'sync.8.context': { value: '', type: SettingItemType.String, public: false },
'sync.9.context': { value: '', type: SettingItemType.String, public: false },
'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },

View File

@@ -181,7 +181,7 @@ const shim = {
throw new Error('Not implemented');
},
uploadBlob: () => {
uploadBlob: (_url: string, _options: any) => {
throw new Error('Not implemented');
},

View File

@@ -109,7 +109,7 @@ class Time {
}
msleep(ms: number) {
return new Promise((resolve) => {
return new Promise((resolve: Function) => {
shim.setTimeout(() => {
resolve();
}, ms);

View File

@@ -1,6 +1,11 @@
const createUuidV4 = require('uuid/v4');
const { customAlphabet } = require('nanoid/non-secure');
// https://zelark.github.io/nano-id-cc/
// https://security.stackexchange.com/a/41749/1873
// > On the other hand, 128 bits (between 21 and 22 characters
// > alphanumeric) is beyond the reach of brute-force attacks pretty much
// > indefinitely
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 22);
export default {

9
packages/server/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
node_modules/
dist
db.sqlite
db-*.sqlite
*.sqlite-journal
*.pid
logs/
tests/temp/
temp/

66
packages/server/README.md Normal file
View File

@@ -0,0 +1,66 @@
# Installing
## Configuration
First copy `.env-sample` to `.env` and edit the values in there:
- `JOPLIN_BASE_URL`: This is the base public URL where the service will be running. For example, if you want it to run from `https://example.com/joplin`, this is what you should set the URL to. The base URL can include the port.
- `JOPLIN_PORT`: The local port on which the Docker container will listen. You would typically map this port to 443 (TLS) with a reverse proxy.
## Install application
```shell
git clone https://github.com/laurent22/joplin
cd joplin
npm install
docker-compose --file docker-compose.server.yml up --detach
```
This will start the server, which will listen on port **22300** on **localhost**.
Due to the restart policy defined in the docker-compose file, the server will be restarted automatically whenever the host reboots.
## Setup reverse proxy
You will then need to expose this server to the internet by setting up a reverse proxy, and that will depend on how your server is currently configured, and whether you already have Nginx or Apache running:
- [Apache Reverse Proxy](https://httpd.apache.org/docs/current/mod/mod_proxy.html)
- [Nginx Reverse Proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
## Setup admin user
For the following instructions, we'll assume that the Joplin server is running on `https://example.com/joplin`.
By default, the instance will be setup with an admin user with email **admin@localhost** and password **admin** and you should change this by opening the admin UI. To do so, open `https://example.com/joplin/login`. From there, go to Profile and change the admin password.
## Setup a user for sync
While the admin user can be used for synchronisation, it is recommended to create a separate non-admin user for it. To do, open the admin UI and navigate to the Users page - from there you can create a new user.
Once this is done, you can use the email and password you specified to sync this user account with your Joplin clients.
## Checking the logs
Checking the log can be done the standard Docker way:
```shell
docker-compose --file docker-compose.server.yml logs
```
# Set up for development
## Setting up the database
### SQLite
The server supports SQLite for development and test units. To use it, open `src/config-dev.ts` and uncomment the sqlite3 config.
### PostgreSQL
It's best to use PostgreSQL as this is what is used in production, however it requires Docker.
To use it, from the monorepo root, run `docker-compose --file docker-compose.server-dev.yml up`, which will start the PostgreSQL database.
## Starting the server
From `packages/server`, run `npm run start-dev`

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,14 @@
module.exports = {
testMatch: [
'**/*.test.js',
],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/assets/',
],
testEnvironment: 'node',
slowTestThreshold: 20,
};

View File

@@ -0,0 +1,4 @@
{
"verbose": true,
"watch": ["dist/"]
}

8087
packages/server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
{
"name": "@joplin/server",
"version": "0.0.0",
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json dist/app.js --env dev",
"start": "node dist/app.js",
"generate-types": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-db --env buildTypes && node dist/tools/generate-types.js && rm -f db-buildTypes.sqlite",
"tsc": "tsc --project tsconfig.json",
"test": "jest",
"test-ci": "npm run test",
"watch": "tsc --watch --project tsconfig.json"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@joplin/lib": "^1.0.9",
"bcryptjs": "^2.4.3",
"bulma": "^0.9.1",
"bulma-prefers-dark": "^0.1.0-beta.0",
"dayjs": "^1.9.8",
"formidable": "^1.2.2",
"fs-extra": "^8.1.0",
"html-entities": "^1.3.1",
"knex": "^0.19.4",
"koa": "^2.8.1",
"mustache": "^3.1.0",
"nanoid": "^2.1.1",
"nodemon": "^2.0.6",
"pg": "^8.5.1",
"query-string": "^6.8.3",
"sqlite3": "^4.1.0",
"yargs": "^14.0.0"
},
"devDependencies": {
"@joplin/tools": "^1.0.9",
"@rmp135/sql-ts": "^1.7.0",
"@types/fs-extra": "^8.0.0",
"@types/jest": "^26.0.15",
"@types/koa": "^2.0.49",
"@types/mustache": "^0.8.32",
"@types/yargs": "^13.0.2",
"jest": "^26.6.3",
"source-map-support": "^0.5.13",
"typescript": "^4.1.2"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
.page-login .login-box .container {
max-width: 400px;
}

View File

@@ -0,0 +1,35 @@
* {
box-sizing: border-box;
}
html {
font-size: 100%;
}
input.form-control {
margin-bottom: 0.5rem;
}
.navbar {
padding: 1rem 3rem;
}
.navbar .logo-container {
align-items: center;
}
.navbar .logo {
height: 50px;
}
.main {
padding: 0 3rem;
}
table.table .nowrap {
white-space: nowrap;
}
table.table .stretch {
width: 100%;
}

View File

@@ -0,0 +1,10 @@
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.authCode {
text-align: center;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
function onDocumentReady(fn) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}

170
packages/server/src/app.ts Normal file
View File

@@ -0,0 +1,170 @@
// Allows displaying error stack traces with TypeScript file paths
import * as Koa from 'koa';
import routes from './routes/routes';
import { ErrorNotFound } from './utils/errors';
import * as fs from 'fs-extra';
import { argv } from 'yargs';
import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute } from './utils/routeUtils';
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
import config, { initConfig, baseUrl } from './config';
import configDev from './config-dev';
import configProd from './config-prod';
import configBuildTypes from './config-buildTypes';
import { createDb, dropDb } from './tools/dbTools';
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection } from './db';
import modelFactory from './models/factory';
import controllerFactory from './controllers/factory';
import { AppContext, Config } from './utils/types';
import FsDriverNode from '@joplin/lib/fs-driver-node';
import mustacheService, { isView, View } from './services/MustacheService';
interface Configs {
[name: string]: Config;
}
const configs: Configs = {
dev: configDev,
prod: configProd,
buildTypes: configBuildTypes,
};
require('source-map-support').install();
const env: string = argv.env as string || 'prod';
const { shimInit } = require('@joplin/lib/shim-init-node.js');
shimInit();
let appLogger_: LoggerWrapper = null;
function appLogger(): LoggerWrapper {
if (!appLogger_) {
appLogger_ = Logger.create('App');
}
return appLogger_;
}
const app = new Koa();
app.use(async (ctx: Koa.Context) => {
appLogger().info(`${ctx.request.method} ${ctx.path}`);
const match: MatchedRoute = null;
try {
const match = findMatchingRoute(ctx.path, routes);
if (match) {
const responseObject = await match.route.exec(match.subPath, ctx);
if (responseObject instanceof Response) {
ctx.response = responseObject.response;
} else if (isView(responseObject)) {
ctx.response.status = 200;
ctx.response.body = await mustacheService.renderView(responseObject);
} else {
ctx.response.status = 200;
ctx.response.body = responseObject;
}
} else {
throw new ErrorNotFound();
}
} catch (error) {
if (error.httpCode >= 400 && error.httpCode < 500) {
appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
} else {
appLogger().error(error);
}
ctx.response.status = error.httpCode ? error.httpCode : 500;
const responseFormat = routeResponseFormat(match, ctx.path);
if (responseFormat === RouteResponseFormat.Html) {
ctx.response.set('Content-Type', 'text/html');
const view: View = {
name: 'error',
path: 'index/error',
content: {
error,
stack: env === 'dev' ? error.stack : '',
},
};
ctx.response.body = await mustacheService.renderView(view);
} else { // JSON
ctx.response.set('Content-Type', 'application/json');
const r: any = { error: error.message };
if (env === 'dev' && error.stack) r.stack = error.stack;
if (error.code) r.code = error.code;
ctx.response.body = r;
}
}
});
async function main() {
const configObject: Config = configs[env];
if (!configObject) throw new Error(`Invalid env: ${env}`);
initConfig(configObject);
await fs.mkdirp(config().logDir);
Logger.fsDriver_ = new FsDriverNode();
const globalLogger = new Logger();
// globalLogger.addTarget(TargetType.File, { path: `${config().logDir}/app.txt` });
globalLogger.addTarget(TargetType.Console, {
format: '%(date_time)s: [%(level)s] %(prefix)s: %(message)s',
formatInfo: '%(date_time)s: %(prefix)s: %(message)s',
});
Logger.initializeGlobalLogger(globalLogger);
const pidFile = argv.pidfile as string;
if (pidFile) {
appLogger().info(`Writing PID to ${pidFile}...`);
fs.removeSync(pidFile as string);
fs.writeFileSync(pidFile, `${process.pid}`);
}
if (argv.migrateDb) {
const db = await connectDb(config().database);
await migrateDb(db);
await disconnectDb(db);
} else if (argv.dropDb) {
await dropDb(config().database, { ignoreIfNotExists: true });
} else if (argv.dropTables) {
const db = await connectDb(config().database);
await dropTables(db);
await disconnectDb(db);
} else if (argv.createDb) {
await createDb(config().database);
} else {
appLogger().info(`Starting server (${env}) on port ${config().port} and PID ${process.pid}...`);
appLogger().info('Public base URL:', baseUrl());
appLogger().info('DB Config:', config().database);
const appContext = app.context as AppContext;
appLogger().info('Trying to connect to database...');
const connectionCheck = await waitForConnection(config().database);
const connectionCheckLogInfo = { ...connectionCheck };
delete connectionCheckLogInfo.connection;
appLogger().info('Connection check:', connectionCheckLogInfo);
appContext.db = connectionCheck.connection;//
appContext.models = modelFactory(appContext.db);
appContext.controllers = controllerFactory(appContext.models);
appLogger().info('Migrating database...');
await migrateDb(appContext.db);
appLogger().info(`Call this for testing: \`curl ${baseUrl()}/api/ping\``);
app.listen(config().port);
}
}
main().catch((error: any) => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,21 @@
import { Config } from './utils/types';
import * as pathUtils from 'path';
const rootDir = pathUtils.dirname(__dirname);
const viewDir = `${pathUtils.dirname(__dirname)}/src/views`;
const envPort = Number(process.env.JOPLIN_PORT);
const config: Config = {
port: (envPort && !isNaN(envPort)) ? envPort : 22300,
viewDir: viewDir,
rootDir: rootDir,
layoutDir: `${viewDir}/layouts`,
logDir: `${rootDir}/logs`,
database: {
client: 'pg',
name: 'joplin',
},
};
export default config;

View File

@@ -0,0 +1,13 @@
import { Config } from './utils/types';
import configBase from './config-base';
const config: Config = {
...configBase,
database: {
name: 'buildTypes',
client: 'sqlite3',
asyncStackTraces: true,
},
};
export default config;

View File

@@ -0,0 +1,22 @@
import { Config } from './utils/types';
import configBase from './config-base';
const config: Config = {
...configBase,
database: {
name: 'dev',
client: 'sqlite3',
asyncStackTraces: true,
},
// database: {
// client: 'pg',
// name: 'joplin',
// user: 'joplin',
// host: 'localhost',
// port: 5432,
// password: 'joplin',
// asyncStackTraces: true,
// },
};
export default config;

View File

@@ -0,0 +1,20 @@
import { Config } from './utils/types';
import configBase from './config-base';
const rootDir = '/home/joplin/';
const config: Config = {
...configBase,
rootDir: rootDir,
logDir: `${rootDir}/logs`,
database: {
client: 'pg',
name: 'joplin',
user: 'joplin',
host: 'db',
port: 5432,
password: 'joplin',
},
};
export default config;

View File

@@ -0,0 +1,13 @@
import { Config } from './utils/types';
import configBase from './config-base';
const config: Config = {
...configBase,
database: {
name: 'DYNAMIC',
client: 'sqlite3',
asyncStackTraces: true,
},
};
export default config;

View File

@@ -0,0 +1,28 @@
import { rtrimSlashes } from '@joplin/lib/path-utils';
import { Config } from './utils/types';
let baseConfig_: Config = null;
let baseUrl_: string = null;
export function initConfig(baseConfig: Config) {
baseConfig_ = baseConfig;
}
function config(): Config {
if (!baseConfig_) throw new Error('Config has not been initialized!');
return baseConfig_;
}
export function baseUrl() {
if (baseUrl_) return baseUrl_;
if (process.env.JOPLIN_BASE_URL) {
baseUrl_ = rtrimSlashes(process.env.JOPLIN_BASE_URL);
} else {
baseUrl_ = `http://localhost:${config().port}`;
}
return baseUrl_;
}
export default config;

View File

@@ -0,0 +1,25 @@
import { User } from '../db';
import { Models } from '../models/factory';
import { ErrorForbidden } from '../utils/errors';
export default abstract class BaseController {
private models_: Models;
public constructor(models: Models) {
this.models_ = models;
}
protected get models(): Models {
return this.models_;
}
protected async initSession(sessionId: string, mustBeAdmin: boolean = false): Promise<User> {
if (!sessionId) throw new ErrorForbidden('Session is required');
const user: User = await this.models.session().sessionUser(sessionId);
if (!user) throw new ErrorForbidden(`Invalid session ID: ${sessionId}`);
if (!user.is_admin && mustBeAdmin) throw new ErrorForbidden('Non-admin user is not allowed');
return user;
}
}

View File

@@ -0,0 +1,535 @@
import { testAssetDir, createUserAndSession, createUser, checkThrowAsync, beforeAllDb, afterAllDb, beforeEachDb, models, controllers } from '../../utils/testUtils';
import * as fs from 'fs-extra';
import { ChangeType, File } from '../../db';
import { ErrorConflict, ErrorForbidden, ErrorNotFound, ErrorUnprocessableEntity } from '../../utils/errors';
import { filePathInfo } from '../../utils/routeUtils';
import { defaultPagination, Pagination, PaginationOrderDir } from '../../models/utils/pagination';
import { msleep } from '../../utils/time';
async function makeTestFile(id: number = 1, ext: string = 'jpg', parentId: string = ''): Promise<File> {
const basename = ext === 'jpg' ? 'photo' : 'poster';
const file: File = {
name: id > 1 ? `${basename}-${id}.${ext}` : `${basename}.${ext}`,
content: await fs.readFile(`${testAssetDir}/${basename}.${ext}`),
// mime_type: `image/${ext}`,
parent_id: parentId,
};
return file;
}
async function makeTestContent(ext: string = 'jpg') {
const basename = ext === 'jpg' ? 'photo' : 'poster';
return await fs.readFile(`${testAssetDir}/${basename}.${ext}`);
}
async function makeTestDirectory(name: string = 'Docs'): Promise<File> {
const file: File = {
name: name,
parent_id: '',
is_directory: 1,
};
return file;
}
async function saveTestFile(sessionId: string, path: string): Promise<File> {
const fileController = controllers().apiFile();
return fileController.putFileContent(
sessionId,
path,
null
);
}
async function saveTestDir(sessionId: string, path: string): Promise<File> {
const fileController = controllers().apiFile();
const parsed = filePathInfo(path);
return fileController.postChild(
sessionId,
parsed.dirname,
{
name: parsed.basename,
is_directory: 1,
}
);
}
describe('FileController', function() {
beforeAll(async () => {
await beforeAllDb('FileController');
});
afterAll(async () => {
await afterAllDb();
});
beforeEach(async () => {
await beforeEachDb();
});
test('should create a file', async function() {
const { user, session } = await createUserAndSession(1, true);
const fileController = controllers().apiFile();
const fileContent = await makeTestContent();
const newFile = await fileController.putFileContent(
session.id,
'root:/photo.jpg:',
fileContent
);
expect(!!newFile.id).toBe(true);
expect(newFile.name).toBe('photo.jpg');
expect(newFile.mime_type).toBe('image/jpeg');
expect(!!newFile.parent_id).toBe(true);
expect(!newFile.content).toBe(true);
expect(newFile.size > 0).toBe(true);
const fileModel = models().file({ userId: user.id });
const newFileReload = await fileModel.loadWithContent(newFile.id);
expect(!!newFileReload).toBe(true);
const newFileHex = fileContent.toString('hex');
const newFileReloadHex = (newFileReload.content as Buffer).toString('hex');
expect(newFileReloadHex.length > 0).toBe(true);
expect(newFileReloadHex).toBe(newFileHex);
});
test('should create sub-directories', async function() {
const { session } = await createUserAndSession(1, true);
const fileController = controllers().apiFile();
const newDir = await fileController.postFile_(session.id, {
is_directory: 1,
name: 'subdir',
});
expect(!!newDir.id).toBe(true);
expect(newDir.is_directory).toBe(1);
const newDir2 = await fileController.postFile_(session.id, {
is_directory: 1,
name: 'subdir2',
parent_id: newDir.id,
});
const newDirReload2 = await fileController.getFile(session.id, 'root:/subdir/subdir2');
expect(newDirReload2.id).toBe(newDir2.id);
expect(newDirReload2.name).toBe(newDir2.name);
});
test('should create files in sub-directory', async function() {
const { session } = await createUserAndSession(1, true);
const fileController = controllers().apiFile();
await fileController.postFile_(session.id, {
is_directory: 1,
name: 'subdir',
});
const newFile = await fileController.putFileContent(
session.id,
'root:/subdir/photo.jpg:',
await makeTestContent()
);
const newFileReload = await fileController.getFile(session.id, 'root:/subdir/photo.jpg');
expect(newFileReload.id).toBe(newFile.id);
expect(newFileReload.name).toBe('photo.jpg');
});
test('should not create a file with an invalid path', async function() {
const { session } = await createUserAndSession(1, true);
const fileController = controllers().apiFile();
const fileContent = await makeTestContent();
const error = await checkThrowAsync(async () => fileController.putFileContent(
session.id,
'root:/does/not/exist/photo.jpg:',
fileContent
));
expect(error instanceof ErrorNotFound).toBe(true);
});
test('should get files', async function() {
const { session: session1, user: user1 } = await createUserAndSession(1);
const { session: session2 } = await createUserAndSession(2);
let file1: File = await makeTestFile(1);
let file2: File = await makeTestFile(2);
let file3: File = await makeTestFile(3);
const fileController = controllers().apiFile();
file1 = await fileController.postFile_(session1.id, file1);
file2 = await fileController.postFile_(session1.id, file2);
file3 = await fileController.postFile_(session2.id, file3);
const fileId1 = file1.id;
const fileId2 = file2.id;
// Can't get someone else file
const error = await checkThrowAsync(async () => fileController.getFile(session1.id, file3.id));
expect(error instanceof ErrorForbidden).toBe(true);
file1 = await fileController.getFile(session1.id, file1.id);
expect(file1.id).toBe(fileId1);
const fileModel = models().file({ userId: user1.id });
const paginatedResults = await fileController.getChildren(session1.id, await fileModel.userRootFileId(), defaultPagination());
const allFiles = paginatedResults.items;
expect(allFiles.length).toBe(2);
expect(JSON.stringify(allFiles.map(f => f.id).sort())).toBe(JSON.stringify([fileId1, fileId2].sort()));
});
test('should not let create a file in a directory not owned by user', async function() {
const { session } = await createUserAndSession(1);
const user2 = await createUser(2);
const fileModel2 = models().file({ userId: user2.id });
const rootFile2 = await fileModel2.userRootFile();
const file: File = await makeTestFile();
file.parent_id = rootFile2.id;
const fileController = controllers().apiFile();
const hasThrown = await checkThrowAsync(async () => fileController.postFile_(session.id, file));
expect(!!hasThrown).toBe(true);
});
test('should update file properties', async function() {
const { session, user } = await createUserAndSession(1, true);
const fileModel = models().file({ userId: user.id });
let file: File = await makeTestFile();
const fileController = controllers().apiFile();
file = await fileController.postFile_(session.id, file);
// Can't have file with empty name
const error = await checkThrowAsync(async () => fileController.patchFile(session.id, file.id, { name: '' }));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
await fileController.patchFile(session.id, file.id, { name: 'modified.jpg' });
file = await fileModel.load(file.id);
expect(file.name).toBe('modified.jpg');
await fileController.patchFile(session.id, file.id, { mime_type: 'image/png' });
file = await fileModel.load(file.id);
expect(file.mime_type).toBe('image/png');
});
test('should not allow duplicate filenames', async function() {
const { session } = await createUserAndSession(1, true);
let file1: File = await makeTestFile(1);
const file2: File = await makeTestFile(1);
const fileController = controllers().apiFile();
file1 = await fileController.postFile_(session.id, file1);
expect(!!file1.id).toBe(true);
expect(file1.name).toBe(file2.name);
const hasThrown = await checkThrowAsync(async () => await fileController.postFile_(session.id, file2));
expect(!!hasThrown).toBe(true);
});
test('should change the file parent', async function() {
const { session: session1, user: user1 } = await createUserAndSession(1);
const { user: user2 } = await createUserAndSession(2);
let hasThrown: any = null;
const fileModel = models().file({ userId: user1.id });
let file: File = await makeTestFile();
let file2: File = await makeTestFile(2);
let dir: File = await makeTestDirectory();
const fileController = controllers().apiFile();
file = await fileController.postFile_(session1.id, file);
file2 = await fileController.postFile_(session1.id, file2);
dir = await fileController.postFile_(session1.id, dir);
// Can't set parent to another non-directory file
hasThrown = await checkThrowAsync(async () => await fileController.patchFile(session1.id, file.id, { parent_id: file2.id }));
expect(!!hasThrown).toBe(true);
const fileModel2 = models().file({ userId: user2.id });
const userRoot2 = await fileModel2.userRootFile();
// Can't set parent to someone else directory
hasThrown = await checkThrowAsync(async () => await fileController.patchFile(session1.id, file.id, { parent_id: userRoot2.id }));
expect(!!hasThrown).toBe(true);
await fileController.patchFile(session1.id, file.id, { parent_id: dir.id });
file = await fileModel.load(file.id);
expect(!!file.parent_id).toBe(true);
expect(file.parent_id).toBe(dir.id);
});
test('should delete a file', async function() {
const { user, session } = await createUserAndSession(1, true);
const fileController = controllers().apiFile();
const fileModel = models().file({ userId: user.id });
const file1: File = await makeTestFile(1);
let file2: File = await makeTestFile(2);
await fileController.postFile_(session.id, file1);
file2 = await fileController.postFile_(session.id, file2);
let allFiles: File[] = await fileModel.all();
const beforeCount: number = allFiles.length;
await fileController.deleteFile(session.id, file2.id);
allFiles = await fileModel.all();
expect(allFiles.length).toBe(beforeCount - 1);
});
test('should create and delete directories', async function() {
const { user, session } = await createUserAndSession(1, true);
const fileController = controllers().apiFile();
const dir1: File = await fileController.postChild(session.id, 'root', { name: 'dir1', is_directory: 1 });
const dir2: File = await fileController.postChild(session.id, 'root:/dir1', { name: 'dir2', is_directory: 1 });
const dirReload2: File = await fileController.getFile(session.id, 'root:/dir1/dir2');
expect(dirReload2.id).toBe(dir2.id);
// Delete one directory
await fileController.deleteFile(session.id, 'root:/dir1/dir2');
const error = await checkThrowAsync(async () => fileController.getFile(session.id, 'root:/dir1/dir2'));
expect(error instanceof ErrorNotFound).toBe(true);
// Delete a directory and its sub-directories and files
const dir3: File = await fileController.postChild(session.id, 'root:/dir1', { name: 'dir3', is_directory: 1 });
const file1: File = await fileController.postFile_(session.id, { name: 'file1', parent_id: dir1.id });
const file2: File = await fileController.postFile_(session.id, { name: 'file2', parent_id: dir3.id });
await fileController.deleteFile(session.id, 'root:/dir1');
const fileModel = models().file({ userId: user.id });
expect(!(await fileModel.load(dir1.id))).toBe(true);
expect(!(await fileModel.load(dir3.id))).toBe(true);
expect(!(await fileModel.load(file1.id))).toBe(true);
expect(!(await fileModel.load(file2.id))).toBe(true);
});
test('should not change the parent when updating a file', async function() {
const { user, session } = await createUserAndSession(1, true);
const fileController = controllers().apiFile();
const fileModel = models().file({ userId: user.id });
const dir1: File = await fileController.postChild(session.id, 'root', { name: 'dir1', is_directory: 1 });
const file1: File = await fileController.putFileContent(session.id, 'root:/dir1/myfile.md', Buffer.from('testing'));
await fileController.putFileContent(session.id, 'root:/dir1/myfile.md', Buffer.from('new content'));
const fileReloaded1 = await fileModel.load(file1.id);
expect(fileReloaded1.parent_id).toBe(dir1.id);
});
test('should not delete someone else file', async function() {
const { session: session1 } = await createUserAndSession(1);
const { session: session2 } = await createUserAndSession(2);
const fileController = controllers().apiFile();
const file1: File = await makeTestFile(1);
let file2: File = await makeTestFile(2);
await fileController.postFile_(session1.id, file1);
file2 = await fileController.postFile_(session2.id, file2);
const error = await checkThrowAsync(async () => await fileController.deleteFile(session1.id, file2.id));
expect(error instanceof ErrorForbidden).toBe(true);
});
test('should let admin change or delete files', async function() {
const { session: adminSession } = await createUserAndSession(1, true);
const { session, user } = await createUserAndSession(2);
let file: File = await makeTestFile();
const fileModel = models().file({ userId: user.id });
const fileController = controllers().apiFile();
file = await fileController.postFile_(session.id, file);
await fileController.patchFile(adminSession.id, file.id, { name: 'modified.jpg' });
file = await fileModel.load(file.id);
expect(file.name).toBe('modified.jpg');
await fileController.deleteFile(adminSession.id, file.id);
expect(!(await fileModel.load(file.id))).toBe(true);
});
test('should update a file content', async function() {
const { session } = await createUserAndSession(1, true);
const file: File = await makeTestFile(1);
const file2: File = await makeTestFile(2, 'png');
const fileController = controllers().apiFile();
const newFile = await fileController.postFile_(session.id, file);
await fileController.putFileContent(session.id, newFile.id, file2.content);
const modFile = await fileController.getFileContent(session.id, newFile.id);
const originalFileHex = (file.content as Buffer).toString('hex');
const modFileHex = (modFile.content as Buffer).toString('hex');
expect(modFileHex.length > 0).toBe(true);
expect(modFileHex === originalFileHex).toBe(false);
expect(modFile.size).toBe(modFile.content.byteLength);
expect(newFile.size).toBe(file.content.byteLength);
});
test('should delete a file content', async function() {
const { session } = await createUserAndSession(1, true);
const file: File = await makeTestFile(1);
const fileController = controllers().apiFile();
const newFile = await fileController.postFile_(session.id, file);
await fileController.putFileContent(session.id, newFile.id, file.content);
await fileController.deleteFileContent(session.id, newFile.id);
const modFile = await fileController.getFile(session.id, newFile.id);
expect(modFile.size).toBe(0);
});
test('should not allow reserved characters', async function() {
const { session } = await createUserAndSession(1, true);
const filenames = [
'invalid*invalid',
'invalid#invalid',
'invalid\\invalid',
];
const fileController = controllers().apiFile();
for (const filename of filenames) {
const error = await checkThrowAsync(async () => fileController.putFileContent(session.id, `root:/${filename}`, null));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
}
});
test('should not allow a directory with the same name', async function() {
const { session } = await createUserAndSession(1, true);
await saveTestDir(session.id, 'root:/somedir:');
let error = await checkThrowAsync(async () => saveTestFile(session.id, 'root:/somedir:'));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
await saveTestFile(session.id, 'root:/somefile.md:');
error = await checkThrowAsync(async () => saveTestDir(session.id, 'root:/somefile.md:'));
expect(error instanceof ErrorConflict).toBe(true);
});
test('should not be possible to delete the root directory', async function() {
const { session } = await createUserAndSession(1, true);
const fileController = controllers().apiFile();
const error = await checkThrowAsync(async () => fileController.deleteFile(session.id, 'root'));
expect(error instanceof ErrorForbidden).toBe(true);
});
test('should support root:/: format, which means root', async function() {
const { session, user } = await createUserAndSession(1, true);
const fileController = controllers().apiFile();
const fileModel = models().file({ userId: user.id });
const root = await fileController.getFile(session.id, 'root:/:');
expect(root.id).toBe(await fileModel.userRootFileId());
});
test('should paginate results', async function() {
const { session: session1, user: user1 } = await createUserAndSession(1);
let file1: File = await makeTestFile(1);
let file2: File = await makeTestFile(2);
let file3: File = await makeTestFile(3);
const fileController = controllers().apiFile();
file1 = await fileController.postFile_(session1.id, file1);
await msleep(1);
file2 = await fileController.postFile_(session1.id, file2);
await msleep(1);
file3 = await fileController.postFile_(session1.id, file3);
const fileModel = models().file({ userId: user1.id });
const rootId = await fileModel.userRootFileId();
const pagination: Pagination = {
limit: 2,
order: [
{
by: 'updated_time',
dir: PaginationOrderDir.ASC,
},
],
page: 1,
};
for (const method of ['page', 'cursor']) {
const page1 = await fileController.getChildren(session1.id, rootId, pagination);
expect(page1.items.length).toBe(2);
expect(page1.has_more).toBe(true);
expect(page1.items[0].id).toBe(file1.id);
expect(page1.items[1].id).toBe(file2.id);
const p = method === 'page' ? { ...pagination, page: 2 } : { cursor: page1.cursor };
const page2 = await fileController.getChildren(session1.id, rootId, p);
expect(page2.items.length).toBe(1);
expect(page2.has_more).toBe(false);
expect(page2.items[0].id).toBe(file3.id);
}
});
test('should track file changes', async function() {
// We only do a basic check because most of the tests for this are in
// ChangeModel.test.ts
const { session: session1 } = await createUserAndSession(1);
let file1: File = await makeTestFile(1);
let file2: File = await makeTestFile(2);
const fileController = controllers().apiFile();
file1 = await fileController.postFile_(session1.id, file1);
await msleep(1); file2 = await fileController.postFile_(session1.id, file2);
const page1 = await fileController.getDelta(session1.id, file1.parent_id, { limit: 1 });
expect(page1.has_more).toBe(true);
expect(page1.items.length).toBe(1);
expect(page1.items[0].type).toBe(ChangeType.Create);
expect(page1.items[0].item.id).toBe(file1.id);
const page2 = await fileController.getDelta(session1.id, file1.parent_id, { cursor: page1.cursor, limit: 1 });
expect(page2.has_more).toBe(true);
expect(page2.items.length).toBe(1);
expect(page2.items[0].type).toBe(ChangeType.Create);
expect(page2.items[0].item.id).toBe(file2.id);
const page3 = await fileController.getDelta(session1.id, file1.parent_id, { cursor: page2.cursor, limit: 1 });
expect(page3.has_more).toBe(false);
expect(page3.items.length).toBe(0);
});
});

View File

@@ -0,0 +1,101 @@
import { File } from '../../db';
import BaseController from '../BaseController';
import { ErrorNotFound } from '../../utils/errors';
import { Pagination } from '../../models/utils/pagination';
import { PaginatedFiles } from '../../models/FileModel';
import { ChangePagination, PaginatedChanges } from '../../models/ChangeModel';
export default class FileController extends BaseController {
// Note: this is only used in tests. To create files with no content
// or directories, use postChild()
public async postFile_(sessionId: string, file: File): Promise<File> {
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
let newFile = fileModel.fromApiInput(file);
newFile = await fileModel.save(file);
return fileModel.toApiOutput(newFile);
}
public async getFile(sessionId: string, fileId: string): Promise<File> {
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
const file: File = await fileModel.entityFromItemId(fileId);
const loadedFile = await fileModel.load(file.id);
if (!loadedFile) throw new ErrorNotFound();
return fileModel.toApiOutput(loadedFile);
}
public async getFileContent(sessionId: string, fileId: string): Promise<File> {
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
let file: File = await fileModel.entityFromItemId(fileId);
file = await fileModel.loadWithContent(file.id);
if (!file) throw new ErrorNotFound();
return file;
}
public async patchFile(sessionId: string, fileId: string, file: File): Promise<File> {
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
const existingFile: File = await fileModel.entityFromItemId(fileId);
const newFile = fileModel.fromApiInput(file);
newFile.id = existingFile.id;
return fileModel.toApiOutput(await fileModel.save(newFile));
}
public async putFileContent(sessionId: string, fileId: string, content: Buffer): Promise<any> {
if (!content) content = Buffer.alloc(0);
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
const file: File = await fileModel.entityFromItemId(fileId, { mustExist: false });
file.content = content;
return fileModel.toApiOutput(await fileModel.save(file, { validationRules: { mustBeFile: true } }));
}
public async deleteFileContent(sessionId: string, fileId: string): Promise<any> {
await this.putFileContent(sessionId, fileId, null);
}
public async getChildren(sessionId: string, dirId: string, pagination: Pagination): Promise<PaginatedFiles> {
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
const parent: File = await fileModel.entityFromItemId(dirId);
return fileModel.toApiOutput(await fileModel.childrens(parent.id, pagination));
}
public async postChild(sessionId: string, fileId: string, child: File): Promise<File> {
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
const parent: File = await fileModel.entityFromItemId(fileId);
child = fileModel.fromApiInput(child);
child.parent_id = parent.id;
return fileModel.toApiOutput(await fileModel.save(child));
}
public async deleteFile(sessionId: string, fileId: string): Promise<void> {
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
try {
const file: File = await fileModel.entityFromItemId(fileId, { mustExist: false });
if (!file.id) return;
await fileModel.delete(file.id);
} catch (error) {
if (error instanceof ErrorNotFound) {
// That's ok - a no-op
} else {
throw error;
}
}
}
public async getDelta(sessionId: string, dirId: string, pagination: ChangePagination): Promise<PaginatedChanges> {
const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id });
const dir: File = await fileModel.entityFromItemId(dirId, { mustExist: true });
const changeModel = this.models.change({ userId: user.id });
return changeModel.byDirectoryId(dir.id, pagination);
}
}

View File

@@ -0,0 +1,54 @@
// import BaseController from '../BaseController';
// import mustacheService from '../../services/MustacheService';
// import { ErrorNotFound } from '../../utils/errors';
// import uuidgen from '../../utils/uuidgen';
// import controllers from '../factory';
// export default class OAuthController extends BaseController {
// async getAuthorize(query: any): Promise<string> {
// const clientModel = this.models.apiClient();
// const client = await clientModel.load(query.client_id);
// if (!client) throw new ErrorNotFound(`client_id missing or invalid client ID: ${query.client_id}`);
// return mustacheService.render('oauth2/authorize', {
// response_type: query.response_type,
// client: client,
// }, {
// cssFiles: ['oauth2/authorize'],
// });
// }
// async postAuthorize(query: any): Promise<string> {
// const clientModel = this.models.apiClient();
// const sessionModel = this.models.session();
// const sessionController = controllers(this.models).session();
// let client = null;
// try {
// client = await clientModel.load(query.client_id);
// if (!client) throw new ErrorNotFound(`client_id missing or invalid client ID: ${query.client_id}`);
// const session = await sessionController.authenticate(query.email, query.password);
// const authCode = uuidgen(32);
// await sessionModel.save({ id: session.id, auth_code: authCode });
// return mustacheService.render('oauth2/authcode', {
// client: client,
// authCode: authCode,
// }, {
// cssFiles: ['oauth2/authorize'],
// });
// } catch (error) {
// return mustacheService.render('oauth2/authorize', {
// response_type: query.response_type,
// client: client,
// error: error,
// }, {
// cssFiles: ['oauth2/authorize'],
// });
// }
// }
// }

View File

@@ -0,0 +1,38 @@
import { createUser, checkThrowAsync, beforeAllDb, afterAllDb, beforeEachDb, controllers } from '../../utils/testUtils';
import { ErrorForbidden } from '../../utils/errors';
describe('SessionController', function() {
beforeAll(async () => {
await beforeAllDb('SessionController');
});
afterAll(async () => {
await afterAllDb();
});
beforeEach(async () => {
await beforeEachDb();
});
it('should authenticate a user and give back a session', async function() {
const user = await createUser(1);
const controller = controllers().apiSession();
const session = await controller.authenticate(user.email, '123456');
expect(!!session).toBe(true);
expect(!!session.id).toBe(true);
expect(!!session.user_id).toBe(true);
});
it('should not give a session for invalid login', async function() {
const user = await createUser(1);
const controller = controllers().apiSession();
let error = await checkThrowAsync(async () => controller.authenticate(user.email, 'wrong'));
expect(error instanceof ErrorForbidden).toBe(true);
error = await checkThrowAsync(async () => controller.authenticate('wrong@wrong.com', '123456'));
expect(error instanceof ErrorForbidden).toBe(true);
});
});

View File

@@ -0,0 +1,19 @@
import { Session, User } from '../../db';
import { checkPassword } from '../../utils/auth';
import { ErrorForbidden } from '../../utils/errors';
import uuidgen from '../../utils/uuidgen';
import BaseController from '../BaseController';
export default class SessionController extends BaseController {
public async authenticate(email: string, password: string): Promise<Session> {
const userModel = this.models.user();
const user: User = await userModel.loadByEmail(email);
if (!user) throw new ErrorForbidden('Invalid username or password');
if (!checkPassword(password, user.password)) throw new ErrorForbidden('Invalid username or password');
const session: Session = { id: uuidgen(), user_id: user.id };
const sessionModel = this.models.session();
return sessionModel.save(session, { isNew: true });
}
}

View File

@@ -0,0 +1,192 @@
import { models, controllers, createUserAndSession, checkThrowAsync, beforeAllDb, afterAllDb, beforeEachDb } from '../../utils/testUtils';
import { File, User } from '../../db';
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
describe('UserController', function() {
beforeAll(async () => {
await beforeAllDb('UserController');
});
afterAll(async () => {
await afterAllDb();
});
beforeEach(async () => {
await beforeEachDb();
});
it('should create a new user along with his root file', async function() {
const { session } = await createUserAndSession(1, true);
const controller = controllers().apiUser();
const newUser = await controller.postUser(session.id, { email: 'test@example.com', password: '123456' });
expect(!!newUser).toBe(true);
expect(!!newUser.id).toBe(true);
expect(!!newUser.is_admin).toBe(false);
expect(!!newUser.email).toBe(true);
expect(!newUser.password).toBe(true);
const userModel = models().user({ userId: newUser.id });
const userFromModel: User = await userModel.load(newUser.id);
expect(!!userFromModel.password).toBe(true);
expect(userFromModel.password === '123456').toBe(false); // Password has been hashed
const fileModel = models().file({ userId: newUser.id });
const rootFile: File = await fileModel.userRootFile();
expect(!!rootFile).toBe(true);
expect(!!rootFile.id).toBe(true);
});
it('should not create anything, neither user, root file nor permissions, if user creation fail', async function() {
const { user, session } = await createUserAndSession(1, true);
const controller = controllers().apiUser();
const fileModel = models().file({ userId: user.id });
const permissionModel = models().permission();
const userModel = models().user({ userId: user.id });
await controller.postUser(session.id, { email: 'test@example.com', password: '123456' });
const beforeFileCount = (await fileModel.all()).length;
const beforeUserCount = (await userModel.all()).length;
const beforePermissionCount = (await permissionModel.all()).length;
expect(beforeFileCount).toBe(2);
expect(beforeUserCount).toBe(2);
let hasThrown = false;
try {
await controller.postUser(session.id, { email: 'test@example.com', password: '123456' });
} catch (error) {
hasThrown = true;
}
expect(hasThrown).toBe(true);
const afterFileCount = (await fileModel.all()).length;
const afterUserCount = (await userModel.all()).length;
const afterPermissionCount = (await permissionModel.all()).length;
expect(beforeFileCount).toBe(afterFileCount);
expect(beforeUserCount).toBe(afterUserCount);
expect(beforePermissionCount).toBe(afterPermissionCount);
});
it('should change user properties', async function() {
const { user, session } = await createUserAndSession(1, true);
const controller = controllers().apiUser();
const userModel = models().user({ userId: user.id });
await controller.patchUser(session.id, { id: user.id, email: 'test2@example.com' });
let modUser: User = await userModel.load(user.id);
expect(modUser.email).toBe('test2@example.com');
const previousPassword = modUser.password;
await controller.patchUser(session.id, { id: user.id, password: 'abcdefgh' });
modUser = await userModel.load(user.id);
expect(!!modUser.password).toBe(true);
expect(modUser.password === previousPassword).toBe(false);
});
it('should get a user', async function() {
const { user, session } = await createUserAndSession();
const controller = controllers().apiUser();
const gotUser = await controller.getUser(session.id, user.id);
expect(gotUser.id).toBe(user.id);
expect(gotUser.email).toBe(user.email);
});
it('should validate user objects', async function() {
const { user: admin, session: adminSession } = await createUserAndSession(1, true);
const { user: user1, session: userSession1 } = await createUserAndSession(2, false);
const { user: user2 } = await createUserAndSession(3, false);
let error = null;
const controller = controllers().apiUser();
// Non-admin user can't create a user
error = await checkThrowAsync(async () => await controller.postUser(userSession1.id, { email: 'newone@example.com', password: '1234546' }));
expect(error instanceof ErrorForbidden).toBe(true);
// Email must be set
error = await checkThrowAsync(async () => await controller.postUser(adminSession.id, { email: '', password: '1234546' }));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
// Password must be set
error = await checkThrowAsync(async () => await controller.postUser(adminSession.id, { email: 'newone@example.com', password: '' }));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
// ID must be set when updating a user
error = await checkThrowAsync(async () => await controller.patchUser(userSession1.id, { email: 'newone@example.com' }));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
// non-admin user cannot modify another user
error = await checkThrowAsync(async () => await controller.patchUser(userSession1.id, { id: user2.id, email: 'newone@example.com' }));
expect(error instanceof ErrorForbidden).toBe(true);
// email must be set
error = await checkThrowAsync(async () => await controller.patchUser(userSession1.id, { id: user1.id, email: '' }));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
// password must be set
error = await checkThrowAsync(async () => await controller.patchUser(userSession1.id, { id: user1.id, password: '' }));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
// non-admin user cannot make a user an admin
error = await checkThrowAsync(async () => await controller.patchUser(userSession1.id, { id: user1.id, is_admin: 1 }));
expect(error instanceof ErrorForbidden).toBe(true);
// non-admin user cannot remove admin bit from themselves
error = await checkThrowAsync(async () => await controller.patchUser(adminSession.id, { id: admin.id, is_admin: 0 }));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
// there is already a user with this email
error = await checkThrowAsync(async () => await controller.patchUser(userSession1.id, { id: user1.id, email: user2.email }));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
// check that the email is valid
error = await checkThrowAsync(async () => await controller.patchUser(userSession1.id, { id: user1.id, email: 'ohno' }));
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
});
it('should delete a user', async function() {
const { user: admin, session: adminSession } = await createUserAndSession(1, true);
const { user: user1, session: session1 } = await createUserAndSession(2, false);
const { user: user2, session: session2 } = await createUserAndSession(3, false);
const controller = controllers().apiUser();
const userModel = models().user({ userId: admin.id });
const allUsers: File[] = await userModel.all();
const beforeCount: number = allUsers.length;
// Can't delete someone else user
const error = await checkThrowAsync(async () => await controller.deleteUser(session1.id, user2.id));
expect(error instanceof ErrorForbidden).toBe(true);
expect((await userModel.all()).length).toBe(beforeCount);
// Admin can delete any user
await controller.deleteUser(adminSession.id, user1.id);
expect((await userModel.all()).length).toBe(beforeCount - 1);
const allFiles = await models().file().all() as File[];
expect(allFiles.length).toBe(2);
expect(!!allFiles.find(f => f.owner_id === admin.id)).toBe(true);
expect(!!allFiles.find(f => f.owner_id === user2.id)).toBe(true);
// Can delete own user
const fileModel = models().file({ userId: user2.id });
expect(!!(await fileModel.userRootFile())).toBe(true);
await controller.deleteUser(session2.id, user2.id);
expect((await userModel.all()).length).toBe(beforeCount - 2);
expect(!!(await fileModel.userRootFile())).toBe(false);
});
});

View File

@@ -0,0 +1,33 @@
import { User } from '../../db';
import BaseController from '../BaseController';
export default class UserController extends BaseController {
public async postUser(sessionId: string, user: User): Promise<User> {
const owner = await this.initSession(sessionId, true);
const userModel = this.models.user({ userId: owner.id });
let newUser = userModel.fromApiInput(user);
newUser = await userModel.save(newUser);
return userModel.toApiOutput(newUser);
}
public async getUser(sessionId: string, userId: string): Promise<User> {
const owner = await this.initSession(sessionId);
const userModel = this.models.user({ userId: owner.id });
return userModel.toApiOutput(await userModel.load(userId));
}
public async patchUser(sessionId: string, user: User): Promise<void> {
const owner = await this.initSession(sessionId);
const userModel = this.models.user({ userId: owner.id });
const newUser = userModel.fromApiInput(user);
await userModel.save(newUser, { isNew: false });
}
public async deleteUser(sessionId: string, userId: string): Promise<void> {
const user = await this.initSession(sessionId);
const userModel = this.models.user({ userId: user.id });
await userModel.delete(userId);
}
}

View File

@@ -0,0 +1,60 @@
import { Models } from '../models/factory';
import FileController from './api/FileController';
// import OAuthController from './api/OAuthController';
import SessionController from './api/SessionController';
import UserController from './api/UserController';
import IndexLoginController from './index/LoginController';
import IndexHomeController from './index/HomeController';
import IndexProfileController from './index/ProfileController';
import IndexUserController from './index/UserController';
import IndexFileController from './index/FileController';
export class Controllers {
private models_: Models;
public constructor(models: Models) {
this.models_ = models;
}
public apiFile() {
return new FileController(this.models_);
}
// public oauth() {
// return new OAuthController(this.models_);
// }
public apiSession() {
return new SessionController(this.models_);
}
public apiUser() {
return new UserController(this.models_);
}
public indexLogin() {
return new IndexLoginController(this.models_);
}
public indexHome() {
return new IndexHomeController(this.models_);
}
public indexProfile() {
return new IndexProfileController(this.models_, this.apiUser());
}
public indexUser() {
return new IndexUserController(this.models_);
}
public indexFiles() {
return new IndexFileController(this.models_);
}
}
export default function(models: Models) {
return new Controllers(models);
}

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