Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson
2026-02-18 02:35:38 +01:00
parent 06ef86b59e
commit 3d17d75b87
90 changed files with 3182 additions and 2184 deletions
+3
View File
@@ -24,6 +24,9 @@ config.json
public/build
public/views/build
# ignore TypeScript build
dist/
public/uploads/*
!public/uploads/.gitkeep
+50 -49
View File
@@ -1,45 +1,46 @@
'use strict'
// app
// external modules
const express = require('express')
import express = require('express')
const ejs = require('ejs')
const passport = require('passport')
const methodOverride = require('method-override')
const cookieParser = require('cookie-parser')
const compression = require('compression')
const session = require('express-session')
import ejs = require('ejs')
import passport = require('passport')
import methodOverride = require('method-override')
import cookieParser = require('cookie-parser')
import compression = require('compression')
import session = require('express-session')
const SequelizeStore = require('connect-session-sequelize')(session.Store)
const fs = require('fs')
const path = require('path')
const { Server } = require('socket.io')
import * as http from 'http'
import * as https from 'https'
import fs = require('fs')
import path = require('path')
import { Server } from 'socket.io'
const morgan = require('morgan')
import morgan = require('morgan')
const passportSocketIo = require('passport.socketio')
const helmet = require('helmet')
const i18n = require('i18n')
const flash = require('connect-flash')
import helmet = require('helmet')
import i18n = require('i18n')
import flash = require('connect-flash')
const apiMetrics = require('prometheus-api-metrics')
// core
const config = require('./lib/config')
const logger = require('./lib/logger')
const errors = require('./lib/errors')
const models = require('./lib/models')
const csp = require('./lib/csp')
const metrics = require('./lib/prometheus')
const { useUnless } = require('./lib/utils')
import config = require('./lib/config')
import logger = require('./lib/logger')
import * as errors from './lib/errors'
import models = require('./lib/models')
import * as csp from './lib/csp'
import * as metrics from './lib/prometheus'
import { useUnless } from './lib/utils'
const supportedLocalesList = Object.keys(require('./locales/_supported.json'))
const supportedLocalesList: string[] = Object.keys(require('../locales/_supported.json'))
// server setup
const app = express()
let server = null
let server: http.Server | https.Server = null as unknown as http.Server | https.Server
if (config.useSSL) {
const ca = (function () {
let i, len
const results = []
for (i = 0, len = config.sslCAPath.length; i < len; i++) {
const ca: string[] = (function () {
const results: string[] = []
for (let i = 0, len = config.sslCAPath.length; i < len; i++) {
results.push(fs.readFileSync(config.sslCAPath[i], 'utf8'))
}
return results
@@ -52,9 +53,9 @@ if (config.useSSL) {
requestCert: false,
rejectUnauthorized: false
}
server = require('https').createServer(options, app)
server = https.createServer(options, app)
} else {
server = require('http').createServer(app)
server = http.createServer(app)
}
// if we manage to provide HTTPS domains, but don't provide TLS ourselves
@@ -65,14 +66,14 @@ if (!config.useSSL && config.protocolUseSSL) {
}
// check if the request is from container healthcheck
function isContainerHealthCheck (req, _) {
function isContainerHealthCheck (req: express.Request, _res: express.Response): boolean {
return req.headers['user-agent'] === 'hedgedoc-container-healthcheck/1.0' && req.ip === '127.0.0.1'
}
// logger
app.use(morgan('combined', {
skip: isContainerHealthCheck,
stream: logger.stream
stream: logger.stream as any
}))
// Register prometheus metrics endpoint
@@ -94,7 +95,7 @@ const io = new Server(server, {
})
// others
const realtime = require('./lib/realtime.js')
const realtime = require('./lib/realtime')
// assign socket io to realtime
realtime.io = io
@@ -147,7 +148,7 @@ i18n.configure({
locales: supportedLocalesList,
cookie: 'locale',
indent: ' ', // this is the style poeditor.com exports it, this creates less churn
directory: path.join(__dirname, '/locales'),
directory: path.join(__dirname, '..', 'locales'),
updateFiles: config.updateI18nFiles
})
@@ -164,7 +165,7 @@ app.use('/uploads', (req, res, next) => {
})
// static files
app.use('/', express.static(path.join(__dirname, '/public'), {
app.use('/', express.static(path.join(__dirname, '..', 'public'), {
maxAge: config.staticCacheTime,
index: false,
redirect: false
@@ -197,12 +198,12 @@ app.use(useUnless(['/status', '/metrics', '/_health'], session({
})))
// session resumption
const tlsSessionStore = {}
server.on('newSession', function (id, data, cb) {
const tlsSessionStore: Record<string, any> = {}
server.on('newSession', function (id: Buffer, data: Buffer, cb: () => void) {
tlsSessionStore[id.toString('hex')] = data
cb()
})
server.on('resumeSession', function (id, cb) {
server.on('resumeSession', function (id: Buffer, cb: (err: Error | null, data: Buffer | null) => void) {
cb(null, tlsSessionStore[id.toString('hex')] || null)
})
@@ -287,8 +288,8 @@ io.sockets.on('connection', realtime.connection)
// listen
function startListen () {
let address
const listenCallback = function () {
let address: string
const listenCallback = function (): void {
const schema = config.useSSL ? 'HTTPS' : 'HTTP'
logger.info('%s Server listening at %s', schema, address)
realtime.maintenance = false
@@ -304,16 +305,16 @@ function startListen () {
}
}
const maxDBTries = 30
let currentDBTry = 1
function syncAndListen () {
const maxDBTries: number = 30
let currentDBTry: number = 1
function syncAndListen (): void {
// sync db then start listen
models.sequelize.authenticate().then(function () {
models.runMigrations().then(() => {
sessionStore.sync()
// check if realtime is ready
if (realtime.isReady()) {
models.Revision.checkAllNotesRevision(function (err, notes) {
models.Revision.checkAllNotesRevision(function (err: any, notes: any[]) {
if (err) throw new Error(err)
if (!notes || notes.length <= 0) return startListen()
})
@@ -322,7 +323,7 @@ function syncAndListen () {
process.exit(1)
}
})
}).catch((dbError) => {
}).catch((dbError: Error) => {
if (currentDBTry < maxDBTries) {
logger.warn(`Database cannot be reached. Try ${currentDBTry} of ${maxDBTries}. (${dbError})`)
currentDBTry++
@@ -345,9 +346,9 @@ process.on('uncaughtException', function (err) {
process.exit(1)
})
let alreadyHandlingTermSignals = false
let alreadyHandlingTermSignals: boolean = false
// install exit handler
function handleTermSignals () {
function handleTermSignals (): void {
if (alreadyHandlingTermSignals) {
logger.info('Forcefully exiting.')
process.exit(1)
@@ -374,11 +375,11 @@ function handleTermSignals () {
}
})
}
const maxCleanTries = 30
let currentCleanTry = 1
const maxCleanTries: number = 30
let currentCleanTry: number = 1
const checkCleanTimer = setInterval(function () {
if (realtime.isReady()) {
models.Revision.checkAllNotesRevision(function (err, notes) {
models.Revision.checkAllNotesRevision(function (err: any, notes: any[]) {
if (err) {
logger.error('Error while saving note revisions: ' + err)
if (currentCleanTry <= maxCleanTries) {
@@ -1,19 +0,0 @@
module.exports = {
buildDomainOriginWithProtocol: function (config, baseProtocol) {
const isStandardHTTPsPort = config.protocolUseSSL && config.port === 443
const isStandardHTTPPort = !config.protocolUseSSL && config.port === 80
if (!config.domain) {
return ''
}
let origin = ''
const protocol = baseProtocol + (config.protocolUseSSL ? 's' : '') + '://'
origin = protocol + config.domain
if (config.urlAddPort) {
if (!isStandardHTTPPort || !isStandardHTTPsPort) {
origin += ':' + config.port
}
}
return origin
}
}
@@ -0,0 +1,25 @@
interface DomainConfig {
protocolUseSSL: boolean
port: number
domain: string
urlAddPort: boolean
}
export function buildDomainOriginWithProtocol (config: DomainConfig, baseProtocol: string): string {
const isStandardHTTPsPort = config.protocolUseSSL && config.port === 443
const isStandardHTTPPort = !config.protocolUseSSL && config.port === 80
if (!config.domain) {
return ''
}
const protocol = baseProtocol + (config.protocolUseSSL ? 's' : '') + '://'
let origin = protocol + config.domain
if (config.urlAddPort) {
// BUG FIX: was `!isStandardHTTPPort || !isStandardHTTPsPort` which is always true
// (one of the two is always false). Should be AND to skip port for either standard port.
if (!isStandardHTTPPort && !isStandardHTTPsPort) {
origin += ':' + config.port
}
}
return origin
}
+65 -65
View File
@@ -1,8 +1,8 @@
'use strict'
const os = require('os')
import * as os from 'os'
module.exports = {
const defaultConfig = {
domain: '',
urlPath: '',
host: '0.0.0.0',
@@ -24,8 +24,8 @@ module.exports = {
addDefaults: true,
addDisqus: false,
addGoogleAnalytics: false,
upgradeInsecureRequests: 'auto',
reportURI: undefined,
upgradeInsecureRequests: 'auto' as string | boolean,
reportURI: undefined as string | undefined,
allowFraming: true,
allowPDFEmbed: true
},
@@ -40,16 +40,14 @@ module.exports = {
disableNoteCreation: false,
forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views'],
defaultPermission: 'editable',
enableUploads: undefined, // 'all', 'registered', 'none' are valid options.
enableUploads: undefined as string | undefined, // 'all', 'registered', 'none' are valid options.
// This is undefined by default and set during runtime based on allowAnonymous and allowAnonymousEdits for backwards-compatibility unless explicitly set.
dbURL: '',
db: {},
db: {} as Record<string, any>,
// ssl path
sslKeyPath: '',
sslCertPath: '',
sslCAPath: '',
sslCAPath: '' as string | string[],
dhParamPath: '',
// other path
viewPath: './public/views',
@@ -76,103 +74,103 @@ module.exports = {
url: 'https://framapic.org/'
},
imgur: {
clientID: undefined
clientID: undefined as string | undefined
},
s3: {
accessKeyId: undefined,
secretAccessKey: undefined,
region: undefined
accessKeyId: undefined as string | undefined,
secretAccessKey: undefined as string | undefined,
region: undefined as string | undefined
},
s3bucket: undefined,
s3bucket: undefined as string | undefined,
s3folder: 'uploads',
s3publicFiles: false,
s3publicFiles: false as boolean | string,
minio: {
accessKey: undefined,
secretKey: undefined,
endPoint: undefined,
accessKey: undefined as string | undefined,
secretKey: undefined as string | undefined,
endPoint: undefined as string | undefined,
secure: true,
port: 9000
},
azure: {
connectionString: undefined,
container: undefined
connectionString: undefined as string | undefined,
container: undefined as string | undefined
},
// authentication
oauth2: {
providerName: undefined,
authorizationURL: undefined,
tokenURL: undefined,
clientID: undefined,
clientSecret: undefined,
scope: undefined,
providerName: undefined as string | undefined,
authorizationURL: undefined as string | undefined,
tokenURL: undefined as string | undefined,
clientID: undefined as string | undefined,
clientSecret: undefined as string | undefined,
scope: undefined as string | undefined,
pkce: false
},
facebook: {
clientID: undefined,
clientSecret: undefined
clientID: undefined as string | undefined,
clientSecret: undefined as string | undefined
},
twitter: {
consumerKey: undefined,
consumerSecret: undefined
consumerKey: undefined as string | undefined,
consumerSecret: undefined as string | undefined
},
github: {
clientID: undefined,
clientSecret: undefined
clientID: undefined as string | undefined,
clientSecret: undefined as string | undefined
},
gitlab: {
baseURL: undefined,
clientID: undefined,
clientSecret: undefined,
scope: undefined,
baseURL: undefined as string | undefined,
clientID: undefined as string | undefined,
clientSecret: undefined as string | undefined,
scope: undefined as string | undefined,
version: 'v4'
},
mattermost: {
baseURL: undefined,
clientID: undefined,
clientSecret: undefined
baseURL: undefined as string | undefined,
clientID: undefined as string | undefined,
clientSecret: undefined as string | undefined
},
dropbox: {
clientID: undefined,
clientSecret: undefined,
appKey: undefined
clientID: undefined as string | undefined,
clientSecret: undefined as string | undefined,
appKey: undefined as string | undefined
},
google: {
clientID: undefined,
clientSecret: undefined,
hostedDomain: undefined
clientID: undefined as string | undefined,
clientSecret: undefined as string | undefined,
hostedDomain: undefined as string | undefined
},
ldap: {
providerName: undefined,
url: undefined,
bindDn: undefined,
bindCredentials: undefined,
searchBase: undefined,
searchFilter: undefined,
searchAttributes: undefined,
usernameField: undefined,
useridField: undefined,
tlsca: undefined
providerName: undefined as string | undefined,
url: undefined as string | undefined,
bindDn: undefined as string | undefined,
bindCredentials: undefined as string | undefined,
searchBase: undefined as string | undefined,
searchFilter: undefined as string | undefined,
searchAttributes: undefined as string | undefined,
usernameField: undefined as string | undefined,
useridField: undefined as string | undefined,
tlsca: undefined as string | undefined
},
saml: {
providerName: undefined,
idpSsoUrl: undefined,
idpCert: undefined,
clientCert: undefined,
issuer: undefined,
providerName: undefined as string | undefined,
idpSsoUrl: undefined as string | undefined,
idpCert: undefined as string | undefined,
clientCert: undefined as string | undefined,
issuer: undefined as string | undefined,
identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
disableRequestedAuthnContext: false,
groupAttribute: undefined,
externalGroups: [],
requiredGroups: [],
groupAttribute: undefined as string | undefined,
externalGroups: [] as string[],
requiredGroups: [] as string[],
attribute: {
id: undefined,
id: undefined as string | undefined,
username: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
},
wantAssertionsSigned: true,
wantAuthnResponseSigned: true
},
email: true,
email: true as boolean,
allowEmailRegister: true,
allowGravatar: true,
openID: false,
@@ -192,3 +190,5 @@ module.exports = {
linkifyHeaderStyle: 'keep-case',
enableStatsApi: true
}
export = defaultConfig
@@ -1,17 +1,19 @@
'use strict'
const fs = require('fs')
import * as fs from 'fs'
function getFile (path) {
if (fs.existsSync(path)) {
return path
function getFile (filePath: string): string | undefined {
if (fs.existsSync(filePath)) {
return filePath
}
return undefined
}
module.exports = {
const defaultSSL = {
sslKeyPath: getFile('/run/secrets/key.pem'),
sslCertPath: getFile('/run/secrets/cert.pem'),
sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [],
sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')!] : [] as string[],
dhParamPath: getFile('/run/secrets/dhparam.pem')
}
export = defaultSSL
-65
View File
@@ -1,65 +0,0 @@
'use strict'
const fs = require('fs')
const path = require('path')
const basePath = path.resolve('/run/secrets/')
function getSecret (secret) {
const filePath = path.join(basePath, secret)
if (fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf-8')
return undefined
}
if (fs.existsSync(basePath)) {
module.exports = {
dbURL: getSecret('dbURL'),
sessionSecret: getSecret('sessionsecret'),
sslKeyPath: getSecret('sslkeypath'),
sslCertPath: getSecret('sslcertpath'),
sslCAPath: getSecret('sslcapath'),
dhParamPath: getSecret('dhparampath'),
s3: {
accessKeyId: getSecret('s3_acccessKeyId'),
secretAccessKey: getSecret('s3_secretAccessKey')
},
azure: {
connectionString: getSecret('azure_connectionString')
},
facebook: {
clientID: getSecret('facebook_clientID'),
clientSecret: getSecret('facebook_clientSecret')
},
twitter: {
consumerKey: getSecret('twitter_consumerKey'),
consumerSecret: getSecret('twitter_consumerSecret')
},
github: {
clientID: getSecret('github_clientID'),
clientSecret: getSecret('github_clientSecret')
},
gitlab: {
clientID: getSecret('gitlab_clientID'),
clientSecret: getSecret('gitlab_clientSecret')
},
mattermost: {
clientID: getSecret('mattermost_clientID'),
clientSecret: getSecret('mattermost_clientSecret')
},
dropbox: {
clientID: getSecret('dropbox_clientID'),
clientSecret: getSecret('dropbox_clientSecret'),
appKey: getSecret('dropbox_appKey')
},
google: {
clientID: getSecret('google_clientID'),
clientSecret: getSecret('google_clientSecret'),
hostedDomain: getSecret('google_hostedDomain')
},
imgur: getSecret('imgur_clientid'),
oauth2: {
clientID: getSecret('oauth2_clientID'),
clientSecret: getSecret('oauth2_clientSecret')
}
}
}
+65
View File
@@ -0,0 +1,65 @@
'use strict'
import * as fs from 'fs'
import * as path from 'path'
const basePath: string = path.resolve('/run/secrets/')
function getSecret (secret: string): string | undefined {
const filePath = path.join(basePath, secret)
if (fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf-8')
return undefined
}
const dockerSecrets = fs.existsSync(basePath) ? {
dbURL: getSecret('dbURL'),
sessionSecret: getSecret('sessionsecret'),
sslKeyPath: getSecret('sslkeypath'),
sslCertPath: getSecret('sslcertpath'),
sslCAPath: getSecret('sslcapath'),
dhParamPath: getSecret('dhparampath'),
s3: {
accessKeyId: getSecret('s3_acccessKeyId'),
secretAccessKey: getSecret('s3_secretAccessKey')
},
azure: {
connectionString: getSecret('azure_connectionString')
},
facebook: {
clientID: getSecret('facebook_clientID'),
clientSecret: getSecret('facebook_clientSecret')
},
twitter: {
consumerKey: getSecret('twitter_consumerKey'),
consumerSecret: getSecret('twitter_consumerSecret')
},
github: {
clientID: getSecret('github_clientID'),
clientSecret: getSecret('github_clientSecret')
},
gitlab: {
clientID: getSecret('gitlab_clientID'),
clientSecret: getSecret('gitlab_clientSecret')
},
mattermost: {
clientID: getSecret('mattermost_clientID'),
clientSecret: getSecret('mattermost_clientSecret')
},
dropbox: {
clientID: getSecret('dropbox_clientID'),
clientSecret: getSecret('dropbox_clientSecret'),
appKey: getSecret('dropbox_appKey')
},
google: {
clientID: getSecret('google_clientID'),
clientSecret: getSecret('google_clientSecret'),
hostedDomain: getSecret('google_hostedDomain')
},
imgur: getSecret('imgur_clientid'),
oauth2: {
clientID: getSecret('oauth2_clientID'),
clientSecret: getSecret('oauth2_clientSecret')
}
} : undefined
export = dockerSecrets
+3 -3
View File
@@ -1,12 +1,12 @@
'use strict'
exports.Environment = {
export const Environment = {
development: 'development',
production: 'production',
test: 'test'
}
} as const
exports.Permission = {
export const Permission: Record<string, string> = {
freely: 'freely',
editable: 'editable',
limited: 'limited',
@@ -1,8 +1,8 @@
'use strict'
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
import { toBooleanConfig, toArrayConfig, toIntegerConfig } from './utils'
module.exports = {
const environment = {
sourceURL: process.env.CMD_SOURCE_URL,
domain: process.env.CMD_DOMAIN,
urlPath: process.env.CMD_URL_PATH,
@@ -166,3 +166,5 @@ module.exports = {
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE,
enableStatsApi: toBooleanConfig(process.env.CMD_ENABLE_STATS_API)
}
export = environment
@@ -1,8 +1,8 @@
'use strict'
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
import { toBooleanConfig, toArrayConfig, toIntegerConfig } from './utils'
module.exports = {
const hackmdEnvironment = {
domain: process.env.HMD_DOMAIN,
urlPath: process.env.HMD_URL_PATH,
port: toIntegerConfig(process.env.HMD_PORT),
@@ -122,3 +122,5 @@ module.exports = {
email: toBooleanConfig(process.env.HMD_EMAIL),
allowEmailRegister: toBooleanConfig(process.env.HMD_ALLOW_EMAIL_REGISTER)
}
export = hackmdEnvironment
+24 -22
View File
@@ -1,17 +1,17 @@
'use strict'
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
const { merge } = require('lodash')
const deepFreeze = require('deep-freeze')
const { Environment, Permission } = require('./enum')
const logger = require('../logger')
const { getGitCommit, getGitHubURL } = require('./utils')
const { buildDomainOriginWithProtocol } = require('./buildDomainOriginWithProtocol')
import * as crypto from 'crypto'
import * as fs from 'fs'
import * as path from 'path'
import { merge } from 'lodash'
import deepFreeze = require('deep-freeze')
import { Environment, Permission } from './enum'
import logger from '../logger'
import { getGitCommit, getGitHubURL } from './utils'
import { buildDomainOriginWithProtocol } from './buildDomainOriginWithProtocol'
const appRootPath = path.resolve(__dirname, '../../')
const env = process.env.NODE_ENV || Environment.development
const appRootPath: string = path.resolve(__dirname, '../../../')
const env: string = process.env.NODE_ENV || Environment.development
const debugConfig = {
debug: (env === Environment.development)
}
@@ -30,11 +30,11 @@ const packageConfig = {
sourceURL
}
const configFilePath = path.resolve(appRootPath, process.env.CMD_CONFIG_FILE ||
const configFilePath: string = path.resolve(appRootPath, process.env.CMD_CONFIG_FILE ||
'config.json')
const fileConfig = fs.existsSync(configFilePath) ? require(configFilePath)[env] : undefined
const fileConfig: Record<string, any> | undefined = fs.existsSync(configFilePath) ? require(configFilePath)[env] : undefined
let config = require('./default')
let config: any = require('./default')
merge(config, require('./defaultSSL'))
merge(config, require('./oldDefault'))
merge(config, debugConfig)
@@ -58,8 +58,8 @@ if (!['strict', 'lax', 'none'].includes(config.cookiePolicy)) {
// load LDAP CA
if (config.ldap.tlsca) {
const ca = config.ldap.tlsca.split(',')
const caContent = []
const ca: string[] = config.ldap.tlsca.split(',')
const caContent: string[] = []
for (const i of ca) {
if (fs.existsSync(i)) {
caContent.push(fs.readFileSync(i, 'utf8'))
@@ -101,7 +101,7 @@ if (config.useSSL === true) {
// cache serverURL
config.serverURL = (function () {
let url = buildDomainOriginWithProtocol(config, 'http')
let url: string = buildDomainOriginWithProtocol(config, 'http')
if (config.urlPath) {
url += '/' + config.urlPath
}
@@ -150,7 +150,7 @@ for (let i = keys.length; i--;) {
// we set the new config using the old key.
if (uppercase.test(keys[i]) &&
config[lowercaseKey] !== undefined &&
fileConfig[keys[i]] === undefined) {
fileConfig && fileConfig[keys[i]] === undefined) {
logger.warn('config.json contains deprecated lowercase setting for ' + keys[i] + '. Please change your config.json file to replace ' + lowercaseKey + ' with ' + keys[i])
config[keys[i]] = config[lowercaseKey]
}
@@ -198,9 +198,11 @@ switch (config.imageUploadType) {
}
// generate correct path
config.sslCAPath.forEach(function (capath, i, array) {
array[i] = path.resolve(appRootPath, capath)
})
if (Array.isArray(config.sslCAPath)) {
config.sslCAPath.forEach(function (capath: string, i: number, array: string[]) {
array[i] = path.resolve(appRootPath, capath)
})
}
config.sslCertPath = path.resolve(appRootPath, config.sslCertPath)
config.sslKeyPath = path.resolve(appRootPath, config.sslKeyPath)
@@ -214,4 +216,4 @@ config.uploadsPath = path.resolve(appRootPath, config.uploadsPath)
// make config readonly
config = deepFreeze(config)
module.exports = config
export = config
@@ -1,6 +1,6 @@
'use strict'
module.exports = {
const oldDefault: Record<string, any> = {
urlpath: undefined,
urladdport: undefined,
alloworigin: undefined,
@@ -38,3 +38,5 @@ module.exports = {
imageuploadtype: undefined,
allowemailregister: undefined
}
export = oldDefault
@@ -1,10 +1,12 @@
'use strict'
const { toBooleanConfig } = require('./utils')
import { toBooleanConfig } from './utils'
module.exports = {
const oldEnvironment = {
debug: toBooleanConfig(process.env.DEBUG),
dburl: process.env.DATABASE_URL,
urlpath: process.env.URL_PATH,
port: process.env.PORT
}
export = oldEnvironment
+12 -12
View File
@@ -1,35 +1,37 @@
'use strict'
const fs = require('fs')
const path = require('path')
import * as fs from 'fs'
import * as path from 'path'
function isPositiveAnswer (value) {
function isPositiveAnswer (value: string): boolean {
const lowerValue = value.toLowerCase()
return lowerValue === 'yes' || lowerValue === '1' || lowerValue === 'true'
}
exports.toBooleanConfig = function toBooleanConfig (configValue) {
export function toBooleanConfig (configValue: string | undefined): boolean | undefined {
if (configValue && typeof configValue === 'string') {
return (isPositiveAnswer(configValue))
}
return configValue
// there is no HEAD information
// ref does not exist in .git/ref/heads
return undefined
}
exports.toArrayConfig = function toArrayConfig (configValue, separator = ',', fallback) {
export function toArrayConfig (configValue: string | undefined, separator: string = ',', fallback?: string[]): string[] | undefined {
if (configValue && typeof configValue === 'string') {
return (configValue.split(separator).map(arrayItem => arrayItem.trim()))
}
return fallback
}
exports.toIntegerConfig = function toIntegerConfig (configValue) {
export function toIntegerConfig (configValue: string | undefined): number | undefined {
if (configValue && typeof configValue === 'string') {
return parseInt(configValue)
}
return configValue
return undefined
}
exports.getGitCommit = function getGitCommit (repodir) {
export function getGitCommit (repodir: string): string | undefined {
try {
// prefer using git to get the current ref, as poking in .git is very fragile
return require('child_process').execSync('git rev-parse HEAD', {
@@ -39,7 +41,6 @@ exports.getGitCommit = function getGitCommit (repodir) {
} catch (e) {
// there was an error running git, try to parse refs ourselves
if (!fs.existsSync(repodir + '/.git/HEAD')) {
// there is no HEAD information
return undefined
}
let reference = fs.readFileSync(repodir + '/.git/HEAD', 'utf8')
@@ -48,7 +49,6 @@ exports.getGitCommit = function getGitCommit (repodir) {
reference = reference.substr(5).replace('\n', '')
const refPath = path.resolve(repodir + '/.git', reference)
if (!fs.existsSync(refPath)) {
// ref does not exist in .git/ref/heads
return undefined
}
reference = fs.readFileSync(refPath, 'utf8')
@@ -58,7 +58,7 @@ exports.getGitCommit = function getGitCommit (repodir) {
}
}
exports.getGitHubURL = function getGitHubURL (repo, reference) {
export function getGitHubURL (repo: string, reference: string): string {
// if it's not a github reference, we handle handle that anyway
if (!repo.startsWith('https://github.com') && !repo.startsWith('git@github.com')) {
return repo
+56 -57
View File
@@ -1,10 +1,11 @@
const config = require('./config')
const { v4: uuidv4 } = require('uuid')
const { buildDomainOriginWithProtocol } = require('./config/buildDomainOriginWithProtocol')
import type { Request, Response, NextFunction } from 'express'
import config = require('./config')
import { v4 as uuidv4 } from 'uuid'
import { buildDomainOriginWithProtocol } from './config/buildDomainOriginWithProtocol'
const CspStrategy = {}
type CspDirectives = Record<string, any[]>
const defaultDirectives = {
const defaultDirectives: CspDirectives = {
defaultSrc: ['\'none\''],
baseUri: ['\'self\''],
connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws'), 'https://vimeo.com/api/v2/video/'],
@@ -24,35 +25,76 @@ const defaultDirectives = {
mediaSrc: ['*']
}
const disqusDirectives = {
const disqusDirectives: CspDirectives = {
scriptSrc: ['https://disqus.com', 'https://*.disqus.com', 'https://*.disquscdn.com'],
styleSrc: ['https://*.disquscdn.com'],
fontSrc: ['https://*.disquscdn.com']
}
const googleAnalyticsDirectives = {
const googleAnalyticsDirectives: CspDirectives = {
scriptSrc: ['https://www.google-analytics.com']
}
const dropboxDirectives = {
const dropboxDirectives: CspDirectives = {
scriptSrc: ['https://www.dropbox.com', '\'unsafe-inline\'']
}
const disallowFramingDirectives = {
const disallowFramingDirectives: CspDirectives = {
frameAncestors: ['\'self\'']
}
const allowPDFEmbedDirectives = {
const allowPDFEmbedDirectives: CspDirectives = {
objectSrc: ['*'], // Chrome and Firefox treat PDFs as objects
frameSrc: ['*'] // Chrome also checks PDFs against frame-src
}
const configuredGitLabInstanceDirectives = {
const configuredGitLabInstanceDirectives: CspDirectives = {
connectSrc: [config.gitlab.baseURL]
}
CspStrategy.computeDirectives = function () {
const directives = {}
function mergeDirectives (existingDirectives: CspDirectives, newDirectives: CspDirectives): void {
for (const propertyName in newDirectives) {
const newDirective = newDirectives[propertyName]
if (newDirective) {
const existingDirective = existingDirectives[propertyName] || []
existingDirectives[propertyName] = existingDirective.concat(newDirective)
}
}
}
function mergeDirectivesIf (condition: any, existingDirectives: CspDirectives, newDirectives: CspDirectives): void {
if (condition) {
mergeDirectives(existingDirectives, newDirectives)
}
}
function getCspNonce (_req: Request, res: Response): string {
return '\'nonce-' + res.locals.nonce + '\''
}
function addInlineScriptExceptions (directives: CspDirectives): void {
directives.scriptSrc.push(getCspNonce)
// TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html
// Any more clean solution appreciated.
directives.scriptSrc.push('\'sha256-81acLZNZISnyGYZrSuoYhpzwDTTxi7vC1YM4uNxqWaM=\'')
}
function addUpgradeUnsafeRequestsOptionTo (directives: CspDirectives): void {
if (config.csp.upgradeInsecureRequests === 'auto' && (config.useSSL || config.protocolUseSSL)) {
directives.upgradeInsecureRequests = []
} else if (config.csp.upgradeInsecureRequests === true) {
directives.upgradeInsecureRequests = []
}
}
function addReportURI (directives: CspDirectives): void {
if (config.csp.reportURI) {
directives.reportUri = config.csp.reportURI
}
}
export function computeDirectives (): CspDirectives {
const directives: CspDirectives = {}
mergeDirectives(directives, config.csp.directives)
mergeDirectivesIf(config.csp.addDefaults, directives, defaultDirectives)
mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives)
@@ -67,50 +109,7 @@ CspStrategy.computeDirectives = function () {
return directives
}
function mergeDirectives (existingDirectives, newDirectives) {
for (const propertyName in newDirectives) {
const newDirective = newDirectives[propertyName]
if (newDirective) {
const existingDirective = existingDirectives[propertyName] || []
existingDirectives[propertyName] = existingDirective.concat(newDirective)
}
}
}
function mergeDirectivesIf (condition, existingDirectives, newDirectives) {
if (condition) {
mergeDirectives(existingDirectives, newDirectives)
}
}
function addInlineScriptExceptions (directives) {
directives.scriptSrc.push(getCspNonce)
// TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html
// Any more clean solution appreciated.
directives.scriptSrc.push('\'sha256-81acLZNZISnyGYZrSuoYhpzwDTTxi7vC1YM4uNxqWaM=\'')
}
function getCspNonce (req, res) {
return '\'nonce-' + res.locals.nonce + '\''
}
function addUpgradeUnsafeRequestsOptionTo (directives) {
if (config.csp.upgradeInsecureRequests === 'auto' && (config.useSSL || config.protocolUseSSL)) {
directives.upgradeInsecureRequests = []
} else if (config.csp.upgradeInsecureRequests === true) {
directives.upgradeInsecureRequests = []
}
}
function addReportURI (directives) {
if (config.csp.reportURI) {
directives.reportUri = config.csp.reportURI
}
}
CspStrategy.addNonceToLocals = function (req, res, next) {
export function addNonceToLocals (_req: Request, res: Response, next: NextFunction): void {
res.locals.nonce = uuidv4()
next()
}
module.exports = CspStrategy
-48
View File
@@ -1,48 +0,0 @@
const config = require('./config')
module.exports = {
errorForbidden: function (res) {
const { req } = res
if (req.user) {
responseError(res, 403, 'Forbidden', 'oh no.')
} else {
if (!req.session) req.session = {}
if (req.originalUrl !== '/403') {
req.session.returnTo = config.serverURL + (req.originalUrl || '/')
req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
}
res.redirect(config.serverURL + '/')
}
},
errorNotFound: function (res) {
responseError(res, 404, 'Not Found', 'oops.')
},
errorBadRequest: function (res) {
responseError(res, 400, 'Bad Request', 'something not right.')
},
errorConflict: function (res) {
responseError(res, 409, 'Conflict', 'This note already exists.')
},
errorTooLong: function (res) {
responseError(res, 413, 'Payload Too Large', 'Shorten your note!')
},
errorTooManyRequests: function (res) {
responseError(res, 429, 'Too Many Requests', 'Try again later.')
},
errorInternalError: function (res) {
responseError(res, 500, 'Internal Error', 'wtf.')
},
errorServiceUnavailable: function (res) {
res.status(503).send('I\'m busy right now, try again later.')
}
}
function responseError (res, code, detail, msg) {
res.status(code).render('error.ejs', {
title: code + ' ' + detail + ' ' + msg,
code,
detail,
msg,
opengraph: []
})
}
+54
View File
@@ -0,0 +1,54 @@
import type { Request, Response } from 'express'
import config = require('./config')
function responseError (res: Response, code: number, detail: string, msg: string): void {
res.status(code).render('error.ejs', {
title: code + ' ' + detail + ' ' + msg,
code,
detail,
msg,
opengraph: []
})
}
export function errorForbidden (res: Response): void {
const req: Request = res.req
if ((req as any).user) {
responseError(res, 403, 'Forbidden', 'oh no.')
} else {
if (!(req as any).session) (req as any).session = {}
if (req.originalUrl !== '/403') {
(req as any).session.returnTo = config.serverURL + (req.originalUrl || '/')
;(req as any).flash('error', 'You are not allowed to access this page. Maybe try logging in?')
}
res.redirect(config.serverURL + '/')
}
}
export function errorNotFound (res: Response): void {
responseError(res, 404, 'Not Found', 'oops.')
}
export function errorBadRequest (res: Response): void {
responseError(res, 400, 'Bad Request', 'something not right.')
}
export function errorConflict (res: Response): void {
responseError(res, 409, 'Conflict', 'This note already exists.')
}
export function errorTooLong (res: Response): void {
responseError(res, 413, 'Payload Too Large', 'Shorten your note!')
}
export function errorTooManyRequests (res: Response): void {
responseError(res, 429, 'Too Many Requests', 'Try again later.')
}
export function errorInternalError (res: Response): void {
responseError(res, 500, 'Internal Error', 'wtf.')
}
export function errorServiceUnavailable (res: Response): void {
res.status(503).send('I\'m busy right now, try again later.')
}
+60 -51
View File
@@ -1,89 +1,90 @@
'use strict'
import type { Request, Response } from 'express'
// history
// external modules
const LZString = require('lz-string')
import LZString = require('lz-string')
// core
const logger = require('./logger')
const models = require('./models')
const errors = require('./errors')
import logger = require('./logger')
import models = require('./models')
import * as errors from './errors'
// public
const History = {
historyGet,
historyPost,
historyDelete,
updateHistory
interface HistoryItem {
id: string
text?: string
time?: number
tags?: string[]
pinned?: boolean
}
function getHistory (userid, callback) {
function getHistory (userid: string, callback: (err: Error | null, history: Record<string, HistoryItem> | null) => void): void {
models.User.findOne({
where: {
id: userid
}
}).then(function (user) {
}).then(function (user: any) {
if (!user) {
return callback(null, null)
}
let history = {}
let historyObj: Record<string, HistoryItem> = {}
if (user.history) {
history = JSON.parse(user.history)
const historyArr: HistoryItem[] = JSON.parse(user.history)
// migrate LZString encoded note id to base64url encoded note id
for (let i = 0, l = history.length; i < l; i++) {
for (let i = 0, l = historyArr.length; i < l; i++) {
// Calculate minimal string length for an UUID that is encoded
// base64 encoded and optimize comparsion by using -1
// this should make a lot of LZ-String parsing errors obsolete
// as we can assume that a nodeId that is 48 chars or longer is a
// noteID.
const base64UuidLength = ((4 * 36) / 3) - 1
if (!(history[i].id.length > base64UuidLength)) {
if (!(historyArr[i].id.length > base64UuidLength)) {
continue
}
try {
const id = LZString.decompressFromBase64(history[i].id)
const id = LZString.decompressFromBase64(historyArr[i].id)
if (id && models.Note.checkNoteIdValid(id)) {
history[i].id = models.Note.encodeNoteId(id)
historyArr[i].id = models.Note.encodeNoteId(id)
}
} catch (err) {
} catch (err: any) {
// most error here comes from LZString, ignore
if (err.message === 'Cannot read property \'charAt\' of undefined') {
logger.warning('Looks like we can not decode "' + history[i].id + '" with LZString. Can be ignored.')
logger.warn('Looks like we can not decode "' + historyArr[i].id + '" with LZString. Can be ignored.')
} else {
logger.error(err)
}
}
}
history = parseHistoryToObject(history)
historyObj = parseHistoryToObject(historyArr)
}
logger.debug(`read history success: ${user.id}`)
return callback(null, history)
}).catch(function (err) {
return callback(null, historyObj)
}).catch(function (err: Error) {
logger.error('read history failed: ' + err)
return callback(err, null)
})
}
function setHistory (userid, history, callback) {
function setHistory (userid: string, history: Record<string, HistoryItem> | HistoryItem[], callback: (err: Error | null, count: number[] | null) => void): void {
models.User.update({
history: JSON.stringify(parseHistoryToArray(history))
}, {
where: {
id: userid
}
}).then(function (count) {
}).then(function (count: number[]) {
return callback(null, count)
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error('set history failed: ' + err)
return callback(err, null)
})
}
function updateHistory (userid, noteId, document, time) {
function updateHistory (userid: string, noteId: string, document: any, time?: number): void {
if (userid && noteId && typeof document !== 'undefined') {
getHistory(userid, function (err, history) {
if (err || !history) return
if (!history[noteId]) {
history[noteId] = {}
history[noteId] = { id: noteId }
}
const noteHistory = history[noteId]
const noteInfo = models.Note.parseNoteInfo(document)
@@ -91,26 +92,26 @@ function updateHistory (userid, noteId, document, time) {
noteHistory.text = noteInfo.title
noteHistory.time = time || Date.now()
noteHistory.tags = noteInfo.tags
setHistory(userid, history, function (err, count) {
setHistory(userid, history, function (err, _count) {
if (err) {
logger.log(err)
logger.error('set history error:', err)
}
})
})
}
}
function parseHistoryToArray (history) {
const _history = []
function parseHistoryToArray (history: Record<string, HistoryItem> | HistoryItem[]): HistoryItem[] {
const _history: HistoryItem[] = []
Object.keys(history).forEach(function (key) {
const item = history[key]
const item = (history as Record<string, HistoryItem>)[key]
_history.push(item)
})
return _history
}
function parseHistoryToObject (history) {
const _history = {}
function parseHistoryToObject (history: HistoryItem[]): Record<string, HistoryItem> {
const _history: Record<string, HistoryItem> = {}
for (let i = 0, l = history.length; i < l; i++) {
const item = history[i]
_history[item.id] = item
@@ -118,9 +119,9 @@ function parseHistoryToObject (history) {
return _history
}
function historyGet (req, res) {
function historyGet (req: Request, res: Response): void {
if (req.isAuthenticated()) {
getHistory(req.user.id, function (err, history) {
getHistory((req as any).user.id, function (err, history) {
if (err) return errors.errorInternalError(res)
if (!history) return errors.errorNotFound(res)
res.send({
@@ -132,16 +133,16 @@ function historyGet (req, res) {
}
}
function historyPost (req, res) {
function historyPost (req: Request, res: Response): void {
if (req.isAuthenticated()) {
const noteId = req.params.noteId
const noteId: string = req.params.noteId as string
if (!noteId) {
if (typeof req.body.history === 'undefined') return errors.errorBadRequest(res)
logger.debug(`SERVER received history from [${req.user.id}]: ${req.body.history}`)
logger.debug(`SERVER received history from [${(req as any).user.id}]: ${req.body.history}`)
try {
const history = JSON.parse(req.body.history)
if (Array.isArray(history)) {
setHistory(req.user.id, history, function (err, count) {
setHistory((req as any).user.id, history, function (err, _count) {
if (err) return errors.errorInternalError(res)
res.end()
})
@@ -153,13 +154,13 @@ function historyPost (req, res) {
}
} else {
if (typeof req.body.pinned === 'undefined') return errors.errorBadRequest(res)
getHistory(req.user.id, function (err, history) {
getHistory((req as any).user.id, function (err, history) {
if (err) return errors.errorInternalError(res)
if (!history) return errors.errorNotFound(res)
if (!history[noteId]) return errors.errorNotFound(res)
if (req.body.pinned === 'true' || req.body.pinned === 'false') {
history[noteId].pinned = (req.body.pinned === 'true')
setHistory(req.user.id, history, function (err, count) {
setHistory((req as any).user.id, history, function (err, _count) {
if (err) return errors.errorInternalError(res)
res.end()
})
@@ -173,28 +174,28 @@ function historyPost (req, res) {
}
}
function historyDelete (req, res) {
function historyDelete (req: Request, res: Response): void {
if (!req.isAuthenticated()) {
return errors.errorForbidden(res)
}
const token = req.query.token
if (!token || token !== req.user.deleteToken) {
if (!token || token !== (req as any).user.deleteToken) {
return errors.errorForbidden(res)
}
const noteId = req.params.noteId
const noteId: string = req.params.noteId as string
if (!noteId) {
setHistory(req.user.id, [], function (err, count) {
setHistory((req as any).user.id, [], function (err, _count) {
if (err) return errors.errorInternalError(res)
res.end()
})
} else {
getHistory(req.user.id, function (err, history) {
getHistory((req as any).user.id, function (err, history) {
if (err) return errors.errorInternalError(res)
if (!history) return errors.errorNotFound(res)
delete history[noteId]
setHistory(req.user.id, history, function (err, count) {
setHistory((req as any).user.id, history, function (err, _count) {
if (err) return errors.errorInternalError(res)
res.end()
})
@@ -202,4 +203,12 @@ function historyDelete (req, res) {
}
}
module.exports = History
// public
const History = {
historyGet,
historyPost,
historyDelete,
updateHistory
}
export = History
@@ -1,19 +1,21 @@
'use strict'
// external modules
const crypto = require('crypto')
const Chance = require('chance')
const config = require('./config')
import * as crypto from 'crypto'
import Chance = require('chance')
// core
exports.generateAvatar = function (name) {
import config = require('./config')
export function generateAvatar (name: string): string {
// use darker colors for better contrast
const color = new Chance(name).color({
const color: string = new Chance(name).color({
format: 'hex',
max_red: 150,
max_green: 150,
max_blue: 150
})
const letter = name.substring(0, 1).toUpperCase()
const letter: string = name.substring(0, 1).toUpperCase()
let svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
svg += '<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="96" width="96" version="1.1" viewBox="0 0 96 96">'
@@ -28,8 +30,8 @@ exports.generateAvatar = function (name) {
return svg
}
exports.generateAvatarURL = function (name, email = '', big = true) {
let photo
export function generateAvatarURL (name: string, email: string = '', big: boolean = true): string {
let photo: string
if (typeof email !== 'string') {
email = '' + name + '@example.com'
}
@@ -37,7 +39,7 @@ exports.generateAvatarURL = function (name, email = '', big = true) {
const hash = crypto.createHash('md5')
hash.update(email.toLowerCase())
const hexDigest = hash.digest('hex')
const hexDigest: string = hash.digest('hex')
if (email !== '' && config.allowGravatar) {
photo = `https://cdn.libravatar.org/avatar/${hexDigest}?default=identicon`
+5 -4
View File
@@ -1,5 +1,6 @@
'use strict'
const { createLogger, format, transports } = require('winston')
import { createLogger, format, transports } from 'winston'
const logger = createLogger({
level: 'debug',
@@ -18,10 +19,10 @@ const logger = createLogger({
exitOnError: false
})
logger.stream = {
write: function (message, encoding) {
;(logger as any).stream = {
write: function (message: string) {
logger.info(message)
}
}
module.exports = logger
export = logger
@@ -2,7 +2,7 @@
// external modules
const Sequelize = require('sequelize')
module.exports = function (sequelize, DataTypes) {
module.exports = function (sequelize: any, DataTypes: any): any {
const Author = sequelize.define('Author', {
id: {
type: Sequelize.INTEGER,
@@ -21,7 +21,7 @@ module.exports = function (sequelize, DataTypes) {
]
})
Author.associate = function (models) {
Author.associate = function (models: any): void {
Author.belongsTo(models.Note, {
foreignKey: 'noteId',
as: 'note',
+20 -20
View File
@@ -1,24 +1,24 @@
'use strict'
// external modules
const fs = require('fs')
const path = require('path')
import * as fs from 'fs'
import * as path from 'path'
const Sequelize = require('sequelize')
const { cloneDeep } = require('lodash')
const Umzug = require('umzug')
import { cloneDeep } from 'lodash'
import Umzug = require('umzug')
// core
const config = require('../config')
const logger = require('../logger')
const { isSQLite } = require('../utils')
import config = require('../config')
import logger = require('../logger')
import { isSQLite } from '../utils'
const dbconfig = cloneDeep(config.db)
dbconfig.logging = config.debug
? (data) => {
? (data: any) => {
logger.info(data)
}
: false
let sequelize = null
let sequelize: any = null
// Heroku specific
if (config.dbURL) {
@@ -29,7 +29,7 @@ if (config.dbURL) {
// [Postgres] Handling NULL bytes
// https://github.com/sequelize/sequelize/issues/6485
function stripNullByte (value) {
function stripNullByte (value: any): string {
value = '' + value
// eslint-disable-next-line no-control-regex
return value ? value.replace(/\u0000/g, '') : value
@@ -37,7 +37,7 @@ function stripNullByte (value) {
sequelize.stripNullByte = stripNullByte
function processData (data, _default, process) {
function processData (data: any, _default: any, process?: (data: any) => any): any {
if (data === undefined) {
return data
} else {
@@ -47,18 +47,18 @@ function processData (data, _default, process) {
sequelize.processData = processData
const db = {}
const db: any = {}
fs.readdirSync(__dirname)
.filter(function (file) {
return (file.indexOf('.') !== 0) && (file !== 'index.js')
.filter(function (file: string) {
return (file.indexOf('.') !== 0) && (file !== 'index.js') && file.endsWith('.js') && !file.endsWith('.js.map')
})
.forEach(function (file) {
.forEach(function (file: string) {
const model = sequelize.import(path.join(__dirname, file))
db[model.name] = model
})
Object.keys(db).forEach(function (modelName) {
Object.keys(db).forEach(function (modelName: string) {
if ('associate' in db[modelName]) {
db[modelName].associate(db)
}
@@ -66,7 +66,7 @@ Object.keys(db).forEach(function (modelName) {
const umzug = new Umzug({
migrations: {
path: path.resolve(__dirname, '..', 'migrations'),
path: path.resolve(__dirname, '..', '..', '..', 'lib', 'migrations'),
params: [
sequelize.getQueryInterface(),
Sequelize.DataTypes
@@ -74,7 +74,7 @@ const umzug = new Umzug({
},
// Required wrapper function required to prevent winstion issue
// https://github.com/winstonjs/winston/issues/1577
logging: message => {
logging: (message: string) => {
logger.info(message)
},
storage: 'sequelize',
@@ -83,7 +83,7 @@ const umzug = new Umzug({
}
})
db.runMigrations = async function runMigrations () {
db.runMigrations = async function runMigrations (): Promise<void> {
// checks migrations and run them if they are not already applied
// exit in case of unsuccessful migrations
const savepointName = 'migration'
@@ -124,4 +124,4 @@ Exiting…`)
db.sequelize = sequelize
db.Sequelize = Sequelize
module.exports = db
export = db
+76 -76
View File
@@ -1,23 +1,23 @@
'use strict'
// external modules
const fs = require('fs')
const path = require('path')
const LZString = require('lz-string')
const base64url = require('base64url')
import * as fs from 'fs'
import * as path from 'path'
import LZString = require('lz-string')
const base64url: any = require('base64url')
const md = require('markdown-it')()
const metaMarked = require('@hedgedoc/meta-marked')
const cheerio = require('cheerio')
const nanoid = require('nanoid')
const metaMarked: any = require('@hedgedoc/meta-marked')
const cheerio: any = require('cheerio')
import { nanoid } from 'nanoid'
const Sequelize = require('sequelize')
const async = require('async')
const moment = require('moment')
const DiffMatchPatch = require('diff-match-patch')
import async = require('async')
import moment = require('moment')
import DiffMatchPatch = require('diff-match-patch')
const dmp = new DiffMatchPatch()
// core
const config = require('../config')
const logger = require('../logger')
const utils = require('../utils')
import config = require('../config')
import logger = require('../logger')
import { isMySQL } from '../utils'
// ot
const ot = require('../ot')
@@ -25,7 +25,7 @@ const ot = require('../ot')
// permission types
const permissionTypes = ['freely', 'editable', 'limited', 'locked', 'protected', 'private']
module.exports = function (sequelize, DataTypes) {
module.exports = function (sequelize: any, DataTypes: any): any {
const Note = sequelize.define('Note', {
id: {
type: DataTypes.UUID,
@@ -36,7 +36,7 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.STRING,
unique: true,
allowNull: false,
defaultValue: () => nanoid.nanoid(10)
defaultValue: () => nanoid(10)
},
alias: {
type: DataTypes.STRING,
@@ -53,28 +53,28 @@ module.exports = function (sequelize, DataTypes) {
},
title: {
type: DataTypes.TEXT,
get: function () {
get: function (this: any) {
return sequelize.processData(this.getDataValue('title'), '')
},
set: function (value) {
set: function (this: any, value: string) {
this.setDataValue('title', sequelize.stripNullByte(value))
}
},
content: {
type: DataTypes.TEXT('long'),
get: function () {
get: function (this: any) {
return sequelize.processData(this.getDataValue('content'), '')
},
set: function (value) {
set: function (this: any, value: string) {
this.setDataValue('content', sequelize.stripNullByte(value))
}
},
authorship: {
type: DataTypes.TEXT('long'),
get: function () {
get: function (this: any) {
return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse)
},
set: function (value) {
set: function (this: any, value: any) {
this.setDataValue('authorship', JSON.stringify(value))
}
},
@@ -87,12 +87,12 @@ module.exports = function (sequelize, DataTypes) {
}, {
paranoid: false,
hooks: {
beforeCreate: function (note, options) {
beforeCreate: function (note: any, options: any) {
return new Promise(function (resolve, reject) {
// if no content specified then use default note
if (!note.content) {
let body = null
let filePath = null
let body: string | null = null
let filePath: string | null = null
if (note.alias) {
filePath = path.join(config.docsPath, path.basename(note.alias) + '.md')
}
@@ -100,8 +100,8 @@ module.exports = function (sequelize, DataTypes) {
filePath = config.defaultNotePath
}
if (Note.checkFileExist(filePath)) {
const fsCreatedTime = moment(fs.statSync(filePath).ctime)
body = fs.readFileSync(filePath, 'utf8')
const fsCreatedTime = moment(fs.statSync(filePath!).ctime)
body = fs.readFileSync(filePath!, 'utf8')
note.title = Note.parseNoteTitle(body)
note.content = body
if (filePath !== config.defaultNotePath) {
@@ -120,9 +120,9 @@ module.exports = function (sequelize, DataTypes) {
return resolve(note)
})
},
afterCreate: function (note, options, callback) {
afterCreate: function (note: any, options: any) {
return new Promise(function (resolve, reject) {
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
sequelize.models.Revision.saveNoteRevision(note, function (err: any, revision: any) {
if (err) {
return reject(err)
}
@@ -133,7 +133,7 @@ module.exports = function (sequelize, DataTypes) {
}
})
Note.associate = function (models) {
Note.associate = function (models: any): void {
Note.belongsTo(models.User, {
foreignKey: 'ownerId',
as: 'owner',
@@ -156,24 +156,24 @@ module.exports = function (sequelize, DataTypes) {
constraints: false
})
}
Note.checkFileExist = function (filePath) {
Note.checkFileExist = function (filePath: string): boolean {
try {
return fs.statSync(filePath).isFile()
} catch (err) {
return false
}
}
Note.encodeNoteId = function (id) {
Note.encodeNoteId = function (id: string): string {
// remove dashes in UUID and encode in url-safe base64
const str = id.replace(/-/g, '')
const hexStr = Buffer.from(str, 'hex')
return base64url.encode(hexStr)
}
Note.decodeNoteId = function (encodedId) {
Note.decodeNoteId = function (encodedId: string): string {
// decode from url-safe base64
const id = base64url.toBuffer(encodedId).toString('hex')
// add dashes between the UUID string parts
const idParts = []
const idParts: string[] = []
idParts.push(id.substr(0, 8))
idParts.push(id.substr(8, 4))
idParts.push(id.substr(12, 4))
@@ -181,22 +181,22 @@ module.exports = function (sequelize, DataTypes) {
idParts.push(id.substr(20, 12))
return idParts.join('-')
}
Note.checkNoteIdValid = function (id) {
Note.checkNoteIdValid = function (id: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
const result = id.match(uuidRegex)
if (result && result.length === 1) { return true } else { return false }
}
Note.parseNoteId = function (noteId, callback) {
Note.parseNoteId = function (noteId: string, callback: (err: any, id: string | null) => void): void {
async.series({
parseNoteIdByAlias: function (_callback) {
parseNoteIdByAlias: function (_callback: any) {
// try to parse note id by alias (e.g. doc)
Note.findOne({
where: utils.isMySQL(sequelize)
where: isMySQL(sequelize)
? sequelize.where(sequelize.fn('BINARY', sequelize.col('alias')), noteId)
: {
alias: noteId
}
}).then(function (note) {
}).then(function (note: any) {
if (note) {
const filePath = path.join(config.docsPath, path.basename(noteId) + '.md')
if (Note.checkFileExist(filePath)) {
@@ -212,8 +212,8 @@ module.exports = function (sequelize, DataTypes) {
title,
content: body,
lastchangeAt: fsModifiedTime
}).then(function (note) {
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
}).then(function (note: any) {
sequelize.models.Revision.saveNoteRevision(note, function (err: any, revision: any) {
if (err) return _callback(err, null)
// update authorship on after making revision of docs
const patch = dmp.patch_fromText(revision.patch)
@@ -224,13 +224,13 @@ module.exports = function (sequelize, DataTypes) {
}
note.update({
authorship
}).then(function (note) {
}).then(function (note: any) {
return callback(null, note.id)
}).catch(function (err) {
}).catch(function (err: any) {
return _callback(err, null)
})
})
}).catch(function (err) {
}).catch(function (err: any) {
return _callback(err, null)
})
} else {
@@ -246,21 +246,21 @@ module.exports = function (sequelize, DataTypes) {
alias: noteId,
owner: null,
permission: 'locked'
}).then(function (note) {
}).then(function (note: any) {
return callback(null, note.id)
}).catch(function (err) {
}).catch(function (err: any) {
return _callback(err, null)
})
} else {
return _callback(null, null)
}
}
}).catch(function (err) {
}).catch(function (err: any) {
return _callback(err, null)
})
},
// parse note id by LZString is deprecated, here for compability
parseNoteIdByLZString: function (_callback) {
parseNoteIdByLZString: function (_callback: any) {
// Calculate minimal string length for an UUID that is encoded
// base64 encoded and optimize comparsion by using -1
// this should make a lot of LZ-String parsing errors obsolete
@@ -274,7 +274,7 @@ module.exports = function (sequelize, DataTypes) {
try {
const id = LZString.decompressFromBase64(noteId)
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
} catch (err) {
} catch (err: any) {
if (err.message === 'Cannot read property \'charAt\' of undefined') {
logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.')
} else {
@@ -283,7 +283,7 @@ module.exports = function (sequelize, DataTypes) {
return _callback(null, null)
}
},
parseNoteIdByBase64Url: function (_callback) {
parseNoteIdByBase64Url: function (_callback: any) {
// try to parse note id by base64url
try {
const id = Note.decodeNoteId(noteId)
@@ -293,7 +293,7 @@ module.exports = function (sequelize, DataTypes) {
return _callback(null, null)
}
},
parseNoteIdByShortId: function (_callback) {
parseNoteIdByShortId: function (_callback: any) {
// try to parse note id by shortId
try {
// old short ids generated by the `shortid` package could be from 7 to 14 characters long
@@ -302,15 +302,15 @@ module.exports = function (sequelize, DataTypes) {
Note.findOne({
// MariaDB and MySQL do case-insensitive comparison by default (unless a collation charset like utf8mb4 is used)
// The binary conversion ensures, case-sensitive comparison.
where: utils.isMySQL(sequelize)
where: isMySQL(sequelize)
? sequelize.where(sequelize.fn('BINARY', sequelize.col('shortid')), noteId)
: {
shortid: noteId
}
}).then(function (note) {
}).then(function (note: any) {
if (!note) return _callback(null, null)
return callback(null, note.id)
}).catch(function (err) {
}).catch(function (err: any) {
return _callback(err, null)
})
} else {
@@ -320,7 +320,7 @@ module.exports = function (sequelize, DataTypes) {
return _callback(err, null)
}
}
}, function (err, result) {
}, function (err: any, result: any) {
if (err) {
logger.error(err)
return callback(err, null)
@@ -328,7 +328,7 @@ module.exports = function (sequelize, DataTypes) {
return callback(null, null)
})
}
Note.parseNoteInfo = function (body) {
Note.parseNoteInfo = function (body: string): { title: string; tags: string[] } {
const parsed = Note.extractMeta(body)
const $ = cheerio.load(md.render(parsed.markdown))
return {
@@ -336,13 +336,13 @@ module.exports = function (sequelize, DataTypes) {
tags: Note.extractNoteTags(parsed.meta, $)
}
}
Note.parseNoteTitle = function (body) {
Note.parseNoteTitle = function (body: string): string {
const parsed = Note.extractMeta(body)
const $ = cheerio.load(md.render(parsed.markdown))
return Note.extractNoteTitle(parsed.meta, $)
}
Note.extractNoteTitle = function (meta, $) {
let title = ''
Note.extractNoteTitle = function (meta: any, $: any): string {
let title: string | number = ''
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) {
title = meta.title
} else {
@@ -352,21 +352,21 @@ module.exports = function (sequelize, DataTypes) {
}
}
if (!title) title = 'Untitled'
return title
return '' + title
}
Note.generateDescription = function (markdown) {
Note.generateDescription = function (markdown: string): string {
return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ')
}
Note.decodeTitle = function (title) {
Note.decodeTitle = function (title: string): string {
return title || 'Untitled'
}
Note.generateWebTitle = function (title) {
Note.generateWebTitle = function (title: string): string {
title = !title || title === 'Untitled' ? 'HedgeDoc - Collaborative markdown notes' : title + ' - HedgeDoc'
return title
}
Note.extractNoteTags = function (meta, $) {
const tags = []
const rawtags = []
Note.extractNoteTags = function (meta: any, $: any): string[] {
const tags: string[] = []
const rawtags: string[] = []
if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) {
const metaTags = ('' + meta.tags).split(',')
for (let i = 0; i < metaTags.length; i++) {
@@ -375,7 +375,7 @@ module.exports = function (sequelize, DataTypes) {
}
} else {
const h6s = $('h6')
h6s.each(function (key, value) {
h6s.each(function (key: number, value: any) {
if (/^tags/gmi.test($(value).text())) {
const codes = $(value).find('code')
for (let i = 0; i < codes.length; i++) {
@@ -397,8 +397,8 @@ module.exports = function (sequelize, DataTypes) {
}
return tags
}
Note.extractMeta = function (content) {
let obj = null
Note.extractMeta = function (content: string): { markdown: string; meta: any } {
let obj: any = null
try {
obj = metaMarked(content)
if (!obj.markdown) obj.markdown = ''
@@ -411,8 +411,8 @@ module.exports = function (sequelize, DataTypes) {
}
return obj
}
Note.parseMeta = function (meta) {
const _meta = {}
Note.parseMeta = function (meta: any): any {
const _meta: any = {}
if (meta) {
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title }
if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description }
@@ -425,15 +425,15 @@ module.exports = function (sequelize, DataTypes) {
}
return _meta
}
Note.parseOpengraph = function (meta, title) {
let _ogdata = {}
Note.parseOpengraph = function (meta: any, title: string): any {
let _ogdata: any = {}
if (meta.opengraph) { _ogdata = meta.opengraph }
if (!(_ogdata.title && (typeof _ogdata.title === 'string' || typeof _ogdata.title === 'number'))) { _ogdata.title = title }
if (!(_ogdata.description && (typeof _ogdata.description === 'string' || typeof _ogdata.description === 'number'))) { _ogdata.description = meta.description || '' }
if (!(_ogdata.type && (typeof _ogdata.type === 'string'))) { _ogdata.type = 'website' }
return _ogdata
}
Note.updateAuthorshipByOperation = function (operation, userId, authorships) {
Note.updateAuthorshipByOperation = function (operation: any[], userId: string | null, authorships: any[]): any[] {
let index = 0
const timestamp = Date.now()
for (let i = 0; i < operation.length; i++) {
@@ -534,8 +534,8 @@ module.exports = function (sequelize, DataTypes) {
}
return authorships
}
Note.transformPatchToOperations = function (patch, contentLength) {
const operations = []
Note.transformPatchToOperations = function (patch: any[], contentLength: number): any[][] {
const operations: any[][] = []
if (patch.length > 0) {
// calculate original content length
for (let j = patch.length - 1; j >= 0; j--) {
@@ -556,7 +556,7 @@ module.exports = function (sequelize, DataTypes) {
let bias = 0
let lengthBias = 0
for (let j = 0; j < patch.length; j++) {
const operation = []
const operation: any[] = []
const p = patch[j]
let currIndex = p.start1
const currLength = contentLength - bias
@@ -1,26 +1,35 @@
'use strict'
// external modules
const Sequelize = require('sequelize')
const async = require('async')
const moment = require('moment')
const childProcess = require('child_process')
const nanoid = require('nanoid')
const path = require('path')
import async = require('async')
import moment = require('moment')
import * as childProcess from 'child_process'
import { nanoid } from 'nanoid'
import * as path from 'path'
const Op = Sequelize.Op
// core
const logger = require('../logger')
import logger = require('../logger')
let dmpWorker = createDmpWorker()
const dmpCallbackCache = {}
interface DmpWorkerMessage {
msg: string
cacheKey: string
error?: any
result?: any
}
function createDmpWorker () {
type DmpCallback = (err: any, result: any) => void
let dmpWorker: childProcess.ChildProcess | null = createDmpWorker()
const dmpCallbackCache: Record<string, DmpCallback> = {}
function createDmpWorker (): childProcess.ChildProcess {
const worker = childProcess.fork(path.resolve(__dirname, '../workers/dmpWorker.js'), {
stdio: 'ignore'
})
logger.debug('dmp worker process started')
worker.on('message', function (data) {
worker.on('message', function (data: DmpWorkerMessage) {
if (!data || !data.msg || !data.cacheKey) {
return logger.error('dmp worker error: not enough data on message')
}
@@ -35,16 +44,16 @@ function createDmpWorker () {
}
delete dmpCallbackCache[cacheKey]
})
worker.on('close', function (code) {
worker.on('close', function (code: number | null) {
dmpWorker = null
logger.debug(`dmp worker process exited with code ${code}`)
})
return worker
}
function sendDmpWorker (data, callback) {
function sendDmpWorker (data: any, callback: DmpCallback): void {
if (!dmpWorker) dmpWorker = createDmpWorker()
const cacheKey = Date.now() + '_' + nanoid.nanoid()
const cacheKey = Date.now() + '_' + nanoid()
dmpCallbackCache[cacheKey] = callback
data = Object.assign(data, {
cacheKey
@@ -52,7 +61,7 @@ function sendDmpWorker (data, callback) {
dmpWorker.send(data)
}
module.exports = function (sequelize, DataTypes) {
module.exports = function (sequelize: any, DataTypes: any): any {
const Revision = sequelize.define('Revision', {
id: {
type: DataTypes.UUID,
@@ -61,28 +70,28 @@ module.exports = function (sequelize, DataTypes) {
},
patch: {
type: DataTypes.TEXT('long'),
get: function () {
get: function (this: any) {
return sequelize.processData(this.getDataValue('patch'), '')
},
set: function (value) {
set: function (this: any, value: string) {
this.setDataValue('patch', sequelize.stripNullByte(value))
}
},
lastContent: {
type: DataTypes.TEXT('long'),
get: function () {
get: function (this: any) {
return sequelize.processData(this.getDataValue('lastContent'), '')
},
set: function (value) {
set: function (this: any, value: string) {
this.setDataValue('lastContent', sequelize.stripNullByte(value))
}
},
content: {
type: DataTypes.TEXT('long'),
get: function () {
get: function (this: any) {
return sequelize.processData(this.getDataValue('content'), '')
},
set: function (value) {
set: function (this: any, value: string) {
this.setDataValue('content', sequelize.stripNullByte(value))
}
},
@@ -91,16 +100,16 @@ module.exports = function (sequelize, DataTypes) {
},
authorship: {
type: DataTypes.TEXT('long'),
get: function () {
get: function (this: any) {
return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse)
},
set: function (value) {
set: function (this: any, value: any) {
this.setDataValue('authorship', value ? JSON.stringify(value) : value)
}
}
})
Revision.associate = function (models) {
Revision.associate = function (models: any): void {
Revision.belongsTo(models.Note, {
foreignKey: 'noteId',
as: 'note',
@@ -109,14 +118,14 @@ module.exports = function (sequelize, DataTypes) {
hooks: true
})
}
Revision.getNoteRevisions = function (note, callback) {
Revision.getNoteRevisions = function (note: any, callback: DmpCallback): void {
Revision.findAll({
where: {
noteId: note.id
},
order: [['createdAt', 'DESC']]
}).then(function (revisions) {
const data = []
}).then(function (revisions: any[]) {
const data: Array<{ time: number; length: number }> = []
for (let i = 0, l = revisions.length; i < l; i++) {
const revision = revisions[i]
data.push({
@@ -125,18 +134,18 @@ module.exports = function (sequelize, DataTypes) {
})
}
callback(null, data)
}).catch(function (err) {
}).catch(function (err: any) {
callback(err, null)
})
}
Revision.getPatchedNoteRevisionByTime = function (note, time, callback) {
Revision.getPatchedNoteRevisionByTime = function (note: any, time: any, callback: DmpCallback): void {
// find all revisions to prepare for all possible calculation
Revision.findAll({
where: {
noteId: note.id
},
order: [['createdAt', 'DESC']]
}).then(function (revisions) {
}).then(function (revisions: any[]) {
if (revisions.length <= 0) return callback(null, null)
// measure target revision position
Revision.count({
@@ -147,22 +156,22 @@ module.exports = function (sequelize, DataTypes) {
}
},
order: [['createdAt', 'DESC']]
}).then(function (count) {
}).then(function (count: number) {
if (count <= 0) return callback(null, null)
sendDmpWorker({
msg: 'get revision',
revisions,
count
}, callback)
}).catch(function (err) {
}).catch(function (err: any) {
return callback(err, null)
})
}).catch(function (err) {
}).catch(function (err: any) {
return callback(err, null)
})
}
Revision.checkAllNotesRevision = function (callback) {
Revision.saveAllNotesRevision(function (err, notes) {
Revision.checkAllNotesRevision = function (callback: DmpCallback): void {
Revision.saveAllNotesRevision(function (err: any, notes: any) {
if (err) return callback(err, null)
if (!notes || notes.length <= 0) {
return callback(null, notes)
@@ -171,7 +180,7 @@ module.exports = function (sequelize, DataTypes) {
}
})
}
Revision.saveAllNotesRevision = function (callback) {
Revision.saveAllNotesRevision = function (callback: DmpCallback): void {
sequelize.models.Note.findAll({
// query all notes that need to save for revision
where: {
@@ -197,10 +206,10 @@ module.exports = function (sequelize, DataTypes) {
}
]
}
}).then(function (notes) {
}).then(function (notes: any[]) {
if (notes.length <= 0) return callback(null, notes)
const savedNotes = []
async.each(notes, function (note, _callback) {
const savedNotes: any[] = []
async.each(notes, function (note: any, _callback: any) {
// revision saving policy: note not been modified for 5 mins or not save for 10 mins
if (note.lastchangeAt && note.savedAt) {
const lastchangeAt = moment(note.lastchangeAt)
@@ -218,7 +227,7 @@ module.exports = function (sequelize, DataTypes) {
savedNotes.push(note)
Revision.saveNoteRevision(note, _callback)
}
}, function (err) {
}, function (err: any) {
if (err) {
return callback(err, null)
}
@@ -226,17 +235,17 @@ module.exports = function (sequelize, DataTypes) {
const result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes
return callback(null, result)
})
}).catch(function (err) {
}).catch(function (err: any) {
return callback(err, null)
})
}
Revision.saveNoteRevision = function (note, callback) {
Revision.saveNoteRevision = function (note: any, callback: DmpCallback): void {
Revision.findAll({
where: {
noteId: note.id
},
order: [['createdAt', 'DESC']]
}).then(function (revisions) {
}).then(function (revisions: any[]) {
if (revisions.length <= 0) {
// if no revision available
Revision.create({
@@ -244,9 +253,9 @@ module.exports = function (sequelize, DataTypes) {
lastContent: note.content ? note.content : '',
length: note.content ? note.content.length : 0,
authorship: note.authorship
}).then(function (revision) {
}).then(function (revision: any) {
Revision.finishSaveNoteRevision(note, revision, callback)
}).catch(function (err) {
}).catch(function (err: any) {
return callback(err, null)
})
} else {
@@ -257,16 +266,16 @@ module.exports = function (sequelize, DataTypes) {
msg: 'create patch',
lastDoc: lastContent,
currDoc: content
}, function (err, patch) {
}, function (err: any, patch: any) {
if (err) logger.error('save note revision error', err)
if (!patch) {
// if patch is empty (means no difference) then just update the latest revision updated time
latestRevision.changed('updatedAt', true)
latestRevision.update({
updatedAt: Date.now()
}).then(function (revision) {
}).then(function (revision: any) {
Revision.finishSaveNoteRevision(note, revision, callback)
}).catch(function (err) {
}).catch(function (err: any) {
return callback(err, null)
})
} else {
@@ -276,31 +285,31 @@ module.exports = function (sequelize, DataTypes) {
content: note.content,
length: note.content.length,
authorship: note.authorship
}).then(function (revision) {
}).then(function (revision: any) {
// clear last revision content to reduce db size
latestRevision.update({
content: null
}).then(function () {
Revision.finishSaveNoteRevision(note, revision, callback)
}).catch(function (err) {
}).catch(function (err: any) {
return callback(err, null)
})
}).catch(function (err) {
}).catch(function (err: any) {
return callback(err, null)
})
}
})
}
}).catch(function (err) {
}).catch(function (err: any) {
return callback(err, null)
})
}
Revision.finishSaveNoteRevision = function (note, revision, callback) {
Revision.finishSaveNoteRevision = function (note: any, revision: any, callback: DmpCallback): void {
note.update({
savedAt: revision.updatedAt
}).then(function () {
return callback(null, revision)
}).catch(function (err) {
}).catch(function (err: any) {
return callback(err, null)
})
}
+3 -3
View File
@@ -1,13 +1,13 @@
'use strict'
// external modules
const nanoid = require('nanoid')
import { nanoid } from 'nanoid'
module.exports = function (sequelize, DataTypes) {
module.exports = function (sequelize: any, DataTypes: any): any {
const Temp = sequelize.define('Temp', {
id: {
type: DataTypes.STRING,
primaryKey: true,
defaultValue: nanoid.nanoid
defaultValue: nanoid
},
data: {
type: DataTypes.TEXT
+16 -16
View File
@@ -1,14 +1,14 @@
'use strict'
// external modules
const Sequelize = require('sequelize')
const scrypt = require('scrypt-kdf')
const filterXSS = require('xss')
import scrypt = require('scrypt-kdf')
const filterXSS: any = require('xss')
// core
const logger = require('../logger')
const { generateAvatarURL } = require('../letter-avatars')
import logger = require('../logger')
import { generateAvatarURL } from '../letter-avatars'
module.exports = function (sequelize, DataTypes) {
module.exports = function (sequelize: any, DataTypes: any): any {
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
@@ -46,11 +46,11 @@ module.exports = function (sequelize, DataTypes) {
}
})
User.prototype.verifyPassword = function (attempt) {
User.prototype.verifyPassword = function (attempt: string): Promise<boolean> {
return scrypt.verify(Buffer.from(this.password, 'hex'), attempt)
}
User.associate = function (models) {
User.associate = function (models: any): void {
User.hasMany(models.Note, {
foreignKey: 'ownerId',
constraints: false
@@ -60,13 +60,13 @@ module.exports = function (sequelize, DataTypes) {
constraints: false
})
}
User.getProfile = function (user) {
User.getProfile = function (user: any): any {
if (!user) {
return null
}
return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null)
}
User.parseProfile = function (profile) {
User.parseProfile = function (profile: any): any {
try {
profile = JSON.parse(profile)
} catch (err) {
@@ -82,8 +82,8 @@ module.exports = function (sequelize, DataTypes) {
}
return profile
}
User.parsePhotoByProfile = function (profile, bigger) {
let photo = null
User.parsePhotoByProfile = function (profile: any, bigger?: boolean): string | null {
let photo: string | null = null
switch (profile.provider) {
case 'facebook':
photo = 'https://graph.facebook.com/' + profile.id + '/picture'
@@ -123,8 +123,8 @@ module.exports = function (sequelize, DataTypes) {
break
case 'google':
photo = profile.photos[0].value
if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400')
else photo = photo.replace(/(\?sz=)\d*$/i, '$196')
if (bigger) photo = photo!.replace(/(\?sz=)\d*$/i, '$1400')
else photo = photo!.replace(/(\?sz=)\d*$/i, '$196')
break
case 'ldap':
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
@@ -142,7 +142,7 @@ module.exports = function (sequelize, DataTypes) {
}
return filterXSS(photo)
}
User.parseProfileByEmail = function (email) {
User.parseProfileByEmail = function (email: string): { name: string; photo: string; biggerphoto: string } {
return {
name: email.substring(0, email.lastIndexOf('@')),
photo: generateAvatarURL('', email, false),
@@ -150,7 +150,7 @@ module.exports = function (sequelize, DataTypes) {
}
}
function updatePasswordHashHook (user, options) {
function updatePasswordHashHook (user: any, options: any): Promise<void> {
// suggested way to hash passwords to be able to do this asynchronously:
// @see https://github.com/sequelize/sequelize/issues/1821#issuecomment-44265819
@@ -158,7 +158,7 @@ module.exports = function (sequelize, DataTypes) {
return Promise.resolve()
}
return scrypt.kdf(user.getDataValue('password'), { logN: 15 }).then(keyBuf => {
return scrypt.kdf(user.getDataValue('password'), { logN: 15 } as any).then((keyBuf: Buffer) => {
user.setDataValue('password', keyBuf.toString('hex'))
})
}
-312
View File
@@ -1,312 +0,0 @@
// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala
if (typeof ot === 'undefined') {
var ot = {};
}
ot.Client = (function (global) {
'use strict';
// Client constructor
function Client (revision) {
this.revision = revision; // the next expected revision number
this.setState(synchronized_); // start state
}
Client.prototype.setState = function (state) {
this.state = state;
};
// Call this method when the user changes the document.
Client.prototype.applyClient = function (operation) {
this.setState(this.state.applyClient(this, operation));
};
// Call this method with a new operation from the server
Client.prototype.applyServer = function (revision, operation) {
this.setState(this.state.applyServer(this, revision, operation));
};
Client.prototype.applyOperations = function (head, operations) {
this.setState(this.state.applyOperations(this, head, operations));
};
Client.prototype.serverAck = function (revision) {
this.setState(this.state.serverAck(this, revision));
};
Client.prototype.serverReconnect = function () {
if (typeof this.state.resend === 'function') { this.state.resend(this); }
};
// Transforms a selection from the latest known server state to the current
// client state. For example, if we get from the server the information that
// another user's cursor is at position 3, but the server hasn't yet received
// our newest operation, an insertion of 5 characters at the beginning of the
// document, the correct position of the other user's cursor in our current
// document is 8.
Client.prototype.transformSelection = function (selection) {
return this.state.transformSelection(selection);
};
// Override this method.
Client.prototype.sendOperation = function (revision, operation) {
throw new Error("sendOperation must be defined in child class");
};
// Override this method.
Client.prototype.applyOperation = function (operation) {
throw new Error("applyOperation must be defined in child class");
};
// In the 'Synchronized' state, there is no pending operation that the client
// has sent to the server.
function Synchronized () {}
Client.Synchronized = Synchronized;
Synchronized.prototype.applyClient = function (client, operation) {
// When the user makes an edit, send the operation to the server and
// switch to the 'AwaitingConfirm' state
client.sendOperation(client.revision, operation);
return new AwaitingConfirm(operation);
};
Synchronized.prototype.applyServer = function (client, revision, operation) {
if (revision - client.revision > 1) {
throw new Error("Invalid revision.");
}
client.revision = revision;
// When we receive a new operation from the server, the operation can be
// simply applied to the current document
client.applyOperation(operation);
return this;
};
Synchronized.prototype.serverAck = function (client, revision) {
throw new Error("There is no pending operation.");
};
// Nothing to do because the latest server state and client state are the same.
Synchronized.prototype.transformSelection = function (x) { return x; };
// Singleton
var synchronized_ = new Synchronized();
// In the 'AwaitingConfirm' state, there's one operation the client has sent
// to the server and is still waiting for an acknowledgement.
function AwaitingConfirm (outstanding) {
// Save the pending operation
this.outstanding = outstanding;
}
Client.AwaitingConfirm = AwaitingConfirm;
AwaitingConfirm.prototype.applyClient = function (client, operation) {
// When the user makes an edit, don't send the operation immediately,
// instead switch to 'AwaitingWithBuffer' state
return new AwaitingWithBuffer(this.outstanding, operation);
};
AwaitingConfirm.prototype.applyServer = function (client, revision, operation) {
if (revision - client.revision > 1) {
throw new Error("Invalid revision.");
}
client.revision = revision;
// This is another client's operation. Visualization:
//
// /\
// this.outstanding / \ operation
// / \
// \ /
// pair[1] \ / pair[0] (new outstanding)
// (can be applied \/
// to the client's
// current document)
var pair = operation.constructor.transform(this.outstanding, operation);
client.applyOperation(pair[1]);
return new AwaitingConfirm(pair[0]);
};
AwaitingConfirm.prototype.serverAck = function (client, revision) {
if (revision - client.revision > 1) {
return new Stale(this.outstanding, client, revision).getOperations();
}
client.revision = revision;
// The client's operation has been acknowledged
// => switch to synchronized state
return synchronized_;
};
AwaitingConfirm.prototype.transformSelection = function (selection) {
return selection.transform(this.outstanding);
};
AwaitingConfirm.prototype.resend = function (client) {
// The confirm didn't come because the client was disconnected.
// Now that it has reconnected, we resend the outstanding operation.
client.sendOperation(client.revision, this.outstanding);
};
// In the 'AwaitingWithBuffer' state, the client is waiting for an operation
// to be acknowledged by the server while buffering the edits the user makes
function AwaitingWithBuffer (outstanding, buffer) {
// Save the pending operation and the user's edits since then
this.outstanding = outstanding;
this.buffer = buffer;
}
Client.AwaitingWithBuffer = AwaitingWithBuffer;
AwaitingWithBuffer.prototype.applyClient = function (client, operation) {
// Compose the user's changes onto the buffer
var newBuffer = this.buffer.compose(operation);
return new AwaitingWithBuffer(this.outstanding, newBuffer);
};
AwaitingWithBuffer.prototype.applyServer = function (client, revision, operation) {
if (revision - client.revision > 1) {
throw new Error("Invalid revision.");
}
client.revision = revision;
// Operation comes from another client
//
// /\
// this.outstanding / \ operation
// / \
// /\ /
// this.buffer / \* / pair1[0] (new outstanding)
// / \/
// \ /
// pair2[1] \ / pair2[0] (new buffer)
// the transformed \/
// operation -- can
// be applied to the
// client's current
// document
//
// * pair1[1]
var transform = operation.constructor.transform;
var pair1 = transform(this.outstanding, operation);
var pair2 = transform(this.buffer, pair1[1]);
client.applyOperation(pair2[1]);
return new AwaitingWithBuffer(pair1[0], pair2[0]);
};
AwaitingWithBuffer.prototype.serverAck = function (client, revision) {
if (revision - client.revision > 1) {
return new StaleWithBuffer(this.outstanding, this.buffer, client, revision).getOperations();
}
client.revision = revision;
// The pending operation has been acknowledged
// => send buffer
client.sendOperation(client.revision, this.buffer);
return new AwaitingConfirm(this.buffer);
};
AwaitingWithBuffer.prototype.transformSelection = function (selection) {
return selection.transform(this.outstanding).transform(this.buffer);
};
AwaitingWithBuffer.prototype.resend = function (client) {
// The confirm didn't come because the client was disconnected.
// Now that it has reconnected, we resend the outstanding operation.
client.sendOperation(client.revision, this.outstanding);
};
function Stale(acknowlaged, client, revision) {
this.acknowlaged = acknowlaged;
this.client = client;
this.revision = revision;
}
Client.Stale = Stale;
Stale.prototype.applyClient = function (client, operation) {
return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
};
Stale.prototype.applyServer = function (client, revision, operation) {
throw new Error("Ignored server-side change.");
};
Stale.prototype.applyOperations = function (client, head, operations) {
var transform = this.acknowlaged.constructor.transform;
for (var i = 0; i < operations.length; i++) {
var op = ot.TextOperation.fromJSON(operations[i]);
var pair = transform(this.acknowlaged, op);
client.applyOperation(pair[1]);
this.acknowlaged = pair[0];
}
client.revision = this.revision;
return synchronized_;
};
Stale.prototype.serverAck = function (client, revision) {
throw new Error("There is no pending operation.");
};
Stale.prototype.transformSelection = function (selection) {
return selection;
};
Stale.prototype.getOperations = function () {
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
return this;
};
function StaleWithBuffer(acknowlaged, buffer, client, revision) {
this.acknowlaged = acknowlaged;
this.buffer = buffer;
this.client = client;
this.revision = revision;
}
Client.StaleWithBuffer = StaleWithBuffer;
StaleWithBuffer.prototype.applyClient = function (client, operation) {
var buffer = this.buffer.compose(operation);
return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
};
StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
throw new Error("Ignored server-side change.");
};
StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
var transform = this.acknowlaged.constructor.transform;
for (var i = 0; i < operations.length; i++) {
var op = ot.TextOperation.fromJSON(operations[i]);
var pair1 = transform(this.acknowlaged, op);
var pair2 = transform(this.buffer, pair1[1]);
client.applyOperation(pair2[1]);
this.acknowlaged = pair1[0];
this.buffer = pair2[0];
}
client.revision = this.revision;
client.sendOperation(client.revision, this.buffer);
return new AwaitingConfirm(this.buffer);
};
StaleWithBuffer.prototype.serverAck = function (client, revision) {
throw new Error("There is no pending operation.");
};
StaleWithBuffer.prototype.transformSelection = function (selection) {
return selection;
};
StaleWithBuffer.prototype.getOperations = function () {
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
return this;
};
return Client;
}(this));
if (typeof module === 'object') {
module.exports = ot.Client;
}
+351
View File
@@ -0,0 +1,351 @@
// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala
'use strict';
import TextOperation = require('./text-operation');
import WrappedOperation = require('./wrapped-operation');
interface ClientState {
applyClient(client: Client, operation: WrappedOperation): ClientState;
applyServer(client: Client, revision: number, operation: WrappedOperation): ClientState;
applyOperations?(client: Client, head: number, operations: (number | string)[][]): ClientState;
serverAck(client: Client, revision: number): ClientState;
transformSelection(selection: any): any;
resend?(client: Client): void;
getOperations?(): ClientState;
}
// Client constructor
class Client {
revision: number;
state: ClientState;
static Synchronized: typeof Synchronized;
static AwaitingConfirm: typeof AwaitingConfirm;
static AwaitingWithBuffer: typeof AwaitingWithBuffer;
static Stale: typeof Stale;
static StaleWithBuffer: typeof StaleWithBuffer;
constructor(revision: number) {
this.revision = revision; // the next expected revision number
this.state = synchronized_; // start state
}
setState(state: ClientState): void {
this.state = state;
}
// Call this method when the user changes the document.
applyClient(operation: WrappedOperation): void {
this.setState(this.state.applyClient(this, operation));
}
// Call this method with a new operation from the server
applyServer(revision: number, operation: WrappedOperation): void {
this.setState(this.state.applyServer(this, revision, operation));
}
applyOperations(head: number, operations: (number | string)[][]): void {
this.setState(this.state.applyOperations!(this, head, operations));
}
serverAck(revision: number): void {
this.setState(this.state.serverAck(this, revision));
}
serverReconnect(): void {
if (typeof this.state.resend === 'function') { this.state.resend(this); }
}
// Transforms a selection from the latest known server state to the current
// client state. For example, if we get from the server the information that
// another user's cursor is at position 3, but the server hasn't yet received
// our newest operation, an insertion of 5 characters at the beginning of the
// document, the correct position of the other user's cursor in our current
// document is 8.
transformSelection(selection: any): any {
return this.state.transformSelection(selection);
}
// Override this method.
sendOperation(revision: number, operation: WrappedOperation): void {
throw new Error("sendOperation must be defined in child class");
}
// Override this method.
applyOperation(operation: WrappedOperation): void {
throw new Error("applyOperation must be defined in child class");
}
// Override this method.
getOperations(base: number, head: number): void {
throw new Error("getOperations must be defined in child class");
}
}
// In the 'Synchronized' state, there is no pending operation that the client
// has sent to the server.
class Synchronized implements ClientState {
applyClient(client: Client, operation: WrappedOperation): ClientState {
// When the user makes an edit, send the operation to the server and
// switch to the 'AwaitingConfirm' state
client.sendOperation(client.revision, operation);
return new AwaitingConfirm(operation);
}
applyServer(client: Client, revision: number, operation: WrappedOperation): ClientState {
if (revision - client.revision > 1) {
throw new Error("Invalid revision.");
}
client.revision = revision;
// When we receive a new operation from the server, the operation can be
// simply applied to the current document
client.applyOperation(operation);
return this;
}
serverAck(_client: Client, _revision: number): ClientState {
throw new Error("There is no pending operation.");
}
// Nothing to do because the latest server state and client state are the same.
transformSelection(x: any): any { return x; }
}
// Singleton
const synchronized_ = new Synchronized();
// In the 'AwaitingConfirm' state, there's one operation the client has sent
// to the server and is still waiting for an acknowledgement.
class AwaitingConfirm implements ClientState {
outstanding: WrappedOperation;
constructor(outstanding: WrappedOperation) {
// Save the pending operation
this.outstanding = outstanding;
}
applyClient(_client: Client, operation: WrappedOperation): ClientState {
// When the user makes an edit, don't send the operation immediately,
// instead switch to 'AwaitingWithBuffer' state
return new AwaitingWithBuffer(this.outstanding, operation);
}
applyServer(client: Client, revision: number, operation: WrappedOperation): ClientState {
if (revision - client.revision > 1) {
throw new Error("Invalid revision.");
}
client.revision = revision;
// This is another client's operation. Visualization:
//
// /\
// this.outstanding / \ operation
// / \
// \ /
// pair[1] \ / pair[0] (new outstanding)
// (can be applied \/
// to the client's
// current document)
const pair = (operation.constructor as typeof WrappedOperation).transform(this.outstanding, operation);
client.applyOperation(pair[1]);
return new AwaitingConfirm(pair[0]);
}
serverAck(client: Client, revision: number): ClientState {
if (revision - client.revision > 1) {
return new Stale(this.outstanding, client, revision).getOperations();
}
client.revision = revision;
// The client's operation has been acknowledged
// => switch to synchronized state
return synchronized_;
}
transformSelection(selection: any): any {
return selection.transform(this.outstanding);
}
resend(client: Client): void {
// The confirm didn't come because the client was disconnected.
// Now that it has reconnected, we resend the outstanding operation.
client.sendOperation(client.revision, this.outstanding);
}
}
// In the 'AwaitingWithBuffer' state, the client is waiting for an operation
// to be acknowledged by the server while buffering the edits the user makes
class AwaitingWithBuffer implements ClientState {
outstanding: WrappedOperation;
buffer: WrappedOperation;
constructor(outstanding: WrappedOperation, buffer: WrappedOperation) {
// Save the pending operation and the user's edits since then
this.outstanding = outstanding;
this.buffer = buffer;
}
applyClient(_client: Client, operation: WrappedOperation): ClientState {
// Compose the user's changes onto the buffer
const newBuffer = this.buffer.compose(operation);
return new AwaitingWithBuffer(this.outstanding, newBuffer);
}
applyServer(client: Client, revision: number, operation: WrappedOperation): ClientState {
if (revision - client.revision > 1) {
throw new Error("Invalid revision.");
}
client.revision = revision;
// Operation comes from another client
//
// /\
// this.outstanding / \ operation
// / \
// /\ /
// this.buffer / \* / pair1[0] (new outstanding)
// / \/
// \ /
// pair2[1] \ / pair2[0] (new buffer)
// the transformed \/
// operation -- can
// be applied to the
// client's current
// document
//
// * pair1[1]
const transform = (operation.constructor as typeof WrappedOperation).transform;
const pair1 = transform(this.outstanding, operation);
const pair2 = transform(this.buffer, pair1[1]);
client.applyOperation(pair2[1]);
return new AwaitingWithBuffer(pair1[0], pair2[0]);
}
serverAck(client: Client, revision: number): ClientState {
if (revision - client.revision > 1) {
return new StaleWithBuffer(this.outstanding, this.buffer, client, revision).getOperations();
}
client.revision = revision;
// The pending operation has been acknowledged
// => send buffer
client.sendOperation(client.revision, this.buffer);
return new AwaitingConfirm(this.buffer);
}
transformSelection(selection: any): any {
return selection.transform(this.outstanding).transform(this.buffer);
}
resend(client: Client): void {
// The confirm didn't come because the client was disconnected.
// Now that it has reconnected, we resend the outstanding operation.
client.sendOperation(client.revision, this.outstanding);
}
}
class Stale implements ClientState {
acknowledged: WrappedOperation;
client: Client;
revision: number;
constructor(acknowledged: WrappedOperation, client: Client, revision: number) {
this.acknowledged = acknowledged;
this.client = client;
this.revision = revision;
}
applyClient(client: Client, operation: WrappedOperation): ClientState {
return new StaleWithBuffer(this.acknowledged, operation, client, this.revision);
}
applyServer(_client: Client, _revision: number, _operation: WrappedOperation): ClientState {
throw new Error("Ignored server-side change.");
}
applyOperations(client: Client, _head: number, operations: (number | string)[][]): ClientState {
const transform = (this.acknowledged.constructor as typeof WrappedOperation).transform;
for (let i = 0; i < operations.length; i++) {
const op = TextOperation.fromJSON(operations[i]);
const pair = transform(this.acknowledged, op as any);
client.applyOperation(pair[1]);
this.acknowledged = pair[0];
}
client.revision = this.revision;
return synchronized_;
}
serverAck(_client: Client, _revision: number): ClientState {
throw new Error("There is no pending operation.");
}
transformSelection(selection: any): any {
return selection;
}
getOperations(): ClientState {
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
return this;
}
}
class StaleWithBuffer implements ClientState {
acknowledged: WrappedOperation;
buffer: WrappedOperation;
client: Client;
revision: number;
constructor(acknowledged: WrappedOperation, buffer: WrappedOperation, client: Client, revision: number) {
this.acknowledged = acknowledged;
this.buffer = buffer;
this.client = client;
this.revision = revision;
}
applyClient(client: Client, operation: WrappedOperation): ClientState {
const buffer = this.buffer.compose(operation);
return new StaleWithBuffer(this.acknowledged, buffer, client, this.revision);
}
applyServer(_client: Client, _revision: number, _operation: WrappedOperation): ClientState {
throw new Error("Ignored server-side change.");
}
applyOperations(client: Client, _head: number, operations: (number | string)[][]): ClientState {
const transform = (this.acknowledged.constructor as typeof WrappedOperation).transform;
for (let i = 0; i < operations.length; i++) {
const op = TextOperation.fromJSON(operations[i]);
const pair1 = transform(this.acknowledged, op as any);
const pair2 = transform(this.buffer, pair1[1]);
client.applyOperation(pair2[1]);
this.acknowledged = pair1[0];
this.buffer = pair2[0];
}
client.revision = this.revision;
client.sendOperation(client.revision, this.buffer);
return new AwaitingConfirm(this.buffer);
}
serverAck(_client: Client, _revision: number): ClientState {
throw new Error("There is no pending operation.");
}
transformSelection(selection: any): any {
return selection;
}
getOperations(): ClientState {
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
return this;
}
}
Client.Synchronized = Synchronized;
Client.AwaitingConfirm = AwaitingConfirm;
Client.AwaitingWithBuffer = AwaitingWithBuffer;
Client.Stale = Stale;
Client.StaleWithBuffer = StaleWithBuffer;
export = Client;
-164
View File
@@ -1,164 +0,0 @@
'use strict';
var EventEmitter = require('events').EventEmitter;
var TextOperation = require('./text-operation');
var WrappedOperation = require('./wrapped-operation');
var Server = require('./server');
var Selection = require('./selection');
var util = require('util');
var logger = require('../logger');
function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) {
EventEmitter.call(this);
Server.call(this, document, operations);
this.users = {};
this.docId = docId;
this.mayWrite = mayWrite || function (_, cb) {
cb(true);
};
this.operationCallback = operationCallback;
}
util.inherits(EditorSocketIOServer, Server);
extend(EditorSocketIOServer.prototype, EventEmitter.prototype);
function extend(target, source) {
for (var key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
}
EditorSocketIOServer.prototype.addClient = function (socket) {
var self = this;
socket.join(this.docId);
var docOut = {
str: this.document,
revision: this.operations.length,
clients: this.users
};
socket.emit('doc', docOut);
socket.on('operation', function (revision, operation, selection) {
socket.origin = 'operation';
self.mayWrite(socket, function (mayWrite) {
if (!mayWrite) {
logger.info("User doesn't have the right to edit.");
return;
}
try {
self.onOperation(socket, revision, operation, selection);
if (typeof self.operationCallback === 'function')
self.operationCallback(socket, operation);
} catch (err) {
setTimeout(function() {
var docOut = {
str: self.document,
revision: self.operations.length,
clients: self.users,
force: true
};
socket.emit('doc', docOut);
}, 100);
}
});
});
socket.on('get_operations', function (base, head) {
self.onGetOperations(socket, base, head);
});
socket.on('selection', function (obj) {
socket.origin = 'selection';
self.mayWrite(socket, function (mayWrite) {
if (!mayWrite) {
logger.info("User doesn't have the right to edit.");
return;
}
self.updateSelection(socket, obj && Selection.fromJSON(obj));
});
});
socket.on('disconnect', function () {
logger.debug("Disconnect");
socket.leave(self.docId);
self.onDisconnect(socket);
/*
if (socket.manager && socket.manager.sockets.clients(self.docId).length === 0) {
self.emit('empty-room');
}
*/
});
};
EditorSocketIOServer.prototype.onOperation = function (socket, revision, operation, selection) {
var wrapped;
try {
wrapped = new WrappedOperation(
TextOperation.fromJSON(operation),
selection && Selection.fromJSON(selection)
);
} catch (exc) {
logger.error("Invalid operation received: ");
logger.error(exc);
throw new Error(exc);
}
try {
var clientId = socket.id;
var wrappedPrime = this.receiveOperation(revision, wrapped);
if(!wrappedPrime) return;
logger.debug("new operation: " + JSON.stringify(wrapped));
this.getClient(clientId).selection = wrappedPrime.meta;
revision = this.operations.length;
socket.emit('ack', revision);
socket.broadcast.in(this.docId).emit(
'operation', clientId, revision,
wrappedPrime.wrapped.toJSON(), wrappedPrime.meta
);
//set document is dirty
this.isDirty = true;
} catch (exc) {
logger.error(exc);
throw new Error(exc);
}
};
EditorSocketIOServer.prototype.onGetOperations = function (socket, base, head) {
var operations = this.operations.slice(base, head).map(function (op) {
return op.wrapped.toJSON();
});
socket.emit('operations', head, operations);
};
EditorSocketIOServer.prototype.updateSelection = function (socket, selection) {
var clientId = socket.id;
if (selection) {
this.getClient(clientId).selection = selection;
} else {
delete this.getClient(clientId).selection;
}
socket.broadcast.to(this.docId).emit('selection', clientId, selection);
};
EditorSocketIOServer.prototype.setName = function (socket, name) {
var clientId = socket.id;
this.getClient(clientId).name = name;
socket.broadcast.to(this.docId).emit('set_name', clientId, name);
};
EditorSocketIOServer.prototype.setColor = function (socket, color) {
var clientId = socket.id;
this.getClient(clientId).color = color;
socket.broadcast.to(this.docId).emit('set_color', clientId, color);
};
EditorSocketIOServer.prototype.getClient = function (clientId) {
return this.users[clientId] || (this.users[clientId] = {});
};
EditorSocketIOServer.prototype.onDisconnect = function (socket) {
var clientId = socket.id;
delete this.users[clientId];
socket.broadcast.to(this.docId).emit('client_left', clientId);
};
module.exports = EditorSocketIOServer;
+179
View File
@@ -0,0 +1,179 @@
'use strict';
import { EventEmitter } from 'events';
import TextOperation = require('./text-operation');
import WrappedOperation = require('./wrapped-operation');
import Server = require('./server');
import Selection = require('./selection');
import logger from '../logger';
interface UserInfo {
name?: string;
color?: string;
selection?: Selection;
}
interface Users {
[clientId: string]: UserInfo;
}
class EditorSocketIOServer extends Server {
users: Users;
docId: string;
mayWrite: (socket: any, cb: (allowed: boolean) => void) => void;
operationCallback?: (socket: any, operation: any) => void;
isDirty: boolean;
constructor(
document: string,
operations: WrappedOperation[],
docId: string,
mayWrite?: (socket: any, cb: (allowed: boolean) => void) => void,
operationCallback?: (socket: any, operation: any) => void
) {
super(document, operations);
this.users = {};
this.docId = docId;
this.mayWrite = mayWrite || function (_, cb) {
cb(true);
};
this.operationCallback = operationCallback;
this.isDirty = false;
}
addClient(socket: any): void {
const self = this;
socket.join(this.docId);
const docOut = {
str: this.document,
revision: this.operations.length,
clients: this.users
};
socket.emit('doc', docOut);
socket.on('operation', function (revision: number, operation: any, selection: any) {
socket.origin = 'operation';
self.mayWrite(socket, function (mayWrite: boolean) {
if (!mayWrite) {
logger.info("User doesn't have the right to edit.");
return;
}
try {
self.onOperation(socket, revision, operation, selection);
if (typeof self.operationCallback === 'function')
self.operationCallback(socket, operation);
} catch (err) {
setTimeout(function () {
const docOut = {
str: self.document,
revision: self.operations.length,
clients: self.users,
force: true
};
socket.emit('doc', docOut);
}, 100);
}
});
});
socket.on('get_operations', function (base: number, head: number) {
self.onGetOperations(socket, base, head);
});
socket.on('selection', function (obj: any) {
socket.origin = 'selection';
self.mayWrite(socket, function (mayWrite: boolean) {
if (!mayWrite) {
logger.info("User doesn't have the right to edit.");
return;
}
self.updateSelection(socket, obj && Selection.fromJSON(obj));
});
});
socket.on('disconnect', function () {
logger.debug("Disconnect");
socket.leave(self.docId);
self.onDisconnect(socket);
/*
if (socket.manager && socket.manager.sockets.clients(self.docId).length === 0) {
self.emit('empty-room');
}
*/
});
}
onOperation(socket: any, revision: number, operation: any, selection: any): void {
let wrapped: WrappedOperation;
try {
wrapped = new WrappedOperation(
TextOperation.fromJSON(operation),
selection && Selection.fromJSON(selection)
);
} catch (exc) {
logger.error("Invalid operation received: ");
logger.error(exc as any);
throw new Error(exc as any);
}
try {
const clientId = socket.id;
const wrappedPrime = this.receiveOperation(revision, wrapped);
if (!wrappedPrime) return;
logger.debug("new operation: " + JSON.stringify(wrapped));
this.getClient(clientId).selection = wrappedPrime.meta;
revision = this.operations.length;
socket.emit('ack', revision);
socket.broadcast.in(this.docId).emit(
'operation', clientId, revision,
wrappedPrime.wrapped.toJSON(), wrappedPrime.meta
);
//set document is dirty
this.isDirty = true;
} catch (exc) {
logger.error(exc as any);
throw new Error(exc as any);
}
}
onGetOperations(socket: any, base: number, head: number): void {
const operations = this.operations.slice(base, head).map(function (op: WrappedOperation) {
return op.wrapped.toJSON();
});
socket.emit('operations', head, operations);
}
updateSelection(socket: any, selection: Selection | null): void {
const clientId = socket.id;
if (selection) {
this.getClient(clientId).selection = selection;
} else {
delete this.getClient(clientId).selection;
}
socket.broadcast.to(this.docId).emit('selection', clientId, selection);
}
setName(socket: any, name: string): void {
const clientId = socket.id;
this.getClient(clientId).name = name;
socket.broadcast.to(this.docId).emit('set_name', clientId, name);
}
setColor(socket: any, color: string): void {
const clientId = socket.id;
this.getClient(clientId).color = color;
socket.broadcast.to(this.docId).emit('set_color', clientId, color);
}
getClient(clientId: string): UserInfo {
return this.users[clientId] || (this.users[clientId] = {});
}
onDisconnect(socket: any): void {
const clientId = socket.id;
delete this.users[clientId];
socket.broadcast.to(this.docId).emit('client_left', clientId);
}
}
// Mixin EventEmitter methods (without overwriting the Server prototype chain)
Object.assign(EditorSocketIOServer.prototype, EventEmitter.prototype);
export = EditorSocketIOServer;
-8
View File
@@ -1,8 +0,0 @@
exports.version = '0.0.15';
exports.TextOperation = require('./text-operation');
exports.SimpleTextOperation = require('./simple-text-operation');
exports.Client = require('./client');
exports.Server = require('./server');
exports.Selection = require('./selection');
exports.EditorSocketIOServer = require('./editor-socketio-server');
+8
View File
@@ -0,0 +1,8 @@
export const version = '0.0.15';
export import TextOperation = require('./text-operation');
export import SimpleTextOperation = require('./simple-text-operation');
export import Client = require('./client');
export import Server = require('./server');
export import Selection = require('./selection');
export import EditorSocketIOServer = require('./editor-socketio-server');
-117
View File
@@ -1,117 +0,0 @@
if (typeof ot === 'undefined') {
// Export for browsers
var ot = {};
}
ot.Selection = (function (global) {
'use strict';
var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation');
// Range has `anchor` and `head` properties, which are zero-based indices into
// the document. The `anchor` is the side of the selection that stays fixed,
// `head` is the side of the selection where the cursor is. When both are
// equal, the range represents a cursor.
function Range (anchor, head) {
this.anchor = anchor;
this.head = head;
}
Range.fromJSON = function (obj) {
return new Range(obj.anchor, obj.head);
};
Range.prototype.equals = function (other) {
return this.anchor === other.anchor && this.head === other.head;
};
Range.prototype.isEmpty = function () {
return this.anchor === this.head;
};
Range.prototype.transform = function (other) {
function transformIndex (index) {
var newIndex = index;
var ops = other.ops;
for (var i = 0, l = other.ops.length; i < l; i++) {
if (TextOperation.isRetain(ops[i])) {
index -= ops[i];
} else if (TextOperation.isInsert(ops[i])) {
newIndex += ops[i].length;
} else {
newIndex -= Math.min(index, -ops[i]);
index += ops[i];
}
if (index < 0) { break; }
}
return newIndex;
}
var newAnchor = transformIndex(this.anchor);
if (this.anchor === this.head) {
return new Range(newAnchor, newAnchor);
}
return new Range(newAnchor, transformIndex(this.head));
};
// A selection is basically an array of ranges. Every range represents a real
// selection or a cursor in the document (when the start position equals the
// end position of the range). The array must not be empty.
function Selection (ranges) {
this.ranges = ranges || [];
}
Selection.Range = Range;
// Convenience method for creating selections only containing a single cursor
// and no real selection range.
Selection.createCursor = function (position) {
return new Selection([new Range(position, position)]);
};
Selection.fromJSON = function (obj) {
var objRanges = obj.ranges || obj;
for (var i = 0, ranges = []; i < objRanges.length; i++) {
ranges[i] = Range.fromJSON(objRanges[i]);
}
return new Selection(ranges);
};
Selection.prototype.equals = function (other) {
if (this.position !== other.position) { return false; }
if (this.ranges.length !== other.ranges.length) { return false; }
// FIXME: Sort ranges before comparing them?
for (var i = 0; i < this.ranges.length; i++) {
if (!this.ranges[i].equals(other.ranges[i])) { return false; }
}
return true;
};
Selection.prototype.somethingSelected = function () {
for (var i = 0; i < this.ranges.length; i++) {
if (!this.ranges[i].isEmpty()) { return true; }
}
return false;
};
// Return the more current selection information.
Selection.prototype.compose = function (other) {
return other;
};
// Update the selection with respect to an operation.
Selection.prototype.transform = function (other) {
for (var i = 0, newRanges = []; i < this.ranges.length; i++) {
newRanges[i] = this.ranges[i].transform(other);
}
return new Selection(newRanges);
};
return Selection;
}(this));
// Export for CommonJS
if (typeof module === 'object') {
module.exports = ot.Selection;
}
+117
View File
@@ -0,0 +1,117 @@
// Export for browsers
'use strict';
import TextOperation = require('./text-operation');
// Range has `anchor` and `head` properties, which are zero-based indices into
// the document. The `anchor` is the side of the selection that stays fixed,
// `head` is the side of the selection where the cursor is. When both are
// equal, the range represents a cursor.
class Range {
anchor: number;
head: number;
constructor(anchor: number, head: number) {
this.anchor = anchor;
this.head = head;
}
static fromJSON(obj: { anchor: number; head: number }): Range {
return new Range(obj.anchor, obj.head);
}
equals(other: Range): boolean {
return this.anchor === other.anchor && this.head === other.head;
}
isEmpty(): boolean {
return this.anchor === this.head;
}
transform(other: TextOperation): Range {
function transformIndex(index: number): number {
let newIndex = index;
const ops = other.ops;
for (let i = 0, l = other.ops.length; i < l; i++) {
if (TextOperation.isRetain(ops[i])) {
index -= ops[i] as number;
} else if (TextOperation.isInsert(ops[i])) {
newIndex += (ops[i] as string).length;
} else {
newIndex -= Math.min(index, -(ops[i] as number));
index += ops[i] as number;
}
if (index < 0) { break; }
}
return newIndex;
}
const newAnchor = transformIndex(this.anchor);
if (this.anchor === this.head) {
return new Range(newAnchor, newAnchor);
}
return new Range(newAnchor, transformIndex(this.head));
}
}
// A selection is basically an array of ranges. Every range represents a real
// selection or a cursor in the document (when the start position equals the
// end position of the range). The array must not be empty.
class Selection {
ranges: Range[];
static Range = Range;
constructor(ranges?: Range[]) {
this.ranges = ranges || [];
}
// Convenience method for creating selections only containing a single cursor
// and no real selection range.
static createCursor(position: number): Selection {
return new Selection([new Range(position, position)]);
}
static fromJSON(obj: { ranges?: { anchor: number; head: number }[] } | { anchor: number; head: number }[]): Selection {
const objRanges = (obj as any).ranges || obj;
const ranges: Range[] = [];
for (let i = 0; i < objRanges.length; i++) {
ranges[i] = Range.fromJSON(objRanges[i]);
}
return new Selection(ranges);
}
equals(other: Selection): boolean {
if (this.ranges.length !== other.ranges.length) { return false; }
// FIXME: Sort ranges before comparing them?
for (let i = 0; i < this.ranges.length; i++) {
if (!this.ranges[i].equals(other.ranges[i])) { return false; }
}
return true;
}
somethingSelected(): boolean {
for (let i = 0; i < this.ranges.length; i++) {
if (!this.ranges[i].isEmpty()) { return true; }
}
return false;
}
// Return the more current selection information.
compose(other: Selection): Selection {
return other;
}
// Update the selection with respect to an operation.
transform(other: TextOperation): Selection {
const newRanges: Range[] = [];
for (let i = 0; i < this.ranges.length; i++) {
newRanges[i] = this.ranges[i].transform(other);
}
return new Selection(newRanges);
}
}
// Export for CommonJS
export = Selection;
+17 -22
View File
@@ -1,39 +1,39 @@
var config = require('../config');
'use strict';
if (typeof ot === 'undefined') {
var ot = {};
}
import config = require('../config');
import WrappedOperation = require('./wrapped-operation');
ot.Server = (function (global) {
'use strict';
class Server {
document: string;
operations: WrappedOperation[];
// Constructor. Takes the current document as a string and optionally the array
// of all operations.
function Server (document, operations) {
constructor(document: string, operations?: WrappedOperation[]) {
this.document = document;
this.operations = operations || [];
}
// Call this method whenever you receive an operation from a client.
Server.prototype.receiveOperation = function (revision, operation) {
receiveOperation(revision: number, operation: WrappedOperation): WrappedOperation | undefined {
if (revision < 0 || this.operations.length < revision) {
throw new Error("operation revision not in history");
}
// Find all operations that the client didn't know of when it sent the
// operation ...
var concurrentOperations = this.operations.slice(revision);
const concurrentOperations = this.operations.slice(revision);
// ... and transform the operation against all these operations ...
var transform = operation.constructor.transform;
for (var i = 0; i < concurrentOperations.length; i++) {
const transform = (operation.constructor as typeof WrappedOperation).transform;
for (let i = 0; i < concurrentOperations.length; i++) {
operation = transform(operation, concurrentOperations[i])[0];
}
// ... and apply that on the document.
var newDocument = operation.apply(this.document);
const newDocument = operation.apply(this.document);
// ignore if exceed the max length of document
if(newDocument.length > config.documentMaxLength && newDocument.length > this.document.length)
return;
if (newDocument.length > config.documentMaxLength && newDocument.length > this.document.length)
return;
this.document = newDocument;
// Store operation in history.
this.operations.push(operation);
@@ -41,12 +41,7 @@ ot.Server = (function (global) {
// It's the caller's responsibility to send the operation to all connected
// clients and an acknowledgement to the creator.
return operation;
};
}
}
return Server;
}(this));
if (typeof module === 'object') {
module.exports = ot.Server;
}
export = Server;
@@ -1,91 +1,15 @@
if (typeof ot === 'undefined') {
// Export for browsers
var ot = {};
}
// Export for browsers
ot.SimpleTextOperation = (function (global) {
'use strict';
var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation');
import TextOperation = require('./text-operation');
function SimpleTextOperation () {}
class SimpleTextOperation {
static Insert: typeof Insert;
static Delete: typeof Delete;
static Noop: typeof Noop;
// Insert the string `str` at the zero-based `position` in the document.
function Insert (str, position) {
if (!this || this.constructor !== SimpleTextOperation) {
// => function was called without 'new'
return new Insert(str, position);
}
this.str = str;
this.position = position;
}
Insert.prototype = new SimpleTextOperation();
SimpleTextOperation.Insert = Insert;
Insert.prototype.toString = function () {
return 'Insert(' + JSON.stringify(this.str) + ', ' + this.position + ')';
};
Insert.prototype.equals = function (other) {
return other instanceof Insert &&
this.str === other.str &&
this.position === other.position;
};
Insert.prototype.apply = function (doc) {
return doc.slice(0, this.position) + this.str + doc.slice(this.position);
};
// Delete `count` many characters at the zero-based `position` in the document.
function Delete (count, position) {
if (!this || this.constructor !== SimpleTextOperation) {
return new Delete(count, position);
}
this.count = count;
this.position = position;
}
Delete.prototype = new SimpleTextOperation();
SimpleTextOperation.Delete = Delete;
Delete.prototype.toString = function () {
return 'Delete(' + this.count + ', ' + this.position + ')';
};
Delete.prototype.equals = function (other) {
return other instanceof Delete &&
this.count === other.count &&
this.position === other.position;
};
Delete.prototype.apply = function (doc) {
return doc.slice(0, this.position) + doc.slice(this.position + this.count);
};
// An operation that does nothing. This is needed for the result of the
// transformation of two deletions of the same character.
function Noop () {
if (!this || this.constructor !== SimpleTextOperation) { return new Noop(); }
}
Noop.prototype = new SimpleTextOperation();
SimpleTextOperation.Noop = Noop;
Noop.prototype.toString = function () {
return 'Noop()';
};
Noop.prototype.equals = function (other) { return other instanceof Noop; };
Noop.prototype.apply = function (doc) { return doc; };
var noop = new Noop();
SimpleTextOperation.transform = function (a, b) {
static transform(a: SimpleTextOperation, b: SimpleTextOperation): [SimpleTextOperation, SimpleTextOperation] {
if (a instanceof Noop || b instanceof Noop) { return [a, b]; }
if (a instanceof Insert && b instanceof Insert) {
@@ -157,32 +81,100 @@ ot.SimpleTextOperation = (function (global) {
];
}
}
};
// Should never reach here
return [a, b];
}
// Convert a normal, composable `TextOperation` into an array of
// `SimpleTextOperation`s.
SimpleTextOperation.fromTextOperation = function (operation) {
var simpleOperations = [];
var index = 0;
for (var i = 0; i < operation.ops.length; i++) {
var op = operation.ops[i];
static fromTextOperation(operation: TextOperation): SimpleTextOperation[] {
const simpleOperations: SimpleTextOperation[] = [];
let index = 0;
for (let i = 0; i < operation.ops.length; i++) {
const op = operation.ops[i];
if (TextOperation.isRetain(op)) {
index += op;
} else if (TextOperation.isInsert(op)) {
simpleOperations.push(new Insert(op, index));
index += op.length;
} else {
simpleOperations.push(new Delete(Math.abs(op), index));
simpleOperations.push(new Delete(Math.abs(op as number), index));
}
}
return simpleOperations;
};
}
}
// Insert the string `str` at the zero-based `position` in the document.
class Insert extends SimpleTextOperation {
str: string;
position: number;
return SimpleTextOperation;
})(this);
constructor(str: string, position: number) {
super();
this.str = str;
this.position = position;
}
toString(): string {
return 'Insert(' + JSON.stringify(this.str) + ', ' + this.position + ')';
}
equals(other: SimpleTextOperation): boolean {
return other instanceof Insert &&
this.str === other.str &&
this.position === other.position;
}
apply(doc: string): string {
return doc.slice(0, this.position) + this.str + doc.slice(this.position);
}
}
// Delete `count` many characters at the zero-based `position` in the document.
class Delete extends SimpleTextOperation {
count: number;
position: number;
constructor(count: number, position: number) {
super();
this.count = count;
this.position = position;
}
toString(): string {
return 'Delete(' + this.count + ', ' + this.position + ')';
}
equals(other: SimpleTextOperation): boolean {
return other instanceof Delete &&
this.count === other.count &&
this.position === other.position;
}
apply(doc: string): string {
return doc.slice(0, this.position) + doc.slice(this.position + this.count);
}
}
// An operation that does nothing. This is needed for the result of the
// transformation of two deletions of the same character.
class Noop extends SimpleTextOperation {
toString(): string {
return 'Noop()';
}
equals(other: SimpleTextOperation): boolean { return other instanceof Noop; }
apply(doc: string): string { return doc; }
}
const noop = new Noop();
SimpleTextOperation.Insert = Insert;
SimpleTextOperation.Delete = Delete;
SimpleTextOperation.Noop = Noop;
// Export for CommonJS
if (typeof module === 'object') {
module.exports = ot.SimpleTextOperation;
}
export = SimpleTextOperation;
@@ -1,18 +1,40 @@
if (typeof ot === 'undefined') {
// Export for browsers
var ot = {};
// Export for browsers
'use strict';
// Operation are essentially lists of ops. There are three types of ops:
//
// * Retain ops: Advance the cursor position by a given number of characters.
// Represented by positive ints.
// * Insert ops: Insert a given string at the current cursor position.
// Represented by strings.
// * Delete ops: Delete the next n characters. Represented by negative ints.
type Op = number | string;
function isRetain(op: Op | undefined): op is number {
return typeof op === 'number' && op > 0;
}
ot.TextOperation = (function () {
'use strict';
function isInsert(op: Op | undefined): op is string {
return typeof op === 'string';
}
// Constructor for new operations.
function TextOperation () {
if (!this || this.constructor !== TextOperation) {
// => function was called without 'new'
return new TextOperation();
}
function isDelete(op: Op | undefined): op is number {
return typeof op === 'number' && op < 0;
}
// Constructor for new operations.
class TextOperation {
ops: Op[];
baseLength: number;
targetLength: number;
static isRetain = isRetain;
static isInsert = isInsert;
static isDelete = isDelete;
constructor() {
// When an operation is applied to an input string, you can think of this as
// if an imaginary cursor runs over the entire string and skips over some
// parts, deletes some parts and inserts characters at some positions. These
@@ -26,90 +48,69 @@ ot.TextOperation = (function () {
this.targetLength = 0;
}
TextOperation.prototype.equals = function (other) {
equals(other: TextOperation): boolean {
if (this.baseLength !== other.baseLength) { return false; }
if (this.targetLength !== other.targetLength) { return false; }
if (this.ops.length !== other.ops.length) { return false; }
for (var i = 0; i < this.ops.length; i++) {
if (this.ops[i] !== other.ops[i]) { return false; }
for (let i = 0; i < this.ops.length; i++) {
if (this.ops[i] !== other.ops[i]) { return false; }
}
return true;
};
// Operation are essentially lists of ops. There are three types of ops:
//
// * Retain ops: Advance the cursor position by a given number of characters.
// Represented by positive ints.
// * Insert ops: Insert a given string at the current cursor position.
// Represented by strings.
// * Delete ops: Delete the next n characters. Represented by negative ints.
var isRetain = TextOperation.isRetain = function (op) {
return typeof op === 'number' && op > 0;
};
var isInsert = TextOperation.isInsert = function (op) {
return typeof op === 'string';
};
var isDelete = TextOperation.isDelete = function (op) {
return typeof op === 'number' && op < 0;
};
}
// After an operation is constructed, the user of the library can specify the
// actions of an operation (skip/insert/delete) with these three builder
// methods. They all return the operation for convenient chaining.
// Skip over a given number of characters.
TextOperation.prototype.retain = function (n) {
retain(n: number): this {
if (typeof n !== 'number') {
throw new Error("retain expects an integer");
}
if (n === 0) { return this; }
this.baseLength += n;
this.targetLength += n;
if (isRetain(this.ops[this.ops.length-1])) {
if (isRetain(this.ops[this.ops.length - 1])) {
// The last op is a retain op => we can merge them into one op.
this.ops[this.ops.length-1] += n;
(this.ops[this.ops.length - 1] as number) += n;
} else {
// Create a new op.
this.ops.push(n);
}
return this;
};
}
// Insert a string at the current position.
TextOperation.prototype.insert = function (str) {
insert(str: string): this {
if (typeof str !== 'string') {
throw new Error("insert expects a string");
}
if (str === '') { return this; }
this.targetLength += str.length;
var ops = this.ops;
if (isInsert(ops[ops.length-1])) {
const ops = this.ops;
if (isInsert(ops[ops.length - 1])) {
// Merge insert op.
ops[ops.length-1] += str;
} else if (isDelete(ops[ops.length-1])) {
ops[ops.length - 1] += str;
} else if (isDelete(ops[ops.length - 1])) {
// It doesn't matter when an operation is applied whether the operation
// is delete(3), insert("something") or insert("something"), delete(3).
// Here we enforce that in this case, the insert op always comes first.
// This makes all operations that have the same effect when applied to
// a document of the right length equal in respect to the `equals` method.
if (isInsert(ops[ops.length-2])) {
ops[ops.length-2] += str;
if (isInsert(ops[ops.length - 2])) {
ops[ops.length - 2] += str;
} else {
ops[ops.length] = ops[ops.length-1];
ops[ops.length-2] = str;
ops[ops.length] = ops[ops.length - 1];
ops[ops.length - 2] = str;
}
} else {
ops.push(str);
}
return this;
};
}
// Delete a string at the current position.
TextOperation.prototype['delete'] = function (n) {
delete(n: number | string): this {
if (typeof n === 'string') { n = n.length; }
if (typeof n !== 'number') {
throw new Error("delete expects an integer or a string");
@@ -117,77 +118,69 @@ ot.TextOperation = (function () {
if (n === 0) { return this; }
if (n > 0) { n = -n; }
this.baseLength -= n;
if (isDelete(this.ops[this.ops.length-1])) {
this.ops[this.ops.length-1] += n;
if (isDelete(this.ops[this.ops.length - 1])) {
(this.ops[this.ops.length - 1] as number) += n;
} else {
this.ops.push(n);
}
return this;
};
}
// Tests whether this operation has no effect.
TextOperation.prototype.isNoop = function () {
isNoop(): boolean {
return this.ops.length === 0 || (this.ops.length === 1 && isRetain(this.ops[0]));
};
}
// Pretty printing.
TextOperation.prototype.toString = function () {
toString(): string {
// map: build a new array by applying a function to every element in an old
// array.
var map = Array.prototype.map || function (fn) {
var arr = this;
var newArr = [];
for (var i = 0, l = arr.length; i < l; i++) {
newArr[i] = fn(arr[i]);
}
return newArr;
};
return map.call(this.ops, function (op) {
return this.ops.map(function (op) {
if (isRetain(op)) {
return "retain " + op;
} else if (isInsert(op)) {
return "insert '" + op + "'";
} else {
return "delete " + (-op);
return "delete " + (-(op as number));
}
}).join(', ');
};
}
// Converts operation into a JSON value.
TextOperation.prototype.toJSON = function () {
toJSON(): Op[] {
return this.ops;
};
}
// Converts a plain JS object into an operation and validates it.
TextOperation.fromJSON = function (ops) {
var o = new TextOperation();
for (var i = 0, l = ops.length; i < l; i++) {
var op = ops[i];
static fromJSON(ops: Op[]): TextOperation {
const o = new TextOperation();
for (let i = 0, l = ops.length; i < l; i++) {
const op = ops[i];
if (isRetain(op)) {
o.retain(op);
} else if (isInsert(op)) {
o.insert(op);
} else if (isDelete(op)) {
o['delete'](op);
o.delete(op);
} else {
throw new Error("unknown operation: " + JSON.stringify(op));
}
}
return o;
};
}
// Apply an operation to a string, returning a new string. Throws an error if
// there's a mismatch between the input string and the operation.
TextOperation.prototype.apply = function (str) {
var operation = this;
if (str.length !== operation.baseLength) {
apply(str: string): string {
if (str.length !== this.baseLength) {
throw new Error("The operation's base length must be equal to the string's length.");
}
var newStr = [], j = 0;
var strIndex = 0;
var ops = this.ops;
for (var i = 0, l = ops.length; i < l; i++) {
var op = ops[i];
const newStr: string[] = [];
let j = 0;
let strIndex = 0;
const ops = this.ops;
for (let i = 0, l = ops.length; i < l; i++) {
const op = ops[i];
if (isRetain(op)) {
if (strIndex + op > str.length) {
throw new Error("Operation can't retain more characters than are left in the string.");
@@ -199,52 +192,52 @@ ot.TextOperation = (function () {
// Insert string.
newStr[j++] = op;
} else { // delete op
strIndex -= op;
strIndex -= (op as number);
}
}
if (strIndex !== str.length) {
throw new Error("The operation didn't operate on the whole string.");
}
return newStr.join('');
};
}
// Computes the inverse of an operation. The inverse of an operation is the
// operation that reverts the effects of the operation, e.g. when you have an
// operation 'insert("hello "); skip(6);' then the inverse is 'delete("hello ");
// skip(6);'. The inverse should be used for implementing undo.
TextOperation.prototype.invert = function (str) {
var strIndex = 0;
var inverse = new TextOperation();
var ops = this.ops;
for (var i = 0, l = ops.length; i < l; i++) {
var op = ops[i];
invert(str: string): TextOperation {
let strIndex = 0;
const inverse = new TextOperation();
const ops = this.ops;
for (let i = 0, l = ops.length; i < l; i++) {
const op = ops[i];
if (isRetain(op)) {
inverse.retain(op);
strIndex += op;
} else if (isInsert(op)) {
inverse['delete'](op.length);
inverse.delete(op.length);
} else { // delete op
inverse.insert(str.slice(strIndex, strIndex - op));
strIndex -= op;
inverse.insert(str.slice(strIndex, strIndex - (op as number)));
strIndex -= (op as number);
}
}
return inverse;
};
}
// Compose merges two consecutive operations into one operation, that
// preserves the changes of both. Or, in other words, for each input string S
// and a pair of consecutive operations A and B,
// apply(apply(S, A), B) = apply(S, compose(A, B)) must hold.
TextOperation.prototype.compose = function (operation2) {
var operation1 = this;
compose(operation2: TextOperation): TextOperation {
const operation1 = this;
if (operation1.targetLength !== operation2.baseLength) {
throw new Error("The base length of the second operation has to be the target length of the first operation");
}
var operation = new TextOperation(); // the combined operation
var ops1 = operation1.ops, ops2 = operation2.ops; // for fast access
var i1 = 0, i2 = 0; // current index into ops1 respectively ops2
var op1 = ops1[i1++], op2 = ops2[i2++]; // current ops
const operation = new TextOperation(); // the combined operation
const ops1 = operation1.ops, ops2 = operation2.ops; // for fast access
let i1 = 0, i2 = 0; // current index into ops1 respectively ops2
let op1: Op | undefined = ops1[i1++], op2: Op | undefined = ops2[i2++]; // current ops
while (true) {
// Dispatch on the type of op1 and op2
if (typeof op1 === 'undefined' && typeof op2 === 'undefined') {
@@ -253,7 +246,7 @@ ot.TextOperation = (function () {
}
if (isDelete(op1)) {
operation['delete'](op1);
operation.delete(op1);
op1 = ops1[i1++];
continue;
}
@@ -311,15 +304,15 @@ ot.TextOperation = (function () {
}
} else if (isRetain(op1) && isDelete(op2)) {
if (op1 > -op2) {
operation['delete'](op2);
operation.delete(op2);
op1 = op1 + op2;
op2 = ops2[i2++];
} else if (op1 === -op2) {
operation['delete'](op2);
operation.delete(op2);
op1 = ops1[i1++];
op2 = ops2[i2++];
} else {
operation['delete'](op1);
operation.delete(op1);
op2 = op2 + op1;
op1 = ops1[i1++];
}
@@ -332,25 +325,6 @@ ot.TextOperation = (function () {
}
}
return operation;
};
function getSimpleOp (operation, fn) {
var ops = operation.ops;
var isRetain = TextOperation.isRetain;
switch (ops.length) {
case 1:
return ops[0];
case 2:
return isRetain(ops[0]) ? ops[1] : (isRetain(ops[1]) ? ops[0] : null);
case 3:
if (isRetain(ops[0]) && isRetain(ops[2])) { return ops[1]; }
}
return null;
}
function getStartIndex (operation) {
if (isRetain(operation.ops[0])) { return operation.ops[0]; }
return 0;
}
// When you use ctrl-z to undo your latest changes, you expect the program not
@@ -361,11 +335,11 @@ ot.TextOperation = (function () {
// returns true if the operations are consecutive insert operations or both
// operations delete text at the same position. You may want to include other
// factors like the time since the last change in your decision.
TextOperation.prototype.shouldBeComposedWith = function (other) {
shouldBeComposedWith(other: TextOperation): boolean {
if (this.isNoop() || other.isNoop()) { return true; }
var startA = getStartIndex(this), startB = getStartIndex(other);
var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
const startA = getStartIndex(this), startB = getStartIndex(other);
const simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
if (!simpleA || !simpleB) { return false; }
if (isInsert(simpleA) && isInsert(simpleB)) {
@@ -379,16 +353,16 @@ ot.TextOperation = (function () {
}
return false;
};
}
// Decides whether two operations should be composed with each other
// if they were inverted, that is
// `shouldBeComposedWith(a, b) = shouldBeComposedWithInverted(b^{-1}, a^{-1})`.
TextOperation.prototype.shouldBeComposedWithInverted = function (other) {
shouldBeComposedWithInverted(other: TextOperation): boolean {
if (this.isNoop() || other.isNoop()) { return true; }
var startA = getStartIndex(this), startB = getStartIndex(other);
var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
const startA = getStartIndex(this), startB = getStartIndex(other);
const simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
if (!simpleA || !simpleB) { return false; }
if (isInsert(simpleA) && isInsert(simpleB)) {
@@ -400,22 +374,22 @@ ot.TextOperation = (function () {
}
return false;
};
}
// Transform takes two operations A and B that happened concurrently and
// produces two operations A' and B' (in an array) such that
// `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the
// heart of OT.
TextOperation.transform = function (operation1, operation2) {
static transform(operation1: TextOperation, operation2: TextOperation): [TextOperation, TextOperation] {
if (operation1.baseLength !== operation2.baseLength) {
throw new Error("Both operations have to have the same base length");
}
var operation1prime = new TextOperation();
var operation2prime = new TextOperation();
var ops1 = operation1.ops, ops2 = operation2.ops;
var i1 = 0, i2 = 0;
var op1 = ops1[i1++], op2 = ops2[i2++];
const operation1prime = new TextOperation();
const operation2prime = new TextOperation();
const ops1 = operation1.ops, ops2 = operation2.ops;
let i1 = 0, i2 = 0;
let op1: Op | undefined = ops1[i1++], op2: Op | undefined = ops2[i2++];
while (true) {
// At every iteration of the loop, the imaginary cursor that both
// operation1 and operation2 have that operates on the input string must
@@ -449,7 +423,7 @@ ot.TextOperation = (function () {
throw new Error("Cannot compose operations: first operation is too long.");
}
var minl;
let minl: number;
if (isRetain(op1) && isRetain(op2)) {
// Simple case: retain/retain
if (op1 > op2) {
@@ -496,7 +470,7 @@ ot.TextOperation = (function () {
op2 = op2 + op1;
op1 = ops1[i1++];
}
operation1prime['delete'](minl);
operation1prime.delete(minl);
} else if (isRetain(op1) && isDelete(op2)) {
if (op1 > -op2) {
minl = -op2;
@@ -511,20 +485,33 @@ ot.TextOperation = (function () {
op2 = op2 + op1;
op1 = ops1[i1++];
}
operation2prime['delete'](minl);
operation2prime.delete(minl);
} else {
throw new Error("The two operations aren't compatible");
}
}
return [operation1prime, operation2prime];
};
}
}
return TextOperation;
function getSimpleOp(operation: TextOperation): Op | null {
const ops = operation.ops;
switch (ops.length) {
case 1:
return ops[0];
case 2:
return isRetain(ops[0]) ? ops[1] : (isRetain(ops[1]) ? ops[0] : null);
case 3:
if (isRetain(ops[0]) && isRetain(ops[2])) { return ops[1]; }
}
return null;
}
}());
function getStartIndex(operation: TextOperation): number {
if (isRetain(operation.ops[0])) { return operation.ops[0]; }
return 0;
}
// Export for CommonJS
if (typeof module === 'object') {
module.exports = ot.TextOperation;
}
export = TextOperation;
-80
View File
@@ -1,80 +0,0 @@
if (typeof ot === 'undefined') {
// Export for browsers
var ot = {};
}
ot.WrappedOperation = (function (global) {
'use strict';
// A WrappedOperation contains an operation and corresponing metadata.
function WrappedOperation (operation, meta) {
this.wrapped = operation;
this.meta = meta;
}
WrappedOperation.prototype.apply = function () {
return this.wrapped.apply.apply(this.wrapped, arguments);
};
WrappedOperation.prototype.invert = function () {
var meta = this.meta;
return new WrappedOperation(
this.wrapped.invert.apply(this.wrapped, arguments),
meta && typeof meta === 'object' && typeof meta.invert === 'function' ?
meta.invert.apply(meta, arguments) : meta
);
};
// Copy all properties from source to target.
function copy (source, target) {
for (var key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
}
function composeMeta (a, b) {
if (a && typeof a === 'object') {
if (typeof a.compose === 'function') { return a.compose(b); }
var meta = {};
copy(a, meta);
copy(b, meta);
return meta;
}
return b;
}
WrappedOperation.prototype.compose = function (other) {
return new WrappedOperation(
this.wrapped.compose(other.wrapped),
composeMeta(this.meta, other.meta)
);
};
function transformMeta (meta, operation) {
if (meta && typeof meta === 'object') {
if (typeof meta.transform === 'function') {
return meta.transform(operation);
}
}
return meta;
}
WrappedOperation.transform = function (a, b) {
var transform = a.wrapped.constructor.transform;
var pair = transform(a.wrapped, b.wrapped);
return [
new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)),
new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped))
];
};
return WrappedOperation;
}(this));
// Export for CommonJS
if (typeof module === 'object') {
module.exports = ot.WrappedOperation;
}
+90
View File
@@ -0,0 +1,90 @@
// Export for browsers
'use strict';
import TextOperation = require('./text-operation');
interface MetaWithInvert {
invert(...args: any[]): any;
}
interface MetaWithCompose {
compose(other: any): any;
}
interface MetaWithTransform {
transform(operation: TextOperation): any;
}
type Meta = any;
// A WrappedOperation contains an operation and corresponing metadata.
class WrappedOperation {
wrapped: TextOperation;
meta: Meta;
constructor(operation: TextOperation, meta?: Meta) {
this.wrapped = operation;
this.meta = meta;
}
apply(...args: [string]): string {
return this.wrapped.apply(...args);
}
invert(...args: [string]): WrappedOperation {
const meta = this.meta;
return new WrappedOperation(
this.wrapped.invert(...args),
meta && typeof meta === 'object' && typeof meta.invert === 'function' ?
meta.invert(...args) : meta
);
}
compose(other: WrappedOperation): WrappedOperation {
return new WrappedOperation(
this.wrapped.compose(other.wrapped),
composeMeta(this.meta, other.meta)
);
}
static transform(a: WrappedOperation, b: WrappedOperation): [WrappedOperation, WrappedOperation] {
const pair = TextOperation.transform(a.wrapped, b.wrapped);
return [
new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)),
new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped))
];
}
}
// Copy all properties from source to target.
function copy(source: Record<string, any>, target: Record<string, any>): void {
for (const key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
}
function composeMeta(a: Meta, b: Meta): Meta {
if (a && typeof a === 'object') {
if (typeof a.compose === 'function') { return a.compose(b); }
const meta: Record<string, any> = {};
copy(a, meta);
copy(b, meta);
return meta;
}
return b;
}
function transformMeta(meta: Meta, operation: TextOperation): Meta {
if (meta && typeof meta === 'object') {
if (typeof meta.transform === 'function') {
return meta.transform(operation);
}
}
return meta;
}
// Export for CommonJS
export = WrappedOperation;
+6 -6
View File
@@ -1,7 +1,7 @@
const promClient = require('prom-client')
const realtime = require('./realtime')
import promClient = require('prom-client')
import realtime = require('./realtime')
exports.setupCustomPrometheusMetrics = function () {
export function setupCustomPrometheusMetrics (): void {
const onlineNotes = new promClient.Gauge({
name: 'hedgedoc_online_notes',
help: 'Notes currently being edited'
@@ -28,12 +28,12 @@ exports.setupCustomPrometheusMetrics = function () {
name: 'hedgedoc_connection_busy',
help: 'Indicates that realtime currently connecting'
})
const connectionSocketQueueLength = new promClient.Gauge({
const connectionSocketQueueGauge = new promClient.Gauge({
name: 'hedgedoc_connection_socket_queue_length',
help: 'Length of connection socket queue',
// The last gauge provides the collect callback for all metrics
collect () {
realtime.getStatus(function (data) {
realtime.getStatus(function (data: any) {
onlineNotes.set(data.onlineNotes)
onlineSessions.set({ type: 'all' }, data.onlineUsers)
onlineSessions.set({ type: 'signed-in' }, data.onlineRegisteredUsers)
@@ -42,7 +42,7 @@ exports.setupCustomPrometheusMetrics = function () {
notesCount.set(data.notesCount)
registeredUsers.set(data.registeredUsers)
isConnectionBusy.set(data.isConnectionBusy ? 1 : 0)
connectionSocketQueueLength.set(data.connectionSocketQueueLength)
connectionSocketQueueGauge.set(data.connectionSocketQueueLength)
})
}
})
+78 -78
View File
@@ -1,25 +1,25 @@
'use strict'
// realtime
// external modules
const cookie = require('cookie')
const cookieParser = require('cookie-parser')
const async = require('async')
const Chance = require('chance')
import cookie = require('cookie')
import cookieParser = require('cookie-parser')
import async = require('async')
import Chance = require('chance')
const chance = new Chance()
const moment = require('moment')
import moment = require('moment')
// core
const config = require('./config')
const logger = require('./logger')
const history = require('./history')
const models = require('./models')
import config = require('./config')
import logger = require('./logger')
import history = require('./history')
import models = require('./models')
// ot
const ot = require('./ot')
import ot = require('./ot')
// public
const realtime = {
io: null,
io: null as any,
onAuthorizeSuccess,
onAuthorizeFail,
secure,
@@ -29,16 +29,16 @@ const realtime = {
maintenance: true
}
function onAuthorizeSuccess (data, accept) {
function onAuthorizeSuccess (data: any, accept: () => void): void {
accept()
}
function onAuthorizeFail (data, message, error, accept) {
function onAuthorizeFail (data: any, message: string, error: any, accept: () => void): void {
accept() // accept whether authorize or not to allow anonymous usage
}
// secure the origin by the cookie
function secure (socket, next) {
function secure (socket: any, next: (err?: Error) => void): void {
try {
const handshakeData = socket.request
if (handshakeData.headers.cookie) {
@@ -60,7 +60,7 @@ function secure (socket, next) {
}
}
function emitCheck (note) {
function emitCheck (note: any): void {
const out = {
title: note.title,
updatetime: note.updatetime,
@@ -73,8 +73,8 @@ function emitCheck (note) {
}
// actions
const users = {}
const notes = {}
const users: Record<string, any> = {}
const notes: Record<string, any> = {}
// update when the note is dirty
setInterval(function () {
async.each(Object.keys(notes), function (key, callback) {
@@ -84,7 +84,7 @@ setInterval(function () {
note.server.isDirty = false
updateNote(note, function (err, _note) {
// handle when note already been clean up
if (!notes[key] || !notes[key].server) return callback(null, null)
if (!notes[key] || !notes[key].server) return callback(null)
if (!_note) {
realtime.io.to(note.id).emit('info', {
code: 404
@@ -100,26 +100,26 @@ setInterval(function () {
}, 0)
}
}
return callback(err, null)
return callback(err)
}
note.updatetime = moment(_note.lastchangeAt).valueOf()
emitCheck(note)
return callback(null, null)
return callback(null)
})
} else {
return callback(null, null)
return callback(null)
}
}, function (err) {
if (err) return logger.error('updater error', err)
})
}, 1000)
function updateNote (note, callback) {
function updateNote (note: any, callback: (err: any, note: any) => void): void {
models.Note.findOne({
where: {
id: note.id
}
}).then(function (_note) {
}).then(function (_note: any) {
if (!_note) return callback(null, null)
// update user note history
const tempUsers = Object.assign({}, note.tempUsers)
@@ -133,11 +133,11 @@ function updateNote (note, callback) {
where: {
id: note.lastchangeuser
}
}).then(function (user) {
}).then(function (user: any) {
if (!user) return callback(null, null)
note.lastchangeuserprofile = models.User.getProfile(user)
return finishUpdateNote(note, _note, callback)
}).catch(function (err) {
}).catch(function (err: any) {
logger.error(err)
return callback(err, null)
})
@@ -148,13 +148,13 @@ function updateNote (note, callback) {
note.lastchangeuserprofile = null
return finishUpdateNote(note, _note, callback)
}
}).catch(function (err) {
}).catch(function (err: any) {
logger.error(err)
return callback(err, null)
})
}
function finishUpdateNote (note, _note, callback) {
function finishUpdateNote (note: any, _note: any, callback: (err: any, note: any) => void): void {
if (!note || !note.server) return callback(null, null)
const body = note.server.document
const title = note.title = models.Note.parseNoteTitle(body)
@@ -165,10 +165,10 @@ function finishUpdateNote (note, _note, callback) {
lastchangeuserId: note.lastchangeuser,
lastchangeAt: Date.now()
}
_note.update(values).then(function (_note) {
_note.update(values).then(function (_note: any) {
saverSleep = false
return callback(null, _note)
}).catch(function (err) {
}).catch(function (err: any) {
logger.error(err)
return callback(err, null)
})
@@ -189,7 +189,7 @@ setInterval(function () {
disconnectSocketQueue.push(socket)
disconnect(socket)
}
return callback(null, null)
return callback(null)
}, function (err) {
if (err) return logger.error('cleaner error', err)
})
@@ -199,7 +199,7 @@ let saverSleep = false
// save note revision in interval
setInterval(function () {
if (saverSleep) return
models.Revision.saveAllNotesRevision(function (err, notes) {
models.Revision.saveAllNotesRevision(function (err: any, notes: any) {
if (err) return logger.error('revision saver failed: ' + err)
if (notes && notes.length <= 0) {
saverSleep = true
@@ -207,11 +207,11 @@ setInterval(function () {
})
}, 60000 * 5)
function getStatus (callback) {
models.Note.count().then(function (notecount) {
const distinctaddresses = []
const regaddresses = []
const distinctregaddresses = []
function getStatus (callback: (data: any) => void): void {
models.Note.count().then(function (notecount: any) {
const distinctaddresses: any[] = []
const regaddresses: any[] = []
const distinctregaddresses: any[] = []
Object.keys(users).forEach(function (key) {
const user = users[key]
if (!user) return
@@ -239,7 +239,7 @@ function getStatus (callback) {
}
}
})
models.User.count().then(function (regcount) {
models.User.count().then(function (regcount: any) {
return callback
? callback({
@@ -256,22 +256,22 @@ function getStatus (callback) {
disconnectSocketQueueLength: disconnectSocketQueue.length
})
: null
}).catch(function (err) {
}).catch(function (err: any) {
return logger.error('count user failed: ' + err)
})
}).catch(function (err) {
}).catch(function (err: any) {
return logger.error('count note failed: ' + err)
})
}
function isReady () {
function isReady (): boolean {
return realtime.io &&
Object.keys(notes).length === 0 && Object.keys(users).length === 0 &&
connectionSocketQueue.length === 0 && !isConnectionBusy &&
disconnectSocketQueue.length === 0 && !isDisconnectBusy
}
function extractNoteIdFromSocket (socket) {
function extractNoteIdFromSocket (socket: any): string | false {
if (!socket || !socket.handshake) {
return false
}
@@ -282,21 +282,21 @@ function extractNoteIdFromSocket (socket) {
}
}
function parseNoteIdFromSocket (socket, callback) {
function parseNoteIdFromSocket (socket: any, callback: (err: any, id: string | null) => void): void {
const noteId = extractNoteIdFromSocket(socket)
if (!noteId) {
return callback(null, null)
}
models.Note.parseNoteId(noteId, function (err, id) {
models.Note.parseNoteId(noteId, function (err: any, id: any) {
if (err || !id) return callback(err, id)
return callback(null, id)
})
}
function emitOnlineUsers (socket) {
function emitOnlineUsers (socket: any): void {
const noteId = socket.noteId
if (!noteId || !notes[noteId]) return
const users = []
const users: any[] = []
Object.keys(notes[noteId].users).forEach(function (key) {
const user = notes[noteId].users[key]
if (user) { users.push(buildUserOutData(user)) }
@@ -307,7 +307,7 @@ function emitOnlineUsers (socket) {
realtime.io.to(noteId).emit('online users', out)
}
function emitUserStatus (socket) {
function emitUserStatus (socket: any): void {
const noteId = socket.noteId
const user = users[socket.id]
if (!noteId || !notes[noteId] || !user) return
@@ -315,7 +315,7 @@ function emitUserStatus (socket) {
socket.broadcast.to(noteId).emit('user status', out)
}
function emitRefresh (socket) {
function emitRefresh (socket: any): void {
const noteId = socket.noteId
if (!noteId || !notes[noteId]) return
const note = notes[noteId]
@@ -335,7 +335,7 @@ function emitRefresh (socket) {
socket.emit('refresh', out)
}
function isDuplicatedInSocketQueue (queue, socket) {
function isDuplicatedInSocketQueue (queue: any[], socket: any): boolean {
for (let i = 0; i < queue.length; i++) {
if (queue[i] && queue[i].id === socket.id) {
return true
@@ -344,7 +344,7 @@ function isDuplicatedInSocketQueue (queue, socket) {
return false
}
function clearSocketQueue (queue, socket) {
function clearSocketQueue (queue: any[], socket: any): void {
for (let i = 0; i < queue.length; i++) {
if (!queue[i] || queue[i].id === socket.id) {
queue.splice(i, 1)
@@ -353,7 +353,7 @@ function clearSocketQueue (queue, socket) {
}
}
function connectNextSocket () {
function connectNextSocket (): void {
setTimeout(function () {
isConnectionBusy = false
if (connectionSocketQueue.length > 0) {
@@ -362,7 +362,7 @@ function connectNextSocket () {
}, 1)
}
function checkViewPermission (req, note) {
function checkViewPermission (req: any, note: any): boolean {
if (note.permission === 'private') {
if (req.user && req.user.logged_in && req.user.id === note.owner) { return true } else { return false }
} else if (note.permission === 'limited' || note.permission === 'protected') {
@@ -373,11 +373,11 @@ function checkViewPermission (req, note) {
}
let isConnectionBusy = false
const connectionSocketQueue = []
const connectionSocketQueue: any[] = []
let isDisconnectBusy = false
const disconnectSocketQueue = []
const disconnectSocketQueue: any[] = []
function finishConnection (socket, noteId, socketId) {
function finishConnection (socket: any, noteId: string, socketId: string): void {
// check view permission
if (!checkViewPermission(socket.request, notes[noteId])) {
return failConnection(403, 'connection forbidden', socket)
@@ -416,7 +416,7 @@ function finishConnection (socket, noteId, socketId) {
}
}
function startConnection (socket) {
function startConnection (socket: any): void {
if (isConnectionBusy) return
isConnectionBusy = true
@@ -446,7 +446,7 @@ function startConnection (socket) {
id: noteId
},
include
}).then(function (note) {
}).then(function (note: any) {
// if client disconnected while we waited for the note, disconnect() cleaned up users[socket.id]
if (!users[socket.id]) {
clearSocketQueue(connectionSocketQueue, socket)
@@ -468,7 +468,7 @@ function startConnection (socket) {
const updatetime = note.lastchangeAt
const server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback)
const authors = {}
const authors: Record<string, any> = {}
for (let i = 0; i < note.authors.length; i++) {
const author = note.authors[i]
const profile = models.User.getProfile(author.user)
@@ -502,7 +502,7 @@ function startConnection (socket) {
}
return finishConnection(socket, noteId, socket.id)
}).catch(function (err) {
}).catch(function (err: any) {
return failConnection(500, err, socket)
})
} else {
@@ -510,7 +510,7 @@ function startConnection (socket) {
}
}
function failConnection (code, err, socket) {
function failConnection (code: number, err: any, socket: any): void {
logger.error(err)
// clear error socket in queue
clearSocketQueue(connectionSocketQueue, socket)
@@ -522,7 +522,7 @@ function failConnection (code, err, socket) {
return socket.disconnect(true)
}
function disconnect (socket) {
function disconnect (socket: any): void {
if (isDisconnectBusy) return
isDisconnectBusy = true
@@ -586,7 +586,7 @@ function disconnect (socket) {
}
}
function buildUserOutData (user) {
function buildUserOutData (user: any): any {
const out = {
id: user.id,
login: user.login,
@@ -601,7 +601,7 @@ function buildUserOutData (user) {
return out
}
function updateUserData (socket, user) {
function updateUserData (socket: any, user: any): void {
// retrieve user data from passport
if (socket.request.user && socket.request.user.logged_in) {
const profile = models.User.getProfile(socket.request.user)
@@ -616,7 +616,7 @@ function updateUserData (socket, user) {
}
}
function ifMayEdit (socket, callback) {
function ifMayEdit (socket: any, callback: (mayEdit: boolean) => void): void {
const noteId = socket.noteId
if (!noteId || !notes[noteId]) return
const note = notes[noteId]
@@ -646,7 +646,7 @@ function ifMayEdit (socket, callback) {
return callback(mayEdit)
}
function operationCallback (socket, operation) {
function operationCallback (socket: any, operation: any): void {
const noteId = socket.noteId
if (!noteId || !notes[noteId]) return
const note = notes[noteId]
@@ -667,7 +667,7 @@ function operationCallback (socket, operation) {
userId,
color: user.color
}
}).spread(function (author, created) {
}).spread(function (author: any, created: any) {
if (author) {
note.authors[author.userId] = {
userid: author.userId,
@@ -676,7 +676,7 @@ function operationCallback (socket, operation) {
name: user.name
}
}
}).catch(function (err) {
}).catch(function (err: any) {
return logger.error('operation callback failed: ' + err)
})
}
@@ -688,12 +688,12 @@ function operationCallback (socket, operation) {
})
}
function updateHistory (userId, note, time) {
function updateHistory (userId: string, note: any, time?: number): void {
const noteId = note.alias ? note.alias : models.Note.encodeNoteId(note.id)
if (note.server) history.updateHistory(userId, noteId, note.server.document, time)
}
function connection (socket) {
function connection (socket: any): void {
if (realtime.maintenance) return
parseNoteIdFromSocket(socket, function (err, noteId) {
if (err) {
@@ -754,7 +754,7 @@ function connection (socket) {
})
// received user status
socket.on('user status', function (data) {
socket.on('user status', function (data: any) {
const noteId = socket.noteId
const user = users[socket.id]
if (!noteId || !notes[noteId] || !user) return
@@ -767,7 +767,7 @@ function connection (socket) {
})
// received note permission change request
socket.on('permission', function (permission) {
socket.on('permission', function (permission: any) {
// need login to do more actions
if (socket.request.user && socket.request.user.logged_in) {
const noteId = socket.noteId
@@ -783,7 +783,7 @@ function connection (socket) {
where: {
id: noteId
}
}).then(function (count) {
}).then(function (count: any) {
if (!count) {
return
}
@@ -805,7 +805,7 @@ function connection (socket) {
}
}
}
}).catch(function (err) {
}).catch(function (err: any) {
return logger.error('update note permission failed: ' + err)
})
}
@@ -825,7 +825,7 @@ function connection (socket) {
where: {
id: noteId
}
}).then(function (count) {
}).then(function (count: any) {
if (!count) return
for (let i = 0, l = note.socks.length; i < l; i++) {
const sock = note.socks[i]
@@ -836,7 +836,7 @@ function connection (socket) {
}, 0)
}
}
}).catch(function (err) {
}).catch(function (err: any) {
return logger.error('delete note failed: ' + err)
})
}
@@ -858,7 +858,7 @@ function connection (socket) {
socket.on('online users', function () {
const noteId = socket.noteId
if (!noteId || !notes[noteId]) return
const users = []
const users: any[] = []
Object.keys(notes[noteId].users).forEach(function (key) {
const user = notes[noteId].users[key]
if (user) { users.push(buildUserOutData(user)) }
@@ -878,7 +878,7 @@ function connection (socket) {
})
// received cursor focus
socket.on('cursor focus', function (data) {
socket.on('cursor focus', function (data: any) {
const noteId = socket.noteId
const user = users[socket.id]
if (!noteId || !notes[noteId] || !user) return
@@ -888,7 +888,7 @@ function connection (socket) {
})
// received cursor activity
socket.on('cursor activity', function (data) {
socket.on('cursor activity', function (data: any) {
const noteId = socket.noteId
const user = users[socket.id]
if (!noteId || !notes[noteId] || !user) return
@@ -917,4 +917,4 @@ function connection (socket) {
})
}
module.exports = realtime
export = realtime
+50 -49
View File
@@ -1,30 +1,24 @@
'use strict'
// response
// external modules
const fs = require('fs')
const path = require('path')
import type { Request, Response, NextFunction } from 'express'
import * as fs from 'fs'
import * as path from 'path'
// core
const config = require('./config')
const logger = require('./logger')
const models = require('./models')
const noteUtil = require('./web/note/util')
const errors = require('./errors')
import config = require('./config')
import logger = require('./logger')
import models = require('./models')
import * as noteUtil from './web/note/util'
import * as errors from './errors'
// public
const response = {
showIndex,
githubActions,
gitlabActions
}
function showIndex (req, res, next) {
const authStatus = req.isAuthenticated()
function showIndex (req: Request, res: Response, _next: NextFunction): void {
const authStatus: boolean = req.isAuthenticated()
const deleteToken = ''
const data = {
const data: Record<string, unknown> = {
signin: authStatus,
infoMessage: req.flash('info'),
errorMessage: req.flash('error'),
infoMessage: (req as any).flash('info'),
errorMessage: (req as any).flash('error'),
imprint: fs.existsSync(path.join(config.docsPath, 'imprint.md')),
privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')),
termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')),
@@ -34,9 +28,9 @@ function showIndex (req, res, next) {
if (authStatus) {
models.User.findOne({
where: {
id: req.user.id
id: (req as any).user.id
}
}).then(function (user) {
}).then(function (user: any) {
if (user) {
data.deleteToken = user.deleteToken
res.render('index.ejs', data)
@@ -47,22 +41,7 @@ function showIndex (req, res, next) {
}
}
function githubActions (req, res, next) {
const noteId = req.params.noteId
noteUtil.findNote(req, res, function (note) {
const action = req.params.action
switch (action) {
case 'gist':
githubActionGist(req, res, note)
break
default:
res.redirect(config.serverURL + '/' + noteId)
break
}
})
}
function githubActionGist (req, res, note) {
function githubActionGist (req: Request, res: Response, note: Record<string, unknown>): void {
const code = req.query.code
const state = req.query.state
if (!code || !state) {
@@ -92,10 +71,10 @@ function githubActionGist (req, res, note) {
if (!accessToken) {
throw new Error('forbidden')
}
const content = note.content
const content = note.content as string
const title = models.Note.decodeTitle(note.title)
const filename = title.replace('/', ' ') + '.md'
const gist = {
const gist: { files: Record<string, { content: string }> } = {
files: {}
}
gist.files[filename] = {
@@ -130,13 +109,13 @@ function githubActionGist (req, res, note) {
}
}
function gitlabActions (req, res, next) {
function githubActions (req: Request, res: Response, next: NextFunction): void {
const noteId = req.params.noteId
noteUtil.findNote(req, res, function (note) {
noteUtil.findNote(req, res, function (note: any) {
const action = req.params.action
switch (action) {
case 'projects':
gitlabActionProjects(req, res, note)
case 'gist':
githubActionGist(req, res, note)
break
default:
res.redirect(config.serverURL + '/' + noteId)
@@ -145,17 +124,17 @@ function gitlabActions (req, res, next) {
})
}
function gitlabActionProjects (req, res, note) {
function gitlabActionProjects (req: Request, res: Response, _note: Record<string, unknown>): void {
if (req.isAuthenticated()) {
models.User.findOne({
where: {
id: req.user.id
id: (req as any).user.id
}
}).then(function (user) {
}).then(function (user: any) {
if (!user) {
return errors.errorNotFound(res)
}
const ret = {
const ret: Record<string, unknown> = {
baseURL: config.gitlab.baseURL,
version: config.gitlab.version,
accesstoken: user.accessToken,
@@ -175,7 +154,7 @@ function gitlabActionProjects (req, res, note) {
}).catch(err => {
logger.error('gitlab action projects failed: ', err)
})
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error('gitlab action projects failed: ' + err)
return errors.errorInternalError(res)
})
@@ -184,4 +163,26 @@ function gitlabActionProjects (req, res, note) {
}
}
module.exports = response
function gitlabActions (req: Request, res: Response, next: NextFunction): void {
const noteId = req.params.noteId
noteUtil.findNote(req, res, function (note: any) {
const action = req.params.action
switch (action) {
case 'projects':
gitlabActionProjects(req, res, note)
break
default:
res.redirect(config.serverURL + '/' + noteId)
break
}
})
}
// public
const response = {
showIndex,
githubActions,
gitlabActions
}
export = response
+15
View File
@@ -0,0 +1,15 @@
declare module 'cheerio' {
function load(html: string): any;
export = load;
}
declare module 'umzug' {
class Umzug {
constructor(options: any);
up(): Promise<any[]>;
down(): Promise<any[]>;
pending(): Promise<any[]>;
executed(): Promise<any[]>;
}
export = Umzug;
}
+9 -5
View File
@@ -1,16 +1,20 @@
'use strict'
exports.isSQLite = function isSQLite (sequelize) {
import type { Request, Response, NextFunction, RequestHandler } from 'express'
export function isSQLite (sequelize: any): boolean {
return sequelize.options.dialect === 'sqlite'
}
exports.isMySQL = function isMySQL (sequelize) {
export function isMySQL (sequelize: any): boolean {
return ['mysql', 'mariadb'].includes(sequelize.options.dialect)
}
exports.getImageMimeType = function getImageMimeType (imagePath) {
export function getImageMimeType (imagePath: string): string | undefined {
const fileExtension = /[^.]+$/.exec(imagePath)
if (!fileExtension) return undefined
switch (fileExtension[0].toLowerCase()) {
case 'bmp':
return 'image/bmp'
@@ -30,8 +34,8 @@ exports.getImageMimeType = function getImageMimeType (imagePath) {
}
}
exports.useUnless = function excludeRoute (paths, middleware) {
return function (req, res, next) {
export function useUnless (paths: string[], middleware: RequestHandler): RequestHandler {
return function (req: Request, res: Response, next: NextFunction) {
if (paths.includes(req.path)) {
return next()
}
@@ -1,12 +1,13 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
const config = require('../../../config')
const { passportGeneralCallback } = require('../utils')
import config = require('../../../config')
import { passportGeneralCallback } from '../utils'
const dropboxAuth = module.exports = Router()
const dropboxAuth = Router()
passport.use(new DropboxStrategy({
apiVersion: '2',
@@ -17,7 +18,7 @@ passport.use(new DropboxStrategy({
pkce: true
}, passportGeneralCallback))
dropboxAuth.get('/auth/dropbox', function (req, res, next) {
dropboxAuth.get('/auth/dropbox', function (req: Request, res: Response, next: NextFunction) {
passport.authenticate('dropbox-oauth2')(req, res, next)
})
@@ -28,3 +29,5 @@ dropboxAuth.get('/auth/dropbox/callback',
failureRedirect: config.serverURL + '/'
})
)
export = dropboxAuth
@@ -1,29 +1,30 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
const validator = require('validator')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
import validator = require('validator')
const LocalStrategy = require('passport-local').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const { urlencodedParser } = require('../../utils')
const errors = require('../../../errors')
const rateLimit = require('../../middleware/rateLimit')
import config = require('../../../config')
import models = require('../../../models')
import logger = require('../../../logger')
import { urlencodedParser } from '../../utils'
import * as errors from '../../../errors'
import rateLimit = require('../../middleware/rateLimit')
const emailAuth = module.exports = Router()
const emailAuth = Router()
passport.use(new LocalStrategy({
usernameField: 'email'
}, function (email, password, done) {
}, function (email: string, password: string, done: Function) {
if (!validator.isEmail(email)) return done(null, false)
models.User.findOne({
where: {
email
}
}).then(function (user) {
}).then(function (user: any) {
if (!user) return done(null, false)
user.verifyPassword(password).then(verified => {
user.verifyPassword(password).then((verified: boolean) => {
if (verified) {
return done(null, user)
} else {
@@ -31,14 +32,14 @@ passport.use(new LocalStrategy({
return done(null, false)
}
})
}).catch(function (err) {
}).catch(function (err: any) {
logger.error(err)
return done(err)
})
}))
if (config.allowEmailRegister) {
emailAuth.post('/register', rateLimit.userEndpoints, urlencodedParser, function (req, res, next) {
emailAuth.post('/register', rateLimit.userEndpoints, urlencodedParser, function (req: Request, res: Response, next: NextFunction) {
if (!req.body.email || !req.body.password) return errors.errorBadRequest(res)
if (!validator.isEmail(req.body.email)) return errors.errorBadRequest(res)
models.User.findOrCreate({
@@ -48,23 +49,23 @@ if (config.allowEmailRegister) {
defaults: {
password: req.body.password
}
}).spread(function (user, created) {
}).spread(function (user: any, created: boolean) {
if (user && created) {
logger.debug('user registered: ' + user.id)
req.flash('info', "You've successfully registered, please sign in.")
;(req as any).flash('info', "You've successfully registered, please sign in.")
return res.redirect(config.serverURL + '/')
}
logger.debug('registration failed. user: ', user)
req.flash('error', 'Failed to register your account.')
;(req as any).flash('error', 'Failed to register your account.')
return res.redirect(config.serverURL + '/')
}).catch(function (err) {
}).catch(function (err: any) {
logger.error('auth callback failed: ' + err)
return errors.errorInternalError(res)
})
})
}
emailAuth.post('/login', rateLimit.userEndpoints, urlencodedParser, function (req, res, next) {
emailAuth.post('/login', rateLimit.userEndpoints, urlencodedParser, function (req: Request, res: Response, next: NextFunction) {
if (!req.body.email || !req.body.password) return errors.errorBadRequest(res)
if (!validator.isEmail(req.body.email)) return errors.errorBadRequest(res)
passport.authenticate('local', {
@@ -73,3 +74,5 @@ emailAuth.post('/login', rateLimit.userEndpoints, urlencodedParser, function (re
failureFlash: 'Invalid email or password.'
})(req, res, next)
})
export = emailAuth
@@ -1,13 +1,14 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const FacebookStrategy = require('passport-facebook').Strategy
const config = require('../../../config')
const { passportGeneralCallback } = require('../utils')
import config = require('../../../config')
import { passportGeneralCallback } from '../utils'
const facebookAuth = module.exports = Router()
const facebookAuth = Router()
passport.use(new FacebookStrategy({
clientID: config.facebook.clientID,
@@ -17,7 +18,7 @@ passport.use(new FacebookStrategy({
pkce: true
}, passportGeneralCallback))
facebookAuth.get('/auth/facebook', function (req, res, next) {
facebookAuth.get('/auth/facebook', function (req: Request, res: Response, next: NextFunction) {
passport.authenticate('facebook')(req, res, next)
})
@@ -28,3 +29,5 @@ facebookAuth.get('/auth/facebook/callback',
failureRedirect: config.serverURL + '/'
})
)
export = facebookAuth
@@ -1,13 +1,14 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const GithubStrategy = require('passport-github').Strategy
const config = require('../../../config')
const response = require('../../../response')
const { passportGeneralCallback } = require('../utils')
import config = require('../../../config')
import response = require('../../../response')
import { passportGeneralCallback } from '../utils'
const githubAuth = module.exports = Router()
const githubAuth = Router()
passport.use(new GithubStrategy({
clientID: config.github.clientID,
@@ -17,7 +18,7 @@ passport.use(new GithubStrategy({
state: true
}, passportGeneralCallback))
githubAuth.get('/auth/github', function (req, res, next) {
githubAuth.get('/auth/github', function (req: Request, res: Response, next: NextFunction) {
passport.authenticate('github')(req, res, next)
})
@@ -31,3 +32,5 @@ githubAuth.get('/auth/github/callback',
// github callback actions
githubAuth.get('/auth/github/callback/:noteId/:action', response.githubActions)
export = githubAuth
@@ -1,13 +1,14 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const GitlabStrategy = require('passport-gitlab2').Strategy
const config = require('../../../config')
const response = require('../../../response')
const { passportGeneralCallback } = require('../utils')
import config = require('../../../config')
import response = require('../../../response')
import { passportGeneralCallback } from '../utils'
const gitlabAuth = module.exports = Router()
const gitlabAuth = Router()
passport.use(new GitlabStrategy({
baseURL: config.gitlab.baseURL,
@@ -19,7 +20,7 @@ passport.use(new GitlabStrategy({
state: true
}, passportGeneralCallback))
gitlabAuth.get('/auth/gitlab', function (req, res, next) {
gitlabAuth.get('/auth/gitlab', function (req: Request, res: Response, next: NextFunction) {
passport.authenticate('gitlab')(req, res, next)
})
@@ -35,3 +36,5 @@ if (!config.gitlab.scope || config.gitlab.scope === 'api') {
// gitlab callback actions
gitlabAuth.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions)
}
export = gitlabAuth
@@ -1,12 +1,13 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const GoogleStrategy = require('passport-google-oauth20').Strategy
const config = require('../../../config')
const { passportGeneralCallback } = require('../utils')
import config = require('../../../config')
import { passportGeneralCallback } from '../utils'
const googleAuth = module.exports = Router()
const googleAuth = Router()
passport.use(new GoogleStrategy({
clientID: config.google.clientID,
@@ -17,7 +18,7 @@ passport.use(new GoogleStrategy({
state: true
}, passportGeneralCallback))
googleAuth.get('/auth/google', function (req, res, next) {
googleAuth.get('/auth/google', function (req: Request, res: Response, next: NextFunction) {
passport.authenticate('google', { scope: ['profile'], hostedDomain: config.google.hostedDomain })(req, res, next)
})
// google auth callback
@@ -27,3 +28,5 @@ googleAuth.get('/auth/google/callback',
failureRedirect: config.serverURL + '/'
})
)
export = googleAuth
+15 -12
View File
@@ -1,26 +1,27 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response } from 'express'
import { Router } from 'express'
import passport = require('passport')
const config = require('../../config')
const logger = require('../../logger')
const models = require('../../models')
import config = require('../../config')
import logger = require('../../logger')
import models = require('../../models')
const authRouter = module.exports = Router()
const authRouter = Router()
// serialize and deserialize
passport.serializeUser(function (user, done) {
passport.serializeUser(function (user: any, done: Function) {
logger.info('serializeUser: ' + user.id)
return done(null, user.id)
})
passport.deserializeUser(function (id, done) {
passport.deserializeUser(function (id: string, done: Function) {
models.User.findOne({
where: {
id
}
}).then(function (user) {
}).then(function (user: any) {
// Don't die on non-existent user
if (user == null) {
return done(null, false, { message: 'Invalid UserID' })
@@ -28,7 +29,7 @@ passport.deserializeUser(function (id, done) {
logger.info('deserializeUser: ' + user.id)
return done(null, user)
}).catch(function (err) {
}).catch(function (err: any) {
logger.error(err)
return done(err, null)
})
@@ -48,11 +49,13 @@ if (config.isEmailEnable) authRouter.use(require('./email'))
if (config.isOpenIDEnable) authRouter.use(require('./openid'))
// logout
authRouter.get('/logout', function (req, res) {
authRouter.get('/logout', function (req: Request, res: Response) {
if (config.debug && req.isAuthenticated()) {
logger.debug('user logout: ' + req.user.id)
logger.debug('user logout: ' + (req as any).user.id)
}
req.logout(() => {
res.redirect(config.serverURL + '/')
})
})
export = authRouter
@@ -1,16 +1,17 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const LDAPStrategy = require('passport-ldapauth')
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const { urlencodedParser } = require('../../utils')
const errors = require('../../../errors')
const { cloneDeep } = require('lodash')
import config = require('../../../config')
import models = require('../../../models')
import logger = require('../../../logger')
import { urlencodedParser } from '../../utils'
import * as errors from '../../../errors'
import { cloneDeep } from 'lodash'
const ldapAuth = module.exports = Router()
const ldapAuth = Router()
// ldapauth-fork mutates the config object, so we need to make a clone of our deep-frozen config
const mutableLdapConfig = cloneDeep(config.ldap)
@@ -25,7 +26,7 @@ passport.use(new LDAPStrategy({
searchAttributes: mutableLdapConfig.searchAttributes || null,
tlsOptions: mutableLdapConfig.tlsOptions || null
}
}, function (user, done) {
}, function (user: any, done: Function) {
let uuid = user.uidNumber || user.uid || user.sAMAccountName || undefined
if (config.ldap.useridField && user[config.ldap.useridField]) {
uuid = user[config.ldap.useridField]
@@ -60,7 +61,7 @@ passport.use(new LDAPStrategy({
defaults: {
profile: stringifiedProfile
}
}).spread(function (user, created) {
}).spread(function (user: any, created: boolean) {
if (user) {
let needSave = false
if (user.profile !== stringifiedProfile) {
@@ -77,13 +78,13 @@ passport.use(new LDAPStrategy({
return done(null, user)
}
}
}).catch(function (err) {
}).catch(function (err: any) {
logger.error('ldap auth failed: ' + err)
return done(err, null)
})
}))
ldapAuth.post('/auth/ldap', urlencodedParser, function (req, res, next) {
ldapAuth.post('/auth/ldap', urlencodedParser, function (req: Request, res: Response, next: NextFunction) {
if (!req.body.username || !req.body.password) return errors.errorBadRequest(res)
passport.authenticate('ldapauth', {
successReturnToOrRedirect: config.serverURL + '/',
@@ -91,3 +92,5 @@ ldapAuth.post('/auth/ldap', urlencodedParser, function (req, res, next) {
failureFlash: true
})(req, res, next)
})
export = ldapAuth
@@ -1,15 +1,16 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const Mattermost = require('mattermost')
const OAuthStrategy = require('passport-oauth2').Strategy
const config = require('../../../config')
const { passportGeneralCallback } = require('../utils')
import config = require('../../../config')
import { passportGeneralCallback } from '../utils'
const mattermost = new Mattermost.Client()
const mattermostAuth = module.exports = Router()
const mattermostAuth = Router()
const mattermostStrategy = new OAuthStrategy({
authorizationURL: config.mattermost.baseURL + '/oauth/authorize',
@@ -20,15 +21,15 @@ const mattermostStrategy = new OAuthStrategy({
state: true
}, passportGeneralCallback)
mattermostStrategy.userProfile = (accessToken, done) => {
mattermostStrategy.userProfile = (accessToken: string, done: Function) => {
mattermost.setUrl(config.mattermost.baseURL)
mattermost.token = accessToken
mattermost.useHeaderToken()
mattermost.getMe(
(data) => {
(data: any) => {
done(null, data)
},
(err) => {
(err: any) => {
done(err)
}
)
@@ -36,7 +37,7 @@ mattermostStrategy.userProfile = (accessToken, done) => {
passport.use(mattermostStrategy)
mattermostAuth.get('/auth/mattermost', function (req, res, next) {
mattermostAuth.get('/auth/mattermost', function (req: Request, res: Response, next: NextFunction) {
passport.authenticate('oauth2')(req, res, next)
})
@@ -47,3 +48,5 @@ mattermostAuth.get('/auth/mattermost/callback',
failureRedirect: config.serverURL + '/'
})
)
export = mattermostAuth
@@ -1,26 +1,29 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
const { Strategy, InternalOAuthError } = require('passport-oauth2')
const config = require('../../../config')
const logger = require('../../../logger')
const { passportGeneralCallback } = require('../utils')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
import { Strategy, InternalOAuthError } from 'passport-oauth2'
import config = require('../../../config')
import logger = require('../../../logger')
import { passportGeneralCallback } from '../utils'
const oauth2Auth = module.exports = Router()
const oauth2Auth = Router()
class OAuth2CustomStrategy extends Strategy {
constructor (options, verify) {
_userProfileURL: string
constructor (options: any, verify: any) {
options.customHeaders = options.customHeaders || {}
super(options, verify)
this.name = 'oauth2'
this._userProfileURL = options.userProfileURL
this._oauth2.useAuthorizationHeaderforGET(true)
;(this as any)._oauth2.useAuthorizationHeaderforGET(true)
}
userProfile (accessToken, done) {
this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
let json, profile
userProfile (accessToken: string, done: Function): void {
;(this as any)._oauth2.get(this._userProfileURL, accessToken, function (err: any, body: string, res: any) {
let json: any, profile: any
if (err) {
return done(new InternalOAuthError('Failed to fetch user profile', err))
@@ -45,17 +48,17 @@ class OAuth2CustomStrategy extends Strategy {
}
}
function extractProfileAttribute (data, path) {
function extractProfileAttribute (data: any, path: string): any {
// can handle stuff like `attrs[0].name`
path = path.split('.')
for (const segment of path) {
const segments = path.split('.')
for (const segment of segments) {
const m = segment.match(/([\d\w]+)\[(.*)\]/)
data = m ? data[m[1]][m[2]] : data[segment]
}
return data
}
function parseProfile (data) {
function parseProfile (data: any): any {
// only try to parse the id if a claim is configured
const id = config.oauth2.userProfileIdAttr ? extractProfileAttribute(data, config.oauth2.userProfileIdAttr) : undefined
const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
@@ -75,7 +78,7 @@ function parseProfile (data) {
}
}
function checkAuthorization (data, done) {
function checkAuthorization (data: any, done: Function): void {
// a role the user must have is set in the config
if (config.oauth2.accessRole) {
// check if we know which claim contains the list of groups a user is in
@@ -84,7 +87,7 @@ function checkAuthorization (data, done) {
logger.error('oauth2: "accessRole" is configured, but "rolesClaim" is missing from the config. Can\'t check group membership!')
} else {
// parse and check role data
let roles = []
let roles: any[] = []
try {
roles = extractProfileAttribute(data, config.oauth2.rolesClaim)
} catch (err) {
@@ -104,9 +107,9 @@ function checkAuthorization (data, done) {
}
}
OAuth2CustomStrategy.prototype.userProfile = function (accessToken, done) {
this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
let json, profile
OAuth2CustomStrategy.prototype.userProfile = function (accessToken: string, done: Function): void {
;(this as any)._oauth2.get(this._userProfileURL, accessToken, function (err: any, body: string, res: any) {
let json: any, profile: any
if (err) {
return done(new InternalOAuthError('Failed to fetch user profile', err))
@@ -142,7 +145,7 @@ passport.use(new OAuth2CustomStrategy({
state: true
}, passportGeneralCallback))
oauth2Auth.get('/auth/oauth2', function (req, res, next) {
oauth2Auth.get('/auth/oauth2', function (req: Request, res: Response, next: NextFunction) {
passport.authenticate('oauth2')(req, res, next)
})
@@ -153,3 +156,5 @@ oauth2Auth.get('/auth/oauth2/callback',
failureRedirect: config.serverURL + '/'
})
)
export = oauth2Auth
@@ -1,20 +1,21 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const OpenIDStrategy = require('@passport-next/passport-openid').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const { urlencodedParser } = require('../../utils')
import config = require('../../../config')
import models = require('../../../models')
import logger = require('../../../logger')
import { urlencodedParser } from '../../utils'
const openIDAuth = module.exports = Router()
const openIDAuth = Router()
passport.use(new OpenIDStrategy({
returnURL: config.serverURL + '/auth/openid/callback',
realm: config.serverURL,
profile: true
}, function (openid, profile, done) {
}, function (openid: string, profile: any, done: Function) {
const stringifiedProfile = JSON.stringify(profile)
models.User.findOrCreate({
where: {
@@ -23,7 +24,7 @@ passport.use(new OpenIDStrategy({
defaults: {
profile: stringifiedProfile
}
}).spread(function (user, created) {
}).spread(function (user: any, created: boolean) {
if (user) {
let needSave = false
if (user.profile !== stringifiedProfile) {
@@ -40,13 +41,13 @@ passport.use(new OpenIDStrategy({
return done(null, user)
}
}
}).catch(function (err) {
}).catch(function (err: any) {
logger.error('auth callback failed: ' + err)
return done(err, null)
})
}))
openIDAuth.post('/auth/openid', urlencodedParser, function (req, res, next) {
openIDAuth.post('/auth/openid', urlencodedParser, function (req: Request, res: Response, next: NextFunction) {
passport.authenticate('openid')(req, res, next)
})
@@ -57,3 +58,5 @@ openIDAuth.get('/auth/openid/callback',
failureRedirect: config.serverURL + '/'
})
)
export = openIDAuth
@@ -1,16 +1,17 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const SamlStrategy = require('@node-saml/passport-saml').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const { urlencodedParser } = require('../../utils')
const fs = require('fs')
const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) }
import config = require('../../../config')
import models = require('../../../models')
import logger = require('../../../logger')
import { urlencodedParser } from '../../utils'
import * as fs from 'fs'
const intersection = function (array1: string[], array2: string[]): string[] { return array1.filter((n) => array2.includes(n)) }
const samlAuth = module.exports = Router()
const samlAuth = Router()
passport.use(
new SamlStrategy(
@@ -23,14 +24,14 @@ passport.use(
: (function () {
try {
return fs.readFileSync(config.saml.clientCert, 'utf-8')
} catch (e) {
} catch (e: any) {
logger.error(`SAML client certificate: ${e.message}`)
}
}()),
idpCert: (function () {
try {
return fs.readFileSync(config.saml.idpCert, 'utf-8')
} catch (e) {
} catch (e: any) {
logger.error(`SAML idp certificate: ${e.message}`)
process.exit(1)
}
@@ -41,7 +42,7 @@ passport.use(
wantAuthnResponseSigned: config.saml.wantAuthnResponseSigned
},
// sign-in
function (user, done) {
function (user: any, done: Function) {
// check authorization if needed
if (config.saml.externalGroups && config.saml.groupAttribute) {
const externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute])
@@ -76,7 +77,7 @@ passport.use(
defaults: {
profile: stringifiedProfile
}
}).spread(function (user, created) {
}).spread(function (user: any, created: boolean) {
if (user) {
let needSave = false
if (user.profile !== stringifiedProfile) {
@@ -93,13 +94,13 @@ passport.use(
return done(null, user)
}
}
}).catch(function (err) {
}).catch(function (err: any) {
logger.error('saml auth failed: ' + err.message)
return done(err, null)
})
},
// logout
function (profile, done) {
function (profile: any, done: Function) {
return done(null, profile)
}
)
@@ -110,13 +111,13 @@ samlAuth.get('/auth/saml',
failureRedirect: config.serverURL + '/',
failureFlash: true
}),
function (req, res) {
function (req: Request, res: Response) {
res.redirect('/')
}
)
samlAuth.use('/auth/saml/callback', urlencodedParser,
function (req, res, next) {
function (req: Request, res: Response, next: NextFunction) {
if (req.method !== 'GET' && req.method !== 'POST') {
return res.status(405).end()
}
@@ -126,12 +127,14 @@ samlAuth.use('/auth/saml/callback', urlencodedParser,
successReturnToOrRedirect: config.serverURL + '/',
failureRedirect: config.serverURL + '/'
}),
function (req, res) {
function (req: Request, res: Response) {
res.redirect('/')
}
)
samlAuth.get('/auth/saml/metadata', function (req, res) {
samlAuth.get('/auth/saml/metadata', function (req: Request, res: Response) {
res.type('application/xml')
res.send(passport._strategy('saml').generateServiceProviderMetadata())
res.send((passport as any)._strategy('saml').generateServiceProviderMetadata())
})
export = samlAuth
@@ -1,13 +1,14 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import passport = require('passport')
const TwitterStrategy = require('passport-twitter').Strategy
const config = require('../../../config')
const { passportGeneralCallback } = require('../utils')
import config = require('../../../config')
import { passportGeneralCallback } from '../utils'
const twitterAuth = module.exports = Router()
const twitterAuth = Router()
passport.use(new TwitterStrategy({
consumerKey: config.twitter.consumerKey,
@@ -15,7 +16,7 @@ passport.use(new TwitterStrategy({
callbackURL: config.serverURL + '/auth/twitter/callback'
}, passportGeneralCallback))
twitterAuth.get('/auth/twitter', function (req, res, next) {
twitterAuth.get('/auth/twitter', function (req: Request, res: Response, next: NextFunction) {
passport.authenticate('twitter')(req, res, next)
})
@@ -26,3 +27,5 @@ twitterAuth.get('/auth/twitter/callback',
failureRedirect: config.serverURL + '/'
})
)
export = twitterAuth
@@ -1,9 +1,9 @@
'use strict'
const models = require('../../models')
const logger = require('../../logger')
import models = require('../../models')
import logger = require('../../logger')
exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) {
export function passportGeneralCallback (accessToken: string, refreshToken: string, profile: any, done: (err: Error | null, user?: any) => void): void {
const stringifiedProfile = JSON.stringify(profile)
models.User.findOrCreate({
where: {
@@ -14,7 +14,7 @@ exports.passportGeneralCallback = function callback (accessToken, refreshToken,
accessToken,
refreshToken
}
}).spread(function (user, created) {
}).spread(function (user: any, created: boolean) {
if (user) {
let needSave = false
if (user.profile !== stringifiedProfile) {
@@ -39,7 +39,7 @@ exports.passportGeneralCallback = function callback (accessToken, refreshToken,
return done(null, user)
}
}
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error('auth callback failed: ' + err)
return done(err, null)
})
-24
View File
@@ -1,24 +0,0 @@
'use strict'
const Router = require('express').Router
const response = require('../response')
const baseRouter = module.exports = Router()
const errors = require('../errors')
// get index
baseRouter.get('/', response.showIndex)
// get 403 forbidden
baseRouter.get('/403', function (req, res) {
errors.errorForbidden(res)
})
// get 404 not found
baseRouter.get('/404', function (req, res) {
errors.errorNotFound(res)
})
// get 500 internal error
baseRouter.get('/500', function (req, res) {
errors.errorInternalError(res)
})
+25
View File
@@ -0,0 +1,25 @@
'use strict'
import type { Request, Response } from 'express'
import { Router } from 'express'
import response = require('../response')
import * as errors from '../errors'
const baseRouter = Router()
// get index
baseRouter.get('/', response.showIndex)
// get 403 forbidden
baseRouter.get('/403', function (_req: Request, res: Response) {
errors.errorForbidden(res)
})
// get 404 not found
baseRouter.get('/404', function (_req: Request, res: Response) {
errors.errorNotFound(res)
})
// get 500 internal error
baseRouter.get('/500', function (_req: Request, res: Response) {
errors.errorInternalError(res)
})
export = baseRouter
@@ -1,10 +1,10 @@
'use strict'
const Router = require('express').Router
import { Router } from 'express'
import { urlencodedParser } from './utils'
import history = require('../history')
const { urlencodedParser } = require('./utils')
const history = require('../history')
const historyRouter = module.exports = Router()
const historyRouter = Router()
// get history
historyRouter.get('/history', history.historyGet)
@@ -16,3 +16,5 @@ historyRouter.post('/history/:noteId', urlencodedParser, history.historyPost)
historyRouter.delete('/history', history.historyDelete)
// delete history by note id
historyRouter.delete('/history/:noteId', history.historyDelete)
export = historyRouter
@@ -1,12 +1,12 @@
'use strict'
const path = require('path')
import * as path from 'path'
const config = require('../../config')
const logger = require('../../logger')
import config = require('../../config')
import logger = require('../../logger')
const azure = require('azure-storage')
exports.uploadImage = function (imagePath, callback) {
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
if (!callback || typeof callback !== 'function') {
logger.error('Callback has to be a function')
return
@@ -19,11 +19,11 @@ exports.uploadImage = function (imagePath, callback) {
const azureBlobService = azure.createBlobService(config.azure.connectionString)
azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err, result, response) {
azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err: any, result: any, response: any) {
if (err) {
callback(new Error(err.message), null)
} else {
azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err, result, response) {
azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err: any, result: any, response: any) {
if (err) {
callback(new Error(err.message), null)
} else {
@@ -1,12 +1,12 @@
'use strict'
const URL = require('url').URL
const path = require('path')
const fs = require('fs')
import { URL } from 'url'
import * as path from 'path'
import * as fs from 'fs'
const config = require('../../config')
const logger = require('../../logger')
import config = require('../../config')
import logger = require('../../logger')
exports.uploadImage = function (imagePath, callback) {
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
if (!callback || typeof callback !== 'function') {
logger.error('Callback has to be a function')
return
@@ -21,7 +21,7 @@ exports.uploadImage = function (imagePath, callback) {
// move image from temporary path to upload directory
try {
fs.copyFileSync(imagePath, path.join(config.uploadsPath, fileName))
} catch (e) {
} catch (e: any) {
callback(new Error(`Error while moving file: ${e.message}`), null)
return
}
@@ -1,9 +1,9 @@
'use strict'
const config = require('../../config')
const logger = require('../../logger')
const fs = require('fs')
import config = require('../../config')
import logger = require('../../logger')
import * as fs from 'fs'
exports.uploadImage = function (imagePath, callback) {
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
if (!callback || typeof callback !== 'function') {
logger.error('Callback has to be a function')
return
@@ -34,10 +34,10 @@ exports.uploadImage = function (imagePath, callback) {
}
return res.json()
})
.then((json) => {
.then((json: any) => {
logger.debug(`SERVER uploadimage success: ${JSON.stringify(json)}`)
callback(null, json.data.link.replace(/^http:\/\//i, 'https://'))
}).catch((err) => {
}).catch((err: any) => {
callback(new Error(err), null)
})
}
@@ -1,35 +1,36 @@
'use strict'
const Router = require('express').Router
import type { Request, Response } from 'express'
import { Router } from 'express'
const formidable = require('formidable')
const path = require('path')
const fs = require('fs')
const { v4: uuidv4 } = require('uuid')
const os = require('os')
const rimraf = require('rimraf')
import * as path from 'path'
import * as fs from 'fs'
import { v4 as uuidv4 } from 'uuid'
import * as os from 'os'
import rimraf = require('rimraf')
const isSvg = require('is-svg')
const { JSDOM } = require('jsdom')
import { JSDOM } from 'jsdom'
const config = require('../../config')
const logger = require('../../logger')
const errors = require('../../errors')
import config = require('../../config')
import logger = require('../../logger')
import * as errors from '../../errors'
let DOMPurify
let DOMPurify: any
try {
const window = new JSDOM('').window
global.window = window
;(global as any).window = window
const createDOMPurify = require('dompurify')
DOMPurify = createDOMPurify(window)
} catch (err) {
logger.error('Failed to initialize DOMPurify for SVG sanitization:', err)
}
const imageRouter = (module.exports = Router())
const imageRouter = Router()
async function checkUploadType (filePath) {
async function checkUploadType (filePath: string): Promise<boolean> {
const extension = path.extname(filePath).toLowerCase()
const FileType = await import('file-type')
let typeFromMagic = await FileType.fileTypeFromFile(filePath)
let typeFromMagic = await FileType.fileTypeFromFile(filePath) as { ext: string; mime: string } | undefined
if (extension === '.svg' && (typeFromMagic === undefined || typeFromMagic.mime === 'application/xml')) {
const fileContent = fs.readFileSync(filePath)
if (isSvg(fileContent)) {
@@ -45,7 +46,7 @@ async function checkUploadType (filePath) {
}
// .jpeg, .jfif, .jpe files are identified by FileType to have the extension jpg
if (['.jpeg', '.jfif', '.jpe'].includes(extension) && typeFromMagic.ext === 'jpg') {
typeFromMagic.ext = extension.substr(1)
(typeFromMagic as any).ext = extension.substr(1)
}
if (extension !== '.' + typeFromMagic.ext) {
logger.error(
@@ -66,7 +67,7 @@ async function checkUploadType (filePath) {
return true
}
function sanitizeSvg (filePath) {
function sanitizeSvg (filePath: string): boolean {
if (!DOMPurify) {
logger.error('SVG sanitization failed: DOMPurify not initialized')
return false
@@ -92,7 +93,7 @@ function sanitizeSvg (filePath) {
}
// upload image
imageRouter.post('/uploadimage', function (req, res) {
imageRouter.post('/uploadimage', function (req: Request, res: Response) {
const uploadsEnabled = config.enableUploads
if (uploadsEnabled === 'none') {
logger.error('Image upload error: Uploads are disabled')
@@ -112,7 +113,7 @@ imageRouter.post('/uploadimage', function (req, res) {
const form = formidable({
keepExtensions: true,
uploadDir: tmpDir,
filename: function (filename, ext) {
filename: function (filename: string, ext: string) {
if (typeof ext !== 'string') {
ext = '.invalid'
}
@@ -120,7 +121,7 @@ imageRouter.post('/uploadimage', function (req, res) {
}
})
form.parse(req, async function (err, fields, files) {
form.parse(req, async function (err: any, fields: any, files: any) {
if (err) {
logger.error(`Image upload error: formidable error: ${err}`)
rimraf.sync(tmpDir)
@@ -148,7 +149,7 @@ imageRouter.post('/uploadimage', function (req, res) {
logger.debug(
`imageRouter: Uploading ${files.image.filepath} using ${config.imageUploadType}`
)
uploadProvider.uploadImage(files.image.filepath, function (err, url) {
uploadProvider.uploadImage(files.image.filepath, function (err: Error | null, url: string) {
rimraf.sync(tmpDir)
if (err !== null) {
logger.error(err)
@@ -162,3 +163,5 @@ imageRouter.post('/uploadimage', function (req, res) {
}
})
})
export = imageRouter
@@ -1,10 +1,10 @@
'use strict'
const config = require('../../config')
const logger = require('../../logger')
import config = require('../../config')
import logger = require('../../logger')
const lutim = require('lutim')
exports.uploadImage = function (imagePath, callback) {
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
if (!callback || typeof callback !== 'function') {
logger.error('Callback has to be a function')
return
@@ -21,10 +21,10 @@ exports.uploadImage = function (imagePath, callback) {
}
lutim.uploadImage(imagePath)
.then(function (json) {
.then(function (json: any) {
logger.debug(`SERVER uploadimage success: ${JSON.stringify(json)}`)
callback(null, lutim.getAPIUrl() + json.msg.short)
}).catch(function (err) {
}).catch(function (err: any) {
callback(new Error(err), null)
})
}
@@ -1,10 +1,10 @@
'use strict'
const fs = require('fs')
const path = require('path')
import * as fs from 'fs'
import * as path from 'path'
const config = require('../../config')
const { getImageMimeType } = require('../../utils')
const logger = require('../../logger')
import config = require('../../config')
import { getImageMimeType } from '../../utils'
import logger = require('../../logger')
const Minio = require('minio')
const minioClient = new Minio.Client({
@@ -15,7 +15,7 @@ const minioClient = new Minio.Client({
secretKey: config.minio.secretKey
})
exports.uploadImage = function (imagePath, callback) {
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
if (!imagePath || typeof imagePath !== 'string') {
callback(new Error('Image path is missing or wrong'), null)
return
@@ -26,20 +26,20 @@ exports.uploadImage = function (imagePath, callback) {
return
}
fs.readFile(imagePath, function (err, buffer) {
fs.readFile(imagePath, function (err: NodeJS.ErrnoException | null, buffer: Buffer) {
if (err) {
callback(new Error(err), null)
callback(new Error(String(err)), null)
return
}
const key = path.join('uploads', path.basename(imagePath))
const protocol = config.minio.secure ? 'https' : 'http'
minioClient.putObject(config.s3bucket, key, buffer, buffer.size, {
minioClient.putObject(config.s3bucket, key, buffer, (buffer as any).size, {
'Content-Type': getImageMimeType(imagePath) || 'application/octet-stream'
}, function (err, data) {
}, function (err: any, data: any) {
if (err) {
callback(new Error(err), null)
callback(new Error(String(err)), null)
return
}
const hidePort = [80, 443].includes(config.minio.port)
@@ -1,16 +1,16 @@
'use strict'
const fs = require('fs')
const path = require('path')
import * as fs from 'fs'
import * as path from 'path'
const config = require('../../config')
const { getImageMimeType } = require('../../utils')
const logger = require('../../logger')
import config = require('../../config')
import { getImageMimeType } from '../../utils'
import logger = require('../../logger')
const AWS = require('aws-sdk')
const awsConfig = new AWS.Config(config.s3)
const s3 = new AWS.S3(awsConfig)
exports.uploadImage = function (imagePath, callback) {
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
if (!imagePath || typeof imagePath !== 'string') {
callback(new Error('Image path is missing or wrong'), null)
return
@@ -21,12 +21,12 @@ exports.uploadImage = function (imagePath, callback) {
return
}
fs.readFile(imagePath, function (err, buffer) {
fs.readFile(imagePath, function (err: NodeJS.ErrnoException | null, buffer: Buffer) {
if (err) {
callback(new Error(err), null)
callback(new Error(String(err)), null)
return
}
const params = {
const params: any = {
Bucket: config.s3bucket,
Key: path.join(config.s3folder, path.basename(imagePath)),
Body: buffer
@@ -37,9 +37,9 @@ exports.uploadImage = function (imagePath, callback) {
if (config.s3publicFiles) { params.ACL = 'public-read' }
logger.debug(`S3 object parameters: ${JSON.stringify(params)}`)
s3.putObject(params, function (err, data) {
s3.putObject(params, function (err: any, data: any) {
if (err) {
callback(new Error(err), null)
callback(new Error(String(err)), null)
return
}
-14
View File
@@ -1,14 +0,0 @@
'use strict'
const logger = require('../../logger')
const errors = require('../../errors')
module.exports = function (req, res, next) {
try {
decodeURIComponent(req.path)
} catch (err) {
logger.error(err)
return errors.errorBadRequest(res)
}
next()
}
+17
View File
@@ -0,0 +1,17 @@
'use strict'
import type { Request, Response, NextFunction } from 'express'
import logger = require('../../logger')
import * as errors from '../../errors'
const checkURIValid = function (req: Request, res: Response, next: NextFunction): void {
try {
decodeURIComponent(req.path)
} catch (err) {
logger.error(err)
return errors.errorBadRequest(res)
}
next()
}
export = checkURIValid
-10
View File
@@ -1,10 +0,0 @@
'use strict'
const config = require('../../config')
module.exports = function (req, res, next) {
res.set({
'HedgeDoc-Version': config.version
})
return next()
}
+13
View File
@@ -0,0 +1,13 @@
'use strict'
import type { Request, Response, NextFunction } from 'express'
import config = require('../../config')
const hedgeDocVersion = function (_req: Request, res: Response, next: NextFunction): void {
res.set({
'HedgeDoc-Version': config.version
})
return next()
}
export = hedgeDocVersion
-33
View File
@@ -1,33 +0,0 @@
'use strict'
const { rateLimit, ipKeyGenerator } = require('express-rate-limit')
const errors = require('../../errors')
const config = require('../../config')
const determineKey = (req) => {
if (req.user) {
return req.user.id
}
return ipKeyGenerator(req.header('cf-connecting-ip') || req.ip)
}
// limits requests to user endpoints (login, signup) to 10 requests per 5 minutes
const userEndpoints = rateLimit({
windowMs: 5 * 60 * 1000,
limit: 10,
keyGenerator: determineKey,
handler: (req, res) => errors.errorTooManyRequests(res)
})
// limits the amount of requests to the new note endpoint per 5 minutes based on configuration
const newNotes = rateLimit({
windowMs: 5 * 60 * 1000,
limit: config.rateLimitNewNotes,
keyGenerator: determineKey,
handler: (req, res) => errors.errorTooManyRequests(res)
})
module.exports = {
userEndpoints,
newNotes
}
+29
View File
@@ -0,0 +1,29 @@
'use strict'
import type { Request, Response, NextFunction } from 'express'
import { rateLimit } from 'express-rate-limit'
import * as errors from '../../errors'
import config = require('../../config')
const determineKey = (req: Request): string => {
if ((req as any).user) {
return (req as any).user.id
}
return req.header('cf-connecting-ip') || req.ip || 'unknown'
}
// limits requests to user endpoints (login, signup) to 10 requests per 5 minutes
export const userEndpoints = rateLimit({
windowMs: 5 * 60 * 1000,
limit: 10,
keyGenerator: determineKey,
handler: (_req: Request, res: Response) => errors.errorTooManyRequests(res)
})
// limits the amount of requests to the new note endpoint per 5 minutes based on configuration
export const newNotes = rateLimit({
windowMs: 5 * 60 * 1000,
limit: config.rateLimitNewNotes,
keyGenerator: determineKey,
handler: (_req: Request, res: Response) => errors.errorTooManyRequests(res)
})
@@ -1,12 +1,13 @@
'use strict'
const config = require('../../config')
import type { Request, Response, NextFunction } from 'express'
import config = require('../../config')
module.exports = function (req, res, next) {
const redirectWithoutTrailingSlashes = function (req: Request, res: Response, next: NextFunction): void {
if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) {
const queryString = req.url.slice(req.path.length)
const urlPath = req.path.slice(0, -1)
let serverURL = config.serverURL
let serverURL: string = config.serverURL
if (config.urlPath) {
serverURL = serverURL.slice(0, -(config.urlPath.length + 1))
}
@@ -15,3 +16,5 @@ module.exports = function (req, res, next) {
next()
}
}
export = redirectWithoutTrailingSlashes
-21
View File
@@ -1,21 +0,0 @@
'use strict'
const toobusy = require('toobusy-js')
const errors = require('../../errors')
const config = require('../../config')
toobusy.maxLag(config.tooBusyLag)
module.exports = function (req, res, next) {
// We dont want to return "toobusy" errors for healthchecks, as that
// will cause the process to be restarted
if (req.baseUrl === '/_health') {
next()
}
if (toobusy()) {
errors.errorServiceUnavailable(res)
} else {
next()
}
}
+23
View File
@@ -0,0 +1,23 @@
'use strict'
import type { Request, Response, NextFunction } from 'express'
import toobusy = require('toobusy-js')
import * as errors from '../../errors'
import config = require('../../config')
toobusy.maxLag(config.tooBusyLag)
const tooBusyMiddleware = function (req: Request, res: Response, next: NextFunction): void {
// We dont want to return "toobusy" errors for healthchecks, as that
// will cause the process to be restarted
if (req.baseUrl === '/_health') {
return next()
}
if (toobusy()) {
errors.errorServiceUnavailable(res)
} else {
next()
}
}
export = tooBusyMiddleware
@@ -1,12 +1,13 @@
const models = require('../../models')
const logger = require('../../logger')
const config = require('../../config')
const errors = require('../../errors')
const nanoid = require('nanoid')
const moment = require('moment')
const querystring = require('querystring')
import type { Request, Response } from 'express'
import models = require('../../models')
import logger = require('../../logger')
import config = require('../../config')
import * as errors from '../../errors'
import nanoid = require('nanoid')
import moment = require('moment')
import querystring = require('querystring')
exports.getInfo = function getInfo (req, res, note) {
export function getInfo (req: Request, res: Response, note: any): void {
const body = note.content
const extracted = models.Note.extractMeta(body)
const markdown = extracted.markdown
@@ -31,7 +32,7 @@ exports.getInfo = function getInfo (req, res, note) {
res.send(data)
}
exports.createGist = function createGist (req, res, note) {
export function createGist (req: Request, res: Response, note: any): void {
const data = {
client_id: config.github.clientID,
redirect_uri: config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist',
@@ -42,12 +43,12 @@ exports.createGist = function createGist (req, res, note) {
res.redirect('https://github.com/login/oauth/authorize?' + query)
}
exports.getRevision = function getRevision (req, res, note) {
const actionId = req.params.actionId
export function getRevision (req: Request, res: Response, note: any): void {
const actionId = req.params.actionId as string
if (actionId) {
const time = moment(parseInt(actionId))
if (time.isValid()) {
models.Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) {
models.Revision.getPatchedNoteRevisionByTime(note, time, function (err: Error | null, content: string | null) {
if (err) {
logger.error(err)
return errors.errorInternalError(res)
@@ -68,7 +69,7 @@ exports.getRevision = function getRevision (req, res, note) {
return errors.errorNotFound(res)
}
} else {
models.Revision.getNoteRevisions(note, function (err, data) {
models.Revision.getNoteRevisions(note, function (err: Error | null, data: unknown) {
if (err) {
logger.error(err)
return errors.errorInternalError(res)
@@ -1,15 +1,16 @@
'use strict'
const models = require('../../models')
const logger = require('../../logger')
const config = require('../../config')
const errors = require('../../errors')
import type { Request, Response, NextFunction } from 'express'
import models = require('../../models')
import logger = require('../../logger')
import config = require('../../config')
import * as errors from '../../errors'
const noteUtil = require('./util')
const noteActions = require('./actions')
import * as noteUtil from './util'
import * as noteActions from './actions'
exports.publishNoteActions = function (req, res, next) {
noteUtil.findNote(req, res, function (note) {
export function publishNoteActions (req: Request, res: Response, next: NextFunction): void {
noteUtil.findNote(req, res, function (note: any) {
const action = req.params.action
switch (action) {
case 'download':
@@ -25,7 +26,7 @@ exports.publishNoteActions = function (req, res, next) {
})
}
exports.showPublishNote = function (req, res, next) {
export function showPublishNote (req: Request, res: Response, next: NextFunction): void {
const include = [{
model: models.User,
as: 'owner'
@@ -33,31 +34,31 @@ exports.showPublishNote = function (req, res, next) {
model: models.User,
as: 'lastchangeuser'
}]
noteUtil.findNote(req, res, function (note) {
noteUtil.findNote(req, res, function (note: any) {
// force to use short id
const shortid = req.params.shortid
if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) {
return res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid))
}
note.increment('viewcount').then(function (note) {
note.increment('viewcount').then(function (note: any) {
if (!note) {
return errors.errorNotFound(res)
}
noteUtil.getPublishData(req, res, note, (data) => {
noteUtil.getPublishData(req, res, note, (data: Record<string, unknown>) => {
res.set({
'Cache-Control': 'private' // only cache by client
})
return res.render('pretty.ejs', data)
})
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error(err)
return errors.errorInternalError(res)
})
}, include)
}
exports.showNote = function (req, res, next) {
noteUtil.findNote(req, res, function (note) {
export function showNote (req: Request, res: Response, next: NextFunction): void {
noteUtil.findNote(req, res, function (note: any) {
// force to use note id
const noteId = req.params.noteId
const id = models.Note.encodeNoteId(note.id)
@@ -81,7 +82,7 @@ exports.showNote = function (req, res, next) {
}, null, true)
}
exports.createFromPOST = function (req, res, next) {
export function createFromPOST (req: Request, res: Response, next: NextFunction): void {
if (config.disableNoteCreation) {
return errors.errorForbidden(res)
}
@@ -92,12 +93,12 @@ exports.createFromPOST = function (req, res, next) {
body = req.body
}
body = body.replace(/[\r]/g, '')
return noteUtil.newNote(req, res, body)
noteUtil.newNote(req, res, body)
}
exports.doAction = function (req, res, next) {
export function doAction (req: Request, res: Response, next: NextFunction): void {
const noteId = req.params.noteId
noteUtil.findNote(req, res, function (note) {
noteUtil.findNote(req, res, function (note: any) {
const action = req.params.action
switch (action) {
case 'publish':
@@ -125,7 +126,7 @@ exports.doAction = function (req, res, next) {
})
}
exports.downloadMarkdown = function (req, res, note) {
export function downloadMarkdown (req: Request, res: Response, note: any): void {
const body = note.content
let filename = models.Note.decodeTitle(note.title)
filename = encodeURIComponent(filename)
@@ -1,18 +1,20 @@
'use strict'
const Router = require('express').Router
const { markdownParser } = require('../utils')
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import { markdownParser } from '../utils'
const router = module.exports = Router()
const router = Router()
const noteController = require('./controller')
const slide = require('./slide')
const rateLimit = require('../middleware/rateLimit')
const config = require('../../config')
import noteController = require('./controller')
import slide = require('./slide')
import rateLimit = require('../middleware/rateLimit')
import config = require('../../config')
const applyRateLimitIfConfigured = (req, res, next) => {
const applyRateLimitIfConfigured = (req: Request, res: Response, next: NextFunction): void => {
if (config.rateLimitNewNotes > 0) {
return rateLimit.newNotes(req, res, next)
rateLimit.newNotes(req, res, next)
return
}
next()
}
@@ -37,3 +39,5 @@ router.get('/:noteId', noteController.showNote)
router.get('/:noteId/:action', noteController.doAction)
// note actions with action id
router.get('/:noteId/:action/:actionId', noteController.doAction)
export = router
+13 -12
View File
@@ -1,11 +1,12 @@
const noteUtil = require('./util')
const models = require('../../models')
const errors = require('../../errors')
const logger = require('../../logger')
const config = require('../../config')
import type { Request, Response, NextFunction } from 'express'
import * as noteUtil from './util'
import models = require('../../models')
import * as errors from '../../errors'
import logger = require('../../logger')
import config = require('../../config')
exports.publishSlideActions = function (req, res, next) {
noteUtil.findNote(req, res, function (note) {
export function publishSlideActions (req: Request, res: Response, next: NextFunction): void {
noteUtil.findNote(req, res, function (note: any) {
const action = req.params.action
if (action === 'edit') {
res.redirect(config.serverURL + '/' + (note.alias ? note.alias : models.Note.encodeNoteId(note.id)) + '?both')
@@ -13,7 +14,7 @@ exports.publishSlideActions = function (req, res, next) {
})
}
exports.showPublishSlide = function (req, res, next) {
export function showPublishSlide (req: Request, res: Response, next: NextFunction): void {
const include = [{
model: models.User,
as: 'owner'
@@ -21,23 +22,23 @@ exports.showPublishSlide = function (req, res, next) {
model: models.User,
as: 'lastchangeuser'
}]
noteUtil.findNote(req, res, function (note) {
noteUtil.findNote(req, res, function (note: any) {
// force to use short id
const shortid = req.params.shortid
if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) {
return res.redirect(config.serverURL + '/p/' + (note.alias || note.shortid))
}
note.increment('viewcount').then(function (note) {
note.increment('viewcount').then(function (note: any) {
if (!note) {
return errors.errorNotFound(res)
}
noteUtil.getPublishData(req, res, note, (data) => {
noteUtil.getPublishData(req, res, note, (data: Record<string, unknown>) => {
res.set({
'Cache-Control': 'private' // only cache by client
})
return res.render('slide.ejs', data)
})
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error(err)
return errors.errorInternalError(res)
})
+26 -25
View File
@@ -1,13 +1,14 @@
const models = require('../../models')
const logger = require('../../logger')
const config = require('../../config')
const errors = require('../../errors')
const fs = require('fs')
const path = require('path')
import type { Request, Response } from 'express'
import models = require('../../models')
import logger = require('../../logger')
import config = require('../../config')
import * as errors from '../../errors'
import * as fs from 'fs'
import * as path from 'path'
exports.findNote = function (req, res, callback, include = null, createIfNotFound = false) {
export function findNote (req: Request, res: Response, callback: (note: any) => void, include: any[] | null = null, createIfNotFound: boolean = false): void {
const id = req.params.noteId || req.params.shortid
models.Note.parseNoteId(id, function (err, _id) {
models.Note.parseNoteId(id, function (err: Error | null, _id: string) {
if (err) {
logger.error(err)
return errors.errorInternalError(res)
@@ -17,7 +18,7 @@ exports.findNote = function (req, res, callback, include = null, createIfNotFoun
id: _id
},
include: include || null
}).then(function (note) {
}).then(function (note: any) {
if (!note && createIfNotFound) {
if (config.disableNoteCreation) {
return errors.errorNotFound(res)
@@ -33,16 +34,16 @@ exports.findNote = function (req, res, callback, include = null, createIfNotFoun
} else {
return callback(note)
}
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error(err)
return errors.errorInternalError(res)
})
})
}
exports.checkViewPermission = function (req, note) {
export function checkViewPermission (req: Request, note: any): boolean {
if (note.permission === 'private') {
return !(!req.isAuthenticated() || note.ownerId !== req.user.id)
return !(!req.isAuthenticated() || note.ownerId !== (req as any).user.id)
} else if (note.permission === 'limited' || note.permission === 'protected') {
return req.isAuthenticated()
} else {
@@ -50,24 +51,24 @@ exports.checkViewPermission = function (req, note) {
}
}
exports.newNote = async function (req, res, body) {
let owner = null
export async function newNote (req: Request, res: Response, body: string): Promise<void> {
let owner: string | null = null
const noteId = req.params.noteId ? req.params.noteId : null
if (req.isAuthenticated()) {
owner = req.user.id
owner = (req as any).user.id
} else if (!config.allowAnonymous) {
return errors.errorForbidden(res)
}
if (noteId) {
if (config.allowFreeURL && !config.forbiddenNoteIDs.includes(noteId) && (!config.requireFreeURLAuthentication || req.isAuthenticated())) {
req.alias = noteId
(req as any).alias = noteId
} else {
return req.method === 'POST' ? errors.errorForbidden(res) : errors.errorNotFound(res)
}
try {
const id = await new Promise((resolve, reject) => {
models.Note.parseNoteId(noteId, (err, id) => {
const id = await new Promise<string>((resolve, reject) => {
models.Note.parseNoteId(noteId, (err: Error | null, id: string) => {
if (err) {
reject(err)
} else {
@@ -86,18 +87,18 @@ exports.newNote = async function (req, res, body) {
}
models.Note.create({
ownerId: owner,
alias: req.alias ? req.alias : null,
alias: (req as any).alias ? (req as any).alias : null,
content: body,
title: models.Note.parseNoteTitle(body)
}).then(function (note) {
}).then(function (note: any) {
return res.redirect(config.serverURL + '/' + (note.alias ? note.alias : models.Note.encodeNoteId(note.id)))
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error('Note could not be created: ' + err)
return errors.errorInternalError(res)
})
}
exports.getPublishData = function (req, res, note, callback) {
export function getPublishData (req: Request, res: Response, note: any, callback: (data: Record<string, unknown>) => void): void {
const body = note.content
const extracted = models.Note.extractMeta(body)
let markdown = extracted.markdown
@@ -129,15 +130,15 @@ exports.getPublishData = function (req, res, note, callback) {
robots: meta.robots || false, // default allow robots
GA: meta.GA,
disqus: meta.disqus,
cspNonce: res.locals.nonce,
cspNonce: (res as any).locals.nonce,
dnt: req.headers.dnt,
opengraph: ogdata
}
callback(data)
}
function isRevealTheme (theme) {
if (fs.existsSync(path.join(__dirname, '..', '..', '..', 'public', 'build', 'reveal.js', 'css', 'theme', theme + '.css'))) {
function isRevealTheme (theme: string): string | undefined {
if (fs.existsSync(path.join(__dirname, '..', '..', '..', '..', 'public', 'build', 'reveal.js', 'css', 'theme', theme + '.css'))) {
return theme
}
return undefined
@@ -1,18 +1,18 @@
'use strict'
const Router = require('express').Router
import type { Request, Response, NextFunction } from 'express'
import { Router } from 'express'
import * as errors from '../errors'
import realtime = require('../realtime')
import config = require('../config')
import models = require('../models')
import logger = require('../logger')
import { urlencodedParser } from './utils'
const errors = require('../errors')
const realtime = require('../realtime')
const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const statusRouter = Router()
const { urlencodedParser } = require('./utils')
const statusRouter = module.exports = Router()
statusRouter.get('/_health', function (req, res) {
// get status
statusRouter.get('/_health', function (_req: Request, res: Response) {
res.set({
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
@@ -23,11 +23,11 @@ statusRouter.get('/_health', function (req, res) {
})
// get status
statusRouter.get('/status', function (req, res, next) {
statusRouter.get('/status', function (req: Request, res: Response, _next: NextFunction) {
if (!config.enableStatsApi) {
return errors.errorForbidden(res)
}
realtime.getStatus(function (data) {
realtime.getStatus(function (data: Record<string, unknown>) {
res.set({
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
@@ -36,8 +36,8 @@ statusRouter.get('/status', function (req, res, next) {
res.send(data)
})
})
// get status
statusRouter.get('/temp', function (req, res) {
statusRouter.get('/temp', function (req: Request, res: Response) {
const host = req.get('host')
if (config.allowOrigin.indexOf(host) === -1) {
errors.errorForbidden(res)
@@ -50,7 +50,7 @@ statusRouter.get('/temp', function (req, res) {
where: {
id: tempid
}
}).then(function (temp) {
}).then(function (temp: any) {
if (!temp) {
errors.errorNotFound(res)
} else {
@@ -58,21 +58,22 @@ statusRouter.get('/temp', function (req, res) {
res.send({
temp: temp.data
})
temp.destroy().catch(function (err) {
temp.destroy().catch(function (err: Error) {
if (err) {
logger.error('remove temp failed: ' + err)
}
})
}
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error(err)
return errors.errorInternalError(res)
})
}
}
})
// post status
statusRouter.post('/temp', urlencodedParser, function (req, res) {
statusRouter.post('/temp', urlencodedParser, function (req: Request, res: Response) {
const host = req.get('host')
if (config.allowOrigin.indexOf(host) === -1) {
errors.errorForbidden(res)
@@ -84,7 +85,7 @@ statusRouter.post('/temp', urlencodedParser, function (req, res) {
logger.debug(`SERVER received temp from [${host}]: ${req.body.data}`)
models.Temp.create({
data
}).then(function (temp) {
}).then(function (temp: any) {
if (temp) {
res.header('Access-Control-Allow-Origin', '*')
res.send({
@@ -94,7 +95,7 @@ statusRouter.post('/temp', urlencodedParser, function (req, res) {
} else {
errors.errorInternalError(res)
}
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error(err)
return errors.errorInternalError(res)
})
@@ -102,7 +103,7 @@ statusRouter.post('/temp', urlencodedParser, function (req, res) {
}
})
statusRouter.get('/config', function (req, res) {
statusRouter.get('/config', function (req: Request, res: Response) {
const data = {
domain: config.domain,
urlpath: config.urlPath,
@@ -113,7 +114,7 @@ statusRouter.get('/config', function (req, res) {
linkifyHeaderStyle: config.linkifyHeaderStyle,
cookiePolicy: config.cookiePolicy,
enableUploads: config.enableUploads,
userToken: req.user ? req.user.deleteToken : ''
userToken: (req as any).user ? (req as any).user.deleteToken : ''
}
res.set({
'Cache-Control': 'private', // only cache by client
@@ -122,3 +123,5 @@ statusRouter.get('/config', function (req, res) {
})
res.render('../js/lib/common/constant.ejs', data)
})
export = statusRouter
+38 -37
View File
@@ -1,35 +1,35 @@
'use strict'
const archiver = require('archiver')
const sanitizeFilename = require('sanitize-filename')
const async = require('async')
const Router = require('express').Router
import type { Request, Response, NextFunction } from 'express'
import archiver = require('archiver')
import sanitizeFilename = require('sanitize-filename')
import async = require('async')
import { Router } from 'express'
import * as errors from '../errors'
import config = require('../config')
import models = require('../models')
import logger = require('../logger')
import { generateAvatar } from '../letter-avatars'
const errors = require('../errors')
const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const { generateAvatar } = require('../letter-avatars')
const UserRouter = module.exports = Router()
const UserRouter = Router()
// get me info
UserRouter.get('/me', function (req, res) {
UserRouter.get('/me', function (req: Request, res: Response) {
if (req.isAuthenticated()) {
models.User.findOne({
where: {
id: req.user.id
id: (req as any).user.id
}
}).then(function (user) {
}).then(function (user: any) {
if (!user) { return errors.errorNotFound(res) }
const profile = models.User.getProfile(user)
res.send({
status: 'ok',
id: req.user.id,
id: (req as any).user.id,
name: profile.name,
photo: profile.photo
})
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error('read me failed: ' + err)
return errors.errorInternalError(res)
})
@@ -41,13 +41,13 @@ UserRouter.get('/me', function (req, res) {
})
// delete the currently authenticated user
UserRouter.get('/me/delete/:token?', function (req, res) {
UserRouter.get('/me/delete/:token?', function (req: Request, res: Response) {
if (req.isAuthenticated()) {
models.User.findOne({
where: {
id: req.user.id
id: (req as any).user.id
}
}).then(function (user) {
}).then(function (user: any) {
if (!user) {
return errors.errorNotFound(res)
}
@@ -58,7 +58,7 @@ UserRouter.get('/me/delete/:token?', function (req, res) {
} else {
return errors.errorForbidden(res)
}
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error('delete user failed: ' + err)
return errors.errorInternalError(res)
})
@@ -68,7 +68,7 @@ UserRouter.get('/me/delete/:token?', function (req, res) {
})
// export the data of the authenticated user
UserRouter.get('/me/export', function (req, res) {
UserRouter.get('/me/export', function (req: Request, res: Response) {
if (req.isAuthenticated()) {
// let output = fs.createWriteStream(__dirname + '/example.zip');
const archive = archiver('zip', {
@@ -77,44 +77,43 @@ UserRouter.get('/me/export', function (req, res) {
res.setHeader('Content-Type', 'application/zip')
res.attachment('archive.zip')
archive.pipe(res)
archive.on('error', function (err) {
archive.on('error', function (err: Error) {
logger.error('export user data failed: ' + err)
return errors.errorInternalError(res)
})
models.User.findOne({
where: {
id: req.user.id
id: (req as any).user.id
}
}).then(function (user) {
}).then(function (user: any) {
models.Note.findAll({
where: {
ownerId: user.id
}
}).then(function (notes) {
const filenames = {}
async.each(notes, function (note, callback) {
const basename = sanitizeFilename(note.title, { replacement: '_' })
let filename
let suffix = ''
}).then(function (notes: any[]) {
const filenames: Record<string, boolean> = {}
async.each(notes, function (note: any, callback: async.ErrorCallback) {
const basename: string = sanitizeFilename(note.title, { replacement: '_' })
let filename: string
let suffix: number | string = ''
do {
const seperator = typeof suffix === 'number' ? '-' : ''
filename = basename + seperator + suffix + '.md'
suffix++
suffix = typeof suffix === 'number' ? suffix + 1 : 0
} while (filenames[filename])
filenames[filename] = true
logger.debug('Write: ' + filename)
archive.append(Buffer.from(note.content), { name: filename, date: note.lastchangeAt })
callback(null, null)
}, function (err) {
callback(null)
}, function (err: Error | null | undefined) {
if (err) {
return errors.errorInternalError(res)
}
archive.finalize()
})
})
}).catch(function (err) {
}).catch(function (err: Error) {
logger.error('export user data failed: ' + err)
return errors.errorInternalError(res)
})
@@ -123,8 +122,10 @@ UserRouter.get('/me/export', function (req, res) {
}
})
UserRouter.get('/user/:username/avatar.svg', function (req, res, next) {
UserRouter.get('/user/:username/avatar.svg', function (req: Request, res: Response, _next: NextFunction) {
res.setHeader('Content-Type', 'image/svg+xml')
res.setHeader('Cache-Control', 'public, max-age=86400')
res.send(generateAvatar(req.params.username))
res.send(generateAvatar(req.params.username as string))
})
export = UserRouter
+3 -3
View File
@@ -1,15 +1,15 @@
'use strict'
const bodyParser = require('body-parser')
import bodyParser = require('body-parser')
// create application/x-www-form-urlencoded parser
exports.urlencodedParser = bodyParser.urlencoded({
export const urlencodedParser = bodyParser.urlencoded({
extended: false,
limit: 1024 * 1024 * 10 // 10 mb
})
// create text/markdown parser
exports.markdownParser = bodyParser.text({
export const markdownParser = bodyParser.text({
inflate: true,
type: ['text/plain', 'text/markdown'],
limit: 1024 * 1024 * 10 // 10 mb
@@ -1,12 +1,22 @@
'use strict'
// external modules
const DiffMatchPatch = require('diff-match-patch')
import DiffMatchPatch = require('diff-match-patch')
const dmp = new DiffMatchPatch()
// core
const logger = require('../logger')
import logger = require('../logger')
process.on('message', function (data) {
interface DmpWorkerMessage {
msg: string
cacheKey: string
lastDoc?: string
currDoc?: string
revisions?: any[]
count?: number
}
process.on('message', function (raw: DmpWorkerMessage) {
const data = raw
if (!data || !data.msg || !data.cacheKey) {
return logger.error('dmp worker error: not enough data')
}
@@ -21,15 +31,15 @@ process.on('message', function (data) {
)
}
try {
const patch = createPatch(data.lastDoc, data.currDoc)
process.send({
const patch = createPatch(data.lastDoc!, data.currDoc!)
process.send!({
msg: 'check',
result: patch,
cacheKey: data.cacheKey
})
} catch (err) {
logger.error('dmp worker error', err)
process.send({
process.send!({
msg: 'error',
error: err,
cacheKey: data.cacheKey
@@ -46,15 +56,15 @@ process.on('message', function (data) {
)
}
try {
const result = getRevision(data.revisions, data.count)
process.send({
const result = getRevision(data.revisions!, data.count!)
process.send!({
msg: 'check',
result,
cacheKey: data.cacheKey
})
} catch (err) {
logger.error('dmp worker error', err)
process.send({
process.send!({
msg: 'error',
error: err,
cacheKey: data.cacheKey
@@ -64,36 +74,48 @@ process.on('message', function (data) {
}
})
function createPatch (lastDoc, currDoc) {
function createPatch (lastDoc: string, currDoc: string): string {
const msStart = new Date().getTime()
const diff = dmp.diff_main(lastDoc, currDoc)
let patch = dmp.patch_make(lastDoc, diff)
patch = dmp.patch_toText(patch)
const patch = dmp.patch_toText(dmp.patch_make(lastDoc, diff))
const msEnd = new Date().getTime()
logger.debug(patch)
logger.debug(msEnd - msStart + 'ms')
return patch
}
function getRevision (revisions, count) {
interface RevisionEntry {
content?: string
lastContent?: string
patch?: string
authorship?: unknown[]
}
interface RevisionResult {
content: string
patch: any[]
authorship: unknown[]
}
function getRevision (revisions: RevisionEntry[], count: number): RevisionResult {
const msStart = new Date().getTime()
let startContent = null
let lastPatch = []
let applyPatches = []
let authorship = []
let startContent: string | null = null
let lastPatch = ''
let applyPatches: any[] = []
let authorship: unknown[] = []
if (count <= Math.round(revisions.length / 2)) {
// start from top to target
for (let i = 0; i < count; i++) {
const revision = revisions[i]
if (i === 0) {
startContent = revision.content || revision.lastContent
startContent = revision.content || revision.lastContent || null
}
if (i !== count - 1) {
const patch = dmp.patch_fromText(revision.patch)
const patch = dmp.patch_fromText(revision.patch!)
applyPatches = applyPatches.concat(patch)
}
lastPatch = revision.patch
authorship = revision.authorship
lastPatch = revision.patch || ''
authorship = revision.authorship || []
}
// swap DIFF_INSERT and DIFF_DELETE to achieve unpatching
for (let i = 0, l = applyPatches.length; i < l; i++) {
@@ -112,19 +134,19 @@ function getRevision (revisions, count) {
for (let i = l; i >= count - 1; i--) {
const revision = revisions[i]
if (i === l) {
startContent = revision.lastContent
authorship = revision.authorship
startContent = revision.lastContent || null
authorship = revision.authorship || []
}
if (revision.patch) {
const patch = dmp.patch_fromText(revision.patch)
applyPatches = applyPatches.concat(patch)
}
lastPatch = revision.patch
authorship = revision.authorship
lastPatch = revision.patch || ''
authorship = revision.authorship || []
}
}
try {
const finalContent = dmp.patch_apply(applyPatches, startContent)[0]
const finalContent = dmp.patch_apply(applyPatches, startContent as string)[0]
const data = {
content: finalContent,
patch: dmp.patch_fromText(lastPatch),
@@ -134,12 +156,12 @@ function getRevision (revisions, count) {
logger.debug(msEnd - msStart + 'ms')
return data
} catch (err) {
throw new Error(err)
throw new Error(err as string)
}
}
// log uncaught exception
process.on('uncaughtException', function (err) {
process.on('uncaughtException', function (err: Error) {
logger.error('An uncaught exception has occured.')
logger.error(err)
logger.error('Process will exit now.')
+33 -1
View File
@@ -14,7 +14,8 @@
"dev": "webpack --config webpack.dev.js --progress --watch",
"heroku-prebuild": "bin/heroku",
"build": "webpack --config webpack.prod.js --progress",
"start": "node app.js"
"build:server": "tsgo -p tsconfig.json",
"start": "node dist/app.js"
},
"dependencies": {
"@hedgedoc/meta-marked": "14.1.0",
@@ -138,6 +139,37 @@
"@eslint/eslintrc": "3.3.3",
"@eslint/js": "9.39.2",
"@hedgedoc/codemirror-5": "5.65.12",
"@types/archiver": "^7.0.0",
"@types/async": "^3.2.25",
"@types/chance": "^1.1.7",
"@types/cheerio": "^1.0.0",
"@types/compression": "^1.8.1",
"@types/connect-flash": "^0.0.40",
"@types/cookie-parser": "^1.4.10",
"@types/deep-freeze": "^0.1.5",
"@types/diff-match-patch": "^1.0.36",
"@types/ejs": "^3.1.5",
"@types/express": "^5.0.6",
"@types/express-session": "^1.18.2",
"@types/helmet": "^4.0.0",
"@types/i18n": "^0.13.12",
"@types/jsdom": "^27.0.0",
"@types/lodash": "^4.17.23",
"@types/method-override": "^3.0.0",
"@types/morgan": "^1.9.10",
"@types/node": "^25.2.3",
"@types/passport": "^1.0.17",
"@types/passport-facebook": "^3.0.4",
"@types/passport-github": "^1.1.13",
"@types/passport-google-oauth20": "^2.0.17",
"@types/passport-local": "^1.0.38",
"@types/passport-oauth2": "^1.8.0",
"@types/sanitize-filename": "^1.6.3",
"@types/toobusy-js": "^0.5.4",
"@types/umzug": "^3.0.0",
"@types/uuid": "^11.0.0",
"@types/validator": "^13.15.10",
"@typescript/native-preview": "^7.0.0-dev.20260217.1",
"abcjs": "6.6.0",
"babel-cli": "6.26.0",
"babel-core": "6.26.3",
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": ".",
"sourceMap": true,
"declaration": false
},
"include": ["lib/**/*", "app.ts"],
"exclude": ["node_modules", "dist", "lib/migrations"]
}
+769 -14
View File
File diff suppressed because it is too large Load Diff