Notes/src/services/utils.js

363 lines
8.1 KiB
JavaScript
Raw Normal View History

2017-10-21 21:10:33 -04:00
"use strict";
const crypto = require('crypto');
const randtoken = require('rand-token').generator({source: 'crypto'});
const unescape = require('unescape');
const escape = require('escape-html');
const sanitize = require("sanitize-filename");
const mimeTypes = require('mime-types');
const path = require('path');
2020-08-28 22:52:57 +02:00
const log = require('./log');
2018-04-02 20:30:00 -04:00
function newEntityId() {
2018-02-11 00:18:59 -05:00
return randomString(12);
}
function randomString(length) {
return randtoken.generate(length);
}
function randomSecureToken(bytes = 32) {
2017-10-29 11:22:41 -04:00
return crypto.randomBytes(bytes).toString('base64');
}
function md5(content) {
return crypto.createHash('md5').update(content).digest('hex');
}
2023-03-15 22:44:08 +01:00
function hashedBlobId(content) {
// sha512 is faster than sha256
const base64Hash = crypto.createHash('sha512').update(content).digest('base64');
// 20 characters of base64 gives us 120 bit of entropy which is plenty enough
return base64Hash.substr(0, 20);
}
function randomBlobId(content) {
// underscore prefix to easily differentiate the random as opposed to hashed
return '_' + randomString(19);
}
function toBase64(plainText) {
return Buffer.from(plainText).toString('base64');
}
/**
* @returns {Buffer}
*/
function fromBase64(encodedText) {
return Buffer.from(encodedText, 'base64');
}
2017-10-29 14:55:48 -04:00
function hmac(secret, value) {
const hmac = crypto.createHmac('sha256', Buffer.from(secret.toString(), 'ASCII'));
hmac.update(value.toString());
return hmac.digest('base64');
}
function isElectron() {
return !!process.versions['electron'];
}
2017-11-21 22:11:27 -05:00
function hash(text) {
return crypto.createHash('sha1').update(text).digest('base64');
}
function isEmptyOrWhitespace(str) {
return str === null || str.match(/^ *$/) !== null;
}
function sanitizeSqlIdentifier(str) {
2019-12-24 16:00:31 +01:00
return str.replace(/[^A-Za-z0-9_]/g, "");
}
function prepareSqlForLike(prefix, str, suffix) {
const value = str
.replace(/\\/g, "\\\\")
.replace(/'/g, "''")
.replace(/_/g, "\\_")
.replace(/%/g, "\\%");
return `'${prefix}${value}${suffix}' ESCAPE '\\'`;
}
2020-08-31 23:13:39 +02:00
function stopWatch(what, func, timeLimit = 0) {
2020-08-28 22:52:57 +02:00
const start = Date.now();
2020-06-20 12:31:38 +02:00
const ret = func();
2020-08-28 22:52:57 +02:00
const tookMs = Date.now() - start;
2020-08-31 23:13:39 +02:00
if (tookMs >= timeLimit) {
log.info(`${what} took ${tookMs}ms`);
}
return ret;
}
function escapeHtml(str) {
return escape(str);
}
function unescapeHtml(str) {
return unescape(str);
}
2018-03-04 12:06:35 -05:00
function toObject(array, fn) {
const obj = {};
for (const item of array) {
const ret = fn(item);
obj[ret[0]] = ret[1];
}
return obj;
}
function stripTags(text) {
return text.replace(/<(?:.|\n)*?>/gm, '');
}
function intersection(a, b) {
return a.filter(value => b.indexOf(value) !== -1);
}
function union(a, b) {
const obj = {};
for (let i = a.length-1; i >= 0; i--) {
obj[a[i]] = a[i];
}
for (let i = b.length-1; i >= 0; i--) {
obj[b[i]] = b[i];
}
const res = [];
for (const k in obj) {
if (obj.hasOwnProperty(k)) { // <-- optional
res.push(obj[k]);
}
}
return res;
}
function escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
function crash() {
if (isElectron()) {
require('electron').app.exit(1);
}
else {
process.exit(1);
}
}
function sanitizeFilenameForHeader(filename) {
let sanitizedFilename = sanitize(filename);
if (sanitizedFilename.trim().length === 0) {
sanitizedFilename = "file";
}
return encodeURIComponent(sanitizedFilename)
}
function getContentDisposition(filename) {
const sanitizedFilename = sanitizeFilenameForHeader(filename);
return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`;
}
const STRING_MIME_TYPES = [
"application/javascript",
"application/x-javascript",
2021-01-30 15:59:59 +01:00
"application/json",
"application/x-sql",
"image/svg+xml"
];
function isStringNote(type, mime) {
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
2022-12-06 23:01:42 +01:00
return ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type)
|| mime.startsWith('text/')
|| STRING_MIME_TYPES.includes(mime);
}
function quoteRegex(url) {
return url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
2020-04-02 22:55:11 +02:00
function replaceAll(string, replaceWhat, replaceWith) {
const quotedReplaceWhat = quoteRegex(replaceWhat);
return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
}
function formatDownloadTitle(filename, type, mime) {
if (!filename) {
filename = "untitled";
}
2021-04-24 11:39:59 +02:00
filename = sanitize(filename);
if (type === 'text') {
return `${filename}.html`;
2022-12-06 23:01:42 +01:00
} else if (['relationMap', 'canvas', 'search'].includes(type)) {
return `${filename}.json`;
} else {
if (!mime) {
return filename;
}
mime = mime.toLowerCase();
const filenameLc = filename.toLowerCase();
const extensions = mimeTypes.extensions[mime];
if (!extensions || extensions.length === 0) {
return filename;
}
2020-04-02 22:55:11 +02:00
for (const ext of extensions) {
if (filenameLc.endsWith(`.${ext}`)) {
return filename;
}
}
if (mime === 'application/octet-stream') {
// we didn't find any good guess for this one, it will be better to just return
// the current name without fake extension. It's possible that the title still preserves to correct
// extension too
return filename;
}
return `${filename}.${extensions[0]}`;
}
2020-04-02 22:55:11 +02:00
}
function removeTextFileExtension(filePath) {
const extension = path.extname(filePath).toLowerCase();
if (extension === '.md' || extension === '.markdown' || extension === '.html') {
return filePath.substr(0, filePath.length - extension.length);
}
else {
return filePath;
}
}
function getNoteTitle(filePath, replaceUnderscoresWithSpaces, noteMeta) {
if (noteMeta) {
return noteMeta.title;
} else {
const basename = path.basename(removeTextFileExtension(filePath));
if (replaceUnderscoresWithSpaces) {
return basename.replace(/_/g, ' ').trim();
}
return basename;
}
}
function timeLimit(promise, limitMs, errorMessage) {
if (!promise || !promise.then) { // it's not actually a promise
return promise;
}
2020-07-28 23:29:12 +02:00
// better stack trace if created outside of promise
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
2020-07-28 23:29:12 +02:00
return new Promise((res, rej) => {
let resolved = false;
2020-06-13 22:34:15 +02:00
promise.then(result => {
resolved = true;
2020-06-13 22:34:15 +02:00
res(result);
2020-07-01 21:33:52 +02:00
})
.catch(error => rej(error));
setTimeout(() => {
if (!resolved) {
2020-07-28 23:29:12 +02:00
rej(error);
}
}, limitMs);
});
}
2020-06-20 21:42:41 +02:00
function deferred() {
return (() => {
let resolve, reject;
let promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
promise.resolve = resolve;
promise.reject = reject;
return promise;
})();
}
function removeDiacritic(str) {
2022-12-30 21:00:42 +01:00
if (!str) {
return "";
}
return str.normalize("NFD").replace(/\p{Diacritic}/gu, "");
}
function normalize(str) {
return removeDiacritic(str).toLowerCase();
}
function filterAttributeName(name) {
return name.replace(/[^\p{L}\p{N}_:]/ug, "");
}
module.exports = {
randomSecureToken,
2017-10-28 12:12:20 -04:00
randomString,
md5,
2018-04-02 20:30:00 -04:00
newEntityId,
toBase64,
2017-10-29 14:55:48 -04:00
fromBase64,
hmac,
2017-11-05 21:56:42 -05:00
isElectron,
hash,
2017-12-16 00:05:37 -05:00
isEmptyOrWhitespace,
sanitizeSqlIdentifier,
prepareSqlForLike,
stopWatch,
escapeHtml,
2018-03-04 12:06:35 -05:00
unescapeHtml,
toObject,
stripTags,
intersection,
union,
escapeRegExp,
crash,
sanitizeFilenameForHeader,
getContentDisposition,
2020-04-02 22:55:11 +02:00
isStringNote,
quoteRegex,
replaceAll,
getNoteTitle,
removeTextFileExtension,
formatDownloadTitle,
2020-06-20 21:42:41 +02:00
timeLimit,
deferred,
removeDiacritic,
normalize,
2023-03-15 22:44:08 +01:00
filterAttributeName,
hashedBlobId,
randomBlobId
};