mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 00:06:30 +01:00
Compare commits
42 Commits
v0.50.3
...
v0.51.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1092c97b5 | ||
|
|
a04becc4ec | ||
|
|
f7d6bda49d | ||
|
|
b250f0a3bf | ||
|
|
df1d94ec61 | ||
|
|
e00fcd93a1 | ||
|
|
0a95d0f6f5 | ||
|
|
091d6a1cf1 | ||
|
|
228564f843 | ||
|
|
17dd6141fb | ||
|
|
77ce56ba84 | ||
|
|
eba824a5b1 | ||
|
|
c9e72f8fb9 | ||
|
|
4dd3fd9674 | ||
|
|
1690a55f7d | ||
|
|
dd29fc26e3 | ||
|
|
67b5921d6c | ||
|
|
1b7bcc5cc1 | ||
|
|
a009b4cb6d | ||
|
|
781be527ce | ||
|
|
f7e5d8f62d | ||
|
|
d6c0fc734f | ||
|
|
18d439dd44 | ||
|
|
d2d2a6c086 | ||
|
|
5260689b8e | ||
|
|
78a2863b78 | ||
|
|
5481375347 | ||
|
|
4da2d2f516 | ||
|
|
67cce5f817 | ||
|
|
9924727729 | ||
|
|
6c9fc364a3 | ||
|
|
1aeb674733 | ||
|
|
df91192b97 | ||
|
|
97fd550402 | ||
|
|
eb579de199 | ||
|
|
5f2984aa57 | ||
|
|
98a79f6475 | ||
|
|
c09da2b7eb | ||
|
|
600f74576d | ||
|
|
a21c49cba7 | ||
|
|
91e3dd022a | ||
|
|
478eca47f4 |
@@ -2,7 +2,7 @@ image:
|
||||
file: .gitpod.dockerfile
|
||||
|
||||
tasks:
|
||||
- before: nvm install 16.13.2 && nvm use 16.13.2
|
||||
- before: nvm install 16.14.2 && nvm use 16.14.2
|
||||
init: npm install
|
||||
command: npm run start-server
|
||||
|
||||
|
||||
1
CODE_OF_CONDUCT
Normal file
1
CODE_OF_CONDUCT
Normal file
@@ -0,0 +1 @@
|
||||
Please treat each other with respect and understanding.
|
||||
@@ -1,6 +1,5 @@
|
||||
# !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!!
|
||||
|
||||
FROM node:16.13.2-alpine
|
||||
FROM node:16.14.2-alpine
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -7,6 +7,10 @@ Trilium Notes是一个分层的笔记应用程序,专注于建立大型个人
|
||||
|
||||

