Compare commits

...

46 Commits

Author SHA1 Message Date
zadam
d616a77d6b release 0.45.10 2021-02-11 23:05:48 +01:00
zadam
5b679930de revert accidentally committed auth changes, #1629 2021-02-11 23:04:42 +01:00
zadam
067ca9ab16 fallback for incorrectly parsed pane widths, #1621 2021-02-08 20:51:38 +01:00
zadam
70708b36ef using custom header for sync authorization to avoid tripping security proxies 2021-02-05 21:59:56 +01:00
zadam
fb3d5f25ac release 0.45.9 2021-02-05 21:38:32 +01:00
zadam
9d7d79ef94 avoid errors from missing note cache during startup 2021-01-30 22:25:40 +01:00
zadam
ba33a0d330 fix conflict between CKEditor build content style and externally provided content style (which are not needed when build CSS is available), #1590 2021-01-29 20:27:16 +01:00
zadam
aea81c9872 fix include note rendering over floating elements, #1590 2021-01-29 20:09:03 +01:00
zadam
806ab22fa8 hide note paths dropdown on changed note, #1572 2021-01-27 20:44:58 +01:00
zadam
9c2b98915e fix glitch in switching between different types of notes 2021-01-23 21:52:39 +01:00
zadam
f2ca9276d6 fix/improve behavior of "sorted" attribute 2021-01-23 21:00:59 +01:00
zadam
48b697f408 select attr name in attr detail dialog did not trigger update, closes #1557 2021-01-21 20:18:33 +01:00
zadam
e1e185e5db added "safe mode" environment variable switch which disables startup scripts and resets tabs 2021-01-15 20:12:14 +01:00
zadam
e06c5703ee config value to disable backups, closes #1533 2021-01-12 21:50:05 +01:00
zadam
fe3bb2c5f6 release 0.45.8 2021-01-11 22:47:11 +01:00
zadam
6afc299efb release 0.45.8 2021-01-11 22:29:31 +01:00
zadam
369274ead7 new env variable to specify start note, #1532 2021-01-11 22:29:02 +01:00
zadam
04e6431c09 use correct class in exported HTMLs so that content style is applied, #1504 2020-12-30 23:20:12 +01:00
zadam
e89057a771 search note content only if not excluded by other expressions 2020-12-25 20:46:04 +01:00
zadam
4f27254e64 fix top margin of images and tables which can obscure their handles 2020-12-25 13:01:35 +01:00
zadam
577dc95ab8 convert   into whitespace also for large notes 2020-12-25 12:55:16 +01:00
zadam
a266d6a3d5 don't strip tags for very large text notes, #1500 2020-12-24 23:37:21 +01:00
zadam
749b6cb57e don't strip tags for very large text notes, #1500 2020-12-24 23:33:42 +01:00
zadam
b0b2951ff6 cleanup 2020-12-22 22:30:04 +01:00
zadam
1f3d73b9fd release 0.45.7 2020-12-22 20:21:15 +01:00
zadam
bdfd760b9d fixed some encryption issues 2020-12-21 23:19:03 +01:00
zadam
7133e60267 make encryption more robust in face of null values, #1483 2020-12-21 22:08:55 +01:00
zadam
fc4edf4aa7 better comment for instanceName 2020-12-21 21:05:34 +01:00
zadam
eaf93a70cd fix inverse relation creation, closes #1498 2020-12-21 20:55:01 +01:00
zadam
b093569ec5 increase toast size limit 2020-12-18 21:23:51 +01:00
zadam
4633c68a0c avoid resorting children on every child add, fixes #1480 2020-12-10 16:10:10 +01:00
zadam
33571e0ef3 better logging for un/protect errors 2020-12-09 22:49:55 +01:00
zadam
31876d2cf9 fix automatically scheduled note deletion 2020-12-09 22:45:34 +01:00
zadam
81c6043cb6 fix printing notes with math, closes #1484 2020-12-09 21:59:30 +01:00
zadam
1982d054ef inherit also note type and mime from template note, closes #1475 2020-12-07 09:35:39 +01:00
zadam
e56979c482 add button to erase deleted notes now into the options 2020-12-06 22:11:49 +01:00
zadam
58555b3660 release 0.45.6 2020-12-04 22:08:24 +01:00
zadam
b7b1324dd0 fixed disabled prefix in case of unsafe import to conform to new attribute name pattern constraints 2020-12-04 22:05:29 +01:00
zadam
e318acc977 fix incorrectly set isInheritable on inherited attrs 2020-11-27 22:33:33 +01:00
zadam
8ae82f5b69 fix "open in new tab" in tree context menu 2020-11-24 23:18:53 +01:00
zadam
26442f418a fix "open in new window" link context menu 2020-11-24 23:06:37 +01:00
zadam
23a432e7d8 don't show imageLinks in link map when they are connecting parent (text note) and child (image), closes #1461 2020-11-24 20:12:49 +01:00
zadam
984ecaf99c show again the table handle and type around 2020-11-23 20:56:14 +01:00
zadam
21b73a86b2 show also keyboard shortcut for duplicateSubtree in context menu 2020-11-23 20:17:53 +01:00
zadam
7d8277699c add keyboard shortcut for duplicate subtree, #1451 2020-11-23 19:44:49 +01:00
zadam
928ed7a034 add keyboard shortcut for include note, closes #1410 2020-11-22 22:44:06 +01:00
48 changed files with 331 additions and 121 deletions

