Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _regroup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"devDependencies": {
"@playwright/test": "1.52.0",
"@stylistic/eslint-plugin": "4.4.1",
"@types/express": "5.0.1",
"@types/express": "5.0.3",
"@types/node": "22.15.30",
"@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.2",
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"express": "4.21.2",
"express": "5.1.0",
"express-openid-connect": "^2.17.1",
"express-rate-limit": "7.5.0",
"express-session": "1.18.1",
Expand Down
23 changes: 20 additions & 3 deletions apps/server/src/routes/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,26 @@ import type { Request, Response, Router } from "express";
import { safeExtractMessageAndStackFromError } from "../services/utils.js";

function handleRequest(req: Request, res: Response) {
// express puts content after first slash into 0 index element

const path = req.params.path + req.params[0];
// handle path from "*path" route wildcard
// in express v4, you could just add
// req.params.path + req.params[0], but with v5
// we get a split array that we have to join ourselves again

// @TriliumNextTODO: remove typecasting once express types are fixed
// they currently only treat req.params as string, while in reality
// it can also be a string[], when using wildcards
const splitPath = req.params.path as unknown as string[];

//const path = splitPath.map(segment => encodeURIComponent(segment)).join("/")
// naively join the "decoded" paths using a slash
// this is to mimick handleRequest behaviour
// as with the previous express v4.
// @TriliumNextTODO: using something like =>
// splitPath.map(segment => encodeURIComponent(segment)).join("/")
// might be safer

const path = splitPath.join("/")

const attributeIds = sql.getColumn<string>("SELECT attributeId FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");

Expand Down Expand Up @@ -70,7 +87,7 @@ function handleRequest(req: Request, res: Response) {
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) => {
router.all("/custom/*path", (req: Request, res: Response, _next) => {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);

Expand Down
164 changes: 115 additions & 49 deletions apps/server/src/routes/electron.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,123 @@
import electron from "electron";
import type { Application } from "express";
import type { ParamsDictionary, Request, Response } from "express-serve-static-core";
import type QueryString from "qs";
import { Session, SessionData } from "express-session";
import { parse as parseQuery } from "qs";
import EventEmitter from "events";

interface Response {
statusCode: number;
getHeader: (name: string) => string;
setHeader: (name: string, value: string) => Response;
header: (name: string, value: string) => Response;
status: (statusCode: number) => Response;
send: (obj: {}) => void; // eslint-disable-line @typescript-eslint/no-empty-object-type
}
type MockedResponse = Response<any, Record<string, any>, number>;

function init(app: Express.Application) {
function init(app: Application) {
electron.ipcMain.on("server-request", (event, arg) => {
const req = {
url: arg.url,
method: arg.method,
body: arg.data,
headers: arg.headers,
session: {
loggedIn: true
}
};

const respHeaders: Record<string, string> = {};

const res: Response = {
statusCode: 200,
getHeader: (name) => respHeaders[name],
setHeader: (name, value) => {
respHeaders[name] = value.toString();
return res;
},
header: (name, value) => {
respHeaders[name] = value.toString();
return res;
},
status: (statusCode) => {
res.statusCode = statusCode;
return res;
},
send: (obj) => {
event.sender.send("server-response", {
url: arg.url,
method: arg.method,
requestId: arg.requestId,
statusCode: res.statusCode,
headers: respHeaders,
body: obj
});
}
};

return (app as any)._router.handle(req, res, () => {});
const req = new FakeRequest(arg);
const res = new FakeResponse(event, arg);

return app.router(req as any, res as any, () => {});
});
}

const fakeSession: Session & Partial<SessionData> = {
id: "session-id", // Placeholder for session ID
cookie: {
originalMaxAge: 3600000, // 1 hour
},
loggedIn: true,
regenerate(callback) {
callback?.(null);
return fakeSession;
},
destroy(callback) {
callback?.(null);
return fakeSession;
},
reload(callback) {
callback?.(null);
return fakeSession;
},
save(callback) {
callback?.(null);
return fakeSession;
},
resetMaxAge: () => fakeSession,
touch: () => fakeSession
};

interface Arg {
url: string;
method: string;
data: any;
headers: Record<string, string>
}

class FakeRequest extends EventEmitter implements Pick<Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>, "url" | "method" | "body" | "headers" | "session" | "query"> {
url: string;
method: string;
body: any;
headers: Record<string, string>;
session: Session & Partial<SessionData>;
query: Record<string, any>;

constructor(arg: Arg) {
super();
this.url = arg.url;
this.method = arg.method;
this.body = arg.data;
this.headers = arg.headers;
this.session = fakeSession;
this.query = parseQuery(arg.url.split("?")[1] || "", { ignoreQueryPrefix: true });
}
}

class FakeResponse extends EventEmitter implements Pick<Response<any, Record<string, any>, number>, "status" | "send" | "json" | "setHeader"> {
private respHeaders: Record<string, string | string[]> = {};
private event: Electron.IpcMainEvent;
private arg: Arg & { requestId: string; };
statusCode: number = 200;
headers: Record<string, string> = {};
locals: Record<string, any> = {};

constructor(event: Electron.IpcMainEvent, arg: Arg & { requestId: string; }) {
super();
this.event = event;
this.arg = arg;
}

getHeader(name) {
return this.respHeaders[name];
}

setHeader(name, value) {
this.respHeaders[name] = value.toString();
return this as unknown as MockedResponse;
}

header(name: string, value?: string | string[]) {
this.respHeaders[name] = value ?? "";
return this as unknown as MockedResponse;
}

status(statusCode) {
this.statusCode = statusCode;
return this as unknown as MockedResponse;
}

send(obj) {
this.event.sender.send("server-response", {
url: this.arg.url,
method: this.arg.method,
requestId: this.arg.requestId,
statusCode: this.statusCode,
headers: this.respHeaders,
body: obj
});
return this as unknown as MockedResponse;
}

json(obj) {
this.send(JSON.stringify(obj));
return this as unknown as MockedResponse;
}
}

export default init;
2 changes: 1 addition & 1 deletion apps/server/src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ function register(app: express.Application) {

apiRoute(GET, "/api/options", optionsApiRoute.getOptions);
// FIXME: possibly change to sending value in the body to avoid host of HTTP server issues with slashes
apiRoute(PUT, "/api/options/:name/:value*", optionsApiRoute.updateOption);
apiRoute(PUT, "/api/options/:name/:value", optionsApiRoute.updateOption);
apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions);
apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes);
apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales);
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@nx/web": "21.1.3",
"@playwright/test": "^1.36.0",
"@triliumnext/server": "workspace:*",
"@types/express": "^4.17.21",
"@types/express": "^5.0.0",
"@types/node": "22.15.30",
"@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.0",
Expand Down Expand Up @@ -81,7 +81,7 @@
"homepage": "https://github.com/TriliumNext/Notes#readme",
"dependencies": {
"axios": "^1.6.0",
"express": "^4.21.2"
"express": "^5.0.0"
},
"packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912",
"pnpm": {
Expand Down
Loading
Loading