Compare commits

...

42 Commits

Author SHA1 Message Date
zadam
c1092c97b5 release 0.51.0-beta 2022-04-10 14:13:51 +02:00
zadam
a04becc4ec dep upgrades 2022-04-10 14:13:45 +02:00
zadam
f7d6bda49d added zooming/panning to mermaid diagrams, closes #2635 2022-04-03 23:14:47 +02:00
zadam
b250f0a3bf Merge remote-tracking branch 'origin/stable' 2022-04-03 14:21:28 +02:00
zadam
df1d94ec61 ckeditor 33 content styles 2022-03-26 14:10:29 +01:00
zadam
e00fcd93a1 added #shareDisallowRobotIndexing label and reworked how the child-image exclusion works 2022-03-22 23:17:47 +01:00
zadam
0a95d0f6f5 ckeditor 33 2022-03-22 22:20:44 +01:00
zadam
091d6a1cf1 added #shareRaw label 2022-03-22 21:20:47 +01:00
zadam
228564f843 Merge remote-tracking branch 'origin/master' 2022-03-22 21:04:52 +01:00
zadam
17dd6141fb Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	Dockerfile
#	package-lock.json
#	package.json
#	src/services/cloning.js
2022-03-22 21:04:30 +01:00
Matt
77ce56ba84 Don't show protected notes in shared tree (#2704) 2022-03-07 23:00:07 +01:00
zadam
eba824a5b1 added app-info method to etapi #2697 2022-03-07 22:57:48 +01:00
zadam
c9e72f8fb9 Merge remote-tracking branch 'origin/master' 2022-02-24 23:22:33 +01:00
zadam
4dd3fd9674 better error when password is not set, #2685 2022-02-24 23:22:20 +01:00
zadam
1690a55f7d Update README.ru.md 2022-02-24 20:38:29 +01:00
zadam
dd29fc26e3 Update README-ZH_CN.md 2022-02-24 20:38:01 +01:00
zadam
67b5921d6c Update README.md 2022-02-24 20:37:12 +01:00
zadam
1b7bcc5cc1 Update README.md 2022-02-22 23:18:10 +01:00
zadam
a009b4cb6d remove debug logging 2022-02-16 22:31:02 +01:00
zadam
781be527ce Merge remote-tracking branch 'origin/master' 2022-02-16 22:16:25 +01:00
zadam
f7e5d8f62d fix displaying hidden notes in the note's children list #2664 2022-02-16 22:16:15 +01:00
Nriver
d6c0fc734f Fix data validation type for prefix (#2660)
* fix param validation

* fix data validation type for `prefix`
2022-02-15 22:49:49 +01:00
zadam
18d439dd44 clipper endpoint should also scan and download images as a fallback, #2621 2022-02-14 20:50:50 +01:00
zadam
d2d2a6c086 ckeditor 32 2022-02-12 22:53:55 +01:00
zadam
5260689b8e include "dump-db" into released artifacts 2022-02-12 22:51:28 +01:00
zadam
78a2863b78 Merge remote-tracking branch 'origin/master' 2022-02-12 22:20:22 +01:00
zadam
5481375347 DB dump tool feature complete 2022-02-12 22:20:15 +01:00
zadam
4da2d2f516 Create CODE_OF_CONDUCT 2022-02-12 13:17:49 +01:00
zadam
67cce5f817 Merge remote-tracking branch 'origin/stable' 2022-02-10 23:40:18 +01:00
zadam
9924727729 Merge remote-tracking branch 'origin/master' 2022-02-10 23:40:10 +01:00
zadam
6c9fc364a3 added "DB dump" tool, WIP 2022-02-10 23:37:25 +01:00
Nriver
1aeb674733 fix param validation (#2624) 2022-02-10 19:28:42 +01:00
zadam
df91192b97 Merge remote-tracking branch 'origin/master' 2022-02-06 14:18:18 +01:00
zadam
97fd550402 add light anonymization option to the existing full anonymization 2022-02-06 13:49:33 +01:00
zadam
eb579de199 Update README.md 2022-02-05 17:21:41 +01:00
zadam
5f2984aa57 Update README.md 2022-02-05 17:20:41 +01:00
zadam
98a79f6475 Update FUNDING.yml 2022-02-05 17:17:07 +01:00
zadam
c09da2b7eb Update FUNDING.yml 2022-02-05 17:12:37 +01:00
zadam
600f74576d added #sentFromSender label, closes #445 2022-02-05 16:23:30 +01:00
zadam
a21c49cba7 added "hideRelations" label to relation map 2022-02-05 13:02:19 +01:00
zadam
91e3dd022a added "move note" search action 2022-02-05 12:06:23 +01:00
zadam
478eca47f4 search string is now a textarea instead of input 2022-02-05 11:27:48 +01:00
62 changed files with 3462 additions and 1323 deletions

View File

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

@@ -0,0 +1 @@
Please treat each other with respect and understanding.

View File

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

View File

@@ -7,6 +7,10 @@ Trilium Notes是一个分层的笔记应用程序专注于建立大型个人
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
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)

View File

@@ -7,6 +7,10 @@ Trilium Notes is a hierarchical note taking application with focus on building l
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
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.

View File

@@ -7,6 +7,10 @@ Trilium Notes это приложение для заметок с иера
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
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))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

25
dump-db/package.json Normal file
View 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"
}
}

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1822
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,8 @@ function uploadImage(req) {
}
}
note.setLabel("sentFromSender");
return {
noteId: noteId
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -949,5 +949,6 @@ module.exports = {
triggerNoteTitleChanged,
eraseDeletedNotesNow,
eraseNotesWithDeleteId,
saveNoteRevision
saveNoteRevision,
downloadImages
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,7 @@
GET {{triliumHost}}/etapi/app-info
Authorization: {{authToken}}
> {%
client.assert(response.status === 200);
client.assert(response.body == "Hi there!");
%}

View File

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