View File

@@ -1,10 +1,13 @@
[General]
# Instance name can be used to distinguish between different instances
# Instance name can be used to distinguish between different instances using backend api.getInstanceName()
instanceName=
# set to true to allow using Trilium without authentication (makes sense for server build only, desktop build doesn't need password)
noAuthentication=false
# set to true to disable backups (e.g. because of limited space on server)
noBackup=false
# Disable automatically generating desktop icon
# noDesktopIcon=true

View File

@@ -1,6 +1,6 @@
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
.ck-widget__selection-handle, .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
display: none;
}

8
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.45.4",
"version": "0.45.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -2654,9 +2654,9 @@
}
},
"electron": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.4.tgz",
"integrity": "sha512-OHP8qMKgW8D8GtH+altB22WJw/lBOyyVdoz5e8D0/iPBmJU3Jm93vO4z4Eh/9DvdSXlH8bMHUCMLL9PVW6f+tw==",
"version": "9.3.5",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.5.tgz",
"integrity": "sha512-EPmDsp7sO0UPtw7nLD1ufse/nBskP+ifXzBgUg9psCUlapkzuwYi6pmLAzKLW/bVjwgyUKwh1OKWILWfOeLGcQ==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.45.5",
"version": "0.45.10",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -77,7 +77,7 @@
},
"devDependencies": {
"cross-env": "7.0.2",
"electron": "9.3.4",
"electron": "9.3.5",
"electron-builder": "22.9.1",
"electron-packager": "15.1.0",
"electron-rebuild": "2.3.2",

View File

@@ -51,6 +51,12 @@ const TPL = `
<label for="erase-notes-after-time-in-seconds">Erase notes after X seconds</label>
<input class="form-control" id="erase-notes-after-time-in-seconds" type="number" min="0">
</div>
<p>You can also trigger erasing manually:</p>
<button id="erase-deleted-notes-now-button" class="btn">Erase deleted notes now</button>
<br/><br/>
</div>
<div>
@@ -117,6 +123,13 @@ export default class ProtectedSessionOptions {
return false;
});
this.$eraseDeletedNotesButton = $("#erase-deleted-notes-now-button");
this.$eraseDeletedNotesButton.on('click', () => {
server.post('notes/erase-deleted-notes-now').then(() => {
toastService.showMessage("Deleted notes have been erased.");
});
});
this.$protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
this.$protectedSessionTimeout.on('change', () => {

View File

@@ -75,14 +75,16 @@ class NoteShort {
this.parentToBranch[parentNoteId] = branchId;
}
addChild(childNoteId, branchId) {
addChild(childNoteId, branchId, sort = true) {
if (!this.children.includes(childNoteId)) {
this.children.push(childNoteId);
}
this.childToBranch[childNoteId] = branchId;
this.sortChildren();
if (sort) {
this.sortChildren();
}
}
sortChildren() {

View File

@@ -130,7 +130,7 @@ function linkContextMenu(e) {
appContext.tabManager.openTabWithNote(notePath);
}
else if (command === 'openNoteInNewWindow') {
appContext.openInNewWindow(notePath);
appContext.triggerCommand('openInWindow', {notePath});
}
}
});

View File

@@ -14,8 +14,17 @@ function setupSplit(left, right) {
return;
}
const leftPaneWidth = options.getInt('leftPaneWidth');
const rightPaneWidth = options.getInt('rightPaneWidth');
let leftPaneWidth = options.getInt('leftPaneWidth');
if (!leftPaneWidth || leftPaneWidth < 5) {
leftPaneWidth = 5;
}
let rightPaneWidth = options.getInt('rightPaneWidth');
if (!rightPaneWidth || rightPaneWidth < 5) {
rightPaneWidth = 5;
}
console.log(leftPaneWidth, rightPaneWidth);
if (left && right) {
instance = Split(['#left-pane', '#center-pane', '#right-pane'], {
@@ -49,4 +58,4 @@ function setupSplit(left, right) {
export default {
setupSplit
};
};

View File

@@ -8,8 +8,8 @@ async function syncNow() {
toastService.showMessage("Sync finished successfully.");
}
else {
if (result.message.length > 100) {
result.message = result.message.substr(0, 100);
if (result.message.length > 200) {
result.message = result.message.substr(0, 200) + "...";
}
toastService.showError("Sync failed: " + result.message);

View File

@@ -87,6 +87,8 @@ class TreeCache {
const branchRows = resp.branches;
const attributeRows = resp.attributes;
const noteIdsToSort = new Set();
for (const noteRow of noteRows) {
const {noteId} = noteRow;
@@ -153,7 +155,9 @@ class TreeCache {
const parentNote = this.notes[branch.parentNoteId];
if (parentNote) {
parentNote.addChild(branch.noteId, branch.branchId);
parentNote.addChild(branch.noteId, branch.branchId, false);
noteIdsToSort.add(parentNote.noteId);
}
}
@@ -178,6 +182,11 @@ class TreeCache {
}
}
}
// sort all of them at once, this avoids repeated sorts (#1480)
for (const noteId of noteIdsToSort) {
this.notes[noteId].sortChildren();
}
}
async reloadNotes(noteIds) {

View File

@@ -2,9 +2,9 @@ import treeService from './tree.js';
import treeCache from "./tree_cache.js";
import hoistedNoteService from './hoisted_note.js';
import clipboard from './clipboard.js';
import protectedSessionHolder from "./protected_session_holder.js";
import noteCreateService from "./note_create.js";
import contextMenu from "./context_menu.js";
import appContext from "./app_context.js";
class TreeContextMenu {
/**
@@ -95,7 +95,7 @@ class TreeContextMenu {
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: "Duplicate subtree(s) here", command: "duplicateSubtree", uiIcon: "empty",
{ title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "empty",
enabled: parentNotSearch && isNotRoot && !isHoisted },
{ title: "----" },
{ title: "Export", command: "exportNote", uiIcon: "empty",
@@ -110,14 +110,7 @@ class TreeContextMenu {
const notePath = treeService.getNotePath(this.node);
if (command === 'openInTab') {
const start = Date.now();
await this.node.load(true);
console.log("Reload took", Date.now() - start, "ms");
// appContext.tabManager.openTabWithNote(notePath);
appContext.tabManager.openTabWithNote(notePath);
}
else if (command === "insertNoteAfter") {
const parentNoteId = this.node.data.parentNoteId;

View File

@@ -234,6 +234,8 @@ export default class AttributeDetailWidget extends TabAwareWidget {
this.$inputName = this.$widget.find('.attr-input-name');
this.$inputName.on('keyup', () => this.userEditedAttribute());
this.$inputName.on('change', () => this.userEditedAttribute());
this.$inputName.on('autocomplete:closed', () => this.userEditedAttribute());
this.$inputName.on('focus', () => {
attributeAutocompleteService.initAttributeNameAutocomplete({

View File

@@ -214,7 +214,8 @@ export default class AttributeListWidget extends TabAwareWidget {
noteId: attribute.noteId,
type: attribute.type,
name: attribute.name,
value: attribute.value
value: attribute.value,
isInheritable: attribute.isInheritable
},
isOwned: false,
x: e.pageX,

View File

@@ -248,13 +248,22 @@ export default class NoteDetailWidget extends TabAwareWidget {
this.$widget.find('.note-detail-printable:visible').printThis({
header: $("<h2>").text(this.note && this.note.title).prop('outerHTML'),
footer: "<script>document.body.className += ' ck-content';</script>",
footer: `
<script src="libraries/katex/katex.min.js"></script>
<script src="libraries/katex/auto-render.min.js"></script>
<script>
document.body.className += ' ck-content printed-content';
renderMathInElement(document.body, {});
</script>
`,
importCSS: false,
loadCSS: [
"libraries/codemirror/codemirror.css",
"libraries/ckeditor/ckeditor-content.css",
"libraries/ckeditor/ckeditor-content.css",
"libraries/bootstrap/css/bootstrap.min.css",
"libraries/katex/katex.min.css",
"stylesheets/print.css",
"stylesheets/relation_map.css",
"stylesheets/themes.css"

View File

@@ -59,6 +59,7 @@ export default class NotePathsWidget extends TabAwareWidget {
this.$currentPath = this.$widget.find('.current-path');
this.$dropdown = this.$widget.find(".dropdown");
this.$dropdownToggle = this.$widget.find('.dropdown-toggle');
this.$notePathList = this.$dropdown.find(".note-path-list");
@@ -100,6 +101,8 @@ export default class NotePathsWidget extends TabAwareWidget {
parentNoteId = noteId;
}
this.$dropdownToggle.dropdown('hide');
}
async renderDropdown() {
@@ -141,20 +144,20 @@ export default class NotePathsWidget extends TabAwareWidget {
async addPath(notePath, isCurrent) {
const title = await treeService.getNotePathTitle(notePath);
const noteLink = await linkService.createNoteLink(notePath, {title});
const $noteLink = await linkService.createNoteLink(notePath, {title});
noteLink
$noteLink
.addClass("dropdown-item");
noteLink
$noteLink
.find('a')
.addClass("no-tooltip-preview");
if (isCurrent) {
noteLink.addClass("current");
$noteLink.addClass("current");
}
this.$notePathList.append(noteLink);
this.$notePathList.append($noteLink);
}
entitiesReloadedEvent({loadResults}) {

View File

@@ -1198,7 +1198,25 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.clearSelectedNodes();
}
canBeMovedUpOrDown(node) {
if (node.data.noteId === 'root') {
return false;
}
const parentNote = treeCache.getNoteFromCache(node.getParent().data.noteId);
if (parentNote && parentNote.hasLabel('sorted')) {
return false;
}
return true;
}
moveNoteUpCommand({node}) {
if (!this.canBeMovedUpOrDown(node)) {
return;
}
const beforeNode = node.getPrevSibling();
if (beforeNode !== null) {
@@ -1207,7 +1225,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
moveNoteDownCommand({node}) {
if (!this.canBeMovedUpOrDown(node)) {
return;
}
const afterNode = node.getNextSibling();
if (afterNode !== null) {
branchService.moveAfterBranch([node.data.branchId], afterNode.data.branchId);
}

View File

@@ -41,6 +41,9 @@ export default class NoteTypeWidget extends TabAwareWidget {
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
this.$noteTypeButton = this.$widget.find(".note-type-button");
this.$noteTypeDesc = this.$widget.find(".note-type-desc");
this.$widget.on('click', '.dropdown-item',
() => this.$widget.find('.dropdown-toggle').dropdown('toggle'));
}
async refreshWithNote(note) {
@@ -64,8 +67,6 @@ export default class NoteTypeWidget extends TabAwareWidget {
const noteType = NOTE_TYPES.find(nt => nt.type === type);
this.save(noteType.type, noteType.mime);
this.$widget.find('.dropdown-toggle').dropdown('toggle');
});
if (this.note.type === noteType.type) {

View File

@@ -38,7 +38,7 @@ const TPL = `
cursor: text !important;
}
.note-detail-editable-text *:first-child {
.note-detail-editable-text *:not(figure):first-child {
margin-top: 0 !important;
}

View File

@@ -67,6 +67,11 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
}
async doRefresh(note) {
// we load CKEditor also for read only notes because they contain content styles required for correct rendering of even read only notes
// we could load just ckeditor-content.css but that causes CSS conflicts when both build CSS and this content CSS is loaded at the same time
// (see https://github.com/zadam/trilium/issues/1590 for example of such conflict)
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
const noteComplement = await treeCache.getNoteComplement(note.noteId);
this.$content.html(noteComplement.content);

View File

@@ -703,6 +703,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
padding: 10px;
border-radius: 10px;
background-color: var(--accented-background-color);
clear: both;
}
.include-note.ck-placeholder::before { /* remove placeholder in otherwise empty note */

View File

@@ -143,6 +143,11 @@ body {
--ck-color-dropdown-panel-background: var(--accented-background-color);
--ck-color-dropdown-panel-border: var(--main-border-color);
/* -- Overrides the default .ck-splitbutton class colors. ----------------------------------- */
--ck-color-split-button-hover-background: var(--ck-color-button-default-hover-background);
--ck-color-split-button-hover-border: var(--main-border-color);
/* -- Overrides the default .ck-input class colors. ----------------------------------------- */
--ck-color-input-background: var(--accented-background-color);
@@ -199,6 +204,9 @@ body {
--ck-color-engine-placeholder-text: var(--muted-text-color);
--ck-z-modal: 10000;
--ck-color-widget-type-around-button: var(--main-border-color);
--ck-color-widget-type-around-button-hover: var(--main-border-color);
}
body {

View File

@@ -23,11 +23,7 @@ function exportBranch(req, res) {
try {
if (type === 'subtree' && (format === 'html' || format === 'markdown')) {
const start = Date.now();
zipExportService.exportToZip(taskContext, branch, format, res);
console.log("Export took", Date.now() - start, "ms");
}
else if (type === 'single') {
singleExportService.exportSingleNote(taskContext, branch, format, res);

View File

@@ -3,15 +3,33 @@
const sql = require('../../services/sql');
function getRelations(noteIds) {
return (sql.getManyRows(`
SELECT noteId, name, value AS targetNoteId
FROM attributes
WHERE (noteId IN (???) OR value IN (???))
AND type = 'relation'
AND isDeleted = 0
AND noteId != ''
AND value != ''
`, Array.from(noteIds)));
noteIds = Array.from(noteIds);
return [
// first read all non-image relations
...sql.getManyRows(`
SELECT noteId, name, value AS targetNoteId
FROM attributes
WHERE (noteId IN (???) OR value IN (???))
AND type = 'relation'
AND name != 'imageLink'
AND isDeleted = 0
AND noteId != ''
AND value != ''`, noteIds),
// ... then read only imageLink relations which are not connecting parent and child
// this is done to not show image links in the trivial case where they are direct children of the note to which they are included. Same heuristic as in note tree
...sql.getManyRows(`
SELECT rel.noteId, rel.name, rel.value AS targetNoteId
FROM attributes AS rel
LEFT JOIN branches ON branches.parentNoteId = rel.noteId AND branches.noteId = rel.value AND branches.isDeleted = 0
WHERE (rel.noteId IN (???) OR rel.value IN (???))
AND rel.type = 'relation'
AND rel.name = 'imageLink'
AND rel.isDeleted = 0
AND rel.noteId != ''
AND rel.value != ''
AND branches.branchId IS NULL`, noteIds)
];
}
function getLinkMap(req) {

View File

@@ -193,6 +193,10 @@ function duplicateSubtree(req) {
return noteService.duplicateSubtree(noteId, parentNoteId);
}
function eraseDeletedNotesNow() {
noteService.eraseDeletedNotesNow();
}
module.exports = {
getNote,
updateNote,
@@ -204,5 +208,6 @@ module.exports = {
setNoteTypeMime,
getRelationMap,
changeTitle,
duplicateSubtree
duplicateSubtree,
eraseDeletedNotesNow
};

View File

@@ -54,11 +54,21 @@ function getBundlesWithLabel(label, value) {
}
function getStartupBundles() {
return getBundlesWithLabel("run", "frontendStartup");
if (!process.env.TRILIUM_SAFE_MODE) {
return getBundlesWithLabel("run", "frontendStartup");
}
else {
return [];
}
}
function getWidgetBundles() {
return getBundlesWithLabel("widget");
if (!process.env.TRILIUM_SAFE_MODE) {
return getBundlesWithLabel("widget");
}
else {
return [];
}
}
function getRelationBundles(req) {

View File

@@ -38,6 +38,8 @@ function saveSyncSeed(req) {
}]
}
log.info("Saved sync seed.");
sqlInit.createDatabaseForSync(options);
}

View File

@@ -57,7 +57,7 @@ function getTree(req) {
const noteIds = sql.getColumn(`
WITH RECURSIVE
treeWithDescendants(noteId, isExpanded) AS (
SELECT noteId, 1 FROM branches WHERE parentNoteId = ? AND isDeleted = 0
SELECT noteId, isExpanded FROM branches WHERE parentNoteId = ? AND isDeleted = 0
UNION
SELECT branches.noteId, branches.isExpanded FROM branches
JOIN treeWithDescendants ON branches.parentNoteId = treeWithDescendants.noteId

View File

@@ -153,6 +153,7 @@ function register(app) {
route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);
apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision);
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow);
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-11-20T22:50:10+01:00", buildRevision: "e5fa1e0ed555c1c2cb4a14c426d7091d62b5beea" };
module.exports = { buildDate:"2021-02-11T23:05:48+01:00", buildRevision: "5b679930de787efc2f70dfe7961cd1677ec2fa0f" };

View File

@@ -650,7 +650,7 @@ class ConsistencyChecks {
// root branch should always be expanded
sql.execute("UPDATE branches SET isExpanded = 1 WHERE branchId = 'root'");
if (this.unrecoveredConsistencyErrors) {
if (!this.unrecoveredConsistencyErrors) {
// we run this only if basic checks passed since this assumes basic data consistency
this.checkTreeCycles();

View File

@@ -52,6 +52,10 @@ function encrypt(key, plainText, ivLength = 13) {
}
function decrypt(key, cipherText, ivLength = 13) {
if (cipherText === null) {
return null;
}
if (!key) {
return "[protected]";
}
@@ -93,6 +97,10 @@ function decrypt(key, cipherText, ivLength = 13) {
function decryptString(dataKey, cipherText) {
const buffer = decrypt(dataKey, cipherText);
if (buffer === null) {
return null;
}
const str = buffer.toString('utf-8');
if (str === 'false') {
@@ -108,4 +116,4 @@ module.exports = {
encrypt,
decrypt,
decryptString
};
};

View File

@@ -143,7 +143,7 @@ function exportToZip(taskContext, branch, format, res) {
const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable();
// if it's a leaf then we'll export it even if it's empty
if (available && ((note.getContent()).length > 0 || childBranches.length === 0)) {
if (available && (note.getContent().length > 0 || childBranches.length === 0)) {
meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames);
}
@@ -234,7 +234,7 @@ function exportToZip(taskContext, branch, format, res) {
<link rel="stylesheet" href="${cssUrl}">
<base target="_parent">
</head>
<body>
<body class="ck-content">
<h1>${utils.escapeHtml(title)}</h1>
${content}
</body>
@@ -433,14 +433,13 @@ ${content}
}
const note = branch.getNote();
const zipFileName = (branch.prefix ? (branch.prefix + " - ") : "") + note.title + ".zip";
const zipFileName = (branch.prefix ? `${branch.prefix} - ` : "") + note.title + ".zip";
res.setHeader('Content-Disposition', utils.getContentDisposition(zipFileName));
res.setHeader('Content-Type', 'application/zip');
zipFile.end();
zipFile.outputStream.pipe(res);
zipFile.end();
taskContext.taskSucceeded();
}

View File

@@ -3,6 +3,7 @@ const scriptService = require('./script');
const treeService = require('./tree');
const noteService = require('./notes');
const repository = require('./repository');
const noteCache = require('./note_cache/note_cache');
const Attribute = require('../entities/attribute');
function runAttachedRelations(note, relationName, originEntity) {
@@ -22,11 +23,15 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
runAttachedRelations(note, 'runOnNoteTitleChange', note);
if (!note.isRoot()) {
const parents = note.getParentNotes();
const noteFromCache = noteCache.notes[note.noteId];
for (const parent of parents) {
if (parent.hasOwnedLabel("sorted")) {
treeService.sortNotesAlphabetically(parent.noteId);
if (!noteFromCache) {
return;
}
for (const parentNote of noteFromCache.parents) {
if (parentNote.hasLabel("sorted")) {
treeService.sortNotesAlphabetically(parentNote.noteId);
}
}
}
@@ -70,12 +75,26 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
if (templateNoteContent) {
note.setContent(templateNoteContent);
}
note.type = templateNote.type;
note.mime = templateNote.mime;
note.save();
}
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
}
else if (entity.type === 'label' && entity.name === 'sorted') {
treeService.sortNotesAlphabetically(entity.noteId);
if (entity.isInheritable) {
const note = noteCache.notes[entity.noteId];
if (note) {
for (const noteId of note.subtreeNoteIds) {
treeService.sortNotesAlphabetically(noteId);
}
}
}
}
}
else if (entityName === 'notes') {
@@ -90,10 +109,10 @@ eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote
function processInverseRelations(entityName, entity, handler) {
if (entityName === 'attributes' && entity.type === 'relation') {
const note = entity.getNote();
const attributes = (note.getOwnedAttributes(entity.name)).filter(relation => relation.type === 'relation-definition');
const relDefinitions = note.getLabels('relation:' + entity.name);
for (const attribute of attributes) {
const definition = attribute.value;
for (const relDefinition of relDefinitions) {
const definition = relDefinition.getDefinition();
if (definition.inverseRelation && definition.inverseRelation.trim()) {
const targetNote = entity.getTargetNote();

View File

@@ -158,7 +158,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}
if (taskContext.data.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) {
attr.name = 'disabled-' + attr.name;
attr.name = 'disabled:' + attr.name;
}
attributes.push(attr);

View File

@@ -184,6 +184,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [
description: "Add note above to the selection",
scope: "note-tree"
},
{
actionName: "duplicateSubtree",
defaultShortcuts: [],
description: "Duplicate subtree",
scope: "note-tree"
},
{
@@ -307,6 +313,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [
description: "Cuts the selection from the current note and creates subnote with the selected text",
scope: "text-detail"
},
{
actionName: "addIncludeNoteToText",
defaultShortcuts: [],
description: "Opens the dialog to include a note",
scope: "text-detail"
},
{
separator: "Attributes (labels & relations)"

View File

@@ -133,6 +133,14 @@ class Note {
return !!this.attributes.find(attr => attr.type === type && attr.name === name);
}
hasLabel(name) {
return this.hasAttribute('label', name);
}
hasRelation(name) {
return this.hasAttribute('relation', name);
}
getLabelValue(name) {
const label = this.attributes.find(attr => attr.type === 'label' && attr.name === name);
@@ -275,6 +283,11 @@ class Note {
return arr.flat();
}
/** @return {String[]} */
get subtreeNoteIds() {
return this.subtreeNotes.map(note => note.noteId);
}
get parentCount() {
return this.parents.length;
}
@@ -338,7 +351,7 @@ class Note {
decrypt() {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
this.title = protectedSessionService.decryptString(note.title);
this.title = protectedSessionService.decryptString(this.title);
this.isDecrypted = true;
}

View File

@@ -1,9 +1,5 @@
"use strict";
const Note = require('./entities/note');
const Branch = require('./entities/branch');
const Attribute = require('./entities/attribute');
class NoteCache {
constructor() {
this.reset();

View File

@@ -2,6 +2,7 @@
const NoteRevision = require('../entities/note_revision');
const dateUtils = require('../services/date_utils');
const log = require('../services/log');
/**
* @param {Note} note
@@ -9,14 +10,21 @@ const dateUtils = require('../services/date_utils');
function protectNoteRevisions(note) {
for (const revision of note.getRevisions()) {
if (note.isProtected !== revision.isProtected) {
const content = revision.getContent();
try {
const content = revision.getContent();
revision.isProtected = note.isProtected;
revision.isProtected = note.isProtected;
// this will force de/encryption
revision.setContent(content);
// this will force de/encryption
revision.setContent(content);
revision.save();
revision.save();
}
catch (e) {
log.error("Could not un/protect note revision ID = " + revision.noteRevisionId);
throw e;
}
}
}
}

View File

@@ -185,18 +185,25 @@ function protectNoteRecursively(note, protect, includingSubTree, taskContext) {
}
function protectNote(note, protect) {
if (protect !== note.isProtected) {
const content = note.getContent();
try {
if (protect !== note.isProtected) {
const content = note.getContent();
note.isProtected = protect;
note.isProtected = protect;
// this will force de/encryption
note.setContent(content);
// this will force de/encryption
note.setContent(content);
note.save();
note.save();
}
noteRevisionService.protectNoteRevisions(note);
}
catch (e) {
log.error("Could not un/protect note ID = " + note.noteId);
noteRevisionService.protectNoteRevisions(note);
throw e;
}
}
function findImageLinks(content, foundLinks) {
@@ -668,8 +675,10 @@ function scanForLinks(note) {
}
}
function eraseDeletedNotes() {
const eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
function eraseDeletedNotes(eraseNotesAfterTimeInSeconds = null) {
if (eraseNotesAfterTimeInSeconds === null) {
eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
}
const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000);
@@ -719,6 +728,10 @@ function eraseDeletedNotes() {
log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`);
}
function eraseDeletedNotesNow() {
eraseDeletedNotes(0);
}
// do a replace in str - all keys should be replaced by the corresponding values
function replaceByMap(str, mapObj) {
const re = new RegExp(Object.keys(mapObj).join("|"),"g");
@@ -739,7 +752,10 @@ function duplicateSubtree(origNoteId, newParentNoteId) {
const res = duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapping);
res.note.title += " (dup)";
if (!res.note.title.endsWith('(dup)')) {
res.note.title += " (dup)";
}
res.note.save();
return res;
@@ -822,9 +838,9 @@ function getNoteIdMapping(origNote) {
sqlInit.dbReady.then(() => {
// first cleanup kickoff 5 minutes after startup
setTimeout(cls.wrap(eraseDeletedNotes), 5 * 60 * 1000);
setTimeout(cls.wrap(() => eraseDeletedNotes()), 5 * 60 * 1000);
setInterval(cls.wrap(eraseDeletedNotes), 4 * 3600 * 1000);
setInterval(cls.wrap(() => eraseDeletedNotes()), 4 * 3600 * 1000);
});
module.exports = {
@@ -838,5 +854,6 @@ module.exports = {
duplicateSubtree,
duplicateSubtreeWithoutRoot,
getUndeletedParentBranches,
triggerNoteTitleChanged
triggerNoteTitleChanged,
eraseDeletedNotesNow
};

View File

@@ -31,10 +31,7 @@ function initNotSyncedOptions(initialized, startNotePath = 'root', opts = {}) {
optionService.createOption('openTabs', JSON.stringify([
{
notePath: startNotePath,
active: true,
sidebar: {
widgets: []
}
active: true
}
]), false);
@@ -103,6 +100,15 @@ function initStartupOptions() {
log.info(`Created missing option "${name}" with default value "${value}"`);
}
}
if (process.env.TRILIUM_START_NOTE_ID || process.env.TRILIUM_SAFE_MODE) {
optionService.setOption('openTabs', JSON.stringify([
{
notePath: process.env.TRILIUM_START_NOTE_ID || 'root',
active: true
}
]));
}
}
function getKeyboardDefaultOptions() {

View File

@@ -43,10 +43,18 @@ function decryptNotes(notes) {
}
function encrypt(plainText) {
if (plainText === null) {
return null;
}
return dataEncryptionService.encrypt(getDataKey(), plainText);
}
function decrypt(cipherText) {
if (cipherText === null) {
return null;
}
return dataEncryptionService.decrypt(getDataKey(), cipherText);
}

View File

@@ -22,9 +22,11 @@ function runNotesWithLabel(runAttrValue) {
}
sqlInit.dbReady.then(() => {
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
if (!process.env.TRILIUM_SAFE_MODE) {
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000);
}
});

View File

@@ -1,4 +1,3 @@
const sql = require('./sql');
const ScriptContext = require('./script_context');
const repository = require('./repository');
const cls = require('./cls');

View File

@@ -32,26 +32,29 @@ class NoteContentProtectedFulltextExp extends Expression {
FROM notes JOIN note_contents USING (noteId)
WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 1`)) {
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in noteCache.notes)) {
continue;
}
try {
content = protectedSessionService.decryptString(content);
}
catch (e) {
log.info('Cannot decrypt content of note', noteId);
log.info(`Cannot decrypt content of note ${noteId}`);
continue;
}
content = content.toLowerCase();
if (type === 'text' && mime === 'text/html') {
content = striptags(content);
if (content.length < 20000) { // striptags is slow for very large notes
content = striptags(content);
}
content = content.replace(/&nbsp;/g, ' ');
}
if (this.tokens.find(token => !content.includes(token))) {
continue;
}
if (inputNoteSet.hasNoteId(noteId) && noteId in noteCache.notes) {
if (!this.tokens.find(token => !content.includes(token))) {
resultNoteSet.add(noteCache.notes[noteId]);
}
}

View File

@@ -26,18 +26,21 @@ class NoteContentUnprotectedFulltextExp extends Expression {
FROM notes JOIN note_contents USING (noteId)
WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 0`)) {
content = content.toString().toLowerCase();
if (type === 'text' && mime === 'text/html') {
content = striptags(content);
content = content.replace(/&nbsp;/g, ' ');
}
if (this.tokens.find(token => !content.includes(token))) {
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in noteCache.notes)) {
continue;
}
if (inputNoteSet.hasNoteId(noteId) && noteId in noteCache.notes) {
content = content.toString().toLowerCase();
if (type === 'text' && mime === 'text/html') {
if (content.length < 20000) { // striptags is slow for very large notes
content = striptags(content);
}
content = content.replace(/&nbsp;/g, ' ');
}
if (!this.tokens.find(token => !content.includes(token))) {
resultNoteSet.add(noteCache.notes[noteId]);
}
}

View File

@@ -9,6 +9,7 @@ const Option = require('../entities/option');
const TaskContext = require('./task_context.js');
const migrationService = require('./migration');
const cls = require('./cls');
const config = require('./config');
const dbReady = utils.deferred();
@@ -131,6 +132,12 @@ function setDbAsInitialized() {
}
dbReady.then(() => {
if (config.General && config.General.noBackup === true) {
log.info("Disabling scheduled backups.");
return;
}
setInterval(() => require('./backup').regularBackup(), 4 * 60 * 60 * 1000);
// kickoff first backup soon after start up

View File

@@ -63,8 +63,6 @@
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
<link href="libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
<!-- Include Fancytree skin and library -->
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="libraries/fancytree/jquery.fancytree-all-deps.min.js"></script>

View File

@@ -127,8 +127,6 @@
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
<link href="libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
<script src="app/mobile.js" crossorigin type="module"></script>
<link href="stylesheets/themes.css" rel="stylesheet">