Compare commits

..

96 Commits

Author SHA1 Message Date
Elian Doran
455edbfb5d chore(server): remove runtime from login 2026-01-07 23:31:28 +02:00
Elian Doran
7288b66d27 chore(client): address requested changes 2026-01-07 23:04:34 +02:00
Elian Doran
3d72ec80bb refactor(client): get rid of any 2026-01-07 22:01:35 +02:00
Elian Doran
f2a74df511 feat(client): use hashes for assets 2026-01-07 21:49:05 +02:00
Elian Doran
68c6052d10 chore(client): remove useless manual chunk 2026-01-07 21:39:35 +02:00
Elian Doran
c4edb56bd4 fix(server): not starting due to serving of assets 2026-01-07 21:38:44 +02:00
Elian Doran
b6a3fe7cfb chore(client): get rid of translation issue 2026-01-07 21:18:09 +02:00
Elian Doran
7a088c5b7d refactor(client): handle everything in bootstrap 2026-01-07 21:11:38 +02:00
Elian Doran
2e845a9faa refactor(client): get rid of runtime in favor of bootstrap script 2026-01-07 21:08:19 +02:00
Elian Doran
ac3ae0dbbe chore(client): fix type issues 2026-01-07 21:02:27 +02:00
Elian Doran
a3fc13de3a refactor(client): extract bootstrap script into separate file 2026-01-07 21:00:40 +02:00
Elian Doran
ee6cbc710c chore(server): remove font size globs 2026-01-07 20:52:27 +02:00
Elian Doran
18d701525e fix(client): print broken due to lack of query forwarding
; Conflicts:
;	apps/client/src/index.html
2026-01-07 20:52:04 +02:00
Elian Doran
e47c848ec8 chore(server): reintegrate mobile layout 2026-01-07 20:51:33 +02:00
Elian Doran
cd64548299 fix(client): load custom fonts 2026-01-07 20:51:29 +02:00
Elian Doran
8645d053de fix(client): ckeditor theme not loaded properly 2026-01-07 20:51:24 +02:00
Elian Doran
91f2dabed7 Merge remote-tracking branch 'origin/main' into lightweight/bootstrap_ejs
; Conflicts:
;	apps/client/src/widgets/layout/StatusBar.tsx
2026-01-07 20:51:17 +02:00
Elian Doran
716612680d Translations update from Hosted Weblate (#8293) 2026-01-07 19:35:42 +02:00
Michael
3800fb85eb Translated using Weblate (German)
Currently translated at 95.4% (1672 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-01-07 18:32:56 +01:00
Rafa Osuna
d807984be4 Translated using Weblate (Spanish)
Currently translated at 92.7% (1624 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-01-07 18:32:56 +01:00
Giovi
2c92ae8898 Translated using Weblate (Italian)
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-01-07 18:32:55 +01:00
Argann Bonneau
3d8cbc81c4 Translated using Weblate (French)
Currently translated at 94.5% (1656 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-01-07 18:32:54 +01:00
Yatrik Patel
d747c94450 Translated using Weblate (Hindi)
Currently translated at 3.4% (4 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-07 18:32:53 +01:00
pythaac
a627d1f96e Translated using Weblate (Korean)
Currently translated at 76.3% (116 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2026-01-07 18:32:53 +01:00
Yatrik Patel
869db5e478 Translated using Weblate (Hindi)
Currently translated at 0.9% (17 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-07 18:32:52 +01:00
Yatrik Patel
73e94d385e Translated using Weblate (Hindi)
Currently translated at 5.9% (23 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-07 18:32:51 +01:00
Kim Nøglegaard
8f4ebeb335 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-07 18:32:51 +01:00
Yatrik Patel
263ee864be Translated using Weblate (Hindi)
Currently translated at 9.2% (14 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-07 18:32:50 +01:00
Elian Doran
f078732624 fix(text): Title is not selected when creating a note via the launcher (#8292) 2026-01-07 19:32:37 +02:00
SngAbc
fac1f6b16c fix(text): Title is not focused when creating a note via the launcher
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-07 10:33:17 +08:00
SiriusXT
a5841c1423 fix(text): Title is not focused when creating a note via the launcher 2026-01-07 10:11:24 +08:00
Elian Doran
aaca18003d Translations update from Hosted Weblate (#8279) 2026-01-06 13:54:24 +02:00
Kim Nøglegaard
5ec521b024 Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.4% (104 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-06 04:01:53 +01:00
Yatrik Patel
b3c0be7559 Translated using Weblate (Hindi)
Currently translated at 3.0% (12 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-06 04:01:51 +01:00
Máté Zsólya
d52b735b99 Translated using Weblate (Hungarian)
Currently translated at 1.9% (34 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hu/
2026-01-06 04:01:49 +01:00
Yatrik Patel
639b1f2863 Translated using Weblate (Hindi)
Currently translated at 5.9% (9 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-06 04:01:47 +01:00
Elian Doran
7f2cc885fe Feat(math): Improve legacy math input with MathLive (#7842) 2026-01-06 00:12:38 +02:00
Elian Doran
19a365a370 fix(sql_console): cannot copy table data (#8268) 2026-01-06 00:10:11 +02:00
Elian Doran
9a50da328e chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 (#8265) 2026-01-05 23:53:05 +02:00
Elian Doran
181e36a7c1 Merge remote-tracking branch 'origin/main' into Meinzzzz/main
; Conflicts:
;	.gitignore
2026-01-05 23:46:12 +02:00
Elian Doran
178508d245 Merge branch 'main' into fix/sql_select_text 2026-01-05 23:43:29 +02:00
Elian Doran
d132d084cf Merge branch 'main' into renovate/rollup-plugin-webpack-stats-2.x 2026-01-05 23:43:06 +02:00
Elian Doran
494b55d685 fix(ckeditor): missing pl locale 2026-01-05 23:39:36 +02:00
Elian Doran
51513d3779 fix(status_bar): count not refreshing properly after change 2026-01-05 21:03:32 +02:00
SngAbc
458398f2ca Merge branch 'main' into fix/sql_select_text 2026-01-05 13:51:45 +08:00
SngAbc
7a6cc4f51e fix(sql_console): cannot copy table data
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-05 12:16:16 +08:00
SiriusXT
f4ccce7de5 fix(sql_console): cannot copy table data 2026-01-05 11:23:50 +08:00
renovate[bot]
f8b5417d6c chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 2026-01-05 01:03:52 +00:00
meinzzzz
87ab41c80c Fix shift+tab behavior in MathInputView 2025-12-23 18:02:40 +01:00
Meinzzzz
d2391f94c0 Fix offline math rendering by bundling local fonts 2025-12-15 21:32:50 +01:00
Meinzzzz
050ddb8c55 Improve css to fix tooltips 2025-12-15 20:17:58 +01:00
Meinzzzz
bc23e0984a Undo unnecessary formatting changes 2025-12-14 22:00:56 +01:00
Meinzzzz
07de353207 Adding comments and improving code quality in math input views 2025-12-14 20:21:42 +01:00
Meinzzzz
c02491d2e6 Remove unnecessary any casts in math plugin 2025-12-12 23:09:20 +01:00
Meinzzzz
a6ede8f905 Improve mathinputview 2025-12-12 21:33:59 +01:00
Meinzzzz
22941a9ce0 Fix sync issues 2025-12-12 19:48:09 +01:00
Meinzzzz
633a09d414 Fix sync bug 2025-12-11 23:06:13 +01:00
Meinzzzz
29f0881c5a Fix clicking issue in Mathfield 2025-12-10 22:44:02 +01:00
Meinzzzz
60debca37b Improve comments 2025-12-10 18:36:34 +01:00
Meinzzzz
30ea81d0fb Improve virtual keyboard logic and fix Tab issues 2025-12-08 22:59:08 +01:00
Meinzzzz
b1d92c4fe6 Fix Tab issues 2025-12-08 22:39:12 +01:00
Meinzzzz
70f46de2d8 MathLive virtual keyboard only appears when focusing the mathfield 2025-12-08 20:30:07 +01:00
Meinzzzz
f1b2d0b870 Increas Mathfield font size and ensure virtual keyboard appears above CKEditor 2025-12-08 20:22:52 +01:00
Meinzzzz
8a385972fc Close Virtual Keyboard when Mathinput is closed 2025-12-08 18:49:06 +01:00
Meinzzzz
28dd85c1d1 Merge upstream changes and resolve conflicts 2025-12-07 23:51:41 +01:00
meinzzzz
827c8e0e72 Refactor: Combine MathLive and LaTeX inputs into one single component 2025-12-07 23:19:48 +01:00
meinzzzz
162c076a14 Improve MathLive integration and lazy loading 2025-12-02 22:30:37 +01:00
meinzzzz
9386465de7 Added mathrender error class for better error handling in math rendering 2025-12-02 22:29:20 +01:00
meinzzzz
acca22f3a1 Improve Synchronization Between Mathlive and rawlatex input 2025-12-02 22:28:16 +01:00
meinzzzz
f8d84814e0 Fix differential d problems 2025-11-26 23:02:34 +01:00
meinzzzz
c46cf41842 Small improvements 2025-11-26 22:48:57 +01:00
meinzzzz
64ab1c4116 Imrovement for Latex 2025-11-26 22:29:29 +01:00
meinzzzz
a6de1041c7 Fix bug in math rendering where old content was not cleared 2025-11-26 21:59:33 +01:00
meinzzzz
c8d34e65ea Improve max window size 2025-11-26 21:49:09 +01:00
meinzzzz
51db729546 Improve and simplify Mathfield integration 2025-11-25 23:27:06 +01:00
meinzzzz
d2052ad236 Disable mathlive sound effects 2025-11-24 21:51:59 +01:00
meinzzzz
9c4301467f Remove unused icons from ckeditor5-math package 2025-11-24 19:46:04 +01:00
meinzzzz
e7355dc0e4 remove gitignore unneccesary changes 2025-11-24 18:43:52 +01:00
meinzzzz
4110fec94f Removed unnecessary declare keyboard 2025-11-24 18:28:59 +01:00
meinzzzz
d5e601eae9 Simpliyfied resize logic for math input form and improved css 2025-11-24 17:56:18 +01:00
meinzzzz
4f044c4a57 Use icons form CKEditor5 icons, instead of testing icons. 2025-11-23 22:43:07 +01:00
meinzzzz
5821c350e1 Fixing class property initialization order 2025-11-23 17:58:51 +01:00
meinzzzz
edba8188fe Fix dark selection colors in MathLive math-field 2025-11-23 13:44:28 +01:00
meinzzzz
1471a72633 refactor: avoid recursive updates in mathLiveInput by normalizing value before updateing 2025-11-23 13:34:22 +01:00
meinzzzz
56834cb88a Improve MathLive and Raw LaTeX input views to propagate mousedown events 2025-11-23 13:29:26 +01:00
meinzzzz
a0f16f9184 Fix typos in mathform.css 2025-11-23 13:09:56 +01:00
meinzzzz
de80eb4806 Improve mathform.css styling for better visual integration 2025-11-22 22:42:34 +01:00
meinzzzz
48a4b81fbe remove automated screenshot files 2025-11-22 21:40:55 +01:00
meinzzzz
e225794f72 Better window focus handling in MathFormView 2025-11-22 21:35:37 +01:00
meinzzzz
4eef30f8b5 Fix names 2025-11-22 00:20:20 +01:00
meinzzzz
569b09609d Remove mathlive dependency and chunking 2025-11-22 00:01:14 +01:00
meinzzzz
39838c25c2 Fixed chaching problems 2025-11-21 23:50:49 +01:00
meinzzzz
49e90c08a9 Better Names for Math UI Components 2025-11-20 22:45:21 +01:00
meinzzzz
e777b06fb8 Math 2025-11-20 18:53:39 +01:00
meinzzzz
497ec2ac74 Merge branch 'main' of https://github.com/Meinzzzz/Trilium-Mathlive 2025-11-20 18:00:18 +01:00
meinzzzz
c5d282d203 Mathlive 2025-11-20 00:09:10 +01:00
240 changed files with 10291 additions and 10370 deletions

2
.gitignore vendored
View File

@@ -51,4 +51,4 @@ upload
# docs
site/
apps/*/coverage
scripts/translation/.language*.json
scripts/translation/.language*.json

View File

@@ -1,112 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title>
</head>
<body id="trilium-app">
<noscript><%= t("javascript-required") %></noscript>
<script>
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
document.getElementsByTagName("body")[0].style.display = "none";
</script>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
<!-- Required for match the PWA's top bar color with the theme -->
<!-- This works even when the user directly changes --root-background in CSS -->
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
<!-- Bootstrap (request server for required information) -->
<script>
async function bootstrap() {
await setupGlob();
loadStylesheets();
loadIcons();
setBodyAttributes();
await loadScripts();
}
async function setupGlob() {
const response = await fetch("./bootstrap");
const json = await response.json();
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
...json,
activeDialog: null
};
}
function loadStylesheets() {
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
const cssToLoad = [];
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
}
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`)
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`)
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`)
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
for (const href of cssToLoad) {
const linkEl = document.createElement("link");
linkEl.href = href;
linkEl.rel = "stylesheet";
document.body.appendChild(linkEl);
}
}
function loadIcons() {
const styleEl = document.createElement("style");
styleEl.innerText = window.glob.iconPackCss;
document.head.appendChild(styleEl);
}
function setBodyAttributes() {
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
const classesToSet = [
device,
`heading-style-${headingStyle}`,
`layout-${layoutOrientation}`,
`platform-${platform}`,
isElectron && "isElectron",
hasNativeTitleBar && "native-titlebar",
hasBackgroundEffects && "background-effects"
].filter(Boolean);
for (const classToSet of classesToSet) {
document.body.classList.add(classToSet);
}
document.body.lang = currentLocale.id;
document.body.dir = currentLocale.rtl ? "rtl" : "ltr";
}
async function loadScripts() {
const assetPath = glob.assetPath;
await import(`./${assetPath}/runtime.js`);
await import(`./${assetPath}/desktop.js`);
}
bootstrap();
</script>
<!-- Required for correct loading of scripts in Electron -->
<script>
if (typeof module === 'object') {window.module = module; module = undefined;}
</script>
</body>
</html>

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title>
</head>
<body id="trilium-app">
<noscript>Trilium requires JavaScript to be enabled.</noscript>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
<!-- Required to match the PWA's top bar color with the theme -->
<!-- This works even when the user directly changes --root-background in CSS -->
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
<script src="./index.ts" type="module"></script>
<!-- Required for correct loading of scripts in Electron -->
<script>
if (typeof module === 'object') {window.module = module; module = undefined;}
</script>
</body>
</html>

View File

@@ -0,0 +1,110 @@
async function bootstrap() {
showSplash();
await setupGlob();
await Promise.all([
initJQuery(),
loadBootstrapCss()
]);
loadStylesheets();
loadIcons();
setBodyAttributes();
await loadScripts();
hideSplash();
}
async function initJQuery() {
const $ = (await import("jquery")).default;
window.$ = $;
window.jQuery = $;
}
async function setupGlob() {
const response = await fetch(`/bootstrap${window.location.search}`);
const json = await response.json();
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
...json,
activeDialog: null
};
}
async function loadBootstrapCss() {
// We have to selectively import Bootstrap CSS based on text direction.
if (glob.isRtl) {
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
} else {
await import("bootstrap/dist/css/bootstrap.min.css");
}
}
function loadStylesheets() {
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
const cssToLoad: string[] = [];
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
cssToLoad.push(`api/fonts`);
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
}
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
for (const href of cssToLoad) {
const linkEl = document.createElement("link");
linkEl.href = href;
linkEl.rel = "stylesheet";
document.head.appendChild(linkEl);
}
}
function loadIcons() {
const styleEl = document.createElement("style");
styleEl.innerText = window.glob.iconPackCss;
document.head.appendChild(styleEl);
}
function setBodyAttributes() {
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
const classesToSet = [
device,
`heading-style-${headingStyle}`,
`layout-${layoutOrientation}`,
`platform-${platform}`,
isElectron && "electron",
hasNativeTitleBar && "native-titlebar",
hasBackgroundEffects && "background-effects"
].filter(Boolean) as string[];
for (const classToSet of classesToSet) {
document.body.classList.add(classToSet);
}
document.body.lang = currentLocale.id;
document.body.dir = currentLocale.rtl ? "rtl" : "ltr";
}
async function loadScripts() {
if (glob.device === "mobile") {
await import("./mobile.js");
} else {
await import("./desktop.js");
}
}
function showSplash() {
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
document.body.style.display = "none";
}
function hideSplash() {
document.body.style.display = "block";
}
bootstrap();

View File

@@ -1,3 +1,7 @@
import "bootstrap/dist/css/bootstrap.min.css";
// @ts-ignore - module = undefined
// Required for correct loading of scripts in Electron
if (typeof module === 'object') {window.module = module; module = undefined;}
document.body.style.display = "block";

View File

@@ -1,15 +0,0 @@
import $ from "jquery";
async function loadBootstrap() {
if (document.body.dir === "rtl") {
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
} else {
await import("bootstrap/dist/css/bootstrap.min.css");
}
}
(window as any).$ = $;
(window as any).jQuery = $;
await loadBootstrap();
$("body").show();

View File

@@ -1,4 +1,14 @@
import { DefinitionObject, LabelType, Multiplicity } from "@triliumnext/commons";
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
type Multiplicity = "single" | "multi";
export interface DefinitionObject {
isPromoted?: boolean;
labelType?: LabelType;
multiplicity?: Multiplicity;
numberPrecision?: number;
promotedAlias?: string;
inverseRelation?: string;
}
function parse(value: string) {
const tokens = value.split(",").map((t) => t.trim());

View File

@@ -25,7 +25,8 @@
},
"widget-list-error": {
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
}
},
"open-script-note": "Script-Notiz öffnen"
},
"add_link": {
"add_link": "Link hinzufügen",
@@ -208,7 +209,8 @@
"info": {
"modalTitle": "Infonachricht",
"closeButton": "Schließen",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "In die Zwischenablage kopieren"
},
"jump_to_note": {
"search_button": "Suche im Volltext",
@@ -695,7 +697,9 @@
"export_as_image": "Als Bild exportieren",
"export_as_image_png": "PNG (Raster)",
"export_as_image_svg": "SVG (Vektor)",
"note_map": "Notizen Karte"
"note_map": "Notizen Karte",
"view_revisions": "Notizrevisionen",
"advanced": "Erweitert"
},
"onclick_button": {
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"

View File

@@ -162,7 +162,8 @@
"other": "Otro",
"quickSearch": "centrarse en la entrada de búsqueda rápida",
"inPageSearch": "búsqueda en la página",
"title": "Hoja de ayuda"
"title": "Hoja de ayuda",
"editShortcuts": "Editar atajos de teclado"
},
"import": {
"importIntoNote": "Importar a nota",

View File

@@ -21,7 +21,7 @@
},
"bundle-error": {
"title": "Echec du chargement d'un script personnalisé",
"message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}"
"message": "Le script n'a pas pu être exécuté à cause de\n\n{{message}}"
},
"widget-list-error": {
"title": "Impossible d'obtenir la liste des widgets depuis le serveur"

View File

@@ -31,5 +31,17 @@
},
"add_link": {
"note": "नोट"
},
"bulk_actions": {
"other": "अन्य"
},
"clone_to": {
"search_for_note_by_its_name": "नोट क नाम से नोट खोजें"
},
"confirm": {
"also_delete_note": "नोट भी डिलीट करें"
},
"delete_notes": {
"delete_notes_preview": "नोट्स प्रिव्यू डिलीट करें"
}
}

View File

@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Nem sikerült betölteni az egyéni szkriptet",
"message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
"message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}"
},
"widget-list-error": {
"title": "A Widget-ek letöltése sikertelen volt"
},
"widget-render-error": {
"title": "Nem sikerült renderelni a React widget-et"
}
},
"add_link": {

View File

@@ -1895,7 +1895,11 @@
"create-child-note": "Crea nota figlio",
"unhoist": "Sganciare",
"toggle-sidebar": "Attiva/disattiva la barra laterale",
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione."
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione.",
"clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}",
"clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})",
"shared-indicator-tooltip": "Questa nota è condivisa pubblicamente",
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}"
},
"title_bar_buttons": {
"window-on-top": "Mantieni la finestra in primo piano"
@@ -2200,7 +2204,14 @@
"execute_sql_description": "Questa nota è una nota SQL. Clicca per eseguire la query SQL.",
"shared_copy_to_clipboard": "Copia link negli appunti",
"shared_open_in_browser": "Apri il link nel browser",
"shared_unshare": "Rimuovi condivisione"
"shared_unshare": "Rimuovi condivisione",
"save_status_saved": "Salvato",
"save_status_saving": "Salvataggio in corso...",
"save_status_unsaved": "Non salvato",
"save_status_error": "Salvataggio non riuscito",
"save_status_saving_tooltip": "Le modifiche sono state salvate.",
"save_status_unsaved_tooltip": "Ci sono modifiche non salvate. Verranno salvate automaticamente tra un attimo.",
"save_status_error_tooltip": "Si è verificato un errore durante il salvataggio della nota. Se possibile, prova a copiare il contenuto della nota altrove e a ricaricare l'applicazione."
},
"breadcrumb": {
"workspace_badge": "Area di lavoro",
@@ -2243,5 +2254,18 @@
"empty_button": "Nascondi il pannello",
"toggle": "Attiva/disattiva pannello destro",
"custom_widget_go_to_source": "Vai al codice sorgente"
},
"pdf": {
"attachments_one": "{{count}} allegato",
"attachments_many": "{{count}} allegati",
"attachments_other": "{{count}} allegati",
"layers_one": "{{count}} livello",
"layers_many": "{{count}} livelli",
"layers_other": "{{count}} livelli",
"pages_one": "{{count}} pagina",
"pages_many": "{{count}} pagine",
"pages_other": "{{count}} pagine",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Caricamento in corso..."
}
}

View File

@@ -1,4 +1,4 @@
import { IconRegistry } from "@triliumnext/commons";
import { IconRegistry, Locale } from "@triliumnext/commons";
import appContext, { AppContext } from "./components/app_context";
import type FNote from "./entities/fnote";
@@ -47,14 +47,25 @@ interface CustomGlobals {
platform?: typeof process.platform;
linter: typeof lint;
hasNativeTitleBar: boolean;
hasBackgroundEffects: boolean;
isElectron: boolean;
isRtl: boolean;
iconRegistry: IconRegistry;
themeCssUrl: string;
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
iconPackCss: string;
headingStyle: "plain" | "underline" | "markdown";
layoutOrientation: "vertical" | "horizontal";
currentLocale: Locale;
}
type RequireMethod = (moduleName: string) => any;
declare global {
interface Window {
$: JQueryStatic;
jQuery: JQueryStatic;
logError(message: string);
logInfo(message: string);

View File

@@ -1,16 +1,14 @@
import "./UserAttributesList.css";
import type { DefinitionObject } from "@triliumnext/commons";
import { ComponentChildren, CSSProperties } from "preact";
import { useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { getReadableTextColor } from "../../services/css_class_manager";
import { formatDateTime } from "../../utils/formatters";
import "./UserAttributesList.css";
import { useTriliumEvent } from "../react/hooks";
import attributes from "../../services/attributes";
import { DefinitionObject } from "../../services/promoted_attribute_definition_parser";
import { formatDateTime } from "../../utils/formatters";
import { ComponentChildren, CSSProperties } from "preact";
import Icon from "../react/Icon";
import NoteLink from "../react/NoteLink";
import { getReadableTextColor } from "../../services/css_class_manager";
interface UserAttributesListProps {
note: FNote;
@@ -31,7 +29,7 @@ export default function UserAttributesDisplay({ note, ignoredAttributes }: UserA
<div className="user-attributes">
{userAttributes?.map(attr => buildUserAttribute(attr))}
</div>
);
)
}
@@ -48,13 +46,13 @@ function useNoteAttributesWithDefinitions(note: FNote, attributesToIgnore: stri
}
function UserAttribute({ attr, children, style }: { attr: AttributeWithDefinitions, children: ComponentChildren, style?: CSSProperties }) {
const className = `${attr.type === "label" ? `label` + ` ${ attr.def.labelType}` : "relation"}`;
const className = `${attr.type === "label" ? "label" + " " + attr.def.labelType : "relation"}`;
return (
<span key={attr.friendlyName} className={`user-attribute type-${className}`} style={style}>
{children}
</span>
);
)
}
function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
@@ -63,7 +61,7 @@ function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
let style: CSSProperties | undefined;
if (attr.type === "label") {
const value = attr.value;
let value = attr.value;
switch (attr.def.labelType) {
case "number":
let formattedValue = value;
@@ -104,7 +102,7 @@ function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
content = <>{defaultLabel}<NoteLink notePath={attr.value} showNoteIcon /></>;
}
return <UserAttribute attr={attr} style={style}>{content}</UserAttribute>;
return <UserAttribute attr={attr} style={style}>{content}</UserAttribute>
}
function getAttributesWithDefinitions(note: FNote, attributesToIgnore: string[] = []): AttributeWithDefinitions[] {

View File

@@ -1,12 +1,11 @@
import { LabelType } from "@triliumnext/commons";
import { JSX } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import type { CellComponent, ColumnDefinition, EmptyCallback, FormatterParams, ValueBooleanCallback, ValueVoidCallback } from "tabulator-tables";
import froca from "../../../services/froca.js";
import Icon from "../../react/Icon.jsx";
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
import { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
import { JSX } from "preact";
import { renderReactWidget } from "../../react/react_utils.jsx";
import Icon from "../../react/Icon.jsx";
import { useEffect, useRef, useState } from "preact/hooks";
import froca from "../../../services/froca.js";
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
type ColumnType = LabelType | "relation";
@@ -79,7 +78,7 @@ export function buildColumnDefinitions({ info, movableRows, existingColumnData,
rowHandle: movableRows,
width: calculateIndexColumnWidth(rowNumberHint, movableRows),
formatter: wrapFormatter(({ cell, formatterParams }) => <div>
{(formatterParams as RowNumberFormatterParams).movableRows && <><span class="bx bx-dots-vertical-rounded" />{" "}</>}
{(formatterParams as RowNumberFormatterParams).movableRows && <><span class="bx bx-dots-vertical-rounded"></span>{" "}</>}
{cell.getRow().getPosition(true)}
</div>),
formatterParams: { movableRows } satisfies RowNumberFormatterParams
@@ -201,14 +200,14 @@ function wrapEditor(Component: (opts: EditorOpts) => JSX.Element): ((
editorParams: {},
) => HTMLElement | false) {
return (cell, _, success, cancel, editorParams) => {
const elWithParams = <Component cell={cell} success={success} cancel={cancel} editorParams={editorParams} />;
const elWithParams = <Component cell={cell} success={success} cancel={cancel} editorParams={editorParams} />
return renderReactWidget(null, elWithParams)[0];
};
}
function NoteFormatter({ cell }: FormatterOpts) {
const noteId = cell.getValue();
const [ note, setNote ] = useState(noteId ? froca.getNoteFromCache(noteId) : null);
const [ note, setNote ] = useState(noteId ? froca.getNoteFromCache(noteId) : null)
useEffect(() => {
if (!noteId || note?.noteId === noteId) return;
@@ -232,5 +231,5 @@ function RelationEditor({ cell, success }: EditorOpts) {
hideAllButtons: true
}}
noteIdChanged={success}
/>;
/>
}

View File

@@ -23,7 +23,7 @@ export default function SqlResults() {
{t("sql_result.no_rows")}
</Alert>
) : (
<div class="sql-console-result-container">
<div className="sql-console-result-container selectable-text">
{results?.map(rows => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {

View File

@@ -286,7 +286,7 @@ function useWatchdogCrashHandling() {
const currentState = watchdog.state;
logInfo(`CKEditor state changed to ${currentState}`);
if (currentState === "ready") {
if (currentState === "ready" && hasCrashed.current) {
hasCrashed.current = false;
watchdog.editor?.focus();
}

View File

@@ -70,21 +70,15 @@ export default defineConfig(() => ({
sourcemap: false,
rollupOptions: {
input: {
desktop: join(__dirname, "src", "desktop.html"),
mobile: join(__dirname, "src", "mobile.ts"),
index: join(__dirname, "src", "index.html"),
login: join(__dirname, "src", "login.ts"),
setup: join(__dirname, "src", "setup.ts"),
set_password: join(__dirname, "src", "set_password.ts"),
runtime: join(__dirname, "src", "runtime.ts"),
print: join(__dirname, "src", "print.tsx")
},
output: {
entryFileNames: "src/[name].js",
chunkFileNames: "src/[name].js",
assetFileNames: "src/[name].[ext]",
manualChunks: {
"ckeditor5": [ "@triliumnext/ckeditor5" ],
"boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ]
"ckeditor5": [ "@triliumnext/ckeditor5" ]
},
},
onwarn(warning, rollupWarn) {

View File

@@ -35,11 +35,11 @@
"sucrase": "3.35.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.71.2",
"@anthropic-ai/sdk": "0.71.2",
"@braintree/sanitize-url": "7.1.1",
"@electron/remote": "2.1.3",
"@preact/preset-vite": "2.10.2",
"@triliumnext/commons": "workspace:*",
"@triliumnext/core": "workspace:*",
"@triliumnext/express-partial-content": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/turndown-plugin-gfm": "workspace:*",
@@ -49,14 +49,17 @@
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
"@types/debounce": "1.2.4",
"@types/ejs": "3.1.5",
"@types/ejs": "3.1.5",
"@types/escape-html": "1.0.4",
"@types/express-http-proxy": "1.6.7",
"@types/express-session": "1.18.2",
"@types/fs-extra": "11.0.4",
"@types/html": "1.0.4",
"@types/ini": "4.1.1",
"@types/ini": "4.1.1",
"@types/mime-types": "3.0.1",
"@types/multer": "2.0.0",
"@types/safe-compare": "1.1.2",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.16.0",
"@types/sax": "1.2.7",
"@types/serve-favicon": "2.5.7",
"@types/serve-static": "2.2.0",
@@ -83,7 +86,8 @@
"ejs": "3.1.10",
"electron": "39.2.7",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"express": "5.2.1",
"express-http-proxy": "2.1.2",
"express-openid-connect": "2.19.3",
@@ -105,12 +109,15 @@
"jimp": "1.6.0",
"lorem-ipsum": "2.0.8",
"marked": "17.0.1",
"mime-types": "3.0.2",
"multer": "2.0.2",
"normalize-strings": "1.1.1",
"ollama": "0.6.3",
"openai": "6.15.0",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.17.0",
"sax": "1.4.3",
"serve-favicon": "2.5.1",
"stream-throttle": "0.1.3",
@@ -121,6 +128,7 @@
"time2fa": "1.4.2",
"tmp": "0.2.5",
"turndown": "7.2.2",
"unescape": "1.0.1",
"vite": "7.3.0",
"ws": "8.18.3",
"xml2js": "0.6.2",

View File

@@ -1,6 +1,6 @@
import anonymizationService from "./services/anonymization.js";
import sqlInit from "./services/sql_init.js";
await import("@triliumnext/core");
await import("./becca/entity_constructor.js");
sqlInit.dbReady.then(async () => {
try {

View File

@@ -1,25 +1,25 @@
import("@triliumnext/core");
import { erase } from "@triliumnext/core";
import compression from "compression";
import cookieParser from "cookie-parser";
import express from "express";
import { auth } from "express-openid-connect";
import helmet from "helmet";
import { t } from "i18next";
import path from "path";
import favicon from "serve-favicon";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import compression from "compression";
import config from "./services/config.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
import assets from "./routes/assets.js";
import routes from "./routes/routes.js";
import custom from "./routes/custom.js";
import error_handlers from "./routes/error_handlers.js";
import routes from "./routes/routes.js";
import config from "./services/config.js";
import log from "./services/log.js";
import openID from "./services/open_id.js";
import { RESOURCE_DIR } from "./services/resource_dir.js";
import { startScheduledCleanup } from "./services/erase.js";
import sql_init from "./services/sql_init.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
import { auth } from "express-openid-connect";
import openID from "./services/open_id.js";
import { t } from "i18next";
import eventService from "./services/events.js";
import log from "./services/log.js";
import "./services/handlers.js";
import "./becca/becca_loader.js";
import { RESOURCE_DIR } from "./services/resource_dir.js";
export default async function buildApp() {
const app = express();
@@ -107,7 +107,7 @@ export default async function buildApp() {
await import("./services/scheduler.js");
erase.startScheduledCleanup();
startScheduledCleanup();
if (utils.isElectron) {
(await import("@electron/remote/main/index.js")).initialize();

View File

@@ -220,7 +220,6 @@
"password-confirmation": "Password confirmation",
"button": "Set password"
},
"javascript-required": "Trilium requires JavaScript to be enabled.",
"setup": {
"heading": "Trilium Notes setup",
"new-document": "I'm a new user, and I want to create a new Trilium document for my notes",

View File

@@ -10,6 +10,18 @@
"creating-and-moving-notes": "नोट्स बनाना और स्थानांतरित करना",
"move-note-up": "नोट को ऊपर ले जाएं",
"move-note-down": "नोट को नीचे ले जाएं",
"note-clipboard": "नोट क्लिपबोर्ड"
"note-clipboard": "नोट क्लिपबोर्ड",
"duplicate-subtree": "डुप्लिकेट सबट्री",
"open-new-tab": "नया टैब खोलें",
"second-tab": "लिस्ट में दूसरी टैब एक्टिवेट करें",
"third-tab": "लिस्ट में तीसरी टैब एक्टिवेट करें",
"fourth-tab": "लिस्ट में चौथी टैब एक्टिवेट करें",
"sixth-tab": "लिस्ट में छठी टैब एक्टिवेट करें",
"seventh-tab": "लिस्ट में सातवीं टैब एक्टिवेट करें",
"eight-tab": "लिस्ट में आठवीं टैब एक्टिवेट करें",
"ninth-tab": "लिस्ट में नौवीं टैब एक्टिवेट करें",
"last-tab": "लिस्ट में आखिरी टैब एक्टिवेट करें",
"show-sql-console": "\"SQL कंसोल\" पेज खोलें",
"show-backend-log": "\"बैकेंड लॉग\" पेज खोलें"
}
}

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<body
>
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
<link href="api/fonts" rel="stylesheet">
<script src="<%= appPath %>/desktop.js" crossorigin type="module"></script>
</body>
</html>

View File

@@ -15,7 +15,6 @@
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-light.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/style.css">
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>
<body lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>">
<div class="container login-page">

View File

@@ -1,137 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
<meta name="theme-color" content="#fff">
<title>Trilium Notes</title>
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<style>
.lds-roller {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 7px;
height: 7px;
border-radius: 50%;
background: #000;
margin: -4px 0 0 -4px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 63px;
left: 63px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 68px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 71px;
left: 48px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 71px;
left: 32px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 68px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 63px;
left: 17px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12px;
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<style id="trilium-icon-packs">
<%- iconPackCss %>
</style>
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>
<body
class="mobile heading-style-<%= headingStyle %>"
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
>
<noscript><%= t("javascript-required") %></noscript>
<div id="context-menu-cover"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
<%- include("./partials/windowGlobal.ejs", locals) %>
<script src="<%= appPath %>/mobile.js" crossorigin type="module"></script>
<link href="api/fonts" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
<% if (themeCssUrl) { %>
<link href="<%= themeCssUrl %>" rel="stylesheet">
<% } %>
<% if (themeUseNextAsBase === "next") { %>
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-dark") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-dark.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-light") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-light.css" rel="stylesheet">
<% } %>
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
</body>
</html>

View File

@@ -1,4 +1,6 @@
import { NotFoundError } from "../errors.js";
import sql from "../services/sql.js";
import NoteSet from "../services/search/note_set.js";
import NotFoundError from "../errors/not_found_error.js";
import type BOption from "./entities/boption.js";
import type BNote from "./entities/bnote.js";
import type BEtapiToken from "./entities/betapi_token.js";
@@ -10,8 +12,6 @@ import type { AttachmentRow, BlobRow, RevisionRow } from "@triliumnext/commons";
import BBlob from "./entities/bblob.js";
import BRecentNote from "./entities/brecent_note.js";
import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import { getSql } from "../services/sql/index.js";
import NoteSet from "../services/search/note_set.js";
/**
* Becca is a backend cache of all notes, branches, and attributes.
@@ -151,7 +151,7 @@ export default class Becca {
}
getRevision(revisionId: string): BRevision | null {
const row = getSql().getRow<RevisionRow | null>("SELECT * FROM revisions WHERE revisionId = ?", [revisionId]);
const row = sql.getRow<RevisionRow | null>("SELECT * FROM revisions WHERE revisionId = ?", [revisionId]);
return row ? new BRevision(row) : null;
}
@@ -170,7 +170,7 @@ export default class Becca {
JOIN blobs USING (blobId)
WHERE attachmentId = ? AND isDeleted = 0`;
return getSql().getRows<AttachmentRow>(query, [attachmentId]).map((row) => new BAttachment(row))[0];
return sql.getRows<AttachmentRow>(query, [attachmentId]).map((row) => new BAttachment(row))[0];
}
getAttachmentOrThrow(attachmentId: string): BAttachment {
@@ -182,7 +182,7 @@ export default class Becca {
}
getAttachments(attachmentIds: string[]): BAttachment[] {
return getSql().getManyRows<AttachmentRow>("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds).map((row) => new BAttachment(row));
return sql.getManyRows<AttachmentRow>("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds).map((row) => new BAttachment(row));
}
getBlob(entity: { blobId?: string }): BBlob | null {
@@ -190,7 +190,7 @@ export default class Becca {
return null;
}
const row = getSql().getRow<BlobRow | null>("SELECT *, LENGTH(content) AS contentLength FROM blobs WHERE blobId = ?", [entity.blobId]);
const row = sql.getRow<BlobRow | null>("SELECT *, LENGTH(content) AS contentLength FROM blobs WHERE blobId = ?", [entity.blobId]);
return row ? new BBlob(row) : null;
}
@@ -227,12 +227,12 @@ export default class Becca {
}
getRecentNotesFromQuery(query: string, params: string[] = []): BRecentNote[] {
const rows = getSql().getRows<BRecentNote>(query, params);
const rows = sql.getRows<BRecentNote>(query, params);
return rows.map((row) => new BRecentNote(row));
}
getRevisionsFromQuery(query: string, params: string[] = []): BRevision[] {
const rows = getSql().getRows<RevisionRow>(query, params);
const rows = sql.getRows<RevisionRow>(query, params);
return rows.map((row) => new BRevision(row));
}

View File

@@ -1,2 +1,7 @@
import { becca } from "@triliumnext/core";
"use strict";
import Becca from "./becca-interface.js";
const becca = new Becca();
export default becca;

View File

@@ -1,26 +1,27 @@
import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "@triliumnext/commons";
import eventService from "../services/events";
"use strict";
import entityConstructor from "../becca/entity_constructor.js";
import { getLog } from "../services/log.js";
import { dbReady } from "../services/sql_init.js";
import ws from "../services/ws.js";
import sql from "../services/sql.js";
import eventService from "../services/events.js";
import becca from "./becca.js";
import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import BAttribute from "./entities/battribute.js";
import BBranch from "./entities/bbranch.js";
import BEtapiToken from "./entities/betapi_token.js";
import log from "../services/log.js";
import BNote from "./entities/bnote.js";
import BBranch from "./entities/bbranch.js";
import BAttribute from "./entities/battribute.js";
import BOption from "./entities/boption.js";
import { getSql } from "../services/sql";
import { getContext } from "../services/context.js";
import BEtapiToken from "./entities/betapi_token.js";
import cls from "../services/cls.js";
import entityConstructor from "../becca/entity_constructor.js";
import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "@triliumnext/commons";
import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import ws from "../services/ws.js";
import { dbReady } from "../services/sql_init.js";
export const beccaLoaded = new Promise<void>(async (res, rej) => {
// We have to import async since options init requires keyboard actions which require translations.
const options_init = (await import("../services/options_init.js")).default;
dbReady.then(() => {
getContext().init(() => {
cls.init(() => {
load();
options_init.initStartupOptions();
@@ -35,7 +36,6 @@ function load() {
becca.reset();
// we know this is slow and the total becca load time is logged
const sql = getSql();
sql.disableSlowQueryLogging(() => {
// using a raw query and passing arrays to avoid allocating new objects,
// this is worth it for the becca load since it happens every run and blocks the app until finished
@@ -72,7 +72,7 @@ function load() {
becca.loaded = true;
getLog().info(`Becca (note cache) load took ${Date.now() - start}ms`);
log.info(`Becca (note cache) load took ${Date.now() - start}ms`);
}
function reload(reason: string) {
@@ -284,7 +284,7 @@ eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => {
try {
becca.decryptProtectedNotes();
} catch (e: any) {
getLog().error(`Could not decrypt protected notes: ${e.message} ${e.stack}`);
log.error(`Could not decrypt protected notes: ${e.message} ${e.stack}`);
}
});

View File

@@ -1,8 +1,8 @@
"use strict";
import becca from "./becca.js";
import { getLog } from "../services/log.js";
import { getHoistedNoteId } from "../services/context.js";
import cls from "../services/cls.js";
import log from "../services/log.js";
function isNotePathArchived(notePath: string[]) {
const noteId = notePath[notePath.length - 1];
@@ -29,7 +29,7 @@ function getNoteTitle(childNoteId: string, parentNoteId?: string) {
const parentNote = parentNoteId ? becca.notes[parentNoteId] : null;
if (!childNote) {
getLog().info(`Cannot find note '${childNoteId}'`);
log.info(`Cannot find note '${childNoteId}'`);
return "[error fetching title]";
}
@@ -50,7 +50,7 @@ function getNoteTitleAndIcon(childNoteId: string, parentNoteId?: string) {
const parentNote = parentNoteId ? becca.notes[parentNoteId] : null;
if (!childNote) {
getLog().info(`Cannot find note '${childNoteId}'`);
log.info(`Cannot find note '${childNoteId}'`);
return {
title: "[error fetching title]"
}
@@ -82,7 +82,7 @@ function getNoteTitleArrayForPath(notePathArray: string[]) {
let hoistedNotePassed = false;
// this is a notePath from outside of hoisted subtree, so the full title path needs to be returned
const hoistedNoteId = getHoistedNoteId();
const hoistedNoteId = cls.getHoistedNoteId();
const outsideOfHoistedSubtree = !notePathArray.includes(hoistedNoteId);
for (const noteId of notePathArray) {

View File

@@ -1,16 +1,16 @@
import eventService from "../../services/events";
"use strict";
import blobService from "../../services/blob.js";
import * as cls from "../../services/context";
import dateUtils from "../../services/utils/date";
import utils from "../../services/utils.js";
import sql from "../../services/sql.js";
import entityChangesService from "../../services/entity_changes.js";
import { getLog } from "../../services/log.js";
import eventService from "../../services/events.js";
import dateUtils from "../../services/date_utils.js";
import cls from "../../services/cls.js";
import log from "../../services/log.js";
import protectedSessionService from "../../services/protected_session.js";
import blobService from "../../services/blob.js";
import type { default as Becca, ConstructorData } from "../becca-interface.js";
import becca from "../becca.js";
import type { ConstructorData,default as Becca } from "../becca-interface.js";
import { getSql } from "../../services/sql";
import { concat2, encodeUtf8, unwrapStringOrBuffer, wrapStringOrBuffer } from "../../services/utils/binary";
import { hash, hashedBlobId, newEntityId, randomString } from "../../services/utils";
interface ContentOpts {
forceSave?: boolean;
@@ -36,7 +36,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
protected beforeSaving(opts?: {}) {
const constructorData = this.constructor as unknown as ConstructorData<T>;
if (!(this as any)[constructorData.primaryKeyName]) {
(this as any)[constructorData.primaryKeyName] = newEntityId();
(this as any)[constructorData.primaryKeyName] = utils.newEntityId();
}
}
@@ -72,7 +72,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
contentToHash += "|deleted";
}
return hash(contentToHash).substr(0, 10);
return utils.hash(contentToHash).substr(0, 10);
}
protected getPojoToSave() {
@@ -111,7 +111,6 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
const pojo = this.getPojoToSave();
const sql = getSql();
sql.transactional(() => {
sql.upsert(entityName, primaryKeyName, pojo);
@@ -138,7 +137,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
return this;
}
protected _setContent(content: string | Uint8Array, opts: ContentOpts = {}) {
protected _setContent(content: string | Buffer, opts: ContentOpts = {}) {
// client code asks to save entity even if blobId didn't change (something else was changed)
opts.forceSave = !!opts.forceSave;
opts.forceFrontendReload = !!opts.forceFrontendReload;
@@ -149,9 +148,9 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
}
if (this.hasStringContent()) {
content = unwrapStringOrBuffer(content);
content = content.toString();
} else {
content = wrapStringOrBuffer(content);
content = Buffer.isBuffer(content) ? content : Buffer.from(content);
}
const unencryptedContentForHashCalculation = this.getUnencryptedContentForHashCalculation(content);
@@ -168,7 +167,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
}
}
getSql().transactional(() => {
sql.transactional(() => {
const newBlobId = this.saveBlob(content, unencryptedContentForHashCalculation, opts);
const oldBlobId = this.blobId;
@@ -184,7 +183,6 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
}
private deleteBlobIfNotUsed(oldBlobId: string) {
const sql = getSql();
if (sql.getValue("SELECT 1 FROM notes WHERE blobId = ? LIMIT 1", [oldBlobId])) {
return;
}
@@ -203,29 +201,24 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
sql.execute("DELETE FROM entity_changes WHERE entityName = 'blobs' AND entityId = ?", [oldBlobId]);
}
private getUnencryptedContentForHashCalculation(unencryptedContent: Uint8Array | string) {
private getUnencryptedContentForHashCalculation(unencryptedContent: Buffer | string) {
if (this.isProtected) {
// a "random" prefix makes sure that the calculated hash/blobId is different for a decrypted/encrypted content
const encryptedPrefixSuffix = "t$[nvQg7q)&_ENCRYPTED_?M:Bf&j3jr_";
if (typeof unencryptedContent === "string") {
return `${encryptedPrefixSuffix}${unencryptedContent}`;
} else {
return concat2(encodeUtf8(encryptedPrefixSuffix), unencryptedContent)
}
return Buffer.isBuffer(unencryptedContent) ? Buffer.concat([Buffer.from(encryptedPrefixSuffix), unencryptedContent]) : `${encryptedPrefixSuffix}${unencryptedContent}`;
} else {
return unencryptedContent;
}
return unencryptedContent;
}
private saveBlob(content: string | Uint8Array, unencryptedContentForHashCalculation: string | Uint8Array, opts: ContentOpts = {}) {
private saveBlob(content: string | Buffer, unencryptedContentForHashCalculation: string | Buffer, opts: ContentOpts = {}) {
/*
* We're using the unencrypted blob for the hash calculation, because otherwise the random IV would
* cause every content blob to be unique which would balloon the database size (esp. with revisioning).
* This has minor security implications (it's easy to infer that given content is shared between different
* notes/attachments), but the trade-off comes out clearly positive.
*/
const newBlobId = hashedBlobId(unencryptedContentForHashCalculation);
const sql = getSql();
const newBlobId = utils.hashedBlobId(unencryptedContentForHashCalculation);
const blobNeedsInsert = !sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [newBlobId]);
if (!blobNeedsInsert) {
@@ -234,7 +227,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
const pojo = {
blobId: newBlobId,
content,
content: content,
dateModified: dateUtils.localNowDateTime(),
utcDateModified: dateUtils.utcNowDateTime()
};
@@ -248,13 +241,13 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
entityChangesService.putEntityChange({
entityName: "blobs",
entityId: newBlobId,
hash,
hash: hash,
isErased: false,
utcDateChanged: pojo.utcDateModified,
isSynced: true,
// overriding componentId will cause the frontend to think the change is coming from a different component
// and thus reload
componentId: opts.forceFrontendReload ? randomString(10) : null
componentId: opts.forceFrontendReload ? utils.randomString(10) : null
});
eventService.emit(eventService.ENTITY_CHANGED, {
@@ -265,16 +258,15 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
return newBlobId;
}
protected _getContent(): string | Uint8Array {
const sql = getSql();
const row = sql.getRow<{ content: string | Uint8Array }>(/*sql*/`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
protected _getContent(): string | Buffer {
const row = sql.getRow<{ content: string | Buffer }>(/*sql*/`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
if (!row) {
const constructorData = this.constructor as unknown as ConstructorData<T>;
throw new Error(`Cannot find content for ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}', blobId '${this.blobId}'`);
}
return blobService.processContent(row.content, this.isProtected || false, this.hasStringContent()) as string | Uint8Array;
return blobService.processContent(row.content, this.isProtected || false, this.hasStringContent());
}
/**
@@ -289,7 +281,6 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
this.utcDateModified = dateUtils.utcNowDateTime();
const sql = getSql();
sql.execute(
/*sql*/`UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ?
WHERE ${constructorData.primaryKeyName} = ?`,
@@ -302,7 +293,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
sql.execute(/*sql*/`UPDATE ${entityName} SET dateModified = ? WHERE ${constructorData.primaryKeyName} = ?`, [this.dateModified, entityId]);
}
getLog().info(`Marking ${entityName} ${entityId} as deleted`);
log.info(`Marking ${entityName} ${entityId} as deleted`);
this.putEntityChange(true);
@@ -316,14 +307,13 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
this.utcDateModified = dateUtils.utcNowDateTime();
const sql = getSql();
sql.execute(
/*sql*/`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ?
WHERE ${constructorData.primaryKeyName} = ?`,
[this.utcDateModified, entityId]
);
getLog().info(`Marking ${entityName} ${entityId} as deleted`);
log.info(`Marking ${entityName} ${entityId} as deleted`);
this.putEntityChange(true);

View File

@@ -1,2 +1,260 @@
import { BAttachment } from "@triliumnext/core";
import type { AttachmentRow } from "@triliumnext/commons";
import dateUtils from "../../services/date_utils.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import protectedSessionService from "../../services/protected_session.js";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import type BBranch from "./bbranch.js";
import type BNote from "./bnote.js";
const attachmentRoleToNoteTypeMapping = {
image: "image",
file: "file"
};
interface ContentOpts {
// TODO: Found in bnote.ts, to check if it's actually used and not a typo.
forceSave?: boolean;
/** will also save this BAttachment entity */
forceFullSave?: boolean;
/** override frontend heuristics on when to reload, instruct to reload */
forceFrontendReload?: boolean;
}
/**
* Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for
* larger amounts of data and generally not accessible to the user.
*/
class BAttachment extends AbstractBeccaEntity<BAttachment> {
static get entityName() {
return "attachments";
}
static get primaryKeyName() {
return "attachmentId";
}
static get hashedProperties() {
return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"];
}
noteId?: number;
attachmentId?: string;
/** either noteId or revisionId to which this attachment belongs */
ownerId!: string;
role!: string;
mime!: string;
title!: string;
type?: keyof typeof attachmentRoleToNoteTypeMapping;
position?: number;
utcDateScheduledForErasureSince?: string | null;
/** optionally added to the entity */
contentLength?: number;
isDecrypted?: boolean;
constructor(row: AttachmentRow) {
super();
this.updateFromRow(row);
this.decrypt();
}
updateFromRow(row: AttachmentRow): void {
if (!row.ownerId?.trim()) {
throw new Error("'ownerId' must be given to initialize a Attachment entity");
} else if (!row.role?.trim()) {
throw new Error("'role' must be given to initialize a Attachment entity");
} else if (!row.mime?.trim()) {
throw new Error("'mime' must be given to initialize a Attachment entity");
} else if (!row.title?.trim()) {
throw new Error("'title' must be given to initialize a Attachment entity");
}
this.attachmentId = row.attachmentId;
this.ownerId = row.ownerId;
this.role = row.role;
this.mime = row.mime;
this.title = row.title;
this.position = row.position;
this.blobId = row.blobId;
this.isProtected = !!row.isProtected;
this.dateModified = row.dateModified;
this.utcDateModified = row.utcDateModified;
this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince;
this.contentLength = row.contentLength;
}
copy(): BAttachment {
return new BAttachment({
ownerId: this.ownerId,
role: this.role,
mime: this.mime,
title: this.title,
blobId: this.blobId,
isProtected: this.isProtected
});
}
getNote(): BNote {
return this.becca.notes[this.ownerId];
}
/** @returns true if the note has string content (not binary) */
override hasStringContent(): boolean {
return utils.isStringNote(this.type, this.mime); // here was !== undefined && utils.isStringNote(this.type, this.mime); I dont know why we need !=undefined. But it filters out canvas libary items
}
isContentAvailable() {
return (
!this.attachmentId || // new attachment which was not encrypted yet
!this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
}
getTitleOrProtected() {
return this.isContentAvailable() ? this.title : "[protected]";
}
decrypt() {
if (!this.isProtected || !this.attachmentId) {
this.isDecrypted = true;
return;
}
if (!this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
try {
this.title = protectedSessionService.decryptString(this.title) || "";
this.isDecrypted = true;
} catch (e: any) {
log.error(`Could not decrypt attachment ${this.attachmentId}: ${e.message} ${e.stack}`);
}
}
}
getContent(): Buffer {
return this._getContent() as Buffer;
}
setContent(content: string | Buffer, opts?: ContentOpts) {
this._setContent(content, opts);
}
convertToNote(): { note: BNote; branch: BBranch } {
// TODO: can this ever be "search"?
if ((this.type as string) === "search") {
throw new Error(`Note of type search cannot have child notes`);
}
if (!this.getNote()) {
throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + "Converting note revision's attachments to note is not (yet) supported.");
}
if (!(this.role in attachmentRoleToNoteTypeMapping)) {
throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`);
}
if (!this.isContentAvailable()) {
// isProtected is the same for attachment
throw new Error(`Cannot convert protected attachment outside of protected session`);
}
const { note, branch } = noteService.createNewNote({
parentNoteId: this.ownerId,
title: this.title,
type: (attachmentRoleToNoteTypeMapping as any)[this.role],
mime: this.mime,
content: this.getContent(),
isProtected: this.isProtected
});
this.markAsDeleted();
const parentNote = this.getNote();
if (this.role === "image" && parentNote.type === "text") {
const origContent = parentNote.getContent();
if (typeof origContent !== "string") {
throw new Error(`Note with ID '${note.noteId} has a text type but non-string content.`);
}
const oldAttachmentUrl = `api/attachments/${this.attachmentId}/image/`;
const newNoteUrl = `api/images/${note.noteId}/`;
const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl);
if (fixedContent !== origContent) {
parentNote.setContent(fixedContent);
}
noteService.asyncPostProcessContent(note, fixedContent);
}
return { note, branch };
}
getFileName() {
const type = this.role === "image" ? "image" : "file";
return utils.formatDownloadTitle(this.title, type, this.mime);
}
override beforeSaving() {
super.beforeSaving();
if (this.position === undefined || this.position === null) {
this.position =
10 +
sql.getValue<number>(
/*sql*/`SELECT COALESCE(MAX(position), 0)
FROM attachments
WHERE ownerId = ?`,
[this.noteId]
);
}
this.dateModified = dateUtils.localNowDateTime();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
attachmentId: this.attachmentId,
ownerId: this.ownerId,
role: this.role,
mime: this.mime,
title: this.title || undefined,
position: this.position,
blobId: this.blobId,
isProtected: !!this.isProtected,
isDeleted: false,
dateModified: this.dateModified,
utcDateModified: this.utcDateModified,
utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince,
contentLength: this.contentLength
};
}
override getPojoToSave() {
const pojo = this.getPojo();
delete pojo.contentLength;
if (pojo.isProtected) {
if (this.isDecrypted) {
pojo.title = protectedSessionService.encrypt(pojo.title || "") || undefined;
} else {
// updating protected note outside of protected session means we will keep original ciphertexts
delete pojo.title;
}
}
return pojo;
}
}
export default BAttachment;

View File

@@ -1,2 +1,227 @@
import { BAttribute } from "@triliumnext/core";
"use strict";
import BNote from "./bnote.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import dateUtils from "../../services/date_utils.js";
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
import sanitizeAttributeName from "../../services/sanitize_attribute_name.js";
import type { AttributeRow, AttributeType } from "@triliumnext/commons";
interface SavingOpts {
skipValidation?: boolean;
}
/**
* Attribute is an abstract concept which has two real uses - label (key - value pair)
* and relation (representing named relationship between source and target note)
*/
class BAttribute extends AbstractBeccaEntity<BAttribute> {
static get entityName() {
return "attributes";
}
static get primaryKeyName() {
return "attributeId";
}
static get hashedProperties() {
return ["attributeId", "noteId", "type", "name", "value", "isInheritable"];
}
attributeId!: string;
noteId!: string;
type!: AttributeType;
name!: string;
position!: number;
value!: string;
isInheritable!: boolean;
constructor(row?: AttributeRow) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
updateFromRow(row: AttributeRow) {
this.update([row.attributeId, row.noteId, row.type, row.name, row.value, row.isInheritable, row.position, row.utcDateModified]);
}
update([attributeId, noteId, type, name, value, isInheritable, position, utcDateModified]: any) {
this.attributeId = attributeId;
this.noteId = noteId;
this.type = type;
this.name = name;
this.position = position;
this.value = value || "";
this.isInheritable = !!isInheritable;
this.utcDateModified = utcDateModified;
return this;
}
override init() {
if (this.attributeId) {
this.becca.attributes[this.attributeId] = this;
}
if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
}
this.becca.notes[this.noteId].ownedAttributes.push(this);
const key = `${this.type}-${this.name.toLowerCase()}`;
this.becca.attributeIndex[key] = this.becca.attributeIndex[key] || [];
this.becca.attributeIndex[key].push(this);
const targetNote = this.targetNote;
if (targetNote) {
targetNote.targetRelations.push(this);
}
}
validate() {
if (!["label", "relation"].includes(this.type)) {
throw new Error(`Invalid attribute type '${this.type}' in attribute '${this.attributeId}' of note '${this.noteId}'`);
}
if (!this.name?.trim()) {
throw new Error(`Invalid empty name in attribute '${this.attributeId}' of note '${this.noteId}'`);
}
if (this.type === "relation" && !(this.value in this.becca.notes)) {
throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it targets not existing note '${this.value}'.`);
}
}
get isAffectingSubtree() {
return this.isInheritable || (this.type === "relation" && ["template", "inherit"].includes(this.name));
}
get targetNoteId() {
// alias
return this.type === "relation" ? this.value : undefined;
}
isAutoLink() {
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
}
get note() {
return this.becca.notes[this.noteId];
}
get targetNote() {
if (this.type === "relation") {
return this.becca.notes[this.value];
}
}
getNote() {
const note = this.becca.getNote(this.noteId);
if (!note) {
throw new Error(`Note '${this.noteId}' of attribute '${this.attributeId}', type '${this.type}', name '${this.name}' does not exist.`);
}
return note;
}
getTargetNote() {
if (this.type !== "relation") {
throw new Error(`Attribute '${this.attributeId}' is not a relation.`);
}
if (!this.value) {
return null;
}
return this.becca.getNote(this.value);
}
isDefinition() {
return this.type === "label" && (this.name.startsWith("label:") || this.name.startsWith("relation:"));
}
getDefinition() {
return promotedAttributeDefinitionParser.parse(this.value);
}
getDefinedName() {
if (this.type === "label" && this.name.startsWith("label:")) {
return this.name.substr(6);
} else if (this.type === "label" && this.name.startsWith("relation:")) {
return this.name.substr(9);
} else {
return this.name;
}
}
override get isDeleted() {
return !(this.attributeId in this.becca.attributes);
}
override beforeSaving(opts: SavingOpts = {}) {
if (!opts.skipValidation) {
this.validate();
}
this.name = sanitizeAttributeName(this.name);
if (!this.value) {
// null value isn't allowed
this.value = "";
}
if (this.position === undefined || this.position === null) {
const maxExistingPosition = this.getNote()
.getAttributes()
.reduce((maxPosition, attr) => Math.max(maxPosition, attr.position || 0), 0);
this.position = maxExistingPosition + 10;
}
if (!this.isInheritable) {
this.isInheritable = false;
}
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
this.becca.attributes[this.attributeId] = this;
}
getPojo() {
return {
attributeId: this.attributeId,
noteId: this.noteId,
type: this.type,
name: this.name,
position: this.position,
value: this.value,
isInheritable: this.isInheritable,
utcDateModified: this.utcDateModified,
isDeleted: false
};
}
createClone(type: AttributeType, name: string, value: string, isInheritable?: boolean) {
return new BAttribute({
noteId: this.noteId,
type: type,
name: name,
value: value,
position: this.position,
isInheritable: isInheritable,
utcDateModified: this.utcDateModified
});
}
}
export default BAttribute;

View File

@@ -13,7 +13,7 @@ class BBlob extends AbstractBeccaEntity<BBlob> {
return ["blobId", "content"];
}
content!: string | Uint8Array;
content!: string | Buffer;
contentLength!: number;
constructor(row: BlobRow) {

View File

@@ -1,2 +1,288 @@
import { BBranch } from "@triliumnext/core";
"use strict";
import BNote from "./bnote.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import dateUtils from "../../services/date_utils.js";
import utils from "../../services/utils.js";
import TaskContext from "../../services/task_context.js";
import cls from "../../services/cls.js";
import log from "../../services/log.js";
import type { BranchRow } from "@triliumnext/commons";
import handlers from "../../services/handlers.js";
/**
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple
* parents.
*
* Note that you should not rely on the branch's identity, since it can change easily with a note's move.
* Always check noteId instead.
*/
class BBranch extends AbstractBeccaEntity<BBranch> {
static get entityName() {
return "branches";
}
static get primaryKeyName() {
return "branchId";
}
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() {
return ["branchId", "noteId", "parentNoteId", "prefix"];
}
branchId?: string;
noteId!: string;
parentNoteId!: string;
prefix!: string | null;
notePosition!: number;
isExpanded!: boolean;
constructor(row?: BranchRow) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
updateFromRow(row: BranchRow) {
this.update([row.branchId, row.noteId, row.parentNoteId, row.prefix, row.notePosition, row.isExpanded, row.utcDateModified]);
}
update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified]: any) {
this.branchId = branchId;
this.noteId = noteId;
this.parentNoteId = parentNoteId;
this.prefix = prefix;
this.notePosition = notePosition;
this.isExpanded = !!isExpanded;
this.utcDateModified = utcDateModified;
return this;
}
override init() {
if (this.branchId) {
this.becca.branches[this.branchId] = this;
}
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
const childNote = this.childNote;
if (!childNote.parentBranches.includes(this)) {
childNote.parentBranches.push(this);
}
if (this.noteId === "root") {
return;
}
const parentNote = this.parentNote;
if (parentNote) {
if (!childNote.parents.includes(parentNote)) {
childNote.parents.push(parentNote);
}
if (!parentNote.children.includes(childNote)) {
parentNote.children.push(childNote);
}
}
}
get childNote(): BNote {
if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
}
return this.becca.notes[this.noteId];
}
getNote(): BNote {
return this.childNote;
}
/** @returns root branch will have undefined parent, all other branches have to have a parent note */
get parentNote(): BNote | undefined {
if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== "none") {
// entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.parentNoteId, new BNote({ noteId: this.parentNoteId }));
}
return this.becca.notes[this.parentNoteId];
}
override get isDeleted() {
return this.branchId == undefined || !(this.branchId in this.becca.branches);
}
/**
* Branch is weak when its existence should not hinder deletion of its note.
* As a result, note with only weak branches should be immediately deleted.
* An example is shared or bookmarked clones - they are created automatically and exist for technical reasons,
* not as user-intended actions. From user perspective, they don't count as real clones and for the purpose
* of deletion should not act as a clone.
*/
get isWeak() {
return ["_share", "_lbBookmarks"].includes(this.parentNoteId);
}
/**
* Delete a branch. If this is a last note's branch, delete the note as well.
*
* @param deleteId - optional delete identified
*
* @returns true if note has been deleted, false otherwise
*/
deleteBranch(deleteId?: string, taskContext?: TaskContext<"deleteNotes">): boolean {
if (!deleteId) {
deleteId = utils.randomString(10);
}
if (!taskContext) {
taskContext = new TaskContext("no-progress-reporting", "deleteNotes", null);
}
taskContext.increaseProgressCount();
const note = this.getNote();
if (!taskContext.noteDeletionHandlerTriggered) {
const parentBranches = note.getParentBranches();
if (parentBranches.length === 1 && parentBranches[0] === this) {
// needs to be run before branches and attributes are deleted and thus attached relations disappear
handlers.runAttachedRelations(note, "runOnNoteDeletion", note);
}
}
if ((this.noteId === "root" || this.noteId === cls.getHoistedNoteId()) && !this.isWeak) {
throw new Error("Can't delete root or hoisted branch/note");
}
this.markAsDeleted(deleteId);
const notDeletedBranches = note.getStrongParentBranches();
if (notDeletedBranches.length === 0) {
for (const weakBranch of note.getParentBranches()) {
weakBranch.markAsDeleted(deleteId);
}
for (const childBranch of note.getChildBranches()) {
if (childBranch) {
childBranch.deleteBranch(deleteId, taskContext);
}
}
// first delete children and then parent - this will show up better in recent changes
log.info(`Deleting note '${note.noteId}'`);
this.becca.notes[note.noteId].isBeingDeleted = true;
for (const attribute of note.getOwnedAttributes().slice()) {
attribute.markAsDeleted(deleteId);
}
for (const relation of note.getTargetRelations()) {
relation.markAsDeleted(deleteId);
}
for (const attachment of note.getAttachments()) {
attachment.markAsDeleted(deleteId);
}
note.markAsDeleted(deleteId);
return true;
} else {
return false;
}
}
override beforeSaving() {
if (!this.noteId || !this.parentNoteId) {
throw new Error(`noteId and parentNoteId are mandatory properties for Branch`);
}
this.branchId = `${this.parentNoteId}_${this.noteId}`;
if (this.notePosition === undefined || this.notePosition === null) {
let maxNotePos = 0;
if (this.parentNote) {
for (const childBranch of this.parentNote.getChildBranches()) {
if (!childBranch) {
continue;
}
if (
maxNotePos < childBranch.notePosition &&
childBranch.noteId !== "_hidden" // hidden has a very large notePosition to always stay last
) {
maxNotePos = childBranch.notePosition;
}
}
}
this.notePosition = maxNotePos + 10;
}
if (!this.isExpanded) {
this.isExpanded = false;
}
if (!this.prefix?.trim()) {
this.prefix = null;
}
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
this.becca.branches[this.branchId] = this;
}
getPojo() {
return {
branchId: this.branchId,
noteId: this.noteId,
parentNoteId: this.parentNoteId,
prefix: this.prefix,
notePosition: this.notePosition,
isExpanded: this.isExpanded,
isDeleted: false,
utcDateModified: this.utcDateModified
};
}
createClone(parentNoteId: string, notePosition?: number) {
const existingBranch = this.becca.getBranchFromChildAndParent(this.noteId, parentNoteId);
if (existingBranch) {
if (notePosition) {
existingBranch.notePosition = notePosition;
}
return existingBranch;
} else {
return new BBranch({
noteId: this.noteId,
parentNoteId: parentNoteId,
notePosition: notePosition || null,
prefix: this.prefix,
isExpanded: this.isExpanded
});
}
}
getParentNote() {
return this.parentNote;
}
}
export default BBranch;

View File

@@ -1,2 +1,89 @@
import { BEtapiToken } from "@triliumnext/core";
"use strict";
import type { EtapiTokenRow } from "@triliumnext/commons";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
/**
* EtapiToken is an entity representing token used to authenticate against Trilium REST API from client applications.
* Used by:
* - Trilium Sender
* - ETAPI clients
*
* The format user is presented with is "<etapiTokenId>_<tokenHash>". This is also called "authToken" to distinguish it
* from tokenHash and token.
*/
class BEtapiToken extends AbstractBeccaEntity<BEtapiToken> {
static get entityName() {
return "etapi_tokens";
}
static get primaryKeyName() {
return "etapiTokenId";
}
static get hashedProperties() {
return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"];
}
etapiTokenId?: string;
name!: string;
tokenHash!: string;
private _isDeleted?: boolean;
constructor(row?: EtapiTokenRow) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
override get isDeleted() {
return !!this._isDeleted;
}
updateFromRow(row: EtapiTokenRow) {
this.etapiTokenId = row.etapiTokenId;
this.name = row.name;
this.tokenHash = row.tokenHash;
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
this.utcDateModified = row.utcDateModified || this.utcDateCreated;
this._isDeleted = !!row.isDeleted;
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
override init() {
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
getPojo() {
return {
etapiTokenId: this.etapiTokenId,
name: this.name,
tokenHash: this.tokenHash,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified,
isDeleted: this.isDeleted
};
}
override beforeSaving() {
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
}
export default BEtapiToken;

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,56 @@
import { BOption } from "@triliumnext/core";
"use strict";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import type { OptionRow } from "@triliumnext/commons";
/**
* Option represents a name-value pair, either directly configurable by the user or some system property.
*/
class BOption extends AbstractBeccaEntity<BOption> {
static get entityName() {
return "options";
}
static get primaryKeyName() {
return "name";
}
static get hashedProperties() {
return ["name", "value"];
}
name!: string;
value!: string;
constructor(row?: OptionRow) {
super();
if (row) {
this.updateFromRow(row);
}
this.becca.options[this.name] = this;
}
updateFromRow(row: OptionRow) {
this.name = row.name;
this.value = row.value;
this.isSynced = !!row.isSynced;
this.utcDateModified = row.utcDateModified;
}
override beforeSaving() {
super.beforeSaving();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
name: this.name,
value: this.value,
isSynced: this.isSynced,
utcDateModified: this.utcDateModified
};
}
}
export default BOption;

View File

@@ -1,2 +1,46 @@
import { BRecentNote } from "@triliumnext/core";
"use strict";
import type { RecentNoteRow } from "@triliumnext/commons";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
/**
* RecentNote represents recently visited note.
*/
class BRecentNote extends AbstractBeccaEntity<BRecentNote> {
static get entityName() {
return "recent_notes";
}
static get primaryKeyName() {
return "noteId";
}
static get hashedProperties() {
return ["noteId", "notePath"];
}
noteId!: string;
notePath!: string;
constructor(row: RecentNoteRow) {
super();
this.updateFromRow(row);
}
updateFromRow(row: RecentNoteRow): void {
this.noteId = row.noteId;
this.notePath = row.notePath;
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
}
getPojo() {
return {
noteId: this.noteId,
notePath: this.notePath,
utcDateCreated: this.utcDateCreated
};
}
}
export default BRecentNote;

View File

@@ -1,2 +1,225 @@
import { BRevision } from "@triliumnext/core";
"use strict";
import protectedSessionService from "../../services/protected_session.js";
import utils from "../../services/utils.js";
import dateUtils from "../../services/date_utils.js";
import becca from "../becca.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import sql from "../../services/sql.js";
import BAttachment from "./battachment.js";
import type { AttachmentRow, NoteType, RevisionPojo, RevisionRow } from "@triliumnext/commons";
import eraseService from "../../services/erase.js";
interface ContentOpts {
/** will also save this BRevision entity */
forceSave?: boolean;
}
interface GetByIdOpts {
includeContentLength?: boolean;
}
/**
* Revision represents a snapshot of note's title and content at some point in the past.
* It's used for seamless note versioning.
*/
class BRevision extends AbstractBeccaEntity<BRevision> {
static get entityName() {
return "revisions";
}
static get primaryKeyName() {
return "revisionId";
}
static get hashedProperties() {
return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"];
}
revisionId?: string;
noteId!: string;
type!: NoteType;
mime!: string;
title!: string;
dateLastEdited?: string;
utcDateLastEdited?: string;
contentLength?: number;
content?: string | Buffer;
constructor(row: RevisionRow, titleDecrypted = false) {
super();
this.updateFromRow(row);
if (this.isProtected && !titleDecrypted) {
const decryptedTitle = protectedSessionService.isProtectedSessionAvailable() ? protectedSessionService.decryptString(this.title) : null;
this.title = decryptedTitle || "[protected]";
}
}
updateFromRow(row: RevisionRow) {
this.revisionId = row.revisionId;
this.noteId = row.noteId;
this.type = row.type;
this.mime = row.mime;
this.isProtected = !!row.isProtected;
this.title = row.title;
this.blobId = row.blobId;
this.dateLastEdited = row.dateLastEdited;
this.dateCreated = row.dateCreated;
this.utcDateLastEdited = row.utcDateLastEdited;
this.utcDateCreated = row.utcDateCreated;
this.utcDateModified = row.utcDateModified;
this.contentLength = row.contentLength;
}
getNote() {
return becca.notes[this.noteId];
}
/** @returns true if the note has string content (not binary) */
override hasStringContent(): boolean {
return utils.isStringNote(this.type, this.mime);
}
isContentAvailable() {
return (
!this.revisionId || // new note which was not encrypted yet
!this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
}
/*
* Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
* part of Revision entity with its own sync. The reason behind this hybrid design is that
* content can be quite large, and it's not necessary to load it / fill memory for any note access even
* if we don't need a content, especially for bulk operations like search.
*
* This is the same approach as is used for Note's content.
*/
getContent(): string | Buffer {
return this._getContent();
}
/**
* @throws Error in case of invalid JSON */
getJsonContent(): {} | null {
const content = this.getContent();
if (!content || typeof content !== "string" || !content.trim()) {
return null;
}
return JSON.parse(content);
}
/** @returns valid object or null if the content cannot be parsed as JSON */
getJsonContentSafely(): {} | null {
try {
return this.getJsonContent();
} catch (e) {
return null;
}
}
setContent(content: string | Buffer, opts: ContentOpts = {}) {
this._setContent(content, opts);
}
getAttachments(): BAttachment[] {
return sql
.getRows<AttachmentRow>(
`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND isDeleted = 0`,
[this.revisionId]
)
.map((row) => new BAttachment(row));
}
getAttachmentById(attachmentId: String, opts: GetByIdOpts = {}): BAttachment | null {
opts.includeContentLength = !!opts.includeContentLength;
const query = opts.includeContentLength
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: /*sql*/`SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [this.revisionId, attachmentId]).map((row) => new BAttachment(row))[0];
}
getAttachmentsByRole(role: string): BAttachment[] {
return sql
.getRows<AttachmentRow>(
`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND role = ?
AND isDeleted = 0
ORDER BY position`,
[this.revisionId, role]
)
.map((row) => new BAttachment(row));
}
getAttachmentByTitle(title: string): BAttachment {
// cannot use SQL to filter by title since it can be encrypted
return this.getAttachments().filter((attachment) => attachment.title === title)[0];
}
/**
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
*/
eraseRevision() {
if (this.revisionId) {
eraseService.eraseRevisions([this.revisionId]);
}
}
override beforeSaving() {
super.beforeSaving();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
revisionId: this.revisionId,
noteId: this.noteId,
type: this.type,
mime: this.mime,
isProtected: this.isProtected,
title: this.title,
blobId: this.blobId,
dateLastEdited: this.dateLastEdited,
dateCreated: this.dateCreated,
utcDateLastEdited: this.utcDateLastEdited,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified,
content: this.content, // used when retrieving full note revision to frontend
contentLength: this.contentLength
} satisfies RevisionPojo;
}
override getPojoToSave() {
const pojo = this.getPojo();
delete pojo.content; // not getting persisted
delete pojo.contentLength; // not getting persisted
if (pojo.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
pojo.title = protectedSessionService.encrypt(this.title) ?? "";
} else {
// updating protected note outside of protected session means we will keep original ciphertexts
pojo.title = "";
}
}
return pojo;
}
}
export default BRevision;

View File

@@ -1,7 +1,7 @@
import becca from "./becca.js";
import { getLog } from "../services/log.js";
import log from "../services/log.js";
import beccaService from "./becca_service.js";
import dateUtils from "../services/utils/date";
import dateUtils from "../services/date_utils.js";
import { parse } from "node-html-parser";
import type BNote from "./entities/bnote.js";
import { SimilarNote } from "@triliumnext/commons";
@@ -359,7 +359,7 @@ async function findSimilarNotes(noteId: string): Promise<SimilarNote[] | undefin
let factor = 1;
if (!value.startsWith) {
getLog().info(`Unexpected falsy value for attribute ${JSON.stringify(attr.getPojo())}`);
log.info(`Unexpected falsy value for attribute ${JSON.stringify(attr.getPojo())}`);
continue;
} else if (value.startsWith("http")) {
value = filterUrlValue(value);

View File

@@ -1,24 +0,0 @@
import { ExecutionContext } from "@triliumnext/core";
import clsHooked from "cls-hooked";
export const namespace = clsHooked.createNamespace("trilium");
export default class ClsHookedExecutionContext implements ExecutionContext {
get<T = any>(key: string): T | undefined {
return namespace.get(key);
}
set(key: string, value: any): void {
namespace.set(key, value);
}
reset(): void {
clsHooked.reset();
}
init<T>(callback: () => T): T {
return namespace.runAndReturn(callback);
}
}

View File

@@ -1,29 +0,0 @@
import { CryptoProvider } from "@triliumnext/core";
import crypto from "crypto";
import { generator } from "rand-token";
const randtoken = generator({ source: "crypto" });
export default class NodejsCryptoProvider implements CryptoProvider {
createHash(algorithm: "sha1", content: string | Uint8Array): Uint8Array {
return crypto.createHash(algorithm).update(content).digest();
}
createCipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): { update(data: Uint8Array): Uint8Array; final(): Uint8Array; } {
return crypto.createCipheriv(algorithm, key, iv);
}
createDecipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array) {
return crypto.createDecipheriv(algorithm, key, iv);
}
randomBytes(size: number): Uint8Array {
return crypto.randomBytes(size);
}
randomString(length: number): string {
return randtoken.generate(length);
}
}

View File

@@ -0,0 +1,12 @@
import HttpError from "./http_error.js";
class ForbiddenError extends HttpError {
constructor(message: string) {
super(message, 403);
this.name = "ForbiddenError";
}
}
export default ForbiddenError;

View File

@@ -0,0 +1,13 @@
class HttpError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.name = "HttpError";
this.statusCode = statusCode;
}
}
export default HttpError;

View File

@@ -0,0 +1,12 @@
import HttpError from "./http_error.js";
class NotFoundError extends HttpError {
constructor(message: string) {
super(message, 404);
this.name = "NotFoundError";
}
}
export default NotFoundError;

View File

@@ -0,0 +1,9 @@
class OpenIdError {
message: string;
constructor(message: string) {
this.message = message;
}
}
export default OpenIdError;

View File

@@ -0,0 +1,12 @@
import HttpError from "./http_error.js";
class ValidationError extends HttpError {
constructor(message: string) {
super(message, 400)
this.name = "ValidationError";
}
}
export default ValidationError;

View File

@@ -1,14 +1,12 @@
import type { NextFunction, Request, RequestHandler, Response, Router } from "express";
import becca from "../becca/becca.js";
import { namespace } from "../cls_provider.js";
import type { ApiRequestHandler, SyncRouteRequestHandler } from "../routes/route_api.js";
import cls from "../services/cls.js";
import config from "../services/config.js";
import etapiTokenService from "../services/etapi_tokens.js";
import log from "../services/log.js";
import sql from "../services/sql.js";
import log from "../services/log.js";
import becca from "../becca/becca.js";
import etapiTokenService from "../services/etapi_tokens.js";
import config from "../services/config.js";
import type { NextFunction, Request, RequestHandler, Response, Router } from "express";
import type { ValidatorMap } from "./etapi-interface.js";
import type { ApiRequestHandler, SyncRouteRequestHandler } from "../routes/route_api.js";
const GENERIC_CODE = "GENERIC";
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
@@ -37,8 +35,8 @@ function sendError(res: Response, statusCode: number, code: string, message: str
.send(
JSON.stringify({
status: statusCode,
code,
message
code: code,
message: message
})
);
}
@@ -53,8 +51,8 @@ function checkEtapiAuth(req: Request, res: Response, next: NextFunction) {
function processRequest(req: Request, res: Response, routeHandler: ApiRequestHandler, next: NextFunction, method: string, path: string) {
try {
namespace.bindEmitter(req);
namespace.bindEmitter(res);
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
cls.init(() => {
cls.set("componentId", "etapi");
@@ -88,9 +86,9 @@ function getAndCheckNote(noteId: string) {
if (note) {
return note;
}
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
} else {
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
}
function getAndCheckAttachment(attachmentId: string) {
@@ -98,9 +96,9 @@ function getAndCheckAttachment(attachmentId: string) {
if (attachment) {
return attachment;
}
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
} else {
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
}
}
function getAndCheckBranch(branchId: string) {
@@ -108,9 +106,9 @@ function getAndCheckBranch(branchId: string) {
if (branch) {
return branch;
}
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
} else {
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
}
}
function getAndCheckAttribute(attributeId: string) {
@@ -118,9 +116,9 @@ function getAndCheckAttribute(attributeId: string) {
if (attribute) {
return attribute;
}
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
} else {
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
}
}
function validateAndPatch(target: any, source: any, allowedProperties: ValidatorMap) {

View File

@@ -1,21 +1,20 @@
import { NoteParams } from "@triliumnext/core";
import type { Request, Router } from "express";
import type { ParsedQs } from "qs";
import becca from "../becca/becca.js";
import zipExportService from "../services/export/zip.js";
import type { ExportFormat } from "../services/export/zip/abstract_provider.js";
import zipImportService from "../services/import/zip.js";
import noteService from "../services/notes.js";
import SearchContext from "../services/search/search_context.js";
import searchService from "../services/search/services/search.js";
import type { SearchParams } from "../services/search/services/types.js";
import TaskContext from "../services/task_context.js";
import utils from "../services/utils.js";
import eu from "./etapi_utils.js";
import type { ValidatorMap } from "./etapi-interface.js";
import mappers from "./mappers.js";
import noteService from "../services/notes.js";
import TaskContext from "../services/task_context.js";
import v from "./validators.js";
import searchService from "../services/search/services/search.js";
import SearchContext from "../services/search/search_context.js";
import zipExportService from "../services/export/zip.js";
import zipImportService from "../services/import/zip.js";
import type { Request, Router } from "express";
import type { ParsedQs } from "qs";
import type { NoteParams } from "../services/note-interface.js";
import type { SearchParams } from "../services/search/services/types.js";
import type { ValidatorMap } from "./etapi-interface.js";
import type { ExportFormat } from "../services/export/zip/abstract_provider.js";
function register(router: Router) {
eu.route(router, "get", "/etapi/notes", (req, res, next) => {

View File

@@ -3,49 +3,9 @@
* are loaded later and will result in an empty string.
*/
import { initializeCore } from "@triliumnext/core";
import ClsHookedExecutionContext from "./cls_provider.js";
import NodejsCryptoProvider from "./crypto_provider.js";
import BetterSqlite3Provider from "./sql_provider.js";
import { initializeTranslations } from "./services/i18n.js";
async function startApplication() {
const config = (await import("./services/config.js")).default;
const { DOCUMENT_PATH } = (await import("./services/data_dir.js")).default;
const dbProvider = new BetterSqlite3Provider();
dbProvider.loadFromFile(DOCUMENT_PATH, config.General.readOnly);
initializeCore({
dbConfig: {
provider: dbProvider,
isReadOnly: config.General.readOnly,
async onTransactionCommit() {
const ws = (await import("./services/ws.js")).default;
ws.sendTransactionEntityChangesToAllClients();
},
async onTransactionRollback() {
const cls = (await import("./services/cls.js")).default;
const becca_loader = (await import("@triliumnext/core")).becca_loader;
const entity_changes = (await import("./services/entity_changes.js")).default;
const log = (await import("./services/log")).default;
const entityChangeIds = cls.getAndClearEntityChangeIds();
if (entityChangeIds.length > 0) {
log.info("Transaction rollback dirtied the becca, forcing reload.");
becca_loader.load();
}
// the maxEntityChangeId has been incremented during failed transaction, need to recalculate
entity_changes.recalculateMaxEntityChangeId();
}
},
crypto: new NodejsCryptoProvider(),
executionContext: new ClsHookedExecutionContext()
});
const { initializeTranslations } = (await import("./services/i18n.js"));
await initializeTranslations();
const startTriliumServer = (await import("./www.js")).default;
await startTriliumServer();

View File

@@ -1,6 +1,5 @@
import { becca_loader } from "@triliumnext/core";
import becca from "../becca/becca.js";
import becca_loader from "../becca/becca_loader.js";
import cls from "../services/cls.js";
import log from "../services/log.js";
import sql from "../services/sql.js";

View File

@@ -1,6 +1,5 @@
import { becca_loader } from "@triliumnext/core";
import becca from "../becca/becca";
import becca_loader from "../becca/becca_loader";
import cls from "../services/cls.js";
import hidden_subtree from "../services/hidden_subtree";

View File

@@ -1,9 +1,9 @@
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
import { blob as blobService, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import blobService from "../../services/blob.js";
import ValidationError from "../../errors/validation_error.js";
import imageService from "../../services/image.js";
import type { Request } from "express";
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
function getAttachmentBlob(req: Request) {
const preview = req.query.preview === "true";
@@ -34,7 +34,7 @@ function getAllAttachments(req: Request) {
function saveAttachment(req: Request) {
const { noteId } = req.params;
const { attachmentId, role, mime, title, content } = req.body;
const matchByQuery = req.query.matchBy;
const matchByQuery = req.query.matchBy
const isValidMatchBy = (typeof matchByQuery === "string") && (matchByQuery === "attachmentId" || matchByQuery === "title");
const matchBy = isValidMatchBy ? matchByQuery : undefined;

View File

@@ -1,12 +1,13 @@
import { UpdateAttributeResponse } from "@triliumnext/commons";
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
"use strict";
import becca from "../../becca/becca.js";
import BAttribute from "../../becca/entities/battribute.js";
import attributeService from "../../services/attributes.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import log from "../../services/log.js";
import attributeService from "../../services/attributes.js";
import BAttribute from "../../becca/entities/battribute.js";
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import { UpdateAttributeResponse } from "@triliumnext/commons";
function getEffectiveNoteAttributes(req: Request) {
const note = becca.getNote(req.params.noteId);
@@ -46,7 +47,7 @@ function updateNoteAttribute(req: Request) {
}
attribute = new BAttribute({
noteId,
noteId: noteId,
name: body.name,
type: body.type,
isInheritable: body.isInheritable
@@ -207,7 +208,7 @@ function createRelation(req: Request) {
if (!attribute) {
attribute = new BAttribute({
noteId: sourceNoteId,
name,
name: name,
type: "relation",
value: targetNoteId
}).save();

View File

@@ -1,7 +1,8 @@
import { becca_service, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import beccaService from "../../becca/becca_service.js";
import ValidationError from "../../errors/validation_error.js";
import cls from "../../services/cls.js";
import log from "../../services/log.js";
import searchService from "../../services/search/services/search.js";
@@ -66,8 +67,8 @@ function getRecentNotes(activeNoteId: string) {
return recentNotes.map((rn) => {
const notePathArray = rn.notePath.split("/");
const { title, icon } = becca_service.getNoteTitleAndIcon(notePathArray[notePathArray.length - 1]);
const notePathTitle = becca_service.getNoteTitleForPath(notePathArray);
const { title, icon } = beccaService.getNoteTitleAndIcon(notePathArray[notePathArray.length - 1]);
const notePathTitle = beccaService.getNoteTitleForPath(notePathArray);
return {
notePath: rn.notePath,

View File

@@ -1,14 +1,17 @@
import { erase as eraseService, events as eventService, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
"use strict";
import becca from "../../becca/becca.js";
import branchService from "../../services/branches.js";
import entityChangesService from "../../services/entity_changes.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js";
import treeService from "../../services/tree.js";
import utils from "../../services/utils.js";
import entityChangesService from "../../services/entity_changes.js";
import treeService from "../../services/tree.js";
import eraseService from "../../services/erase.js";
import becca from "../../becca/becca.js";
import TaskContext from "../../services/task_context.js";
import branchService from "../../services/branches.js";
import log from "../../services/log.js";
import ValidationError from "../../errors/validation_error.js";
import eventService from "../../services/events.js";
import type { Request } from "express";
/**
* Code in this file deals with moving and cloning branches. The relationship between note and parent note is unique
@@ -253,7 +256,7 @@ function deleteBranch(req: Request) {
}
return {
noteDeleted
noteDeleted: noteDeleted
};
}
@@ -269,7 +272,7 @@ function setPrefix(req: Request) {
function setPrefixBatch(req: Request) {
const { branchIds, prefix } = req.body;
if (!Array.isArray(branchIds)) {
throw new ValidationError("branchIds must be an array");
}

View File

@@ -1,15 +1,16 @@
import { sanitize, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import { parse } from "node-html-parser";
import path from "path";
import type BNote from "../../becca/entities/bnote.js";
import ValidationError from "../../errors/validation_error.js";
import appInfo from "../../services/app_info.js";
import attributeFormatter from "../../services/attribute_formatter.js";
import attributeService from "../../services/attributes.js";
import cloneService from "../../services/cloning.js";
import dateNoteService from "../../services/date_notes.js";
import dateUtils from "../../services/date_utils.js";
import htmlSanitizer from "../../services/html_sanitizer.js";
import imageService from "../../services/image.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
@@ -31,13 +32,13 @@ async function addClipping(req: Request) {
const clipperInbox = await getClipperInboxNote();
const pageUrl = sanitize.sanitizeUrl(req.body.pageUrl);
const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl);
let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType);
if (!clippingNote) {
clippingNote = noteService.createNewNote({
parentNoteId: clipperInbox.noteId,
title,
title: title,
content: "",
type: "text"
}).note;
@@ -98,8 +99,8 @@ async function getClipperInboxNote() {
async function createNote(req: Request) {
const { content, images, labels } = req.body;
const clipType = sanitize.sanitizeHtml(req.body.clipType);
const pageUrl = sanitize.sanitizeUrl(req.body.pageUrl);
const clipType = htmlSanitizer.sanitize(req.body.clipType);
const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl);
const trimmedTitle = (typeof req.body.title === "string") ? req.body.title.trim() : "";
const title = trimmedTitle || `Clipped note from ${pageUrl}`;
@@ -125,7 +126,7 @@ async function createNote(req: Request) {
if (labels) {
for (const labelName in labels) {
const labelValue = sanitize.sanitizeHtml(labels[labelName]);
const labelValue = htmlSanitizer.sanitize(labels[labelName]);
note.setLabel(labelName, labelValue);
}
}
@@ -146,7 +147,7 @@ async function createNote(req: Request) {
}
export function processContent(images: Image[], note: BNote, content: string) {
let rewrittenContent = sanitize.sanitizeHtml(content);
let rewrittenContent = htmlSanitizer.sanitize(content);
if (images) {
for (const { src, dataUrl, imageId } of images) {
@@ -197,11 +198,11 @@ function openNote(req: Request) {
return {
result: "ok"
};
} else {
return {
result: "open-in-browser"
};
}
return {
result: "open-in-browser"
};
}
function handshake() {

View File

@@ -1,13 +1,15 @@
import { BackupDatabaseNowResponse, DatabaseCheckIntegrityResponse } from "@triliumnext/commons";
import { becca_loader, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
"use strict";
import anonymizationService from "../../services/anonymization.js";
import backupService from "../../services/backup.js";
import consistencyChecksService from "../../services/consistency_checks.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import log from "../../services/log.js";
import backupService from "../../services/backup.js";
import anonymizationService from "../../services/anonymization.js";
import consistencyChecksService from "../../services/consistency_checks.js";
import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import sql_init from "../../services/sql_init.js";
import becca_loader from "../../becca/becca_loader.js";
import { BackupDatabaseNowResponse, DatabaseCheckIntegrityResponse } from "@triliumnext/commons";
function getExistingBackups() {
return backupService.getExistingBackups();

View File

@@ -1,12 +1,14 @@
import { NotFoundError, ValidationError } from "@triliumnext/core";
import type { Request, Response } from "express";
"use strict";
import becca from "../../becca/becca.js";
import opmlExportService from "../../services/export/opml.js";
import singleExportService from "../../services/export/single.js";
import zipExportService from "../../services/export/zip.js";
import log from "../../services/log.js";
import singleExportService from "../../services/export/single.js";
import opmlExportService from "../../services/export/opml.js";
import becca from "../../becca/becca.js";
import TaskContext from "../../services/task_context.js";
import log from "../../services/log.js";
import NotFoundError from "../../errors/not_found_error.js";
import type { Request, Response } from "express";
import ValidationError from "../../errors/validation_error.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
function exportBranch(req: Request, res: Response) {

View File

@@ -1,4 +1,5 @@
import { ValidationError } from "@triliumnext/core";
import chokidar from "chokidar";
import type { Request, Response } from "express";
import fs from "fs";
@@ -8,6 +9,7 @@ import tmp from "tmp";
import becca from "../../becca/becca.js";
import type BAttachment from "../../becca/entities/battachment.js";
import type BNote from "../../becca/entities/bnote.js";
import ValidationError from "../../errors/validation_error.js";
import dataDirs from "../../services/data_dir.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
@@ -121,7 +123,7 @@ function attachmentContentProvider(req: Request) {
return streamContent(attachment.getContent(), attachment.getFileName(), attachment.mime);
}
async function streamContent(content: string | Uint8Array, fileName: string, mimeType: string) {
async function streamContent(content: string | Buffer, fileName: string, mimeType: string) {
if (typeof content === "string") {
content = Buffer.from(content, "utf8");
}
@@ -168,7 +170,7 @@ function saveAttachmentToTmpDir(req: Request) {
const createdTemporaryFiles = new Set<string>();
function saveToTmpDir(fileName: string, content: string | Uint8Array, entityType: string, entityId: string) {
function saveToTmpDir(fileName: string, content: string | Buffer, entityType: string, entityId: string) {
const tmpObj = tmp.fileSync({
postfix: fileName,
tmpdir: dataDirs.TMP_DIR

View File

@@ -1,12 +1,11 @@
"use strict";
import type { Request, Response } from "express";
import fs from "fs";
import imageService from "../../services/image.js";
import becca from "../../becca/becca.js";
import fs from "fs";
import type { Request, Response } from "express";
import type BNote from "../../becca/entities/bnote.js";
import type BRevision from "../../becca/entities/brevision.js";
import imageService from "../../services/image.js";
import { RESOURCE_DIR } from "../../services/resource_dir.js";
function returnImageFromNote(req: Request, res: Response) {
@@ -43,7 +42,7 @@ function returnImageInt(image: BNote | BRevision | null, res: Response) {
}
export function renderSvgAttachment(image: BNote | BRevision, res: Response, attachmentName: string) {
let svg: string | Uint8Array = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
let svg: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
const attachment = image.getAttachmentByTitle(attachmentName);
if (attachment) {

View File

@@ -1,16 +1,18 @@
import { becca_loader,ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import path from "path";
"use strict";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import cls from "../../services/cls.js";
import enexImportService from "../../services/import/enex.js";
import opmlImportService from "../../services/import/opml.js";
import singleImportService from "../../services/import/single.js";
import zipImportService from "../../services/import/zip.js";
import singleImportService from "../../services/import/single.js";
import cls from "../../services/cls.js";
import path from "path";
import becca from "../../becca/becca.js";
import beccaLoader from "../../becca/becca_loader.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import type BNote from "../../becca/entities/bnote.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
async function importNotesToBranch(req: Request) {
@@ -86,7 +88,7 @@ async function importNotesToBranch(req: Request) {
setTimeout(
() =>
taskContext.taskSucceeded({
parentNoteId,
parentNoteId: parentNoteId,
importedNoteId: note?.noteId
}),
1000
@@ -94,7 +96,7 @@ async function importNotesToBranch(req: Request) {
}
// import has deactivated note events so becca is not updated, instead we force it to reload
becca_loader.load();
beccaLoader.load();
return note.getPojo();
}
@@ -136,7 +138,7 @@ function importAttachmentsToNote(req: Request) {
setTimeout(
() =>
taskContext.taskSucceeded({
parentNoteId
parentNoteId: parentNoteId
}),
1000
);

View File

@@ -1,18 +1,20 @@
import { events as eventService, getInstanceId } from "@triliumnext/core";
import type { Request } from "express";
"use strict";
import appInfo from "../../services/app_info.js";
import dateUtils from "../../services/date_utils.js";
import passwordEncryptionService from "../../services/encryption/password_encryption.js";
import recoveryCodeService from "../../services/encryption/recovery_codes";
import etapiTokenService from "../../services/etapi_tokens.js";
import options from "../../services/options.js";
import protectedSessionService from "../../services/protected_session.js";
import sql from "../../services/sql.js";
import sqlInit from "../../services/sql_init.js";
import totp from "../../services/totp";
import utils from "../../services/utils.js";
import dateUtils from "../../services/date_utils.js";
import instanceId from "../../services/instance_id.js";
import passwordEncryptionService from "../../services/encryption/password_encryption.js";
import protectedSessionService from "../../services/protected_session.js";
import appInfo from "../../services/app_info.js";
import eventService from "../../services/events.js";
import sqlInit from "../../services/sql_init.js";
import sql from "../../services/sql.js";
import ws from "../../services/ws.js";
import etapiTokenService from "../../services/etapi_tokens.js";
import type { Request } from "express";
import totp from "../../services/totp";
import recoveryCodeService from "../../services/encryption/recovery_codes";
/**
* @swagger
@@ -113,7 +115,7 @@ function loginSync(req: Request) {
req.session.loggedIn = true;
return {
instanceId: getInstanceId(),
instanceId: instanceId,
maxEntityChangeId: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1")
};
}

View File

@@ -1,15 +1,18 @@
import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
import { blob as blobService, erase as eraseService, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
"use strict";
import becca from "../../becca/becca.js";
import type BBranch from "../../becca/entities/bbranch.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js";
import eraseService from "../../services/erase.js";
import treeService from "../../services/tree.js";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import blobService from "../../services/blob.js";
import type { Request } from "express";
import type BBranch from "../../becca/entities/bbranch.js";
import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
/**
* @swagger

View File

@@ -1,9 +1,9 @@
import type { OptionNames } from "@triliumnext/commons";
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import config from "../../services/config.js";
import { changeLanguage, getLocales } from "../../services/i18n.js";
import log from "../../services/log.js";

View File

@@ -1,15 +1,16 @@
import { ChangePasswordResponse } from "@triliumnext/commons";
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
"use strict";
import passwordService from "../../services/encryption/password.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import { ChangePasswordResponse } from "@triliumnext/commons";
function changePassword(req: Request): ChangePasswordResponse {
if (passwordService.isPasswordSet()) {
return passwordService.changePassword(req.body.current_password, req.body.new_password);
} else {
return passwordService.setPassword(req.body.new_password);
}
return passwordService.setPassword(req.body.new_password);
}
function resetPassword(req: Request) {

View File

@@ -1,14 +1,18 @@
import { EditedNotesResponse, RevisionItem, RevisionPojo } from "@triliumnext/commons";
import { becca_service, binary_utils, blob as blobService, erase as eraseService, NotePojo } from "@triliumnext/core";
import type { Request, Response } from "express";
import path from "path";
"use strict";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import type BRevision from "../../becca/entities/brevision.js";
import cls from "../../services/cls.js";
import sql from "../../services/sql.js";
import beccaService from "../../becca/becca_service.js";
import utils from "../../services/utils.js";
import sql from "../../services/sql.js";
import cls from "../../services/cls.js";
import path from "path";
import becca from "../../becca/becca.js";
import blobService from "../../services/blob.js";
import eraseService from "../../services/erase.js";
import type { Request, Response } from "express";
import type BRevision from "../../becca/entities/brevision.js";
import type BNote from "../../becca/entities/bnote.js";
import type { NotePojo } from "../../becca/becca-interface.js";
import { EditedNotesResponse, RevisionItem, RevisionPojo, RevisionRow } from "@triliumnext/commons";
interface NotePath {
noteId: string;
@@ -52,7 +56,7 @@ function getRevision(req: Request) {
revision.content = revision.getContent();
if (revision.content && revision.type === "image") {
revision.content = binary_utils.encodeBase64(revision.content);
revision.content = revision.content.toString("base64");
}
}
@@ -162,7 +166,7 @@ function getEditedNotesOnDate(req: Request) {
)
ORDER BY isDeleted
LIMIT 50`,
{ date: `${req.params.date}%` }
{ date: `${req.params.date}%` }
);
let notes = becca.getNotes(noteIds, true);
@@ -187,7 +191,7 @@ function getNotePathData(note: BNote): NotePath | undefined {
const retPath = note.getBestNotePath();
if (retPath) {
const noteTitle = becca_service.getNoteTitleForPath(retPath);
const noteTitle = beccaService.getNoteTitleForPath(retPath);
let branchId;
@@ -200,7 +204,7 @@ function getNotePathData(note: BNote): NotePath | undefined {
return {
noteId: note.noteId,
branchId,
branchId: branchId,
title: noteTitle,
notePath: retPath,
path: retPath.join("/")

View File

@@ -1,14 +1,17 @@
import { becca_service,ValidationError } from "@triliumnext/core";
"use strict";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import attributeFormatter from "../../services/attribute_formatter.js";
import SearchContext from "../../services/search/search_context.js";
import searchService, { EMPTY_RESULT, type SearchNoteResult } from "../../services/search/services/search.js";
import bulkActionService from "../../services/bulk_actions.js";
import cls from "../../services/cls.js";
import hoistedNoteService from "../../services/hoisted_note.js";
import SearchContext from "../../services/search/search_context.js";
import attributeFormatter from "../../services/attribute_formatter.js";
import ValidationError from "../../errors/validation_error.js";
import type SearchResult from "../../services/search/search_result.js";
import searchService, { EMPTY_RESULT, type SearchNoteResult } from "../../services/search/services/search.js";
import hoistedNoteService from "../../services/hoisted_note.js";
import beccaService from "../../becca/becca_service.js";
function searchFromNote(req: Request): SearchNoteResult {
const note = becca.getNoteOrThrow(req.params.noteId);
@@ -69,7 +72,7 @@ function quickSearch(req: Request) {
// Map to API format
const searchResults = trimmed.map((result) => {
const { title, icon } = becca_service.getNoteTitleAndIcon(result.noteId);
const { title, icon } = beccaService.getNoteTitleAndIcon(result.noteId);
return {
notePath: result.notePath,
noteTitle: title,
@@ -79,7 +82,7 @@ function quickSearch(req: Request) {
highlightedContentSnippet: result.highlightedContentSnippet,
attributeSnippet: result.attributeSnippet,
highlightedAttributeSnippet: result.highlightedAttributeSnippet,
icon
icon: icon
};
});
@@ -87,7 +90,7 @@ function quickSearch(req: Request) {
return {
searchResultNoteIds: resultNoteIds,
searchResults,
searchResults: searchResults,
error: searchContext.getError()
};
}

View File

@@ -1,9 +1,9 @@
import { utils } from "@triliumnext/core";
import type { Request } from "express";
import imageType from "image-type";
import imageService from "../../services/image.js";
import noteService from "../../services/notes.js";
import sanitizeAttributeName from "../../services/sanitize_attribute_name.js";
import specialNotesService from "../../services/special_notes.js";
async function uploadImage(req: Request) {
@@ -43,14 +43,14 @@ async function uploadImage(req: Request) {
const labels = JSON.parse(labelsStr);
for (const { name, value } of labels) {
note.setLabel(utils.sanitizeAttributeName(name), value);
note.setLabel(sanitizeAttributeName(name), value);
}
}
note.setLabel("sentFromSender");
return {
noteId
noteId: noteId
};
}
@@ -72,7 +72,7 @@ async function saveNote(req: Request) {
if (req.body.labels) {
for (const { name, value } of req.body.labels) {
note.setLabel(utils.sanitizeAttributeName(name), value);
note.setLabel(sanitizeAttributeName(name), value);
}
}

View File

@@ -1,15 +1,17 @@
import { SimilarNoteResponse } from "@triliumnext/commons";
import { similarity } from "@triliumnext/core";
"use strict";
import type { Request } from "express";
import similarityService from "../../becca/similarity.js";
import becca from "../../becca/becca.js";
import { SimilarNoteResponse } from "@triliumnext/commons";
async function getSimilarNotes(req: Request) {
const noteId = req.params.noteId;
const _note = becca.getNoteOrThrow(noteId);
return (await similarity.findSimilarNotes(noteId) satisfies SimilarNoteResponse);
return (await similarityService.findSimilarNotes(noteId) satisfies SimilarNoteResponse);
}
export default {

View File

@@ -1,8 +1,9 @@
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
"use strict";
import becca from "../../becca/becca.js";
import sql from "../../services/sql.js";
import becca from "../../becca/becca.js";
import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
interface Table {

View File

@@ -1,20 +1,21 @@
import { type EntityChange,SyncTestResponse } from "@triliumnext/commons";
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import { t } from "i18next";
"use strict";
import consistencyChecksService from "../../services/consistency_checks.js";
import contentHashService from "../../services/content_hash.js";
import syncService from "../../services/sync.js";
import syncUpdateService from "../../services/sync_update.js";
import entityChangesService from "../../services/entity_changes.js";
import log from "../../services/log.js";
import optionService from "../../services/options.js";
import sql from "../../services/sql.js";
import sqlInit from "../../services/sql_init.js";
import syncService from "../../services/sync.js";
import optionService from "../../services/options.js";
import contentHashService from "../../services/content_hash.js";
import log from "../../services/log.js";
import syncOptions from "../../services/sync_options.js";
import syncUpdateService from "../../services/sync_update.js";
import utils, { safeExtractMessageAndStackFromError } from "../../services/utils.js";
import ws from "../../services/ws.js";
import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import consistencyChecksService from "../../services/consistency_checks.js";
import { t } from "i18next";
import { SyncTestResponse, type EntityChange } from "@triliumnext/commons";
async function testSync(): Promise<SyncTestResponse> {
try {
@@ -286,10 +287,10 @@ function update(req: Request) {
if (pageIndex !== pageCount - 1) {
return;
}
body = JSON.parse(partialRequests[requestId].payload);
delete partialRequests[requestId];
} else {
body = JSON.parse(partialRequests[requestId].payload);
delete partialRequests[requestId];
}
}
const { entities, instanceId } = body;

View File

@@ -1,10 +1,11 @@
import type { AttributeRow, BranchRow, NoteRow } from "@triliumnext/commons";
import { NotFoundError } from "@triliumnext/core";
import type { Request } from "express";
"use strict";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import log from "../../services/log.js";
import NotFoundError from "../../errors/not_found_error.js";
import type { Request } from "express";
import type BNote from "../../becca/entities/bnote.js";
import type { AttributeRow, BranchRow, NoteRow } from "@triliumnext/commons";
function getNotesAndBranchesAndAttributes(_noteIds: string[] | Set<string>) {
const noteIds = new Set(_noteIds);

View File

@@ -1,10 +1,13 @@
import express from "express";
import rateLimit from "express-rate-limit";
import { existsSync } from "fs";
import path from "path";
import type serveStatic from "serve-static";
import { assetUrlFragment } from "../services/asset_path.js";
import auth from "../services/auth.js";
import { getResourceDir, isDev } from "../services/utils.js";
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
if (!isDev) {
@@ -20,6 +23,11 @@ async function register(app: express.Application) {
const srcRoot = path.join(__dirname, "..", "..");
const resourceDir = getResourceDir();
const rootLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
if (process.env.NODE_ENV === "development") {
const { createServer: createViteServer } = await import("vite");
const clientDir = path.join(srcRoot, "../client");
@@ -32,13 +40,12 @@ async function register(app: express.Application) {
css: { devSourcemap: true }
});
app.use(`/${assetUrlFragment}/`, vite.middlewares);
app.get(`/`, (req, res, next) => {
// We force the page to not be cached since on mobile the CSRF token can be
// broken when closing the browser and coming back in to the page.
// The page is restored from cache, but the API call fail.
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
req.url = `/${assetUrlFragment}/src/desktop.html`;
app.get(`/`, [ rootLimiter, auth.checkAuth, csrfMiddleware ], (req, res, next) => {
req.url = `/${assetUrlFragment}/src/index.html`;
vite.middlewares(req, res, next);
});
app.get(`/index.ts`, (req, res, next) => {
req.url = `/${assetUrlFragment}/src/index.ts`;
vite.middlewares(req, res, next);
});
app.use(`/node_modules/@excalidraw/excalidraw/dist/prod`, persistentCacheStatic(path.join(srcRoot, "../../node_modules/@excalidraw/excalidraw/dist/prod")));
@@ -48,7 +55,14 @@ async function register(app: express.Application) {
throw new Error(`Public directory is missing at: ${path.resolve(publicDir)}`);
}
app.use(`/${assetUrlFragment}/src`, persistentCacheStatic(path.join(publicDir, "src")));
app.get(`/`, [ rootLimiter, auth.checkAuth, csrfMiddleware ], (_, res) => {
// We force the page to not be cached since on mobile the CSRF token can be
// broken when closing the browser and coming back in to the page.
// The page is restored from cache, but the API call fail.
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.sendFile(path.join(publicDir, "src", "index.html"));
});
app.use("/assets", persistentCacheStatic(path.join(publicDir, "assets")));
app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(publicDir, "stylesheets")));
app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(publicDir, "fonts")));
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(publicDir, "translations")));

View File

@@ -1,13 +1,11 @@
import type { Request, Response, Router } from "express";
import becca from "../becca/becca.js";
import { namespace } from "../cls_provider.js";
import cls from "../services/cls.js";
import log from "../services/log.js";
import scriptService from "../services/script.js";
import sql from "../services/sql.js";
import { normalizeCustomHandlerPattern,safeExtractMessageAndStackFromError } from "../services/utils.js";
import fileService from "./api/files.js";
import scriptService from "../services/script.js";
import cls from "../services/cls.js";
import sql from "../services/sql.js";
import becca from "../becca/becca.js";
import type { Request, Response, Router } from "express";
import { safeExtractMessageAndStackFromError, normalizeCustomHandlerPattern } from "../services/utils.js";
function handleRequest(req: Request, res: Response) {
@@ -29,7 +27,7 @@ function handleRequest(req: Request, res: Response) {
// splitPath.map(segment => encodeURIComponent(segment)).join("/")
// might be safer
const path = splitPath.join("/");
const path = splitPath.join("/")
const attributeIds = sql.getColumn<string>("SELECT attributeId FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");
@@ -98,8 +96,8 @@ function register(router: Router) {
// explicitly no CSRF middleware since it's meant to allow integration from external services
router.all("/custom/*path", (req: Request, res: Response, _next) => {
namespace.bindEmitter(req);
namespace.bindEmitter(res);
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
cls.init(() => handleRequest(req, res));
});

View File

@@ -1,7 +1,8 @@
import { ForbiddenError, HttpError, NotFoundError } from "@triliumnext/core";
import type { Application, NextFunction, Request, Response } from "express";
import log from "../services/log.js";
import NotFoundError from "../errors/not_found_error.js";
import ForbiddenError from "../errors/forbidden_error.js";
import HttpError from "../errors/http_error.js";
function register(app: Application) {

View File

@@ -44,10 +44,6 @@ export function bootstrap(req: Request, res: Response) {
isElectron,
hasNativeTitleBar: isElectron && nativeTitleBarVisible,
hasBackgroundEffects: isElectron && isWindows11 && !nativeTitleBarVisible && options.backgroundEffects === "true",
// TODO: These font size don't actually seem to be used.
mainFontSize: parseInt(options.mainFontSize, 10),
treeFontSize: parseInt(options.treeFontSize, 10),
detailFontSize: parseInt(options.detailFontSize, 10),
maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"),
maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"),
instanceName: config.General ? config.General.instanceName : null,

View File

@@ -1,19 +1,18 @@
import { ValidationError } from "@triliumnext/core";
import crypto from "crypto";
import type { Request, Response } from 'express';
import appPath from "../services/app_path.js";
import assetPath, { assetUrlFragment } from "../services/asset_path.js";
import myScryptService from "../services/encryption/my_scrypt.js";
import openIDEncryption from '../services/encryption/open_id_encryption.js';
import passwordService from "../services/encryption/password.js";
import recoveryCodeService from '../services/encryption/recovery_codes.js';
import { getCurrentLocale } from "../services/i18n.js";
import log from "../services/log.js";
import openID from '../services/open_id.js';
import optionService from "../services/options.js";
import totp from '../services/totp.js';
import utils from "../services/utils.js";
import optionService from "../services/options.js";
import myScryptService from "../services/encryption/my_scrypt.js";
import log from "../services/log.js";
import passwordService from "../services/encryption/password.js";
import assetPath, { assetUrlFragment } from "../services/asset_path.js";
import appPath from "../services/app_path.js";
import ValidationError from "../errors/validation_error.js";
import type { Request, Response } from 'express';
import totp from '../services/totp.js';
import recoveryCodeService from '../services/encryption/recovery_codes.js';
import openID from '../services/open_id.js';
import openIDEncryption from '../services/encryption/open_id_encryption.js';
import { getCurrentLocale } from "../services/i18n.js";
function loginPage(req: Request, res: Response) {
// Login page is triggered twice. Once here, and another time (see sendLoginError) if the password is failed.
@@ -24,9 +23,9 @@ function loginPage(req: Request, res: Response) {
ssoEnabled: openID.isOpenIDEnabled(),
ssoIssuerName: openID.getSSOIssuerName(),
ssoIssuerIcon: openID.getSSOIssuerIcon(),
assetPath,
assetPath: assetPath,
assetPathFragment: assetUrlFragment,
appPath,
appPath: appPath,
currentLocale: getCurrentLocale()
});
}
@@ -182,9 +181,9 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to
wrongTotp: errorType === 'totp',
totpEnabled: totp.isTotpEnabled(),
ssoEnabled: openID.isOpenIDEnabled(),
assetPath,
assetPath: assetPath,
assetPathFragment: assetUrlFragment,
appPath,
appPath: appPath,
currentLocale: getCurrentLocale()
});
}

View File

@@ -1,15 +1,15 @@
import { AbstractBeccaEntity,NotFoundError, ValidationError } from "@triliumnext/core";
import express, { type RequestHandler } from "express";
import multer from "multer";
import { namespace } from "../cls_provider.js";
import auth from "../services/auth.js";
import cls from "../services/cls.js";
import entityChangesService from "../services/entity_changes.js";
import log from "../services/log.js";
import cls from "../services/cls.js";
import sql from "../services/sql.js";
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
import entityChangesService from "../services/entity_changes.js";
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import NotFoundError from "../errors/not_found_error.js";
import ValidationError from "../errors/validation_error.js";
import auth from "../services/auth.js";
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
const MAX_ALLOWED_FILE_SIZE_MB = 250;
export const router = express.Router();
@@ -67,9 +67,9 @@ export function apiResultHandler(req: express.Request, res: express.Response, re
return send(res, statusCode, response);
} else if (result === undefined) {
return send(res, 204, "");
} else {
return send(res, 200, result);
}
return send(res, 200, result);
}
function send(res: express.Response, statusCode: number, response: unknown) {
@@ -81,14 +81,14 @@ function send(res: express.Response, statusCode: number, response: unknown) {
res.status(statusCode).send(response);
return response.length;
} else {
const json = JSON.stringify(response);
res.setHeader("Content-Type", "application/json");
res.status(statusCode).send(json);
return json.length;
}
const json = JSON.stringify(response);
res.setHeader("Content-Type", "application/json");
res.status(statusCode).send(json);
return json.length;
}
export function apiRoute(method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler) {
@@ -112,8 +112,8 @@ function internalRoute(method: HttpMethod, path: string, middleware: express.Han
const start = Date.now();
try {
namespace.bindEmitter(req);
namespace.bindEmitter(res);
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
const result = cls.init(() => {
cls.set("componentId", req.headers["trilium-component-id"]);
@@ -193,7 +193,7 @@ export function createUploadMiddleware(): RequestHandler {
const uploadMiddleware = createUploadMiddleware();
export const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
uploadMiddleware(req, res, (err) => {
uploadMiddleware(req, res, function (err) {
if (err?.code === "LIMIT_FILE_SIZE") {
res.setHeader("Content-Type", "text/plain").status(400).send(`Cannot upload file because it excceeded max allowed file size of ${MAX_ALLOWED_FILE_SIZE_MB} MiB`);
} else {

View File

@@ -1,11 +1,21 @@
import { AppInfo } from "@triliumnext/commons";
import { app_info as coreAppInfo } from "@triliumnext/core";
import path from "path";
import build from "./build.js";
import packageJson from "../../package.json" with { type: "json" };
import dataDir from "./data_dir.js";
import { AppInfo } from "@triliumnext/commons";
const APP_DB_VERSION = 233;
const SYNC_VERSION = 36;
const CLIPPER_PROTOCOL_VERSION = "1.0";
export default {
...coreAppInfo,
appVersion: packageJson.version,
dbVersion: APP_DB_VERSION,
nodeVersion: process.version,
syncVersion: SYNC_VERSION,
buildDate: build.buildDate,
buildRevision: build.buildRevision,
dataDirectory: path.resolve(dataDir.TRILIUM_DATA_DIR),
clipperProtocolVersion: CLIPPER_PROTOCOL_VERSION,
utcDateTime: new Date().toISOString()
} satisfies AppInfo;

View File

@@ -1,38 +1,41 @@
import { type AttributeRow, dayjs, formatLogMessage } from "@triliumnext/commons";
import { type AbstractBeccaEntity, Becca, NoteParams } from "@triliumnext/core";
import axios from "axios";
import * as cheerio from "cheerio";
import xml2js from "xml2js";
import becca from "../becca/becca.js";
import type BAttachment from "../becca/entities/battachment.js";
import type BAttribute from "../becca/entities/battribute.js";
import type BBranch from "../becca/entities/bbranch.js";
import type BEtapiToken from "../becca/entities/betapi_token.js";
import type BNote from "../becca/entities/bnote.js";
import type BOption from "../becca/entities/boption.js";
import type BRevision from "../becca/entities/brevision.js";
import appInfo from "./app_info.js";
import attributeService from "./attributes.js";
import type { ApiParams } from "./backend_script_api_interface.js";
import backupService from "./backup.js";
import branchService from "./branches.js";
import cloningService from "./cloning.js";
import config from "./config.js";
import dateNoteService from "./date_notes.js";
import exportService from "./export/zip.js";
import log from "./log.js";
import noteService from "./notes.js";
import optionsService from "./options.js";
import SearchContext from "./search/search_context.js";
import sql from "./sql.js";
import { randomString, escapeHtml, unescapeHtml } from "./utils.js";
import attributeService from "./attributes.js";
import dateNoteService from "./date_notes.js";
import treeService from "./tree.js";
import config from "./config.js";
import axios from "axios";
import { dayjs } from "@triliumnext/commons";
import xml2js from "xml2js";
import * as cheerio from "cheerio";
import cloningService from "./cloning.js";
import appInfo from "./app_info.js";
import searchService from "./search/services/search.js";
import SearchContext from "./search/search_context.js";
import becca from "../becca/becca.js";
import ws from "./ws.js";
import SpacedUpdate from "./spaced_update.js";
import specialNotesService from "./special_notes.js";
import sql from "./sql.js";
import branchService from "./branches.js";
import exportService from "./export/zip.js";
import syncMutex from "./sync_mutex.js";
import treeService from "./tree.js";
import { escapeHtml, randomString, unescapeHtml } from "./utils.js";
import ws from "./ws.js";
import backupService from "./backup.js";
import optionsService from "./options.js";
import { formatLogMessage } from "@triliumnext/commons";
import type BNote from "../becca/entities/bnote.js";
import type AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import type BBranch from "../becca/entities/bbranch.js";
import type BAttribute from "../becca/entities/battribute.js";
import type BAttachment from "../becca/entities/battachment.js";
import type BRevision from "../becca/entities/brevision.js";
import type BEtapiToken from "../becca/entities/betapi_token.js";
import type BOption from "../becca/entities/boption.js";
import type { AttributeRow } from "@triliumnext/commons";
import type Becca from "../becca/becca-interface.js";
import type { NoteParams } from "./note-interface.js";
import type { ApiParams } from "./backend_script_api_interface.js";
/**
* A whole number
@@ -503,7 +506,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
throw new Error(`Unable to find parent note with ID ${parentNote}.`);
}
const extraOptions: NoteParams = {
let extraOptions: NoteParams = {
..._extraOptions,
content: "",
type: "text",
@@ -617,13 +620,13 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
}
const parentNoteId = opts.isVisible ? "_lbVisibleLaunchers" : "_lbAvailableLaunchers";
const noteId = `al_${ opts.id}`;
const noteId = "al_" + opts.id;
const launcherNote =
becca.getNote(noteId) ||
specialNotesService.createLauncher({
noteId,
parentNoteId,
noteId: noteId,
parentNoteId: parentNoteId,
launcherType: opts.type
}).note;
@@ -677,7 +680,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
ws.sendMessageToAllClients({
type: "execute-script",
script,
script: script,
params: prepareParams(params),
startNoteId: this.startNote?.noteId,
currentNoteId: this.currentNote.noteId,
@@ -693,9 +696,9 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
return params.map((p) => {
if (typeof p === "function") {
return `!@#Function: ${p.toString()}`;
} else {
return p;
}
return p;
});
}
};

View File

@@ -1,6 +1,5 @@
import type { AbstractBeccaEntity } from "@triliumnext/core";
import type { Request, Response } from "express";
import type AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import type BNote from "../becca/entities/bnote.js";
export interface ApiParams {

View File

@@ -0,0 +1,5 @@
export interface Blob {
blobId: string;
content: string | Buffer;
utcDateModified: string;
}

View File

@@ -1,9 +1,8 @@
import { BlobRow } from "@triliumnext/commons";
import becca from "../becca/becca.js";
import { NotFoundError } from "../errors";
import NotFoundError from "../errors/not_found_error.js";
import protectedSessionService from "./protected_session.js";
import { decodeUtf8 } from "./utils/binary.js";
import { hash } from "./utils/index.js";
import { hash } from "./utils.js";
import type { Blob } from "./blob-interface.js";
function getBlobPojo(entityName: string, entityId: string, opts?: { preview: boolean }) {
// TODO: Unused opts.
@@ -22,37 +21,36 @@ function getBlobPojo(entityName: string, entityId: string, opts?: { preview: boo
if (!entity.hasStringContent()) {
pojo.content = null;
} else {
pojo.content = processContent(pojo.content, !!entity.isProtected, true) as string | Uint8Array;
pojo.content = processContent(pojo.content, !!entity.isProtected, true);
}
return pojo;
}
function processContent(content: Uint8Array | string | null, isProtected: boolean, isStringContent: boolean) {
function processContent(content: Buffer | string | null, isProtected: boolean, isStringContent: boolean) {
if (isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
content = content === null ? null : protectedSessionService.decrypt(content as Uint8Array);
content = content === null ? null : protectedSessionService.decrypt(content);
} else {
content = "";
}
}
if (isStringContent) {
if (content === null) return "";
if (typeof content === "string") return content;
return decodeUtf8(content as Uint8Array);
}
// see https://github.com/zadam/trilium/issues/3523
// IIRC a zero-sized buffer can be returned as null from the database
if (content === null) {
// this will force de/encryption
content = new Uint8Array(0);
}
return content === null ? "" : content.toString("utf-8");
} else {
// see https://github.com/zadam/trilium/issues/3523
// IIRC a zero-sized buffer can be returned as null from the database
if (content === null) {
// this will force de/encryption
content = Buffer.alloc(0);
}
return content;
return content;
}
}
function calculateContentHash({ blobId, content }: Pick<BlobRow, "blobId" | "content">) {
function calculateContentHash({ blobId, content }: Blob) {
return hash(`${blobId}|${content.toString()}`);
}

View File

@@ -1,12 +1,11 @@
import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons";
import { erase as eraseService } from "@triliumnext/core";
import becca from "../becca/becca.js";
import type BNote from "../becca/entities/bnote.js";
import branchService from "./branches.js";
import cloningService from "./cloning.js";
import log from "./log.js";
import becca from "../becca/becca.js";
import cloningService from "./cloning.js";
import branchService from "./branches.js";
import { randomString } from "./utils.js";
import eraseService from "./erase.js";
import type BNote from "../becca/entities/bnote.js";
import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons";
type ActionHandler<T> = (action: T, note: BNote) => void;

View File

@@ -1,2 +1,190 @@
import { cloning } from "@triliumnext/core";
export default cloning;
"use strict";
import sql from "./sql.js";
import eventChangesService from "./entity_changes.js";
import treeService from "./tree.js";
import BBranch from "../becca/entities/bbranch.js";
import becca from "../becca/becca.js";
import log from "./log.js";
import { CloneResponse } from "@triliumnext/commons";
function cloneNoteToParentNote(noteId: string, parentNoteId: string, prefix: string | null = null): CloneResponse {
if (!(noteId in becca.notes) || !(parentNoteId in becca.notes)) {
return { success: false, message: "Note cannot be cloned because either the cloned note or the intended parent is deleted." };
}
const parentNote = becca.getNote(parentNoteId);
if (!parentNote) {
return { success: false, message: "Note cannot be cloned because the parent note could not be found." };
}
if (parentNote.type === "search") {
return {
success: false,
message: "Can't clone into a search note"
};
}
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
if (!validationResult.success) {
return validationResult;
}
const branch = new BBranch({
noteId: noteId,
parentNoteId: parentNoteId,
prefix: prefix,
isExpanded: false
}).save();
log.info(`Cloned note '${noteId}' to a new parent note '${parentNoteId}' with prefix '${prefix}'`);
return {
success: true,
branchId: branch.branchId,
notePath: `${parentNote.getBestNotePathString()}/${noteId}`
};
}
function cloneNoteToBranch(noteId: string, parentBranchId: string, prefix?: string) {
const parentBranch = becca.getBranch(parentBranchId);
if (!parentBranch) {
return { success: false, message: `Parent branch '${parentBranchId}' does not exist.` };
}
const ret = cloneNoteToParentNote(noteId, parentBranch.noteId, prefix);
parentBranch.isExpanded = true; // the new target should be expanded, so it immediately shows up to the user
parentBranch.save();
return ret;
}
function ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefix?: string) {
if (!(noteId in becca.notes)) {
return { branch: null, success: false, message: `Note '${noteId}' is deleted.` };
} else if (!(parentNoteId in becca.notes)) {
return { branch: null, success: false, message: `Note '${parentNoteId}' is deleted.` };
}
const parentNote = becca.getNote(parentNoteId);
if (!parentNote) {
return { branch: null, success: false, message: "Can't find parent note." };
}
if (parentNote.type === "search") {
return { branch: null, success: false, message: "Can't clone into a search note" };
}
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
if (!validationResult.success) {
return validationResult;
}
const branch = new BBranch({
noteId: noteId,
parentNoteId: parentNoteId,
prefix: prefix,
isExpanded: false
}).save();
log.info(`Ensured note '${noteId}' is in parent note '${parentNoteId}' with prefix '${branch.prefix}'`);
return { branch: branch, success: true };
}
function ensureNoteIsAbsentFromParent(noteId: string, parentNoteId: string) {
const branchId = sql.getValue<string>(/*sql*/`SELECT branchId FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]);
const branch = becca.getBranch(branchId);
if (branch) {
if (!branch.isWeak && branch.getNote().getStrongParentBranches().length <= 1) {
return {
success: false,
message: `Cannot remove branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' because this would delete the note as well.`
};
}
branch.deleteBranch();
log.info(`Ensured note '${noteId}' is NOT in parent note '${parentNoteId}'`);
return { success: true };
}
}
function toggleNoteInParent(present: boolean, noteId: string, parentNoteId: string, prefix?: string) {
if (present) {
return ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
} else {
return ensureNoteIsAbsentFromParent(noteId, parentNoteId);
}
}
function cloneNoteAfter(noteId: string, afterBranchId: string) {
if (["_hidden", "root"].includes(noteId)) {
return { success: false, message: `Cloning the note '${noteId}' is forbidden.` };
}
const afterBranch = becca.getBranch(afterBranchId);
if (!afterBranch) {
return { success: false, message: `Branch '${afterBranchId}' does not exist.` };
}
if (afterBranch.noteId === "_hidden") {
return { success: false, message: "Cannot clone after the hidden branch." };
}
const afterNote = becca.getBranch(afterBranchId);
if (!(noteId in becca.notes)) {
return { success: false, message: `Note to be cloned '${noteId}' is deleted or does not exist.` };
} else if (!afterNote || !(afterNote.parentNoteId in becca.notes)) {
return { success: false, message: `After note '${afterNote?.parentNoteId}' is deleted or does not exist.` };
}
const parentNote = becca.getNote(afterNote.parentNoteId);
if (!parentNote || parentNote.type === "search") {
return {
success: false,
message: "Can't clone into a search note"
};
}
const validationResult = treeService.validateParentChild(afterNote.parentNoteId, noteId);
if (!validationResult.success) {
return validationResult;
}
// we don't change utcDateModified, so other changes are prioritized in case of conflict
// also we would have to sync all those modified branches otherwise hash checks would fail
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [afterNote.parentNoteId, afterNote.notePosition]);
eventChangesService.putNoteReorderingEntityChange(afterNote.parentNoteId);
const branch = new BBranch({
noteId: noteId,
parentNoteId: afterNote.parentNoteId,
notePosition: afterNote.notePosition + 10,
isExpanded: false
}).save();
log.info(`Cloned note '${noteId}' into parent note '${afterNote.parentNoteId}' after note '${afterNote.noteId}', branch '${afterBranchId}'`);
return { success: true, branchId: branch.branchId };
}
export default {
cloneNoteToBranch,
cloneNoteToParentNote,
ensureNoteIsPresentInParent,
ensureNoteIsAbsentFromParent,
toggleNoteInParent,
cloneNoteAfter
};

View File

@@ -1,79 +1,109 @@
import clsHooked from "cls-hooked";
import type { EntityChange } from "@triliumnext/commons";
import { cls } from "@triliumnext/core";
const namespace = clsHooked.createNamespace("trilium");
function init<T>(callback: () => T) {
return cls.getContext().init(callback);
type Callback = (...args: any[]) => any;
function init(callback: Callback) {
return namespace.runAndReturn(callback);
}
function wrap(callback: Callback) {
return () => {
try {
init(callback);
} catch (e: any) {
console.log(`Error occurred: ${e.message}: ${e.stack}`);
}
};
}
function get(key: string) {
return namespace.get(key);
}
function set(key: string, value: any) {
namespace.set(key, value);
}
function getHoistedNoteId() {
return cls.getHoistedNoteId();
return namespace.get("hoistedNoteId") || "root";
}
function getComponentId() {
return cls.getComponentId();
return namespace.get("componentId");
}
function getLocalNowDateTime() {
return namespace.get("localNowDateTime");
}
/** @deprecated */
function disableEntityEvents() {
cls.disableEntityEvents();
namespace.set("disableEntityEvents", true);
}
/** @deprecated */
function enableEntityEvents() {
cls.enableEntityEvents();
namespace.set("disableEntityEvents", false);
}
function isEntityEventsDisabled() {
return cls.isEntityEventsDisabled();
return !!namespace.get("disableEntityEvents");
}
/** @deprecated */
function setMigrationRunning(running: boolean) {
cls.setMigrationRunning(running);
namespace.set("migrationRunning", !!running);
}
/** @deprecated */
function isMigrationRunning() {
return cls.isMigrationRunning();
return !!namespace.get("migrationRunning");
}
function disableSlowQueryLogging(disable: boolean) {
namespace.set("disableSlowQueryLogging", disable);
}
function isSlowQueryLoggingDisabled() {
return !!namespace.get("disableSlowQueryLogging");
}
function getAndClearEntityChangeIds() {
const entityChangeIds = cls.getContext().get("entityChangeIds") || [];
const entityChangeIds = namespace.get("entityChangeIds") || [];
cls.getContext().set("entityChangeIds", []);
namespace.set("entityChangeIds", []);
return entityChangeIds;
}
function putEntityChange(entityChange: EntityChange) {
cls.putEntityChange(entityChange);
}
if (namespace.get("ignoreEntityChangeIds")) {
return;
}
function ignoreEntityChangeIds() {
cls.getContext().set("ignoreEntityChangeIds", true);
}
const entityChangeIds = namespace.get("entityChangeIds") || [];
function get(key: string) {
return cls.getContext().get(key);
}
// store only ID since the record can be modified (e.g., in erase)
entityChangeIds.push(entityChange.id);
function set(key: string, value: unknown) {
cls.getContext().set(key, value);
namespace.set("entityChangeIds", entityChangeIds);
}
function reset() {
cls.getContext().reset();
clsHooked.reset();
}
export const wrap = cls.wrap;
function ignoreEntityChangeIds() {
namespace.set("ignoreEntityChangeIds", true);
}
export default {
init,
wrap,
get,
set,
namespace,
getHoistedNoteId,
getComponentId,
getLocalNowDateTime,
disableEntityEvents,
enableEntityEvents,
isEntityEventsDisabled,
@@ -81,6 +111,8 @@ export default {
getAndClearEntityChangeIds,
putEntityChange,
ignoreEntityChangeIds,
disableSlowQueryLogging,
isSlowQueryLoggingDisabled,
setMigrationRunning,
isMigrationRunning
};

View File

@@ -1,19 +1,22 @@
import type { BranchRow } from "@triliumnext/commons";
import type { EntityChange } from "@triliumnext/commons";
import { becca_loader, erase as eraseService, utils } from "@triliumnext/core";
"use strict";
import becca from "../becca/becca.js";
import BBranch from "../becca/entities/bbranch.js";
import noteTypesService from "../services/note_types.js";
import { hashedBlobId, randomString } from "../services/utils.js";
import cls from "./cls.js";
import entityChangesService from "./entity_changes.js";
import log from "./log.js";
import optionsService from "./options.js";
import sql from "./sql.js";
import sqlInit from "./sql_init.js";
import syncMutexService from "./sync_mutex.js";
import log from "./log.js";
import ws from "./ws.js";
import syncMutexService from "./sync_mutex.js";
import cls from "./cls.js";
import entityChangesService from "./entity_changes.js";
import optionsService from "./options.js";
import BBranch from "../becca/entities/bbranch.js";
import becca from "../becca/becca.js";
import { hash as getHash, hashedBlobId, randomString } from "../services/utils.js";
import eraseService from "../services/erase.js";
import sanitizeAttributeName from "./sanitize_attribute_name.js";
import noteTypesService from "../services/note_types.js";
import type { BranchRow } from "@triliumnext/commons";
import type { EntityChange } from "@triliumnext/commons";
import becca_loader from "../becca/becca_loader.js";
const noteTypes = noteTypesService.getNoteTypeNames();
class ConsistencyChecks {
@@ -81,11 +84,11 @@ class ConsistencyChecks {
}
return true;
} else {
logError(`Tree cycle detected at parent-child relationship: '${parentNoteId}' - '${noteId}', whole path: '${path}'`);
this.unrecoveredConsistencyErrors = true;
}
logError(`Tree cycle detected at parent-child relationship: '${parentNoteId}' - '${noteId}', whole path: '${path}'`);
this.unrecoveredConsistencyErrors = true;
} else {
const newPath = path.slice();
newPath.push(noteId);
@@ -183,7 +186,7 @@ class ConsistencyChecks {
if (note.getParentBranches().length === 0) {
const newBranch = new BBranch({
parentNoteId: "root",
noteId,
noteId: noteId,
prefix: "recovered"
}).save();
@@ -346,7 +349,7 @@ class ConsistencyChecks {
if (this.autoFix) {
const branch = new BBranch({
parentNoteId: "root",
noteId,
noteId: noteId,
prefix: "recovered"
}).save();
@@ -482,18 +485,18 @@ class ConsistencyChecks {
if (!blobAlreadyExists) {
// manually creating row since this can also affect deleted notes
sql.upsert("blobs", "blobId", {
noteId,
noteId: noteId,
content: blankContent,
utcDateModified: fakeDate,
dateModified: fakeDate
});
const hash = utils.hash(randomString(10));
const hash = getHash(randomString(10));
entityChangesService.putEntityChange({
entityName: "blobs",
entityId: blobId,
hash,
hash: hash,
isErased: false,
utcDateChanged: fakeDate,
isSynced: true
@@ -802,7 +805,7 @@ class ConsistencyChecks {
const attrNames = sql.getColumn<string>(/*sql*/`SELECT DISTINCT name FROM attributes`);
for (const origName of attrNames) {
const fixedName = utils.sanitizeAttributeName(origName);
const fixedName = sanitizeAttributeName(origName);
if (fixedName !== origName) {
if (this.autoFix) {
@@ -908,7 +911,7 @@ class ConsistencyChecks {
ws.sendMessageToAllClients({ type: "consistency-checks-failed" });
} else {
log.info(`All consistency checks passed ${ this.fixedIssues ? "after some fixes" : "with no errors detected" } (took ${elapsedTimeMs}ms)`);
log.info(`All consistency checks passed ` + (this.fixedIssues ? "after some fixes" : "with no errors detected") + ` (took ${elapsedTimeMs}ms)`);
}
}
}

View File

@@ -1,7 +1,9 @@
import { erase as eraseService,utils } from "@triliumnext/core";
"use strict";
import log from "./log.js";
import sql from "./sql.js";
import { hash } from "./utils.js";
import log from "./log.js";
import eraseService from "./erase.js";
type SectorHash = Record<string, string>;
@@ -46,7 +48,7 @@ function getEntityHashes() {
for (const entityHashMap of Object.values(hashMap)) {
for (const key in entityHashMap) {
entityHashMap[key] = utils.hash(entityHashMap[key]);
entityHashMap[key] = hash(entityHashMap[key]);
}
}

View File

@@ -1,2 +1,107 @@
import { date_utils } from "@triliumnext/core";
export default date_utils;
import { dayjs } from "@triliumnext/commons";
import cls from "./cls.js";
const LOCAL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss.SSSZZ";
const UTC_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ssZ";
function utcNowDateTime() {
return utcDateTimeStr(new Date());
}
// CLS date time is important in web deployments - server often runs in different time zone than user is located in,
// so we'd prefer client timezone to be used to record local dates. For this reason, requests from clients contain
// "trilium-local-now-datetime" header which is then stored in CLS
function localNowDateTime() {
return cls.getLocalNowDateTime() || dayjs().format(LOCAL_DATETIME_FORMAT);
}
function localNowDate() {
const clsDateTime = cls.getLocalNowDateTime();
if (clsDateTime) {
return clsDateTime.substr(0, 10);
} else {
const date = new Date();
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
}
function pad(num: number) {
return num <= 9 ? `0${num}` : `${num}`;
}
function utcDateStr(date: Date) {
return date.toISOString().split("T")[0];
}
function utcDateTimeStr(date: Date) {
return date.toISOString().replace("T", " ");
}
/**
* @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr().
* also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time
*/
function parseDateTime(str: string) {
try {
return new Date(Date.parse(str));
} catch (e: any) {
throw new Error(`Can't parse date from '${str}': ${e.stack}`);
}
}
function parseLocalDate(str: string) {
const datePart = str.substr(0, 10);
// not specifying the timezone and specifying the time means Date.parse() will use the local timezone
return parseDateTime(`${datePart} 12:00:00.000`);
}
function getDateTimeForFile() {
return new Date().toISOString().substr(0, 19).replace(/:/g, "");
}
function validateLocalDateTime(str: string | null | undefined) {
if (!str) {
return;
}
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}[+-][0-9]{4}/.test(str)) {
return `Invalid local date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110+0200'`;
}
if (!dayjs(str, LOCAL_DATETIME_FORMAT)) {
return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`;
}
}
function validateUtcDateTime(str: string | undefined) {
if (!str) {
return;
}
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z/.test(str)) {
return `Invalid UTC date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110Z'`;
}
if (!dayjs(str, UTC_DATETIME_FORMAT)) {
return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`;
}
}
export default {
LOCAL_DATETIME_FORMAT,
UTC_DATETIME_FORMAT,
utcNowDateTime,
localNowDateTime,
localNowDate,
utcDateStr,
utcDateTimeStr,
parseDateTime,
parseLocalDate,
getDateTimeForFile,
validateLocalDateTime,
validateUtcDateTime
};

View File

@@ -0,0 +1,114 @@
import crypto from "crypto";
import log from "../log.js";
function arraysIdentical(a: any[] | Buffer, b: any[] | Buffer) {
let i = a.length;
if (i !== b.length) return false;
while (i--) {
if (a[i] !== b[i]) return false;
}
return true;
}
function shaArray(content: crypto.BinaryLike) {
// we use this as a simple checksum and don't rely on its security, so SHA-1 is good enough
return crypto.createHash("sha1").update(content).digest();
}
function pad(data: Buffer): Buffer {
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 encrypt(key: Buffer, plainText: Buffer | string) {
if (!key) {
throw new Error("No data key!");
}
const plainTextBuffer = Buffer.isBuffer(plainText) ? plainText : Buffer.from(plainText);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-128-cbc", pad(key), pad(iv));
const digest = shaArray(plainTextBuffer).slice(0, 4);
const digestWithPayload = Buffer.concat([digest, plainTextBuffer]);
const encryptedData = Buffer.concat([cipher.update(digestWithPayload), cipher.final()]);
const encryptedDataWithIv = Buffer.concat([iv, encryptedData]);
return encryptedDataWithIv.toString("base64");
}
function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | null {
if (cipherText === null) {
return null;
}
if (!key) {
return Buffer.from("[protected]");
}
try {
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
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: any) {
// recovery from https://github.com/zadam/trilium/issues/510
if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) {
log.info("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
return (Buffer.isBuffer(cipherText) ? cipherText : Buffer.from(cipherText));
} else {
throw e;
}
}
}
function decryptString(dataKey: Buffer, cipherText: string) {
const buffer = decrypt(dataKey, cipherText);
if (buffer === null) {
return null;
} else if (buffer === false) {
log.error(`Could not decrypt string. Buffer: ${buffer}`);
throw new Error("Could not decrypt string.");
}
return buffer.toString("utf-8");
}
export default {
encrypt,
decrypt,
decryptString
};

View File

@@ -1,9 +1,9 @@
import { data_encryption, OpenIdError } from "@triliumnext/core";
import myScryptService from "./my_scrypt.js";
import utils, { constantTimeCompare } from "../utils.js";
import dataEncryptionService from "./data_encryption.js";
import sql from "../sql.js";
import sqlInit from "../sql_init.js";
import utils, { constantTimeCompare } from "../utils.js";
import myScryptService from "./my_scrypt.js";
import OpenIdError from "../../errors/open_id_error.js";
function saveUser(subjectIdentifier: string, name: string, email: string) {
if (isUserSaved()) return false;
@@ -16,7 +16,7 @@ function saveUser(subjectIdentifier: string, name: string, email: string) {
verificationSalt
);
if (!verificationHash) {
throw new OpenIdError("Verification hash undefined!");
throw new OpenIdError("Verification hash undefined!")
}
const userIDEncryptedDataKey = setDataKey(
@@ -35,10 +35,10 @@ function saveUser(subjectIdentifier: string, name: string, email: string) {
userIDVerificationHash: utils.toBase64(verificationHash),
salt: verificationSalt,
derivedKey: derivedKeySalt,
userIDEncryptedDataKey,
userIDEncryptedDataKey: userIDEncryptedDataKey,
isSetup: "true",
username: name,
email
email: email
};
sql.upsert("user_data", "tmpID", data);
@@ -53,7 +53,7 @@ function isSubjectIdentifierSaved() {
function isUserSaved() {
const isSaved = sql.getValue<string>("SELECT isSetup FROM user_data;");
return isSaved === "true";
return isSaved === "true" ? true : false;
}
function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
@@ -102,7 +102,7 @@ function setDataKey(
console.error("SOMETHING WENT WRONG SAVING USER ID DERIVED KEY");
return undefined;
}
const newEncryptedDataKey = data_encryption.encrypt(
const newEncryptedDataKey = dataEncryptionService.encrypt(
subjectIdentifierDerivedKey,
plainTextDataKey
);
@@ -127,7 +127,7 @@ function getDataKey(subjectIdentifier: string) {
console.error("SOMETHING WENT WRONG SAVING USER ID DERIVED KEY");
return undefined;
}
const decryptedDataKey = data_encryption.decrypt(
const decryptedDataKey = dataEncryptionService.decrypt(
subjectIdentifierDerivedKey,
encryptedDataKey.toString()
);

View File

@@ -1,8 +1,7 @@
import { data_encryption } from "@triliumnext/core";
import optionService from "../options.js";
import { constantTimeCompare,toBase64 } from "../utils.js";
import myScryptService from "./my_scrypt.js";
import { toBase64, constantTimeCompare } from "../utils.js";
import dataEncryptionService from "./data_encryption.js";
function verifyPassword(password: string) {
const givenPasswordHash = toBase64(myScryptService.getVerificationHash(password));
@@ -16,10 +15,10 @@ function verifyPassword(password: string) {
return constantTimeCompare(givenPasswordHash, dbPasswordHash);
}
function setDataKey(password: string, plainTextDataKey: string | Buffer | Uint8Array) {
function setDataKey(password: string, plainTextDataKey: string | Buffer) {
const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
const newEncryptedDataKey = data_encryption.encrypt(passwordDerivedKey, plainTextDataKey);
const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey);
optionService.setOption("encryptedDataKey", newEncryptedDataKey);
}
@@ -29,7 +28,7 @@ function getDataKey(password: string) {
const encryptedDataKey = optionService.getOption("encryptedDataKey");
const decryptedDataKey = data_encryption.decrypt(passwordDerivedKey, encryptedDataKey);
const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey);
return decryptedDataKey;
}

View File

@@ -1,9 +1,8 @@
import type { OptionNames } from "@triliumnext/commons";
import { data_encryption } from "@triliumnext/core";
import optionService from "../options.js";
import { constantTimeCompare,randomSecureToken, toBase64 } from "../utils.js";
import myScryptService from "./my_scrypt.js";
import { randomSecureToken, toBase64, constantTimeCompare } from "../utils.js";
import dataEncryptionService from "./data_encryption.js";
import type { OptionNames } from "@triliumnext/commons";
const TOTP_OPTIONS: Record<string, OptionNames> = {
SALT: "totpEncryptionSalt",
@@ -33,7 +32,7 @@ function setTotpSecret(secret: string) {
const verificationHash = toBase64(myScryptService.getVerificationHash(secret));
optionService.setOption(TOTP_OPTIONS.VERIFICATION_HASH, verificationHash);
const encryptedSecret = data_encryption.encrypt(
const encryptedSecret = dataEncryptionService.encrypt(
Buffer.from(encryptionSalt),
secret
);
@@ -49,7 +48,7 @@ function getTotpSecret(): string | null {
}
try {
const decryptedSecret = data_encryption.decrypt(
const decryptedSecret = dataEncryptionService.decrypt(
Buffer.from(encryptionSalt),
encryptedSecret
);

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