2017-10-21 21:10:33 -04:00
|
|
|
"use strict";
|
|
|
|
|
2024-07-18 21:37:45 +03:00
|
|
|
import crypto from "crypto";
|
2024-07-18 22:25:03 +03:00
|
|
|
import { generator } from "rand-token";
|
2024-07-18 21:37:45 +03:00
|
|
|
import unescape from "unescape";
|
|
|
|
import escape from "escape-html";
|
|
|
|
import sanitize from "sanitize-filename";
|
|
|
|
import mimeTypes from "mime-types";
|
|
|
|
import path from "path";
|
2024-11-02 11:49:33 +02:00
|
|
|
import { fileURLToPath } from "url";
|
|
|
|
import env from "./env.js";
|
|
|
|
import { dirname, join } from "path";
|
2017-10-28 19:55:55 -04:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
const randtoken = generator({ source: "crypto" });
|
2024-07-18 22:25:03 +03:00
|
|
|
|
2025-01-22 18:57:06 +01:00
|
|
|
export const isMac = process.platform === "darwin";
|
|
|
|
|
|
|
|
export const isWindows = process.platform === "win32";
|
|
|
|
|
|
|
|
export const isElectron = !!process.versions["electron"];
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function newEntityId() {
|
2018-02-11 00:18:59 -05:00
|
|
|
return randomString(12);
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function randomString(length: number): string {
|
2017-12-09 20:44:06 -05:00
|
|
|
return randtoken.generate(length);
|
2017-10-14 23:31:44 -04:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function randomSecureToken(bytes = 32) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return crypto.randomBytes(bytes).toString("base64");
|
2017-10-28 19:55:55 -04:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function md5(content: crypto.BinaryLike) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return crypto.createHash("md5").update(content).digest("hex");
|
2018-11-05 12:52:50 +01:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function hashedBlobId(content: string | Buffer) {
|
2023-09-18 23:45:00 +02:00
|
|
|
if (content === null || content === undefined) {
|
|
|
|
content = "";
|
|
|
|
}
|
|
|
|
|
2023-03-15 22:44:08 +01:00
|
|
|
// sha512 is faster than sha256
|
2025-01-09 18:07:02 +02:00
|
|
|
const base64Hash = crypto.createHash("sha512").update(content).digest("base64");
|
2023-03-15 22:44:08 +01:00
|
|
|
|
2023-07-28 15:55:26 +02:00
|
|
|
// we don't want such + and / in the IDs
|
2025-01-09 18:07:02 +02:00
|
|
|
const kindaBase62Hash = base64Hash.replaceAll("+", "X").replaceAll("/", "Y");
|
2023-07-28 15:55:26 +02:00
|
|
|
|
|
|
|
// 20 characters of base62 gives us ~120 bit of entropy which is plenty enough
|
|
|
|
return kindaBase62Hash.substr(0, 20);
|
2023-03-15 22:44:08 +01:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function toBase64(plainText: string | Buffer) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return Buffer.from(plainText).toString("base64");
|
2017-10-14 23:31:44 -04:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function fromBase64(encodedText: string) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return Buffer.from(encodedText, "base64");
|
2017-10-14 23:31:44 -04:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function hmac(secret: any, value: any) {
|
2025-01-09 18:07:02 +02:00
|
|
|
const hmac = crypto.createHmac("sha256", Buffer.from(secret.toString(), "ascii"));
|
2017-10-29 14:55:48 -04:00
|
|
|
hmac.update(value.toString());
|
2025-01-09 18:07:02 +02:00
|
|
|
return hmac.digest("base64");
|
2017-10-29 14:55:48 -04:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function hash(text: string) {
|
2023-11-27 23:23:55 +01:00
|
|
|
text = text.normalize();
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
return crypto.createHash("sha1").update(text).digest("base64");
|
2017-11-21 22:11:27 -05:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function isEmptyOrWhitespace(str: string) {
|
2017-11-26 23:10:23 -05:00
|
|
|
return str === null || str.match(/^ *$/) !== null;
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function sanitizeSqlIdentifier(str: string) {
|
2019-12-24 16:00:31 +01:00
|
|
|
return str.replace(/[^A-Za-z0-9_]/g, "");
|
2019-12-18 22:58:30 +01:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function escapeHtml(str: string) {
|
2018-11-05 12:52:50 +01:00
|
|
|
return escape(str);
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function unescapeHtml(str: string) {
|
2018-02-26 20:47:34 -05:00
|
|
|
return unescape(str);
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function toObject<T, K extends string | number | symbol, V>(array: T[], fn: (item: T) => [K, V]): Record<K, V> {
|
2024-02-18 20:29:23 +02:00
|
|
|
const obj: Record<K, V> = {} as Record<K, V>; // TODO: unsafe?
|
2018-03-04 12:06:35 -05:00
|
|
|
|
|
|
|
for (const item of array) {
|
|
|
|
const ret = fn(item);
|
|
|
|
|
|
|
|
obj[ret[0]] = ret[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function stripTags(text: string) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return text.replace(/<(?:.|\n)*?>/gm, "");
|
2018-05-27 12:26:34 -04:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function union<T extends string | number | symbol>(a: T[], b: T[]): T[] {
|
2024-02-18 20:29:23 +02:00
|
|
|
const obj: Record<T, T> = {} as Record<T, T>; // TODO: unsafe?
|
2018-06-03 20:42:25 -04:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
for (let i = a.length - 1; i >= 0; i--) {
|
2018-06-03 20:42:25 -04:00
|
|
|
obj[a[i]] = a[i];
|
|
|
|
}
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
for (let i = b.length - 1; i >= 0; i--) {
|
2018-06-03 20:42:25 -04:00
|
|
|
obj[b[i]] = b[i];
|
|
|
|
}
|
|
|
|
|
2024-02-16 21:38:09 +02:00
|
|
|
const res: T[] = [];
|
2018-06-03 20:42:25 -04:00
|
|
|
|
|
|
|
for (const k in obj) {
|
2025-01-09 18:07:02 +02:00
|
|
|
if (obj.hasOwnProperty(k)) {
|
|
|
|
// <-- optional
|
2018-06-03 20:42:25 -04:00
|
|
|
res.push(obj[k]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function escapeRegExp(str: string) {
|
2018-11-07 09:35:29 +01:00
|
|
|
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export async function crash() {
|
2025-01-22 18:57:06 +01:00
|
|
|
if (isElectron) {
|
2024-07-18 23:26:21 +03:00
|
|
|
(await import("electron")).app.exit(1);
|
|
|
|
} else {
|
2018-11-18 09:05:50 +01:00
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function sanitizeFilenameForHeader(filename: string) {
|
2019-01-13 10:22:17 +01:00
|
|
|
let sanitizedFilename = sanitize(filename);
|
|
|
|
|
|
|
|
if (sanitizedFilename.trim().length === 0) {
|
|
|
|
sanitizedFilename = "file";
|
|
|
|
}
|
|
|
|
|
2023-03-16 18:34:39 +01:00
|
|
|
return encodeURIComponent(sanitizedFilename);
|
2019-01-13 10:22:17 +01:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function getContentDisposition(filename: string) {
|
2019-01-13 10:22:17 +01:00
|
|
|
const sanitizedFilename = sanitizeFilenameForHeader(filename);
|
|
|
|
|
|
|
|
return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`;
|
|
|
|
}
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
const STRING_MIME_TYPES = new Set(["application/javascript", "application/x-javascript", "application/json", "application/x-sql", "image/svg+xml"]);
|
2019-10-31 21:58:34 +01:00
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function isStringNote(type: string | undefined, mime: string) {
|
2020-07-01 22:42:59 +02:00
|
|
|
// render and book are string note in the sense that they are expected to contain empty string
|
2025-01-09 18:07:02 +02:00
|
|
|
return (type && ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type)) || mime.startsWith("text/") || STRING_MIME_TYPES.has(mime);
|
2019-10-31 21:58:34 +01:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function quoteRegex(url: string) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return url.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
|
2020-05-12 10:28:31 +02:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function replaceAll(string: string, replaceWhat: string, replaceWith: string) {
|
2020-05-12 10:28:31 +02:00
|
|
|
const quotedReplaceWhat = quoteRegex(replaceWhat);
|
|
|
|
|
|
|
|
return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function formatDownloadTitle(fileName: string, type: string | null, mime: string) {
|
2025-01-09 18:07:02 +02:00
|
|
|
const fileNameBase = !fileName ? "untitled" : sanitize(fileName);
|
2020-05-12 10:28:31 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
const getExtension = () => {
|
|
|
|
if (type === "text") return ".html";
|
|
|
|
if (type === "relationMap" || type === "canvas" || type === "search") return ".json";
|
|
|
|
if (!mime) return "";
|
2021-04-24 11:39:59 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
const mimeLc = mime.toLowerCase();
|
2020-05-12 10:28:31 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
// better to just return the current name without a fake extension
|
|
|
|
// it's possible that the title still preserves the correct extension anyways
|
|
|
|
if (mimeLc === "application/octet-stream") return "";
|
2020-05-12 10:28:31 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
// if fileName has an extension matching the mime already - reuse it
|
|
|
|
const mimeTypeFromFileName = mimeTypes.lookup(fileName);
|
|
|
|
if (mimeTypeFromFileName === mimeLc) return "";
|
2020-04-02 22:55:11 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
// as last resort try to get extension from mimeType
|
|
|
|
const extensions = mimeTypes.extension(mime);
|
|
|
|
return extensions ? `.${extensions}` : "";
|
|
|
|
};
|
2020-05-12 10:28:31 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
return `${fileNameBase}${getExtension()}`;
|
2020-04-02 22:55:11 +02:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function removeTextFileExtension(filePath: string) {
|
2020-05-30 16:15:00 -05:00
|
|
|
const extension = path.extname(filePath).toLowerCase();
|
|
|
|
|
2024-10-20 00:17:51 +03:00
|
|
|
switch (extension) {
|
|
|
|
case ".md":
|
|
|
|
case ".markdown":
|
|
|
|
case ".html":
|
|
|
|
case ".htm":
|
2025-01-02 12:24:57 +01:00
|
|
|
return filePath.substring(0, filePath.length - extension.length);
|
2024-10-20 00:17:51 +03:00
|
|
|
default:
|
|
|
|
return filePath;
|
2020-05-30 16:15:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: { title?: string }) {
|
2024-04-03 22:46:14 +03:00
|
|
|
if (noteMeta?.title) {
|
2020-05-30 16:15:00 -05:00
|
|
|
return noteMeta.title;
|
|
|
|
} else {
|
|
|
|
const basename = path.basename(removeTextFileExtension(filePath));
|
2022-07-06 23:09:16 +02:00
|
|
|
if (replaceUnderscoresWithSpaces) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return basename.replace(/_/g, " ").trim();
|
2020-05-30 16:15:00 -05:00
|
|
|
}
|
|
|
|
return basename;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string): Promise<T> {
|
2025-01-09 18:07:02 +02:00
|
|
|
if (!promise || !promise.then) {
|
|
|
|
// it's not actually a promise
|
2021-02-20 23:17:29 +01:00
|
|
|
return promise;
|
|
|
|
}
|
|
|
|
|
2020-07-28 23:29:12 +02:00
|
|
|
// better stack trace if created outside of promise
|
2021-02-20 23:17:29 +01:00
|
|
|
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
|
2020-07-28 23:29:12 +02:00
|
|
|
|
2020-06-13 10:23:36 +02:00
|
|
|
return new Promise((res, rej) => {
|
|
|
|
let resolved = false;
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
promise
|
|
|
|
.then((result) => {
|
|
|
|
resolved = true;
|
2020-06-13 10:23:36 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
res(result);
|
|
|
|
})
|
|
|
|
.catch((error) => rej(error));
|
2020-06-13 10:23:36 +02:00
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!resolved) {
|
2020-07-28 23:29:12 +02:00
|
|
|
rej(error);
|
2020-06-13 10:23:36 +02:00
|
|
|
}
|
|
|
|
}, limitMs);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-02-16 21:38:09 +02:00
|
|
|
interface DeferredPromise<T> extends Promise<T> {
|
2025-01-09 18:07:02 +02:00
|
|
|
resolve: (value: T | PromiseLike<T>) => void;
|
|
|
|
reject: (reason?: any) => void;
|
2024-02-16 21:38:09 +02:00
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function deferred<T>(): DeferredPromise<T> {
|
2020-06-20 21:42:41 +02:00
|
|
|
return (() => {
|
2024-02-16 21:38:09 +02:00
|
|
|
let resolve!: (value: T | PromiseLike<T>) => void;
|
2024-12-22 15:42:15 +02:00
|
|
|
let reject!: (reason?: any) => void;
|
2020-06-20 21:42:41 +02:00
|
|
|
|
2024-02-16 21:38:09 +02:00
|
|
|
let promise = new Promise<T>((res, rej) => {
|
2020-06-20 21:42:41 +02:00
|
|
|
resolve = res;
|
|
|
|
reject = rej;
|
2024-02-16 21:38:09 +02:00
|
|
|
}) as DeferredPromise<T>;
|
2020-06-20 21:42:41 +02:00
|
|
|
|
|
|
|
promise.resolve = resolve;
|
|
|
|
promise.reject = reject;
|
2024-02-16 21:38:09 +02:00
|
|
|
return promise as DeferredPromise<T>;
|
2020-06-20 21:42:41 +02:00
|
|
|
})();
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function removeDiacritic(str: string) {
|
2022-12-30 21:00:42 +01:00
|
|
|
if (!str) {
|
|
|
|
return "";
|
|
|
|
}
|
2023-08-25 13:01:53 +08:00
|
|
|
str = str.toString();
|
2021-09-27 22:09:55 +02:00
|
|
|
return str.normalize("NFD").replace(/\p{Diacritic}/gu, "");
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function normalize(str: string) {
|
2021-09-27 22:09:55 +02:00
|
|
|
return removeDiacritic(str).toLowerCase();
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function toMap<T extends Record<string, any>>(list: T[], key: keyof T): Record<string, T> {
|
2024-02-16 21:38:09 +02:00
|
|
|
const map: Record<string, T> = {};
|
2023-09-08 21:53:57 +02:00
|
|
|
|
|
|
|
for (const el of list) {
|
|
|
|
map[el[key]] = el;
|
|
|
|
}
|
|
|
|
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
|
2025-01-02 12:24:57 +01:00
|
|
|
export function isString(x: any) {
|
2023-11-26 23:51:04 +01:00
|
|
|
return Object.prototype.toString.call(x) === "[object String]";
|
|
|
|
}
|
|
|
|
|
2025-01-23 20:18:05 +01:00
|
|
|
// try to turn 'true' and 'false' strings from process.env variables into boolean values or undefined
|
|
|
|
export function envToBoolean(val: string | undefined) {
|
|
|
|
if (val === undefined || typeof val !== "string") return undefined;
|
|
|
|
|
|
|
|
const valLc = val.toLowerCase().trim();
|
|
|
|
|
|
|
|
if (valLc === "true") return true;
|
|
|
|
if (valLc === "false") return false;
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2024-11-02 11:49:33 +02:00
|
|
|
/**
|
|
|
|
* Returns the directory for resources. On Electron builds this corresponds to the `resources` subdirectory inside the distributable package.
|
|
|
|
* On development builds, this simply refers to the root directory of the application.
|
2024-12-22 15:42:15 +02:00
|
|
|
*
|
2024-11-02 11:49:33 +02:00
|
|
|
* @returns the resource dir.
|
|
|
|
*/
|
|
|
|
export function getResourceDir() {
|
2025-01-22 18:57:06 +01:00
|
|
|
if (isElectron && !env.isDev()) {
|
2024-11-02 11:49:33 +02:00
|
|
|
return process.resourcesPath;
|
|
|
|
} else {
|
|
|
|
return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
2024-12-22 15:42:15 +02:00
|
|
|
}
|
2024-11-02 11:49:33 +02:00
|
|
|
}
|
|
|
|
|
2024-07-18 21:47:30 +03:00
|
|
|
export default {
|
2017-10-28 19:55:55 -04:00
|
|
|
randomSecureToken,
|
2017-10-28 12:12:20 -04:00
|
|
|
randomString,
|
2018-11-05 12:52:50 +01:00
|
|
|
md5,
|
2018-04-02 20:30:00 -04:00
|
|
|
newEntityId,
|
2017-10-14 23:31:44 -04:00
|
|
|
toBase64,
|
2017-10-29 14:55:48 -04:00
|
|
|
fromBase64,
|
2017-11-05 10:41:54 -05:00
|
|
|
hmac,
|
2017-11-05 21:56:42 -05:00
|
|
|
isElectron,
|
2017-11-26 23:10:23 -05:00
|
|
|
hash,
|
2017-12-16 00:05:37 -05:00
|
|
|
isEmptyOrWhitespace,
|
2019-12-18 22:58:30 +01:00
|
|
|
sanitizeSqlIdentifier,
|
2018-11-05 12:52:50 +01:00
|
|
|
escapeHtml,
|
2018-03-04 12:06:35 -05:00
|
|
|
unescapeHtml,
|
2018-05-27 12:26:34 -04:00
|
|
|
toObject,
|
2018-06-03 20:42:25 -04:00
|
|
|
stripTags,
|
2018-11-07 09:35:29 +01:00
|
|
|
union,
|
2018-11-18 09:05:50 +01:00
|
|
|
escapeRegExp,
|
2019-01-13 10:22:17 +01:00
|
|
|
crash,
|
2019-10-31 21:58:34 +01:00
|
|
|
getContentDisposition,
|
2020-04-02 22:55:11 +02:00
|
|
|
isStringNote,
|
2020-05-12 10:28:31 +02:00
|
|
|
quoteRegex,
|
|
|
|
replaceAll,
|
2020-05-30 16:15:00 -05:00
|
|
|
getNoteTitle,
|
|
|
|
removeTextFileExtension,
|
2020-06-13 10:23:36 +02:00
|
|
|
formatDownloadTitle,
|
2020-06-20 21:42:41 +02:00
|
|
|
timeLimit,
|
2021-09-27 22:09:55 +02:00
|
|
|
deferred,
|
|
|
|
removeDiacritic,
|
2021-10-27 22:13:54 +02:00
|
|
|
normalize,
|
2023-03-15 22:44:08 +01:00
|
|
|
hashedBlobId,
|
2023-09-08 21:53:57 +02:00
|
|
|
toMap,
|
2024-11-05 02:58:21 +02:00
|
|
|
isString,
|
2025-01-02 18:45:41 +01:00
|
|
|
getResourceDir,
|
|
|
|
isMac,
|
2025-01-23 20:18:05 +01:00
|
|
|
isWindows,
|
|
|
|
envToBoolean
|
2020-05-12 10:28:31 +02:00
|
|
|
};
|