|
||||
|
||||
Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/).
|
||||
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/>
|
||||
|
||||
## 特性
|
||||
|
||||
* 笔记可以排列成任意深的树。单个笔记可以放在树中的多个位置(请参阅[克隆](https://github.com/zadam/trilium/wiki/Cloning-notes))
|
||||
|
||||
@@ -7,6 +7,10 @@ Trilium Notes is a hierarchical note taking application with focus on building l
|
||||
|
||||

|
||||
|
||||
Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/).
|
||||
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/>
|
||||
|
||||
## Features
|
||||
|
||||
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://github.com/zadam/trilium/wiki/Cloning-notes))
|
||||
@@ -16,6 +20,7 @@ Trilium Notes is a hierarchical note taking application with focus on building l
|
||||
* Seamless [note versioning](https://github.com/zadam/trilium/wiki/Note-revisions)
|
||||
* Note [attributes](https://github.com/zadam/trilium/wiki/Attributes) can be used for note organization, querying and advanced [scripting](https://github.com/zadam/trilium/wiki/Scripts)
|
||||
* [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization) with self-hosted sync server
|
||||
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
|
||||
* [Sharing](https://github.com/zadam/trilium/wiki/Sharing) (publishing) notes to public internet
|
||||
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes) with per-note granularity
|
||||
* [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map) for visualizing notes and their relations
|
||||
@@ -60,6 +65,10 @@ npm run start-server
|
||||
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
|
||||
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map)
|
||||
|
||||
## Donating
|
||||
|
||||
You can donate using GitHub Sponsors, [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2).
|
||||
|
||||
## License
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
@@ -7,6 +7,10 @@ Trilium Notes – это приложение для заметок с иера
|
||||
|
||||

|
||||
|
||||
Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/).
|
||||
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/>
|
||||
|
||||
## Возможности
|
||||
|
||||
* Заметки можно расположить в виде дерева произвольной глубины. Отдельную заметку можно разместить в нескольких местах дерева (см. [клонирование](https://github.com/zadam/trilium/wiki/Cloning-notes))
|
||||
|
||||
@@ -29,6 +29,8 @@ rm -r $BUILD_DIR/swiftshader
|
||||
|
||||
cp bin/tpl/anonymize-database.sql $BUILD_DIR/
|
||||
|
||||
cp -r dump-db $BUILD_DIR/
|
||||
|
||||
cp bin/tpl/trilium-portable.sh $BUILD_DIR/
|
||||
chmod 755 $BUILD_DIR/trilium-portable.sh
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ mv "./dist/Trilium Notes-darwin-x64" $BUILD_DIR
|
||||
|
||||
cp bin/tpl/anonymize-database.sql $BUILD_DIR/
|
||||
|
||||
cp -r dump-db $BUILD_DIR/
|
||||
|
||||
echo "Zipping mac x64 electron distribution..."
|
||||
|
||||
VERSION=`jq -r ".version" package.json`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PKG_DIR=dist/trilium-linux-x64-server
|
||||
NODE_VERSION=16.13.2
|
||||
NODE_VERSION=16.14.2
|
||||
|
||||
if [ "$1" != "DONTCOPY" ]
|
||||
then
|
||||
@@ -30,6 +30,8 @@ chmod 755 $PKG_DIR/trilium.sh
|
||||
|
||||
cp bin/tpl/anonymize-database.sql $PKG_DIR/
|
||||
|
||||
cp -r dump-db $PKG_DIR/
|
||||
|
||||
VERSION=`jq -r ".version" package.json`
|
||||
|
||||
cd dist
|
||||
|
||||
@@ -27,6 +27,8 @@ rm -r $BUILD_DIR/swiftshader
|
||||
|
||||
cp bin/tpl/anonymize-database.sql $BUILD_DIR/
|
||||
|
||||
cp -r dump-db $BUILD_DIR/
|
||||
|
||||
cp bin/tpl/trilium-{portable,no-cert-check,safe-mode}.{bat,ps1} $BUILD_DIR/
|
||||
|
||||
echo "Zipping windows x64 electron distribution..."
|
||||
|
||||
@@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
n exec 16.13.2 npm run webpack
|
||||
n exec 16.14.2 npm run webpack
|
||||
|
||||
DIR=$1
|
||||
|
||||
@@ -14,6 +14,9 @@ mkdir $DIR
|
||||
|
||||
echo "Copying Trilium to build directory $DIR"
|
||||
|
||||
cp -r dump-db $DIR/
|
||||
rm -rf $DIR/dump-db/node_modules
|
||||
|
||||
cp -r images $DIR/
|
||||
cp -r libraries $DIR/
|
||||
cp -r src $DIR/
|
||||
@@ -27,7 +30,7 @@ cp -r electron.js $DIR/
|
||||
cp webpack-* $DIR/
|
||||
|
||||
# run in subshell (so we return to original dir)
|
||||
(cd $DIR && n exec 16.13.2 npm install --only=prod)
|
||||
(cd $DIR && n exec 16.14.2 npm install --only=prod)
|
||||
|
||||
# cleanup of useless files in dependencies
|
||||
rm -r $DIR/node_modules/image-q/demo
|
||||
|
||||
@@ -4,4 +4,4 @@ const anonymizationService = require('../src/services/anonymization');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
fs.writeFileSync(path.resolve(__dirname, 'tpl', 'anonymize-database.sql'), anonymizationService.getAnonymizationScript());
|
||||
fs.writeFileSync(path.resolve(__dirname, 'tpl', 'anonymize-database.sql'), anonymizationService.getFullAnonymizationScript());
|
||||
|
||||
34
dump-db/README.md
Normal file
34
dump-db/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Trilium Notes DB dump tool
|
||||
|
||||
This is a simple tool to dump the content of Trilium's document.db onto filesystem.
|
||||
|
||||
It is meant as a last resort solution when the standard mean to access your data (through main Trilium application) fail.
|
||||
|
||||
## Installation
|
||||
|
||||
This tool requires node.js, testing has been done on 16.14.2, but it will probably work on other versions as well.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
See output of `node dump-db.js --help`:
|
||||
|
||||
```
|
||||
dump-db.js <path_to_document> <target_directory>
|
||||
|
||||
dump the contents of document.db into the target directory
|
||||
|
||||
Positionals:
|
||||
path_to_document path to the document.db
|
||||
target_directory path of the directory into which the notes should be dumped
|
||||
|
||||
Options:
|
||||
--help Show help [boolean]
|
||||
--version Show version number [boolean]
|
||||
--password Set password to be able to decrypt protected notes.[string]
|
||||
--include-deleted If set to true, dump also deleted notes.
|
||||
[boolean] [default: false]
|
||||
```
|
||||
33
dump-db/dump-db.js
Executable file
33
dump-db/dump-db.js
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const yargs = require('yargs/yargs')
|
||||
const { hideBin } = require('yargs/helpers')
|
||||
const dumpService = require("./inc/dump.js");
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.command('$0 <path_to_document> <target_directory>', 'dump the contents of document.db into the target directory', (yargs) => {
|
||||
return yargs
|
||||
.positional('path_to_document', { describe: 'path to the document.db' })
|
||||
.positional('target_directory', { describe: 'path of the directory into which the notes should be dumped' })
|
||||
}, (argv) => {
|
||||
try {
|
||||
dumpService.dumpDocument(argv.path_to_document, argv.target_directory, {
|
||||
includeDeleted: argv.includeDeleted,
|
||||
password: argv.password
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Unrecoverable error:`, e);
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.option('password', {
|
||||
type: 'string',
|
||||
description: 'Set password to be able to decrypt protected notes.'
|
||||
})
|
||||
.option('include-deleted', {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'If set to true, dump also deleted notes.'
|
||||
})
|
||||
.parse();
|
||||
43
dump-db/inc/data_key.js
Normal file
43
dump-db/inc/data_key.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const crypto = require("crypto");
|
||||
const sql = require("./sql.js");
|
||||
const decryptService = require("./decrypt.js");
|
||||
|
||||
function getDataKey(password) {
|
||||
if (!password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const passwordDerivedKey = getPasswordDerivedKey(password);
|
||||
|
||||
const encryptedDataKey = getOption('encryptedDataKey');
|
||||
|
||||
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16);
|
||||
|
||||
return decryptedDataKey;
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Cannot read data key, the entered password might be wrong. The underlying error: '${e.message}', stack:\n${e.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getPasswordDerivedKey(password) {
|
||||
const salt = getOption('passwordDerivedKeySalt');
|
||||
|
||||
return getScryptHash(password, salt);
|
||||
}
|
||||
|
||||
function getScryptHash(password, salt) {
|
||||
const hashed = crypto.scryptSync(password, salt, 32,
|
||||
{N: 16384, r:8, p:1});
|
||||
|
||||
return hashed;
|
||||
}
|
||||
|
||||
function getOption(name) {
|
||||
return sql.getValue("SELECT value FROM options WHERE name = ?", [name]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getDataKey
|
||||
};
|
||||
92
dump-db/inc/decrypt.js
Normal file
92
dump-db/inc/decrypt.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const crypto = require("crypto");
|
||||
|
||||
function decryptString(dataKey, cipherText) {
|
||||
const buffer = decrypt(dataKey, cipherText);
|
||||
|
||||
if (buffer === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const str = buffer.toString('utf-8');
|
||||
|
||||
if (str === 'false') {
|
||||
throw new Error("Could not decrypt string.");
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function decrypt(key, cipherText, ivLength = 13) {
|
||||
if (cipherText === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
return "[protected]";
|
||||
}
|
||||
|
||||
try {
|
||||
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), 'base64');
|
||||
const iv = cipherTextBufferWithIv.slice(0, ivLength);
|
||||
|
||||
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', pad(key), pad(iv));
|
||||
|
||||
const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]);
|
||||
|
||||
const digest = decryptedBytes.slice(0, 4);
|
||||
const payload = decryptedBytes.slice(4);
|
||||
|
||||
const computedDigest = shaArray(payload).slice(0, 4);
|
||||
|
||||
if (!arraysIdentical(digest, computedDigest)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
catch (e) {
|
||||
// recovery from https://github.com/zadam/trilium/issues/510
|
||||
if (e.message && e.message.includes("WRONG_FINAL_BLOCK_LENGTH")) {
|
||||
log.info("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
|
||||
|
||||
return cipherText;
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pad(data) {
|
||||
if (data.length > 16) {
|
||||
data = data.slice(0, 16);
|
||||
}
|
||||
else if (data.length < 16) {
|
||||
const zeros = Array(16 - data.length).fill(0);
|
||||
|
||||
data = Buffer.concat([data, Buffer.from(zeros)]);
|
||||
}
|
||||
|
||||
return Buffer.from(data);
|
||||
}
|
||||
|
||||
function arraysIdentical(a, b) {
|
||||
let i = a.length;
|
||||
if (i !== b.length) return false;
|
||||
while (i--) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function shaArray(content) {
|
||||
// we use this as simple checksum and don't rely on its security so SHA-1 is good enough
|
||||
return crypto.createHash('sha1').update(content).digest();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
decrypt,
|
||||
decryptString
|
||||
};
|
||||
171
dump-db/inc/dump.js
Normal file
171
dump-db/inc/dump.js
Normal file
@@ -0,0 +1,171 @@
|
||||
const fs = require("fs");
|
||||
const sanitize = require("sanitize-filename");
|
||||
const sql = require("./sql.js");
|
||||
const decryptService = require("./decrypt.js");
|
||||
const dataKeyService = require("./data_key.js");
|
||||
const extensionService = require("./extension.js");
|
||||
|
||||
function dumpDocument(documentPath, targetPath, options) {
|
||||
const stats = {
|
||||
succeeded: 0,
|
||||
failed: 0,
|
||||
protected: 0,
|
||||
deleted: 0
|
||||
};
|
||||
|
||||
validatePaths(documentPath, targetPath);
|
||||
|
||||
sql.openDatabase(documentPath);
|
||||
|
||||
const dataKey = dataKeyService.getDataKey(options.password);
|
||||
|
||||
const existingPaths = {};
|
||||
const noteIdToPath = {};
|
||||
|
||||
dumpNote(targetPath, 'root');
|
||||
|
||||
printDumpResults(stats, options);
|
||||
|
||||
function dumpNote(targetPath, noteId) {
|
||||
console.log(`Reading note '${noteId}'`);
|
||||
|
||||
let childTargetPath, note, fileNameWithPath;
|
||||
|
||||
try {
|
||||
note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
|
||||
|
||||
if (note.isDeleted) {
|
||||
stats.deleted++;
|
||||
|
||||
if (!options.includeDeleted) {
|
||||
console.log(`Note '${noteId}' is deleted and --include-deleted option is not used, skipping.`);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (note.isProtected) {
|
||||
stats.protected++;
|
||||
|
||||
note.title = decryptService.decryptString(dataKey, note.title);
|
||||
}
|
||||
|
||||
let safeTitle = sanitize(note.title);
|
||||
|
||||
if (safeTitle.length > 20) {
|
||||
safeTitle = safeTitle.substring(0, 20);
|
||||
}
|
||||
|
||||
childTargetPath = targetPath + '/' + safeTitle;
|
||||
|
||||
for (let i = 1; i < 100000 && childTargetPath in existingPaths; i++) {
|
||||
childTargetPath = targetPath + '/' + safeTitle + '_' + i;
|
||||
}
|
||||
|
||||
existingPaths[childTargetPath] = true;
|
||||
|
||||
if (note.noteId in noteIdToPath) {
|
||||
const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[note.noteId]}`;
|
||||
|
||||
console.log(message);
|
||||
|
||||
fs.writeFileSync(childTargetPath, message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let {content} = sql.getRow("SELECT content FROM note_contents WHERE noteId = ?", [noteId]);
|
||||
|
||||
if (content !== null && note.isProtected && dataKey) {
|
||||
content = decryptService.decrypt(dataKey, content);
|
||||
}
|
||||
|
||||
if (isContentEmpty(content)) {
|
||||
console.log(`Note '${noteId}' is empty, skipping.`);
|
||||
} else {
|
||||
fileNameWithPath = extensionService.getFileName(note, childTargetPath, safeTitle);
|
||||
|
||||
fs.writeFileSync(fileNameWithPath, content);
|
||||
|
||||
stats.succeeded++;
|
||||
|
||||
console.log(`Dumped note '${noteId}' into ${fileNameWithPath} successfully.`);
|
||||
}
|
||||
|
||||
noteIdToPath[noteId] = childTargetPath;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`DUMPERROR: Writing '${noteId}' failed with error '${e.message}':\n${e.stack}`);
|
||||
|
||||
stats.failed++;
|
||||
}
|
||||
|
||||
const childNoteIds = sql.getColumn("SELECT noteId FROM branches WHERE parentNoteId = ?", [noteId]);
|
||||
|
||||
if (childNoteIds.length > 0) {
|
||||
if (childTargetPath === fileNameWithPath) {
|
||||
childTargetPath += '_dir';
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(childTargetPath, {recursive: true});
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`DUMPERROR: Creating directory ${childTargetPath} failed with error '${e.message}'`);
|
||||
}
|
||||
|
||||
for (const childNoteId of childNoteIds) {
|
||||
dumpNote(childTargetPath, childNoteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printDumpResults(stats, options) {
|
||||
console.log('\n----------------------- STATS -----------------------');
|
||||
console.log('Successfully dumpted notes: ', stats.succeeded.toString().padStart(5, ' '));
|
||||
console.log('Protected notes: ', stats.protected.toString().padStart(5, ' '), options.password ? '' : '(skipped)');
|
||||
console.log('Failed notes: ', stats.failed.toString().padStart(5, ' '));
|
||||
console.log('Deleted notes: ', stats.deleted.toString().padStart(5, ' '), options.includeDeleted ? "(dumped)" : "(at least, skipped)");
|
||||
console.log('-----------------------------------------------------');
|
||||
|
||||
if (!options.password && stats.protected > 0) {
|
||||
console.log("\nWARNING: protected notes are present in the document but no password has been provided. Protected notes have not been dumped.");
|
||||
}
|
||||
}
|
||||
|
||||
function isContentEmpty(content) {
|
||||
if (!content) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof content === "string") {
|
||||
return !content.trim() || content.trim() === '<p></p>';
|
||||
}
|
||||
else if (Buffer.isBuffer(content)) {
|
||||
return content.length === 0;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function validatePaths(documentPath, targetPath) {
|
||||
if (!fs.existsSync(documentPath)) {
|
||||
console.error(`Path to document '${documentPath}' has not been found. Run with --help to see usage.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
const ret = fs.mkdirSync(targetPath, {recursive: true});
|
||||
|
||||
if (!ret) {
|
||||
console.error(`Target path '${targetPath}' could not be created. Run with --help to see usage.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dumpDocument
|
||||
};
|
||||
34
dump-db/inc/extension.js
Normal file
34
dump-db/inc/extension.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const path = require("path");
|
||||
const mimeTypes = require("mime-types");
|
||||
|
||||
function getFileName(note, childTargetPath, safeTitle) {
|
||||
let existingExtension = path.extname(safeTitle).toLowerCase();
|
||||
let newExtension;
|
||||
|
||||
if (note.type === 'text') {
|
||||
newExtension = 'html';
|
||||
} else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') {
|
||||
newExtension = 'js';
|
||||
} else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
|
||||
newExtension = null;
|
||||
} else {
|
||||
if (note.mime?.toLowerCase()?.trim() === "image/jpg") { // image/jpg is invalid but pretty common
|
||||
newExtension = 'jpg';
|
||||
} else {
|
||||
newExtension = mimeTypes.extension(note.mime) || "dat";
|
||||
}
|
||||
}
|
||||
|
||||
let fileNameWithPath = childTargetPath;
|
||||
|
||||
// if the note is already named with extension (e.g. "jquery"), then it's silly to append exact same extension again
|
||||
if (newExtension && existingExtension !== "." + newExtension.toLowerCase()) {
|
||||
fileNameWithPath += "." + newExtension;
|
||||
}
|
||||
|
||||
return fileNameWithPath;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getFileName
|
||||
};
|
||||
17
dump-db/inc/sql.js
Normal file
17
dump-db/inc/sql.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const Database = require("better-sqlite3");
|
||||
let dbConnection;
|
||||
|
||||
const openDatabase = (documentPath) => { dbConnection = new Database(documentPath, { readonly: true }) };
|
||||
|
||||
const getRow = (query, params = []) => dbConnection.prepare(query).get(params);
|
||||
const getRows = (query, params = []) => dbConnection.prepare(query).all(params);
|
||||
const getValue = (query, params = []) => dbConnection.prepare(query).pluck().get(params);
|
||||
const getColumn = (query, params = []) => dbConnection.prepare(query).pluck().all(params);
|
||||
|
||||
module.exports = {
|
||||
openDatabase,
|
||||
getRow,
|
||||
getRows,
|
||||
getValue,
|
||||
getColumn
|
||||
};
|
||||
1553
dump-db/package-lock.json
generated
Normal file
1553
dump-db/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
dump-db/package.json
Normal file
25
dump-db/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "dump-db",
|
||||
"version": "1.0.0",
|
||||
"description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes",
|
||||
"main": "dump-db.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/zadam/trilium.git"
|
||||
},
|
||||
"author": "zadam",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/zadam/trilium/issues"
|
||||
},
|
||||
"homepage": "https://github.com/zadam/trilium/dump-db#readme",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "7.5.0",
|
||||
"mime-types": "2.1.34",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"yargs": "17.3.1"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,12 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* CKEditor 5 (v33.0.0) content styles.
|
||||
* Generated on Fri, 11 Mar 2022 14:34:26 GMT.
|
||||
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
|
||||
*/
|
||||
|
||||
:root {
|
||||
--ck-color-image-caption-background: hsl(0, 0%, 97%);
|
||||
--ck-color-image-caption-text: hsl(0, 0%, 20%);
|
||||
@@ -22,6 +28,43 @@
|
||||
--ck-todo-list-checkmark-size: 16px;
|
||||
}
|
||||
|
||||
/* ckeditor5-basic-styles/theme/code.css */
|
||||
.ck-content code {
|
||||
background-color: hsla(0, 0%, 78%, 0.3);
|
||||
padding: .15em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
/* ckeditor5-font/theme/fontsize.css */
|
||||
.ck-content .text-tiny {
|
||||
font-size: .7em;
|
||||
}
|
||||
/* ckeditor5-font/theme/fontsize.css */
|
||||
.ck-content .text-small {
|
||||
font-size: .85em;
|
||||
}
|
||||
/* ckeditor5-font/theme/fontsize.css */
|
||||
.ck-content .text-big {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
/* ckeditor5-font/theme/fontsize.css */
|
||||
.ck-content .text-huge {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
/* ckeditor5-block-quote/theme/blockquote.css */
|
||||
.ck-content blockquote {
|
||||
overflow: hidden;
|
||||
padding-right: 1.5em;
|
||||
padding-left: 1.5em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-style: italic;
|
||||
border-left: solid 5px hsl(0, 0%, 80%);
|
||||
}
|
||||
/* ckeditor5-block-quote/theme/blockquote.css */
|
||||
.ck-content[dir="rtl"] blockquote {
|
||||
border-left: 0;
|
||||
border-right: solid 5px hsl(0, 0%, 80%);
|
||||
}
|
||||
/* ckeditor5-highlight/theme/highlight.css */
|
||||
.ck-content .marker-yellow {
|
||||
background-color: var(--ck-highlight-marker-yellow);
|
||||
@@ -48,21 +91,53 @@
|
||||
color: var(--ck-highlight-pen-green);
|
||||
background-color: transparent;
|
||||
}
|
||||
/* ckeditor5-font/theme/fontsize.css */
|
||||
.ck-content .text-tiny {
|
||||
font-size: .7em;
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image {
|
||||
display: table;
|
||||
clear: both;
|
||||
text-align: center;
|
||||
margin: 0.9em auto;
|
||||
min-width: 50px;
|
||||
}
|
||||
/* ckeditor5-font/theme/fontsize.css */
|
||||
.ck-content .text-small {
|
||||
font-size: .85em;
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
/* ckeditor5-font/theme/fontsize.css */
|
||||
.ck-content .text-big {
|
||||
font-size: 1.4em;
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image-inline {
|
||||
/*
|
||||
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
|
||||
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
|
||||
* This strange behavior does not happen with inline-flex.
|
||||
*/
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
/* ckeditor5-font/theme/fontsize.css */
|
||||
.ck-content .text-huge {
|
||||
font-size: 1.8em;
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image-inline picture {
|
||||
display: flex;
|
||||
}
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image-inline picture,
|
||||
.ck-content .image-inline img {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* ckeditor5-image/theme/imagecaption.css */
|
||||
.ck-content .image > figcaption {
|
||||
display: table-caption;
|
||||
caption-side: bottom;
|
||||
word-break: break-word;
|
||||
color: var(--ck-color-image-caption-text);
|
||||
background-color: var(--ck-color-image-caption-background);
|
||||
padding: .6em;
|
||||
font-size: .75em;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
/* ckeditor5-image/theme/imagestyle.css */
|
||||
.ck-content .image-style-block-align-left,
|
||||
@@ -125,54 +200,6 @@
|
||||
.ck-content .image-inline.image-style-align-right {
|
||||
margin-left: var(--ck-inline-image-style-spacing);
|
||||
}
|
||||
/* ckeditor5-image/theme/imagecaption.css */
|
||||
.ck-content .image > figcaption {
|
||||
display: table-caption;
|
||||
caption-side: bottom;
|
||||
word-break: break-word;
|
||||
color: var(--ck-color-image-caption-text);
|
||||
background-color: var(--ck-color-image-caption-background);
|
||||
padding: .6em;
|
||||
font-size: .75em;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image {
|
||||
display: table;
|
||||
clear: both;
|
||||
text-align: center;
|
||||
margin: 0.9em auto;
|
||||
min-width: 50px;
|
||||
}
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image-inline {
|
||||
/*
|
||||
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
|
||||
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
|
||||
* This strange behavior does not happen with inline-flex.
|
||||
*/
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image-inline picture {
|
||||
display: flex;
|
||||
}
|
||||
/* ckeditor5-image/theme/image.css */
|
||||
.ck-content .image-inline picture,
|
||||
.ck-content .image-inline img {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized {
|
||||
max-width: 100%;
|
||||
@@ -187,141 +214,9 @@
|
||||
.ck-content .image.image_resized > figcaption {
|
||||
display: block;
|
||||
}
|
||||
/* ckeditor5-code-block/theme/codeblock.css */
|
||||
.ck-content pre {
|
||||
padding: 1em;
|
||||
color: hsl(0, 0%, 20.8%);
|
||||
background: hsla(0, 0%, 78%, 0.3);
|
||||
border: 1px solid hsl(0, 0%, 77%);
|
||||
border-radius: 2px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
tab-size: 4;
|
||||
white-space: pre-wrap;
|
||||
font-style: normal;
|
||||
min-width: 200px;
|
||||
}
|
||||
/* ckeditor5-code-block/theme/codeblock.css */
|
||||
.ck-content pre code {
|
||||
background: unset;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
/* ckeditor5-horizontal-line/theme/horizontalline.css */
|
||||
.ck-content hr {
|
||||
margin: 15px 0;
|
||||
height: 4px;
|
||||
background: hsl(0, 0%, 87%);
|
||||
border: 0;
|
||||
}
|
||||
/* ckeditor5-block-quote/theme/blockquote.css */
|
||||
.ck-content blockquote {
|
||||
overflow: hidden;
|
||||
padding-right: 1.5em;
|
||||
padding-left: 1.5em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
/* ckeditor5-language/theme/language.css */
|
||||
.ck-content span[lang] {
|
||||
font-style: italic;
|
||||
border-left: solid 5px hsl(0, 0%, 80%);
|
||||
}
|
||||
/* ckeditor5-block-quote/theme/blockquote.css */
|
||||
.ck-content[dir="rtl"] blockquote {
|
||||
border-left: 0;
|
||||
border-right: solid 5px hsl(0, 0%, 80%);
|
||||
}
|
||||
/* ckeditor5-basic-styles/theme/code.css */
|
||||
.ck-content code {
|
||||
background-color: hsla(0, 0%, 78%, 0.3);
|
||||
padding: .15em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content .table {
|
||||
margin: 0.9em auto;
|
||||
display: table;
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content .table table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px double hsl(0, 0%, 70%);
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content .table table td,
|
||||
.ck-content .table table th {
|
||||
min-width: 2em;
|
||||
padding: .4em;
|
||||
border: 1px solid hsl(0, 0%, 75%);
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content .table table th {
|
||||
font-weight: bold;
|
||||
background: hsla(0, 0%, 0%, 5%);
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content[dir="rtl"] .table th {
|
||||
text-align: right;
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content[dir="ltr"] .table th {
|
||||
text-align: left;
|
||||
}
|
||||
/* ckeditor5-table/theme/tablecaption.css */
|
||||
.ck-content .table > figcaption {
|
||||
display: table-caption;
|
||||
caption-side: top;
|
||||
word-break: break-word;
|
||||
text-align: center;
|
||||
color: var(--ck-color-table-caption-text);
|
||||
background-color: var(--ck-color-table-caption-background);
|
||||
padding: .6em;
|
||||
font-size: .75em;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||
.ck-content .page-break {
|
||||
position: relative;
|
||||
clear: both;
|
||||
padding: 5px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||
.ck-content .page-break::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-bottom: 2px dashed hsl(0, 0%, 77%);
|
||||
width: 100%;
|
||||
}
|
||||
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||
.ck-content .page-break__label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: .3em .6em;
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid hsl(0, 0%, 77%);
|
||||
border-radius: 2px;
|
||||
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
color: hsl(0, 0%, 20%);
|
||||
background: hsl(0, 0%, 100%);
|
||||
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
/* ckeditor5-media-embed/theme/mediaembed.css */
|
||||
.ck-content .media {
|
||||
clear: both;
|
||||
margin: 0.9em 0;
|
||||
display: block;
|
||||
min-width: 15em;
|
||||
}
|
||||
/* ckeditor5-list/theme/todolist.css */
|
||||
.ck-content .todo-list {
|
||||
@@ -390,9 +285,120 @@
|
||||
.ck-content .todo-list .todo-list__label .todo-list__label__description {
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* ckeditor5-language/theme/language.css */
|
||||
.ck-content span[lang] {
|
||||
font-style: italic;
|
||||
/* ckeditor5-media-embed/theme/mediaembed.css */
|
||||
.ck-content .media {
|
||||
clear: both;
|
||||
margin: 0.9em 0;
|
||||
display: block;
|
||||
min-width: 15em;
|
||||
}
|
||||
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||
.ck-content .page-break {
|
||||
position: relative;
|
||||
clear: both;
|
||||
padding: 5px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||
.ck-content .page-break::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-bottom: 2px dashed hsl(0, 0%, 77%);
|
||||
width: 100%;
|
||||
}
|
||||
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||
.ck-content .page-break__label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: .3em .6em;
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid hsl(0, 0%, 77%);
|
||||
border-radius: 2px;
|
||||
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
color: hsl(0, 0%, 20%);
|
||||
background: hsl(0, 0%, 100%);
|
||||
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content .table {
|
||||
margin: 0.9em auto;
|
||||
display: table;
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content .table table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px double hsl(0, 0%, 70%);
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content .table table td,
|
||||
.ck-content .table table th {
|
||||
min-width: 2em;
|
||||
padding: .4em;
|
||||
border: 1px solid hsl(0, 0%, 75%);
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content .table table th {
|
||||
font-weight: bold;
|
||||
background: hsla(0, 0%, 0%, 5%);
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content[dir="rtl"] .table th {
|
||||
text-align: right;
|
||||
}
|
||||
/* ckeditor5-table/theme/table.css */
|
||||
.ck-content[dir="ltr"] .table th {
|
||||
text-align: left;
|
||||
}
|
||||
/* ckeditor5-table/theme/tablecaption.css */
|
||||
.ck-content .table > figcaption {
|
||||
display: table-caption;
|
||||
caption-side: top;
|
||||
word-break: break-word;
|
||||
text-align: center;
|
||||
color: var(--ck-color-table-caption-text);
|
||||
background-color: var(--ck-color-table-caption-background);
|
||||
padding: .6em;
|
||||
font-size: .75em;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
/* ckeditor5-code-block/theme/codeblock.css */
|
||||
.ck-content pre {
|
||||
padding: 1em;
|
||||
color: hsl(0, 0%, 20.8%);
|
||||
background: hsla(0, 0%, 78%, 0.3);
|
||||
border: 1px solid hsl(0, 0%, 77%);
|
||||
border-radius: 2px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
tab-size: 4;
|
||||
white-space: pre-wrap;
|
||||
font-style: normal;
|
||||
min-width: 200px;
|
||||
}
|
||||
/* ckeditor5-code-block/theme/codeblock.css */
|
||||
.ck-content pre code {
|
||||
background: unset;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
/* ckeditor5-horizontal-line/theme/horizontalline.css */
|
||||
.ck-content hr {
|
||||
margin: 15px 0;
|
||||
height: 4px;
|
||||
background: hsl(0, 0%, 87%);
|
||||
border: 0;
|
||||
}
|
||||
/* ckeditor5-mention/theme/mention.css */
|
||||
.ck-content .mention {
|
||||
@@ -409,4 +415,3 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
libraries/ckeditor/ckeditor.js
vendored
5
libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
libraries/wheel-zoom.min.js
vendored
2
libraries/wheel-zoom.min.js
vendored
File diff suppressed because one or more lines are too long
1822
package-lock.json
generated
1822
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.50.3",
|
||||
"version": "0.51.0-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -26,38 +26,38 @@
|
||||
"dependencies": {
|
||||
"archiver": "5.3.0",
|
||||
"async-mutex": "0.3.2",
|
||||
"axios": "0.25.0",
|
||||
"axios": "0.26.1",
|
||||
"better-sqlite3": "7.4.5",
|
||||
"chokidar": "3.5.3",
|
||||
"cls-hooked": "4.2.2",
|
||||
"commonmark": "0.30.0",
|
||||
"cookie-parser": "1.4.6",
|
||||
"csurf": "1.11.0",
|
||||
"dayjs": "1.10.7",
|
||||
"dayjs": "1.11.0",
|
||||
"ejs": "3.1.6",
|
||||
"electron-debug": "3.2.0",
|
||||
"electron-dl": "3.3.0",
|
||||
"electron-dl": "3.3.1",
|
||||
"electron-find": "1.0.7",
|
||||
"electron-window-state": "5.0.3",
|
||||
"@electron/remote": "2.0.8",
|
||||
"express": "4.17.2",
|
||||
"express": "4.17.3",
|
||||
"express-partial-content": "1.0.2",
|
||||
"express-rate-limit": "6.2.0",
|
||||
"express-rate-limit": "6.3.0",
|
||||
"express-session": "1.17.2",
|
||||
"fs-extra": "10.0.0",
|
||||
"fs-extra": "10.0.1",
|
||||
"helmet": "5.0.2",
|
||||
"html": "1.0.0",
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "5.0.0",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"image-type": "4.1.0",
|
||||
"ini": "2.0.0",
|
||||
"ini": "3.0.0",
|
||||
"is-animated": "2.0.2",
|
||||
"is-svg": "4.3.2",
|
||||
"jimp": "0.16.1",
|
||||
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||
"jsdom": "19.0.0",
|
||||
"mime-types": "2.1.34",
|
||||
"mime-types": "2.1.35",
|
||||
"multer": "1.4.4",
|
||||
"node-abi": "3.8.0",
|
||||
"normalize-strings": "1.1.1",
|
||||
@@ -67,9 +67,9 @@
|
||||
"request": "2.88.2",
|
||||
"rimraf": "3.0.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.6.1",
|
||||
"sanitize-html": "2.7.0",
|
||||
"sax": "1.2.4",
|
||||
"semver": "7.3.5",
|
||||
"semver": "7.3.6",
|
||||
"serve-favicon": "2.5.0",
|
||||
"session-file-store": "1.5.0",
|
||||
"stream-throttle": "0.1.3",
|
||||
@@ -77,13 +77,13 @@
|
||||
"tmp": "0.2.1",
|
||||
"turndown": "7.1.1",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "8.4.2",
|
||||
"ws": "8.5.0",
|
||||
"yauzl": "2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "16.1.0",
|
||||
"electron-builder": "22.14.5",
|
||||
"electron": "16.2.1",
|
||||
"electron-builder": "23.0.3",
|
||||
"electron-packager": "15.4.0",
|
||||
"electron-rebuild": "3.2.7",
|
||||
"esm": "3.2.25",
|
||||
@@ -91,7 +91,7 @@
|
||||
"jsdoc": "3.6.10",
|
||||
"lorem-ipsum": "2.0.4",
|
||||
"rcedit": "3.0.1",
|
||||
"webpack": "5.68.0",
|
||||
"webpack": "5.72.0",
|
||||
"webpack-cli": "4.9.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
12
src/etapi/app_info.js
Normal file
12
src/etapi/app_info.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const appInfo = require('../services/app_info');
|
||||
const eu = require("./etapi_utils.js");
|
||||
|
||||
function register(router) {
|
||||
eu.route(router, 'get', '/etapi/app-info', (req, res, next) => {
|
||||
res.status(200).json(appInfo);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register
|
||||
};
|
||||
@@ -589,6 +589,24 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/app-info:
|
||||
get:
|
||||
description: returns information about the running Trilium instance
|
||||
operationId: getAppInfo
|
||||
responses:
|
||||
'200':
|
||||
description: app info
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AppInfo'
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
EtapiTokenAuth:
|
||||
@@ -777,6 +795,48 @@ components:
|
||||
type: string
|
||||
pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z'
|
||||
example: 2021-12-31 19:18:11.939Z
|
||||
AppInfo:
|
||||
type: object
|
||||
required:
|
||||
- statu
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
appVersion:
|
||||
type: string
|
||||
description: Trilium version
|
||||
example: 0.50.2
|
||||
dbVersion:
|
||||
type: integer
|
||||
format: int32
|
||||
description: DB version
|
||||
example: 194
|
||||
syncVersion:
|
||||
type: integer
|
||||
format: int32
|
||||
description: Sync protocol version
|
||||
example: 25
|
||||
buildDate:
|
||||
type: string
|
||||
format: date-time
|
||||
description: build date
|
||||
example: 2022-02-09T22:52:36+01:00
|
||||
buildRevision:
|
||||
type: string
|
||||
description: git build revision
|
||||
example: 23daaa2387a0655685377f0a541d154aeec2aae8
|
||||
dataDirectory:
|
||||
type: string
|
||||
description: data directory where Trilium stores files
|
||||
example: /home/user/data
|
||||
clipperProtocolVersion:
|
||||
type: string
|
||||
description: version of the supported Trilium Web Clipper protocol
|
||||
example: 1.0
|
||||
utcDateTime:
|
||||
type: string
|
||||
description: current UTC date time
|
||||
example: 2022-03-07T21:54:25.277Z
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
|
||||
@@ -46,7 +46,7 @@ function register(router) {
|
||||
'mime': [v.notNull, v.isString],
|
||||
'content': [v.notNull, v.isString],
|
||||
'notePosition': [v.notNull, v.isInteger],
|
||||
'prefix': [v.notNull, v.isInteger],
|
||||
'prefix': [v.notNull, v.isString],
|
||||
'isExpanded': [v.notNull, v.isBoolean],
|
||||
'noteId': [v.notNull, v.isValidEntityId],
|
||||
'branchId': [v.notNull, v.isValidEntityId],
|
||||
|
||||
@@ -25,10 +25,20 @@ const TPL = `
|
||||
|
||||
<h4>Anonymize database</h4>
|
||||
|
||||
<h5>Full anonymization</h5>
|
||||
|
||||
<p>This action will create a new copy of the database and anonymize it (remove all note content and leave only structure and some non-sensitive metadata)
|
||||
for sharing online for debugging purposes without fear of leaking your personal data.</p>
|
||||
|
||||
<button id="anonymize-button" class="btn">Save anonymized database</button><br/><br/>
|
||||
<button id="anonymize-full-button" class="btn">Save fully anonymized database</button><br/><br/>
|
||||
|
||||
<h5>Light anonymization</h5>
|
||||
|
||||
<p>This action will create a new copy of the database and do a light anonymization on it - specifically only content of all notes will be removed, but titles and attributes will remaing. Additionally, custom JS frontend/backend script notes and custom widgets will remain. This provides more context to debug the issues.</p>
|
||||
|
||||
<p>You can decide yourself if you want to provide fully or lightly anonymized database. Even fully anonymized DB is very useful, however in some cases lightly anonymized database can speed up the process of bug identification and fixing.</p>
|
||||
|
||||
<button id="anonymize-light-button" class="btn">Save lightly anonymized database</button><br/><br/>
|
||||
|
||||
<h4>Vacuum database</h4>
|
||||
|
||||
@@ -42,7 +52,8 @@ export default class AdvancedOptions {
|
||||
|
||||
this.$forceFullSyncButton = $("#force-full-sync-button");
|
||||
this.$fillEntityChangesButton = $("#fill-entity-changes-button");
|
||||
this.$anonymizeButton = $("#anonymize-button");
|
||||
this.$anonymizeFullButton = $("#anonymize-full-button");
|
||||
this.$anonymizeLightButton = $("#anonymize-light-button");
|
||||
this.$vacuumDatabaseButton = $("#vacuum-database-button");
|
||||
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
|
||||
this.$checkIntegrityButton = $("#check-integrity-button");
|
||||
@@ -59,14 +70,25 @@ export default class AdvancedOptions {
|
||||
toastService.showMessage("Sync rows filled successfully");
|
||||
});
|
||||
|
||||
this.$anonymizeButton.on('click', async () => {
|
||||
const resp = await server.post('database/anonymize');
|
||||
this.$anonymizeFullButton.on('click', async () => {
|
||||
const resp = await server.post('database/anonymize/full');
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError("Could not create anonymized database, check backend logs for details");
|
||||
}
|
||||
else {
|
||||
toastService.showMessage(`Created anonymized database in ${resp.anonymizedFilePath}`, 10000);
|
||||
toastService.showMessage(`Created fully anonymized database in ${resp.anonymizedFilePath}`, 10000);
|
||||
}
|
||||
});
|
||||
|
||||
this.$anonymizeLightButton.on('click', async () => {
|
||||
const resp = await server.post('database/anonymize/light');
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError("Could not create anonymized database, check backend logs for details");
|
||||
}
|
||||
else {
|
||||
toastService.showMessage(`Created lightly anonymized database in ${resp.anonymizedFilePath}`, 10000);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -215,6 +215,10 @@ const ATTR_HELP = {
|
||||
"shareAlias": "define an alias using which the note will be available under https://your_trilium_host/share/[your_alias]",
|
||||
"shareOmitDefaultCss": "default share page CSS will be omitted. Use when you make extensive styling changes.",
|
||||
"shareRoot": "marks note which is served on /share root.",
|
||||
"shareRaw": "note will be served in its raw format, without HTML wrapper",
|
||||
"shareDisallowRobotIndexing": `will forbid robot indexing of this note via <code>X-Robots-Tag: noindex</code> header`,
|
||||
"displayRelations": "comma delimited names of relations which should be displayed. All other ones will be hidden.",
|
||||
"hideRelations": "comma delimited names of relations which should be hidden. All other ones will be displayed.",
|
||||
},
|
||||
"relation": {
|
||||
"runOnNoteCreation": "executes when note is created on backend",
|
||||
|
||||
@@ -17,11 +17,12 @@ const TPL = `<div class="mermaid-widget">
|
||||
.mermaid-render {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="mermaid-error alert alert-warning">
|
||||
<p><strong>The diagram could not displayed. See <a href="https://mermaid-js.github.io/mermaid/#/flowchart?id=graph">help and examples</a>.</strong></p>
|
||||
<p><strong>The diagram could not be displayed. See <a href="https://mermaid-js.github.io/mermaid/#/flowchart?id=graph">help and examples</a>.</strong></p>
|
||||
<p class="error-content"></p>
|
||||
</div>
|
||||
|
||||
@@ -70,8 +71,25 @@ export default class MermaidWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$display.empty();
|
||||
|
||||
const libLoaded = libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM);
|
||||
|
||||
try {
|
||||
mermaid.mermaidAPI.render('mermaid-graph-' + idCounter++, content, content => this.$display.html(content));
|
||||
const idNumber = idCounter++;
|
||||
|
||||
mermaid.mermaidAPI.render('mermaid-graph-' + idNumber, content, async content => {
|
||||
this.$display.html(content);
|
||||
|
||||
await libLoaded;
|
||||
|
||||
this.$display.attr("id", 'mermaid-render-' + idNumber);
|
||||
|
||||
WZoom.create('#mermaid-render-' + idNumber, {
|
||||
type: 'html',
|
||||
maxScale: 10,
|
||||
speed: 20,
|
||||
zoomOnClick: false
|
||||
});
|
||||
});
|
||||
|
||||
this.$errorContainer.hide();
|
||||
} catch (e) {
|
||||
|
||||
@@ -275,7 +275,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
const label = attrs.find(attr =>
|
||||
attr.type === 'label'
|
||||
&& ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'displayRelations'].includes(attr.name)
|
||||
&& ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'displayRelations', 'hideRelations'].includes(attr.name)
|
||||
&& attributeService.isAffecting(attr, this.note));
|
||||
|
||||
const relation = attrs.find(attr =>
|
||||
|
||||
@@ -23,6 +23,7 @@ import Limit from "../search_options/limit.js";
|
||||
import DeleteNoteRevisionsSearchAction from "../search_actions/delete_note_revisions.js";
|
||||
import Debug from "../search_options/debug.js";
|
||||
import appContext from "../../services/app_context.js";
|
||||
import MoveNoteSearchAction from "../search_actions/move_note.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="search-definition-widget">
|
||||
@@ -127,10 +128,14 @@ const TPL = `
|
||||
action
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#" data-action-add="moveNote">
|
||||
Move note</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteNote">
|
||||
Delete note</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteNoteRevisions">
|
||||
Delete note revisions</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="moveNote">
|
||||
Delete note revisions</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteLabel">
|
||||
Delete label</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteRelation">
|
||||
@@ -193,6 +198,7 @@ const OPTION_CLASSES = [
|
||||
const ACTION_CLASSES = {};
|
||||
|
||||
for (const clazz of [
|
||||
MoveNoteSearchAction,
|
||||
DeleteNoteSearchAction,
|
||||
DeleteNoteRevisionsSearchAction,
|
||||
DeleteLabelSearchAction,
|
||||
|
||||
58
src/public/app/widgets/search_actions/move_note.js
Normal file
58
src/public/app/widgets/search_actions/move_note.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Move note</div>
|
||||
|
||||
<div style="margin-right: 10px;" class="text-nowrap">to</div>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control target-parent-note" placeholder="target parent note"/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="button-column">
|
||||
<div class="dropdown help-dropdown">
|
||||
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu dropdown-menu-right p-4">
|
||||
<p>On all matched notes:</p>
|
||||
|
||||
<ul>
|
||||
<li>move note to the new parent if note has only one parent (i.e. the old placement is removed and new placement into the new parent is created)</li>
|
||||
<li>clone note to the new parent if note has multiple clones/placements (it's not clear which placement should be removed)</li>
|
||||
<li>nothing will happen if note cannot be moved to the target note (i.e. this would create a tree cycle)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="bx bx-x icon-action action-conf-del"></span>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class MoveNoteSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "moveNote"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
||||
const $targetParentNote = $action.find('.target-parent-note');
|
||||
noteAutocompleteService.initNoteAutocomplete($targetParentNote);
|
||||
$targetParentNote.setNote(this.actionDef.targetParentNoteId);
|
||||
|
||||
$targetParentNote.on('autocomplete:closed', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.saveAction({
|
||||
targetParentNoteId: $targetParentNote.getSelectedNoteId()
|
||||
});
|
||||
}, 1000)
|
||||
|
||||
$targetParentNote.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const TPL = `
|
||||
<tr>
|
||||
<td class="title-column">Search string:</td>
|
||||
<td>
|
||||
<input type="text" class="form-control search-string" placeholder="fulltext keywords, #tag = value ...">
|
||||
<textarea class="form-control search-string" placeholder="fulltext keywords, #tag = value ..."></textarea>
|
||||
</td>
|
||||
<td class="button-column">
|
||||
<div class="dropdown help-dropdown">
|
||||
@@ -46,6 +46,9 @@ export default class SearchString extends AbstractSearchOption {
|
||||
this.$searchString.on('input', () => this.spacedUpdate.scheduleUpdate());
|
||||
|
||||
utils.bindElShortcut(this.$searchString, 'return', async () => {
|
||||
// this also in effect disallows new lines in query string.
|
||||
// on one hand this makes sense since search string is a label
|
||||
// on the other hand it could be nice for structuring long search string. It's probably a niche case though.
|
||||
await this.spacedUpdate.updateNowIfNecessary();
|
||||
|
||||
this.triggerCommand('refreshResults');
|
||||
|
||||
@@ -49,6 +49,7 @@ class ImageTypeWidget extends TypeWidget {
|
||||
libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM).then(() => {
|
||||
WZoom.create('#' + this.$imageView.attr("id"), {
|
||||
maxScale: 10,
|
||||
speed: 20,
|
||||
zoomOnClick: false
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ const treeService = require('../../services/tree');
|
||||
const noteService = require('../../services/notes');
|
||||
const becca = require('../../becca/becca');
|
||||
const TaskContext = require('../../services/task_context');
|
||||
const branchService = require("../../services/branches");
|
||||
const log = require("../../services/log.js");
|
||||
|
||||
/**
|
||||
* Code in this file deals with moving and cloning branches. Relationship between note and parent note is unique
|
||||
@@ -23,29 +25,7 @@ function moveBranchToParent(req) {
|
||||
return [400, `One or both branches ${branchId}, ${parentBranchId} have not been found`];
|
||||
}
|
||||
|
||||
if (branchToMove.parentNoteId === parentBranch.noteId) {
|
||||
return { success: true }; // no-op
|
||||
}
|
||||
|
||||
const validationResult = treeService.validateParentChild(parentBranch.noteId, branchToMove.noteId, branchId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return [200, validationResult];
|
||||
}
|
||||
|
||||
const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentBranch.noteId]);
|
||||
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
|
||||
|
||||
// expanding so that the new placement of the branch is immediately visible
|
||||
parentBranch.isExpanded = true;
|
||||
parentBranch.save();
|
||||
|
||||
const newBranch = branchToMove.createClone(parentBranch.noteId, newNotePos);
|
||||
newBranch.save();
|
||||
|
||||
branchToMove.markAsDeleted();
|
||||
|
||||
return { success: true };
|
||||
return branchService.moveBranchToBranch(branchToMove, parentBranch, branchId);
|
||||
}
|
||||
|
||||
function moveBranchBeforeNote(req) {
|
||||
@@ -101,6 +81,8 @@ function moveBranchBeforeNote(req) {
|
||||
// if sorting is not needed then still the ordering might have changed above manually
|
||||
entityChangesService.addNoteReorderingEntityChange(parentNote.noteId);
|
||||
|
||||
log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} before note ${beforeBranch.noteId}, branch ${beforeBranchId}`);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -150,6 +132,8 @@ function moveBranchAfterNote(req) {
|
||||
// if sorting is not needed then still the ordering might have changed above manually
|
||||
entityChangesService.addNoteReorderingEntityChange(parentNote.noteId);
|
||||
|
||||
log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} after note ${afterNote.noteId}, branch ${afterBranchId}`);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +144,9 @@ function processContent(images, note, content) {
|
||||
}
|
||||
}
|
||||
|
||||
// fallback if parsing/downloading images fails for some reason on the extension side (
|
||||
rewrittenContent = noteService.downloadImages(note.noteId, rewrittenContent);
|
||||
|
||||
return rewrittenContent;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ const backupService = require('../../services/backup');
|
||||
const anonymizationService = require('../../services/anonymization');
|
||||
const consistencyChecksService = require('../../services/consistency_checks');
|
||||
|
||||
async function anonymize() {
|
||||
return await anonymizationService.createAnonymizedCopy();
|
||||
async function anonymize(req) {
|
||||
return await anonymizationService.createAnonymizedCopy(req.params.type);
|
||||
}
|
||||
|
||||
async function backupDatabase() {
|
||||
|
||||
@@ -153,7 +153,10 @@ function getRelationMap(req) {
|
||||
.split(",")
|
||||
.map(token => token.trim());
|
||||
|
||||
console.log("displayRelations", displayRelations);
|
||||
const hideRelationsVal = relationMapNote.getLabelValue('hideRelations');
|
||||
const hideRelations = !hideRelationsVal ? [] : hideRelationsVal
|
||||
.split(",")
|
||||
.map(token => token.trim());
|
||||
|
||||
const foundNoteIds = sql.getColumn(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
|
||||
const notes = becca.getNotes(foundNoteIds);
|
||||
@@ -163,7 +166,9 @@ function getRelationMap(req) {
|
||||
|
||||
resp.relations = resp.relations.concat(note.getRelations()
|
||||
.filter(relation => !relation.isAutoLink() || displayRelations.includes(relation.name))
|
||||
.filter(relation => displayRelations.length === 0 || displayRelations.includes(relation.name))
|
||||
.filter(relation => displayRelations.length > 0
|
||||
? displayRelations.includes(relation.name)
|
||||
: !hideRelations.includes(relation.name))
|
||||
.filter(relation => noteIds.includes(relation.value))
|
||||
.map(relation => ({
|
||||
attributeId: relation.attributeId,
|
||||
|
||||
@@ -6,6 +6,8 @@ const log = require('../../services/log');
|
||||
const scriptService = require('../../services/script');
|
||||
const searchService = require('../../services/search/services/search');
|
||||
const noteRevisionService = require("../../services/note_revisions");
|
||||
const branchService = require("../../services/branches");
|
||||
const cloningService = require("../../services/cloning");
|
||||
const {formatAttrForSearch} = require("../../services/attribute_formatter");
|
||||
|
||||
async function searchFromNoteInt(note) {
|
||||
@@ -92,6 +94,26 @@ const ACTION_HANDLERS = {
|
||||
setRelationTarget: (action, note) => {
|
||||
note.setRelation(action.relationName, action.targetNoteId);
|
||||
},
|
||||
moveNote: (action, note) => {
|
||||
const targetParentNote = becca.getNote(action.targetParentNoteId);
|
||||
|
||||
if (!targetParentNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
let res;
|
||||
|
||||
if (note.getParentBranches().length > 1) {
|
||||
res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId);
|
||||
}
|
||||
else {
|
||||
res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
|
||||
}
|
||||
|
||||
if (!res.success) {
|
||||
log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`);
|
||||
}
|
||||
},
|
||||
executeScript: (action, note) => {
|
||||
if (!action.script || !action.script.trim()) {
|
||||
log.info("Ignoring executeScript since the script is empty.")
|
||||
|
||||
@@ -29,6 +29,8 @@ function uploadImage(req) {
|
||||
}
|
||||
}
|
||||
|
||||
note.setLabel("sentFromSender");
|
||||
|
||||
return {
|
||||
noteId: noteId
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@ const fontsRoute = require('./api/fonts');
|
||||
const etapiTokensApiRoutes = require('./api/etapi_tokens');
|
||||
const shareRoutes = require('../share/routes');
|
||||
const etapiAuthRoutes = require('../etapi/auth');
|
||||
const etapiAppInfoRoutes = require('../etapi/app_info');
|
||||
const etapiAttributeRoutes = require('../etapi/attributes');
|
||||
const etapiBranchRoutes = require('../etapi/branches');
|
||||
const etapiNoteRoutes = require('../etapi/notes');
|
||||
@@ -324,7 +325,7 @@ function register(app) {
|
||||
|
||||
apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema);
|
||||
apiRoute(POST, '/api/sql/execute/:noteId', sqlRoute.execute);
|
||||
route(POST, '/api/database/anonymize', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false);
|
||||
route(POST, '/api/database/anonymize/:type', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false);
|
||||
|
||||
// backup requires execution outside of transaction
|
||||
route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false);
|
||||
@@ -391,6 +392,7 @@ function register(app) {
|
||||
shareRoutes.register(router);
|
||||
|
||||
etapiAuthRoutes.register(router);
|
||||
etapiAppInfoRoutes.register(router);
|
||||
etapiAttributeRoutes.register(router);
|
||||
etapiBranchRoutes.register(router);
|
||||
etapiNoteRoutes.register(router);
|
||||
|
||||
@@ -5,7 +5,7 @@ const dateUtils = require("./date_utils");
|
||||
const Database = require("better-sqlite3");
|
||||
const sql = require("./sql");
|
||||
|
||||
function getAnonymizationScript() {
|
||||
function getFullAnonymizationScript() {
|
||||
// we want to delete all non-builtin attributes because they can contain sensitive names and values
|
||||
// on the other hand builtin/system attrs should not contain any sensitive info
|
||||
const builtinAttrNames = BUILTIN_ATTRIBUTES
|
||||
@@ -33,18 +33,37 @@ VACUUM;
|
||||
return anonymizeScript;
|
||||
}
|
||||
|
||||
async function createAnonymizedCopy() {
|
||||
function getLightAnonymizationScript() {
|
||||
return `
|
||||
UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL AND noteId NOT IN (
|
||||
SELECT noteId FROM notes WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
|
||||
);
|
||||
UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL AND noteRevisionId NOT IN (
|
||||
SELECT noteRevisionId FROM note_revisions WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
|
||||
);
|
||||
`;
|
||||
}
|
||||
|
||||
async function createAnonymizedCopy(type) {
|
||||
if (!['full', 'light'].includes(type)) {
|
||||
throw new Error(`Unrecognized anonymization type '${type}'`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
|
||||
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
|
||||
}
|
||||
|
||||
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
|
||||
const anonymizedFile = `${dataDir.ANONYMIZED_DB_DIR}/anonymized-${type}-${dateUtils.getDateTimeForFile()}.db`;
|
||||
|
||||
await sql.copyDatabase(anonymizedFile);
|
||||
|
||||
const db = new Database(anonymizedFile);
|
||||
|
||||
db.exec(getAnonymizationScript());
|
||||
const anonymizationScript = type === 'light'
|
||||
? getLightAnonymizationScript()
|
||||
: getFullAnonymizationScript();
|
||||
|
||||
db.exec(anonymizationScript);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -55,6 +74,6 @@ async function createAnonymizedCopy() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAnonymizationScript,
|
||||
getFullAnonymizationScript,
|
||||
createAnonymizedCopy
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
const build = require('./build');
|
||||
const dateUtils = require('./date_utils');
|
||||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
@@ -17,5 +16,5 @@ module.exports = {
|
||||
buildRevision: build.buildRevision,
|
||||
dataDirectory: TRILIUM_DATA_DIR,
|
||||
clipperProtocolVersion: CLIPPER_PROTOCOL_VERSION,
|
||||
utcDateTime: dateUtils.utcNowDateTime() // for timezone inference
|
||||
utcDateTime: new Date().toISOString() // for timezone inference
|
||||
};
|
||||
|
||||
@@ -128,10 +128,6 @@ function isAttributeDangerous(type, name) {
|
||||
);
|
||||
}
|
||||
|
||||
function getBuiltinAttributeNames() {
|
||||
return BUILTIN_ATTRIBUTES;
|
||||
}
|
||||
|
||||
function sanitizeAttributeName(origName) {
|
||||
let fixedName;
|
||||
|
||||
@@ -156,6 +152,5 @@ module.exports = {
|
||||
getAttributeNames,
|
||||
isAttributeType,
|
||||
isAttributeDangerous,
|
||||
getBuiltinAttributeNames,
|
||||
sanitizeAttributeName
|
||||
};
|
||||
|
||||
@@ -94,6 +94,12 @@ function reject(req, res, message) {
|
||||
function checkCredentials(req, res, next) {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
res.status(400).send('Database is not initialized yet.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passwordService.isPasswordSet()) {
|
||||
res.status(400).send('Password has not been set yet. Please set a password and repeat the action');
|
||||
return;
|
||||
}
|
||||
|
||||
const header = req.headers['trilium-cred'] || '';
|
||||
|
||||
46
src/services/branches.js
Normal file
46
src/services/branches.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const treeService = require("./tree.js");
|
||||
const sql = require("./sql.js");
|
||||
|
||||
function moveBranchToNote(sourceBranch, targetParentNoteId) {
|
||||
if (sourceBranch.parentNoteId === targetParentNoteId) {
|
||||
return {success: true}; // no-op
|
||||
}
|
||||
|
||||
const validationResult = treeService.validateParentChild(targetParentNoteId, sourceBranch.noteId, sourceBranch.branchId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return [200, validationResult];
|
||||
}
|
||||
|
||||
const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [targetParentNoteId]);
|
||||
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
|
||||
|
||||
const newBranch = sourceBranch.createClone(targetParentNoteId, newNotePos);
|
||||
newBranch.save();
|
||||
|
||||
sourceBranch.markAsDeleted();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
branch: newBranch
|
||||
};
|
||||
}
|
||||
|
||||
function moveBranchToBranch(sourceBranch, targetParentBranch) {
|
||||
const res = moveBranchToNote(sourceBranch, targetParentBranch.noteId);
|
||||
|
||||
if (!res.success) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// expanding so that the new placement of the branch is immediately visible
|
||||
targetParentBranch.isExpanded = true;
|
||||
targetParentBranch.save();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
moveBranchToBranch,
|
||||
moveBranchToNote
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2022-03-22T21:30:21+01:00", buildRevision: "2f57d55bea11a840a158fb0431d267186a32482a" };
|
||||
module.exports = { buildDate:"2022-04-10T14:13:51+02:00", buildRevision: "a04becc4ec653e21c2c80aa9d9ef5b7c9a8e1aa8" };
|
||||
|
||||
@@ -46,6 +46,10 @@ module.exports = [
|
||||
{ type: 'label', name: 'shareAlias' },
|
||||
{ type: 'label', name: 'shareOmitDefaultCss' },
|
||||
{ type: 'label', name: 'shareRoot' },
|
||||
{ type: 'label', name: 'shareRaw' },
|
||||
{ type: 'label', name: 'shareDisallowRobotIndexing' },
|
||||
{ type: 'label', name: 'displayRelations' },
|
||||
{ type: 'label', name: 'hideRelations' },
|
||||
|
||||
// relation names
|
||||
{ type: 'relation', name: 'internalLink' },
|
||||
|
||||
@@ -35,6 +35,8 @@ function cloneNoteToNote(noteId, parentNoteId, prefix) {
|
||||
isExpanded: 0
|
||||
}).save();
|
||||
|
||||
log.info(`Cloned note ${noteId} to new parent note ${parentNoteId} with prefix ${prefix}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
branchId: branch.branchId,
|
||||
@@ -75,7 +77,7 @@ function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
|
||||
isExpanded: 0
|
||||
}).save();
|
||||
|
||||
log.info(`Creating new branch between child '${noteId}' and parent '${parentNoteId}'`);
|
||||
log.info(`Ensured note ${noteId} is in parent note ${parentNoteId} with prefix ${prefix}`);
|
||||
}
|
||||
|
||||
function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
|
||||
@@ -89,6 +91,8 @@ function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
|
||||
|
||||
const deleteId = utils.randomString(10);
|
||||
noteService.deleteBranch(branch, deleteId, new TaskContext());
|
||||
|
||||
log.info(`Ensured note ${noteId} is NOT in parent note ${parentNoteId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +132,8 @@ function cloneNoteAfter(noteId, afterBranchId) {
|
||||
isExpanded: 0
|
||||
}).save();
|
||||
|
||||
log.info(`Cloned note ${noteId} into parent note ${afterNote.parentNoteId} after note ${afterNote.noteId}, branch ${afterBranchId}`);
|
||||
|
||||
return { success: true, branchId: branch.branchId };
|
||||
}
|
||||
|
||||
|
||||
@@ -949,5 +949,6 @@ module.exports = {
|
||||
triggerNoteTitleChanged,
|
||||
eraseDeletedNotesNow,
|
||||
eraseNotesWithDeleteId,
|
||||
saveNoteRevision
|
||||
saveNoteRevision,
|
||||
downloadImages
|
||||
};
|
||||
|
||||
@@ -20,23 +20,39 @@ function getSharedSubTreeRoot(note) {
|
||||
return getSharedSubTreeRoot(parentNote);
|
||||
}
|
||||
|
||||
function addNoIndexHeader(note, res) {
|
||||
if (note.hasLabel('shareDisallowRobotIndexing')) {
|
||||
res.setHeader('X-Robots-Tag', 'noindex');
|
||||
}
|
||||
}
|
||||
|
||||
function register(router) {
|
||||
function renderNote(note, res) {
|
||||
if (note) {
|
||||
const {header, content, isEmpty} = contentRenderer.getContent(note);
|
||||
|
||||
const subRoot = getSharedSubTreeRoot(note);
|
||||
|
||||
res.render("share/page", {
|
||||
note,
|
||||
header,
|
||||
content,
|
||||
isEmpty,
|
||||
subRoot
|
||||
});
|
||||
} else {
|
||||
if (!note) {
|
||||
res.status(404).render("share/404");
|
||||
return;
|
||||
}
|
||||
|
||||
addNoIndexHeader(note, res);
|
||||
|
||||
if (note.hasLabel('shareRaw') || ['image', 'file'].includes(note.type)) {
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
res.send(note.getContent());
|
||||
return;
|
||||
}
|
||||
|
||||
const {header, content, isEmpty} = contentRenderer.getContent(note);
|
||||
|
||||
const subRoot = getSharedSubTreeRoot(note);
|
||||
|
||||
res.render("share/page", {
|
||||
note,
|
||||
header,
|
||||
content,
|
||||
isEmpty,
|
||||
subRoot
|
||||
});
|
||||
}
|
||||
|
||||
router.get(['/share', '/share/'], (req, res, next) => {
|
||||
@@ -63,6 +79,8 @@ function register(router) {
|
||||
return res.status(404).send(`Note ${noteId} not found`);
|
||||
}
|
||||
|
||||
addNoIndexHeader(note, res);
|
||||
|
||||
res.json(note.getPojoWithAttributes());
|
||||
});
|
||||
|
||||
@@ -74,6 +92,8 @@ function register(router) {
|
||||
return res.status(404).send(`Note ${noteId} not found`);
|
||||
}
|
||||
|
||||
addNoIndexHeader(note, res);
|
||||
|
||||
const utils = require("../services/utils");
|
||||
|
||||
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
|
||||
@@ -96,6 +116,8 @@ function register(router) {
|
||||
return res.status(400).send("Requested note is not an image");
|
||||
}
|
||||
|
||||
addNoIndexHeader(image, res);
|
||||
|
||||
res.set('Content-Type', image.mime);
|
||||
|
||||
res.send(image.getContent());
|
||||
@@ -110,6 +132,8 @@ function register(router) {
|
||||
return res.status(404).send(`Note ${noteId} not found`);
|
||||
}
|
||||
|
||||
addNoIndexHeader(note, res);
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ class Attribute extends AbstractEntity {
|
||||
const linkedChildNote = this.note.getChildNotes().find(childNote => childNote.noteId === this.value);
|
||||
|
||||
if (linkedChildNote) {
|
||||
this.note.children = this.note.children.filter(childNote => childNote.noteId !== this.value);
|
||||
const branch = this.shaca.getBranchFromChildAndParent(this.noteId, linkedChildNote.noteId);
|
||||
|
||||
linkedChildNote.parents = linkedChildNote.parents.filter(parentNote => parentNote.noteId !== this.noteId);
|
||||
branch.isHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ class Branch extends AbstractEntity {
|
||||
this.prefix = prefix;
|
||||
/** @param {boolean} */
|
||||
this.isExpanded = !!isExpanded;
|
||||
/** @param {boolean} */
|
||||
this.isHidden = false;
|
||||
|
||||
const childNote = this.childNote;
|
||||
const parentNote = this.parentNote;
|
||||
|
||||
@@ -58,10 +58,21 @@ class Note extends AbstractEntity {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
getVisibleChildNotes() {
|
||||
return this.getChildBranches()
|
||||
.filter(branch => !branch.isHidden)
|
||||
.map(branch => branch.getNote())
|
||||
.filter(childNote => !childNote.hasLabel('shareHiddenFromTree') && !childNote.isProtected);
|
||||
}
|
||||
|
||||
hasChildren() {
|
||||
return this.children && this.children.length > 0;
|
||||
}
|
||||
|
||||
hasVisibleChildren() {
|
||||
return this.getVisibleChildNotes().length > 0;
|
||||
}
|
||||
|
||||
getChildBranches() {
|
||||
return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
<% for (const jsRelation of note.getRelations("shareJs")) { %>
|
||||
<script type="module" src="api/notes/<%= jsRelation.value %>/download"></script>
|
||||
<% } %>
|
||||
<% if (note.hasLabel('shareDisallowRobotIndexing')) { %>
|
||||
<meta name="robots" content="noindex,follow" />
|
||||
<% } %>
|
||||
<%- header %>
|
||||
<title><%= note.title %></title>
|
||||
</head>
|
||||
@@ -46,7 +49,7 @@
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (note.hasChildren()) { %>
|
||||
<% if (note.hasVisibleChildren()) { %>
|
||||
<nav id="childLinks" class="<% if (isEmpty) { %>grid<% } else { %>list<% } %>">
|
||||
<% if (!isEmpty) { %>
|
||||
<hr>
|
||||
@@ -54,7 +57,7 @@
|
||||
<% } %>
|
||||
|
||||
<ul>
|
||||
<% for (const childNote of note.getChildNotes()) { %>
|
||||
<% for (const childNote of note.getVisibleChildNotes()) { %>
|
||||
<li>
|
||||
<a href="<%= childNote.shareId %>"
|
||||
class="type-<%= childNote.type %>"><%= childNote.title %></a>
|
||||
@@ -67,7 +70,7 @@
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (subRoot.hasChildren()) { %>
|
||||
<% if (subRoot.hasVisibleChildren()) { %>
|
||||
<button id="toggleMenuButton"></button>
|
||||
|
||||
<nav id="menu">
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
|
||||
<% if (note.hasChildren()) { %>
|
||||
<ul>
|
||||
<% note.getChildNotes().forEach(function (childNote) { %>
|
||||
<% if (!childNote.hasLabel("shareHiddenFromTree")) { %>
|
||||
<% note.getVisibleChildNotes().forEach(function (childNote) { %>
|
||||
<li>
|
||||
<%- include('tree_item', {note: childNote}) %>
|
||||
</li>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
7
test-etapi/app-info.http
Normal file
7
test-etapi/app-info.http
Normal file
@@ -0,0 +1,7 @@
|
||||
GET {{triliumHost}}/etapi/app-info
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body == "Hi there!");
|
||||
%}
|
||||
@@ -96,8 +96,14 @@ POST {{triliumHost}}/etapi/create-note
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/app-info
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
### Fake URL will get a 404 even without token
|
||||
|
||||
GET {{triliumHost}}/etapi/zzzzzz
|
||||
|
||||
> {% client.assert(response.status === 404); %}
|
||||
> {% client.assert(response.status === 404); %}
|
||||
|
||||
Reference in New Issue
Block a user