Compare commits
191 Commits
hedgeagent
...
dddd3a7ed5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dddd3a7ed5 | ||
|
|
95ba7f6fb5 | ||
|
|
788676aba4 | ||
|
|
dcdf30b4c4 | ||
|
|
074f7251e9 | ||
|
|
6ea80ef636 | ||
|
|
c66f4c2ec2 | ||
|
|
10dd207ff4 | ||
|
|
9a91b286ea | ||
|
|
65b39de212 | ||
|
|
f2b50dd290 | ||
|
|
b7c46b0a90 | ||
|
|
01e1b6bd4d | ||
|
|
d62e095553 | ||
|
|
8340e3f3a2 | ||
|
|
286add5d0d | ||
|
|
5504ac52b2 | ||
|
|
9b008ccbab | ||
|
|
37b23c86c5 | ||
|
|
07d5997bb4 | ||
|
|
03314ae9c4 | ||
|
|
a83e074ab1 | ||
|
|
1464fc87cf | ||
|
|
f181334990 | ||
|
|
28e8e34c79 | ||
|
|
d9e2e6674b | ||
|
|
9586121695 | ||
|
|
66732eff2d | ||
|
|
b2cb53143a | ||
|
|
766d00c4d9 | ||
|
|
d1ad8d71fe | ||
|
|
da2c065e59 | ||
|
|
ffcf79ef07 | ||
|
|
d05cfe4e9f | ||
|
|
ed7f374434 | ||
|
|
16dd9a9faf | ||
|
|
0dda149c7f | ||
|
|
0a7fc28d8b | ||
|
|
95660a6665 | ||
|
|
efcb6d472a | ||
|
|
a16f574283 | ||
|
|
3aae6316a7 | ||
|
|
88e73180fd | ||
|
|
7ee513b0cc | ||
|
|
c0cc5ff0bb | ||
|
|
5e112e96df | ||
|
|
9c1b36df95 | ||
|
|
844c390bbc | ||
|
|
297f30ecba | ||
|
|
de588c19be | ||
|
|
44beea5d54 | ||
|
|
2391e406cb | ||
|
|
b03489859a | ||
|
|
cda70628fa | ||
|
|
0749c31100 | ||
|
|
3f50054609 | ||
|
|
1a7c27470e | ||
|
|
c77d64ad6e | ||
|
|
0ebf0dbc6b | ||
|
|
d20619315e | ||
|
|
a741859060 | ||
|
|
f6c1feb26f | ||
|
|
2c5b38e3bd | ||
|
|
a48df18d9a | ||
|
|
3c37dcfa26 | ||
|
|
7215f75624 | ||
|
|
3889b84ac6 | ||
|
|
1a4ce7a33a | ||
|
|
c02dc05bec | ||
|
|
1e75f7462d | ||
|
|
e4d5124211 | ||
|
|
179719473c | ||
|
|
e0351704c9 | ||
|
|
f115b2d084 | ||
|
|
4007201575 | ||
|
|
fe15adaf31 | ||
|
|
c489497e45 | ||
|
|
fbd7307f16 | ||
|
|
dd40a6c04b | ||
|
|
291b9d592d | ||
|
|
16d426704f | ||
|
|
209391c7fc | ||
|
|
e9a2c514dd | ||
|
|
6676d640ba | ||
|
|
71a4d19a83 | ||
|
|
d80f26be9a | ||
|
|
1851f34d3f | ||
|
|
32b433fa62 | ||
|
|
865e5b4180 | ||
|
|
b940b52e71 | ||
|
|
f443e5bbd4 | ||
|
|
be6273ec8f | ||
|
|
528530ebf3 | ||
|
|
0de5c5c1a9 | ||
|
|
86e20c9c48 | ||
|
|
8046666943 | ||
|
|
7783aa7cc5 | ||
|
|
8b9463358d | ||
|
|
72b3696af5 | ||
|
|
07505cdd61 | ||
|
|
8891d75baa | ||
|
|
d7c1e45814 | ||
|
|
523cb8ea1f | ||
|
|
1fb904a137 | ||
|
|
c77676faee | ||
|
|
f1070734b1 | ||
|
|
814271dc0b | ||
|
|
fa00e34635 | ||
|
|
c145549af4 | ||
|
|
193446daed | ||
|
|
cc9194bfca | ||
|
|
2abfdb9628 | ||
|
|
03775f0f80 | ||
|
|
f0f0b3a3b4 | ||
|
|
b09975a3ba | ||
|
|
8b00b6419c | ||
|
|
20de73bb65 | ||
|
|
bb32d61a7e | ||
|
|
325c659e4b | ||
|
|
0b27277612 | ||
|
|
5123f3b57d | ||
|
|
ba91457a51 | ||
|
|
31933a9f7d | ||
|
|
fbd2cdbc2f | ||
|
|
0432642231 | ||
|
|
451ae33f0d | ||
|
|
8d10830dc4 | ||
|
|
954d84129d | ||
|
|
779682d35f | ||
|
|
254c1227d2 | ||
|
|
e1c28c46e6 | ||
|
|
7ca140d9a7 | ||
|
|
78ff6fc49f | ||
|
|
ca31580db5 | ||
|
|
5a5e12a693 | ||
|
|
c4841575c9 | ||
|
|
65bf11554a | ||
|
|
f51f8dcc19 | ||
|
|
ee1435748f | ||
|
|
cbed25c170 | ||
|
|
90febd954e | ||
|
|
f7642f37f9 | ||
|
|
32f119f7ac | ||
|
|
f5fceed8e2 | ||
|
|
c75b02bc66 | ||
|
|
8ffea9963a | ||
|
|
11b299d186 | ||
|
|
2adc315f13 | ||
|
|
6c91a8728e | ||
|
|
cdedc8df33 | ||
|
|
f0c0200eb9 | ||
|
|
5cefc3cfe1 | ||
|
|
5d00de9892 | ||
|
|
5603af245c | ||
|
|
7e2a783161 | ||
|
|
edf1f62b2e | ||
|
|
6a4ab2bc5b | ||
|
|
fd4d5bfbd1 | ||
|
|
7408217b6a | ||
|
|
2e66e4d5ec | ||
|
|
145e3ee318 | ||
|
|
f910000d2b | ||
|
|
b930fe04ce | ||
|
|
74daa0e7a1 | ||
|
|
6fc411d17f | ||
|
|
0e9e2177cf | ||
|
|
f529214660 | ||
|
|
ebf98130d7 | ||
|
|
d2dd6762a7 | ||
|
|
14edc1a7a5 | ||
|
|
58d436c924 | ||
|
|
a3b49a6e24 | ||
|
|
a96e9cbdf9 | ||
|
|
a2da69ede5 | ||
|
|
8725196df6 | ||
|
|
7c9d1ec712 | ||
|
|
092e4d428d | ||
|
|
67b1917467 | ||
|
|
e7055b43a4 | ||
|
|
eba2b2d711 | ||
|
|
cb7588d6fa | ||
|
|
48a207571b | ||
|
|
b76f397983 | ||
|
|
e77b9ced58 | ||
|
|
98e9e6cefc | ||
|
|
35f0b44906 | ||
|
|
92efa38948 | ||
|
|
4df25c436b | ||
|
|
72e38c6bfd | ||
|
|
b0911e4434 | ||
|
|
9cb00262f6 |
6
.github/actions/setup-node/action.yml
vendored
6
.github/actions/setup-node/action.yml
vendored
@@ -19,14 +19,14 @@ runs:
|
||||
shell: bash
|
||||
|
||||
- name: Cache yarn cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-master
|
||||
key: ${{ runner.os }}-node${{ inputs.NODEJS_VERSION }}-yarn-master
|
||||
|
||||
- name: Set up NodeJS
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: ${{ inputs.NODEJS_VERSION }}
|
||||
|
||||
|
||||
6
.github/workflows/build-and-test.yml
vendored
6
.github/workflows/build-and-test.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['18', '20', '22', '24']
|
||||
node: ['20', '22', '24']
|
||||
name: Node ${{ matrix.node }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
run: yarn run mocha-suite
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7.0.1
|
||||
if: github.ref == 'refs/heads/master' && matrix.node-version == '22'
|
||||
with:
|
||||
name: Prebuild with Node.js ${{ matrix.node-version }}
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
name: Lint files
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,8 +16,6 @@ backups/
|
||||
*.log
|
||||
*.sqlite
|
||||
|
||||
*~
|
||||
|
||||
# ignore config files
|
||||
config.json
|
||||
.sequelizerc
|
||||
|
||||
2
.mailmap
2
.mailmap
@@ -33,3 +33,5 @@ Philip Molares <philip.molares@udo.edu> Philip Molares <git@molar.es>
|
||||
Yannick Bungers <git@innay.de> Yannick Bungers <github@innay.de>
|
||||
|
||||
Renovate Bot <bot@renovateapp.com> renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
|
||||
|
||||
Tobias Dillig <tobias.dillig@frachtwerk.de> Tobi <tldev-de@users.noreply.github.com>
|
||||
|
||||
942
.yarn/releases/yarn-4.12.0.cjs
vendored
942
.yarn/releases/yarn-4.12.0.cjs
vendored
File diff suppressed because one or more lines are too long
940
.yarn/releases/yarn-4.13.0.cjs
vendored
Executable file
940
.yarn/releases/yarn-4.13.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
@@ -4,4 +4,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||
|
||||
9
AUTHORS
9
AUTHORS
@@ -9,6 +9,7 @@ Alex Garcia <alexsebastian.garcia@gmail.com>
|
||||
Alexander Hesse <alexander.hesse@sandstorm-media.de>
|
||||
Alexander Wellbrock <a.wellbrock@mailbox.org>
|
||||
Amolith <amolith@nixnet.xyz>
|
||||
Andre Julius <noromoron@gmail.com>
|
||||
Andreas Boesen <618847+Happy86@users.noreply.github.com>
|
||||
Antoine Aflalo <antoine@warrantymaster.com>
|
||||
aptalca <aptalca@users.noreply.github.com>
|
||||
@@ -18,6 +19,7 @@ Bartlomiej Szala <fenix440@gmail.com>
|
||||
BoHong Li <a60814billy@gmail.com>
|
||||
Bryan Davis <bd808@wikimedia.org>
|
||||
butlerx <butlerx@notthe.cloud>
|
||||
bxff <51504045+bxff@users.noreply.github.com>
|
||||
Carles Bruguera <carlesba@gmail.com>
|
||||
chandi <git@chandi.it>
|
||||
chbmb <chbmb@linuxserver.io>
|
||||
@@ -116,6 +118,7 @@ Martijnpold <martijntje7@gmail.com>
|
||||
Martin Turoci <martinturoci@gmail.com>
|
||||
Marvin Gaube <dev@marvingaube.de>
|
||||
Matej Cotman <matej.cotman@eficode.com>
|
||||
Mathias Kende <mathias@kende.fr>
|
||||
Mathias Merscher <Mathias.Merscher@dg-i.net>
|
||||
Matt Fisher <matt.fisher@nsidc.org>
|
||||
Matteo Savatteri <matteosavatteri@lcm.mi.infn.it>
|
||||
@@ -134,6 +137,7 @@ Nicolas Dietrich <nidi@mailbox.org>
|
||||
Nils VAN ZUIJLEN <nils.van-zuijlen@mailo.com>
|
||||
Norihito Nakae <norihito.n@gmail.com>
|
||||
NV <nvsofts@gmail.com>
|
||||
odpay <odpay@odpay.net>
|
||||
Oliver Herrmann <oliver@monoxane.com>
|
||||
Ondřej Slabý <kron258@gmail.com>
|
||||
oupala <oupala@users.noreply.github.com>
|
||||
@@ -164,6 +168,7 @@ Salim B <salim@posteo.de>
|
||||
Sandro Jäckel <sandro.jaeckel@gmail.com>
|
||||
schneems <richard.schneeman@gmail.com>
|
||||
Sergio Valverde <svg153@users.noreply.github.com>
|
||||
Shivam Shukla <lucifer84670@gmail.com>
|
||||
Simeon Keske <git@n0emis.eu>
|
||||
Simon C <simonc@linux.com>
|
||||
Simon Fish <si@mon.fish>
|
||||
@@ -184,6 +189,7 @@ Thor77 <thor77@thor77.org>
|
||||
Tilman Vatteroth <git@tilmanvatteroth.de>
|
||||
tkqubo <tk.qubo@gmail.com>
|
||||
tkykm <tkykm@users.noreply.github.com>
|
||||
Tobias Dillig <tobias.dillig@frachtwerk.de>
|
||||
Tobias Kremer <tobias.kremer@gmail.com>
|
||||
Tom Wyckhuys <tomwyckhuys@gmail.com>
|
||||
Toma Tasovac <ttasovac@humanistika.org>
|
||||
@@ -193,10 +199,12 @@ VaiTon <eyadlorenzo@gmail.com>
|
||||
vazontang <31801725+vazontang@users.noreply.github.com>
|
||||
Victor Berger <victor.berger@m4x.org>
|
||||
Vojtěch Káně <vojtech.kane@gmail.com>
|
||||
Will Hawkins <whh8b@obs.cr>
|
||||
WilliButz <wbutz@cyberfnord.de>
|
||||
Wonder Chang <iwonder.tw@gmail.com>
|
||||
Xaver Maierhofer <xaver.maierhofer@xwissen.info>
|
||||
Xavier Marques <xaviermarques4f@gmail.com>
|
||||
xenein <76600392+xenein@users.noreply.github.com>
|
||||
Xiaodong Xu <xxdlhy@gmail.com>
|
||||
xnum <s000032001@gmail.com>
|
||||
Yannick Bungers <git@innay.de>
|
||||
@@ -209,3 +217,4 @@ Zearin <Zearin@users.noreply.github.com>
|
||||
Éric Gaspar <46165813+ericgaspar@users.noreply.github.com>
|
||||
Ömer Erdinç Yağmurlu <omeryagmurlu@gmail.com>
|
||||
蒼時弦也 <elct9620@frost.tw>
|
||||
지지 ᚠד (Jiji Freya Daniel) Maslowski <info@orangecms.org>
|
||||
|
||||
33
Dockerfile
33
Dockerfile
@@ -1,33 +0,0 @@
|
||||
############################
|
||||
# 1) Builder: bygg HedgeDoc
|
||||
############################
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# H??mta din branch som tarball (ingen git beh??vs)
|
||||
ADD https://git.sa6anw.se/sa6anw/hedgedoc/archive/refs/heads/hedgeagent/1.10.5.tar.gz /tmp/hedgedoc.tar.gz
|
||||
|
||||
RUN tar -xzf /tmp/hedgedoc.tar.gz --strip-components=1 \
|
||||
&& rm /tmp/hedgedoc.tar.gz
|
||||
|
||||
# Installera deps + bygg frontend/backend
|
||||
RUN corepack enable \
|
||||
&& yarn install --immutable \
|
||||
&& yarn build
|
||||
|
||||
|
||||
########################################
|
||||
# 2) Runtime: officiell HedgeDoc-image
|
||||
########################################
|
||||
FROM quay.io/hedgedoc/hedgedoc:1.10.5
|
||||
|
||||
USER root
|
||||
|
||||
# Ers??tt upstream-koden
|
||||
RUN rm -rf /hedgedoc
|
||||
|
||||
# Kopiera f??rdigbyggd app med r??tt UID/GID
|
||||
COPY --from=builder --chown=10000:10000 /build /hedgedoc
|
||||
|
||||
USER 10000
|
||||
18
README.md
18
README.md
@@ -1,21 +1,3 @@
|
||||
# HedgeDoc-HedgeAgent
|
||||
|
||||
This is a customized version that makes it possible to invite an AI agent to a note via the menu. "Invite Agent" sends the `note_id` to hedgeagent.
|
||||
|
||||
To start an instance for development, the following is required:
|
||||
1. ```cp config.json.example config.json ```
|
||||
2. Update ```"domain": "change this" ```
|
||||
3. ```.yarn/releases/yarn-4.12.0.cjs install```
|
||||
4. ```.yarn/releases/yarn-4.12.0.cjs run build```
|
||||
5. ```.yarn/releases/yarn-4.12.0.cjs start```
|
||||
|
||||
To start for production:
|
||||
1. `docker compose up -d`
|
||||
|
||||
Note: Use UID 10000 for data folders.
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
# HedgeDoc
|
||||
|
||||
30
app.js
30
app.js
@@ -85,11 +85,14 @@ if (config.enableStatsApi) {
|
||||
const io = new Server(server, {
|
||||
pingInterval: config.heartbeatInterval,
|
||||
pingTimeout: config.heartbeatTimeout,
|
||||
cookie: false,
|
||||
cors: {
|
||||
origin: config.serverURL,
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
},
|
||||
connectionStateRecovery: {
|
||||
maxDisconnectionDuration: 2 * 60 * 1000,
|
||||
skipMiddlewares: false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -156,6 +159,13 @@ app.use(cookieParser())
|
||||
app.use(i18n.init)
|
||||
|
||||
// routes without sessions
|
||||
// security headers for uploads
|
||||
app.use('/uploads', (req, res, next) => {
|
||||
res.set('Content-Disposition', 'attachment')
|
||||
res.set('Content-Security-Policy', "default-src 'none'; sandbox")
|
||||
next()
|
||||
})
|
||||
|
||||
// static files
|
||||
app.use('/', express.static(path.join(__dirname, '/public'), {
|
||||
maxAge: config.staticCacheTime,
|
||||
@@ -166,13 +176,6 @@ app.use('/docs', express.static(path.resolve(__dirname, config.docsPath), {
|
||||
maxAge: config.staticCacheTime,
|
||||
redirect: false
|
||||
}))
|
||||
// This is done by an additional middleware, instead of setHeaders of express.static, because for what ever reason
|
||||
// the latter did not work
|
||||
app.use('/uploads', (req, res, next) => {
|
||||
res.set('Content-Disposition', 'attachment')
|
||||
res.set('Content-Security-Policy', "default-src 'none'")
|
||||
next()
|
||||
})
|
||||
app.use('/uploads', express.static(path.resolve(__dirname, config.uploadsPath), {
|
||||
maxAge: config.staticCacheTime,
|
||||
redirect: false
|
||||
@@ -253,6 +256,15 @@ app.locals.authProviders = {
|
||||
allowEmailRegister: config.allowEmailRegister
|
||||
}
|
||||
|
||||
// If none of the form-based methods (LDAP, Email, OpenID) are configured and
|
||||
// exactly one redirect-style external provider is, skip the "Choose method"
|
||||
// dialog and link "Sign In" straight to that provider's login page.
|
||||
const externalProviderKeys = ['facebook', 'twitter', 'github', 'gitlab', 'mattermost', 'dropbox', 'google', 'saml', 'oauth2']
|
||||
const enabledExternalProviders = externalProviderKeys.filter((key) => app.locals.authProviders[key])
|
||||
app.locals.authProviders.autoLoginProvider = (!app.locals.authProviders.ldap && !app.locals.authProviders.email && !app.locals.authProviders.openID && enabledExternalProviders.length === 1)
|
||||
? enabledExternalProviders[0]
|
||||
: null
|
||||
|
||||
// Export/Import menu items
|
||||
app.locals.enableDropBoxSave = config.isDropboxEnable
|
||||
app.locals.enableGitHubGist = config.isGitHubEnable
|
||||
@@ -263,7 +275,7 @@ app.use(require('./lib/web/statusRouter'))
|
||||
app.use(require('./lib/web/auth'))
|
||||
app.use(require('./lib/web/historyRouter'))
|
||||
app.use(require('./lib/web/userRouter'))
|
||||
app.use(require('./lib/web/imageRouter'))
|
||||
app.use(require('./lib/web/imageRouter').default)
|
||||
app.use(require('./lib/web/note/router'))
|
||||
|
||||
// response not found if no any route matxches
|
||||
|
||||
@@ -7,6 +7,7 @@ logger.transports.forEach((transport) => transport.level = 'warning')
|
||||
const models = require('../lib/models/')
|
||||
const readline = require('readline-sync')
|
||||
const minimist = require('minimist')
|
||||
const { ValidationError } = require('sequelize')
|
||||
|
||||
function showUsage (tips) {
|
||||
console.log(`${tips}
|
||||
@@ -48,12 +49,21 @@ async function createUser (argv) {
|
||||
}
|
||||
|
||||
// Lets try to create, and check success
|
||||
const ref = await models.User.create({ email: argv['add'], password: pass })
|
||||
if (ref === undefined) {
|
||||
console.log(`Could not create user with email ${argv['add']}`)
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.log(`Created user with email ${argv['add']}`)
|
||||
try {
|
||||
const ref = await models.User.create({ email: argv['add'], password: pass })
|
||||
if (ref === undefined) {
|
||||
console.log(`Could not create user with email ${argv['add']}`)
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.log(`Created user with email ${argv['add']}`)
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ValidationError) {
|
||||
console.log('Could not create user, since the email address is invalid.')
|
||||
process.exit(1)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,4 +127,7 @@ if (typeof argv[action] !== 'string') {
|
||||
// Call respective processing functions
|
||||
options[action](argv).then(function () {
|
||||
process.exit(0);
|
||||
}).catch(err => {
|
||||
console.error('An error occurred:', err)
|
||||
process.exit(1)
|
||||
});
|
||||
|
||||
@@ -18,20 +18,12 @@
|
||||
"production": {
|
||||
"domain": "change this",
|
||||
"loglevel": "info",
|
||||
"hsts": {
|
||||
"enable": true,
|
||||
"maxAgeSeconds": 31536000,
|
||||
"includeSubdomains": true,
|
||||
"preload": true
|
||||
},
|
||||
"csp": {
|
||||
"enable": true,
|
||||
"directives": {
|
||||
},
|
||||
"upgradeInsecureRequests": "auto",
|
||||
"addDefaults": true,
|
||||
"addDisqus": true,
|
||||
"addGoogleAnalytics": true
|
||||
"addDefaults": true
|
||||
},
|
||||
"cookiePolicy": "lax",
|
||||
"db": {
|
||||
@@ -42,103 +34,8 @@
|
||||
"port": "5432",
|
||||
"dialect": "postgres"
|
||||
},
|
||||
"facebook": {
|
||||
"clientID": "change this",
|
||||
"clientSecret": "change this"
|
||||
},
|
||||
"twitter": {
|
||||
"consumerKey": "change this",
|
||||
"consumerSecret": "change this"
|
||||
},
|
||||
"github": {
|
||||
"clientID": "change this",
|
||||
"clientSecret": "change this"
|
||||
},
|
||||
"gitlab": {
|
||||
"baseURL": "change this",
|
||||
"clientID": "change this",
|
||||
"clientSecret": "change this",
|
||||
"scope": "use 'read_user' scope for auth user only or remove this property if you need gitlab snippet import/export support (will result to be default scope 'api')",
|
||||
"version": "use 'v4' if gitlab version > 11, 'v3' otherwise. Default to 'v4'"
|
||||
},
|
||||
"mattermost": {
|
||||
"baseURL": "change this",
|
||||
"clientID": "change this",
|
||||
"clientSecret": "change this"
|
||||
},
|
||||
"dropbox": {
|
||||
"clientID": "change this",
|
||||
"clientSecret": "change this",
|
||||
"appKey": "change this"
|
||||
},
|
||||
"google": {
|
||||
"clientID": "change this",
|
||||
"clientSecret": "change this",
|
||||
"apiKey": "change this"
|
||||
},
|
||||
"ldap": {
|
||||
"url": "ldap://change_this",
|
||||
"bindDn": null,
|
||||
"bindCredentials": null,
|
||||
"searchBase": "change this",
|
||||
"searchFilter": "change this",
|
||||
"searchAttributes": ["change this"],
|
||||
"usernameField": "change this e.g. cn",
|
||||
"useridField": "change this e.g. uid",
|
||||
"tlsOptions": {
|
||||
"changeme": "See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback"
|
||||
}
|
||||
},
|
||||
"saml": {
|
||||
"idpSsoUrl": "change: authentication endpoint of IdP",
|
||||
"idpCert": "change: certificate file path of IdP in PEM format",
|
||||
"issuer": "change or delete: identity of the service provider (default: config.serverURL)",
|
||||
"identifierFormat": "change or delete: name identifier format (default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')",
|
||||
"disableRequestedAuthnContext": "change or delete: true to allow any authentication method, false restricts to password authentication method (default: false)",
|
||||
"wantAssertionsSigned": "change or delete: true to enforce signed assertions, false allows unsigned assertions (default: true)",
|
||||
"wantAuthnResponseSigned": "change or delete: true to enforce signed response, false allows unsigned response (default: true)",
|
||||
"groupAttribute": "change or delete: attribute name for group list (ex: memberOf)",
|
||||
"requiredGroups": [ "change or delete: group names that allowed" ],
|
||||
"externalGroups": [ "change or delete: group names that not allowed" ],
|
||||
"attribute": {
|
||||
"id": "change or delete this: attribute map for `id` (default: NameID)",
|
||||
"username": "change or delete this: attribute map for `username` (default: NameID)",
|
||||
"email": "change or delete this: attribute map for `email` (default: NameID)"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"baseURL": "https://auth.example.com/",
|
||||
"userProfileURL": "https://auth.example.com/oauth2/userinfo/",
|
||||
"tokenURL": "https://auth.example.com/oauth2/token/",
|
||||
"authorizationURL": "https://auth.example.com/oauth2/authorize/",
|
||||
"clientID": "change-this-id",
|
||||
"clientSecret": "change-this-secret",
|
||||
"scope": "openid profile user",
|
||||
"userProfileUsernameAttr": "preferred_username",
|
||||
"userProfileEmailAttr": "email",
|
||||
"userProfileDisplayNameAttr": "name",
|
||||
"pkce": true
|
||||
},
|
||||
"imgur": {
|
||||
"clientID": "change this"
|
||||
},
|
||||
"minio": {
|
||||
"accessKey": "change this",
|
||||
"secretKey": "change this",
|
||||
"endPoint": "change this",
|
||||
"secure": true,
|
||||
"port": 9000
|
||||
},
|
||||
"s3": {
|
||||
"accessKeyId": "change this",
|
||||
"secretAccessKey": "change this",
|
||||
"region": "change this"
|
||||
},
|
||||
"s3bucket": "change this",
|
||||
"azure":
|
||||
{
|
||||
"connectionString": "change this",
|
||||
"container": "change this"
|
||||
}
|
||||
"allowAnonymous": false,
|
||||
"allowAnonymousEdits": true,
|
||||
"enableStatsApi": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
services:
|
||||
database:
|
||||
image: postgres:13.4-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=hedgedoc
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=hedgedoc
|
||||
volumes:
|
||||
- ./db:/var/lib/postgresql/data
|
||||
restart: always
|
||||
networks:
|
||||
- internal
|
||||
|
||||
app:
|
||||
build: .
|
||||
image: hedgedoc-hedgeagent:1.10.5
|
||||
container_name: hedgedoc
|
||||
environment:
|
||||
CMD_DB_URL: postgres://hedgedoc:password@database:5432/hedgedoc
|
||||
CMD_PORT: "3000"
|
||||
CMD_HOST: "0.0.0.0"
|
||||
CMD_DOMAIN: hedgedoc.sa6anw.se
|
||||
CMD_URL_ADDPORT: "false"
|
||||
CMD_PROTOCOL_USESSL: "true"
|
||||
CMD_EMAIL: "true"
|
||||
CMD_ALLOW_EMAIL_REGISTER: "true"
|
||||
volumes:
|
||||
- ./uploads:/hedgedoc/public/uploads
|
||||
restart: always
|
||||
depends_on:
|
||||
- database
|
||||
# Since I use Caddy I don't need this
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
|
||||
## If you use Caddy like I do keep below
|
||||
networks:
|
||||
- internal
|
||||
- proxy
|
||||
|
||||
labels:
|
||||
caddy: hedgedoc.sa6anw.se
|
||||
caddy.reverse_proxy: "{{upstreams 3000}}"
|
||||
|
||||
networks:
|
||||
internal:
|
||||
internal: true
|
||||
proxy:
|
||||
external: true
|
||||
## If you don't, you can deleta all of this
|
||||
@@ -6,14 +6,14 @@ Environment variables take precedence over configurations from the config files.
|
||||
our own options, but we also list node-specific options you can configure this way.
|
||||
|
||||
- Environment variables are processed in [
|
||||
`lib/config/environment.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/environment.js) - so this is
|
||||
the first place to look if anything is missing not obvious from this document. The default values are defined in [
|
||||
`lib/config/environment.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/environment.js) which makes it
|
||||
the first place to look if anything is missing or not obvious from this document. The default values are defined in [
|
||||
`lib/config/default.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/default.js), in case you wonder
|
||||
if you even need to override it.
|
||||
|
||||
- The config file is processed in [
|
||||
`lib/config/index.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/index.js) - so this is the first
|
||||
place to look if anything is missing not obvious from this document. The default values are defined in [
|
||||
`lib/config/index.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/index.js) which makes it
|
||||
the first place to look if anything is missing or not obvious from this document. The default values are defined in [
|
||||
`lib/config/default.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/default.js), in case you wonder
|
||||
if you even need to override it. To get started, it is a good idea to take the [
|
||||
`config.json.example`](https://github.com/hedgedoc/hedgedoc/tree/master/config.json.example) and copy it
|
||||
@@ -31,28 +31,30 @@ to work.*
|
||||
|
||||
## HedgeDoc basics
|
||||
|
||||
| config file | environment | **default** and example value | description |
|
||||
|----------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| | `CMD_CONFIG_FILE` | **no default**, `/path/to/config.json` | optional override for the path to HedgeDoc's config file |
|
||||
| `db` | | **`undefined`**, `{ "dialect": "sqlite", "storage": "./db.hedgedoc.sqlite" }` | set the db configs, [see more here](https://sequelize.org/v5/manual/dialects.html) |
|
||||
| `db.username` | `CMD_DB_USERNAME` | **`undefined`**, `hedgedoc-db-user` | Username used to authenticate to the database (host). |
|
||||
| `db.password` | `CMD_DB_PASSWORD` | **`undefined`** | Password used to authenticate to the database (host). |
|
||||
| `db.database` | `CMD_DB_DATABASE` | **`undefined`**, `hedgedoc` | Name of the database used to store hedgedoc data. |
|
||||
| `db.host` | `CMD_DB_HOST` | **`undefined`**, `db-host.example.com` | Hostname used to connect the database server. |
|
||||
| `db.post` | `CMD_DB_PORT` | **`undefined`**, `5432` | Port used to connect the database server. |
|
||||
| `db.dialect` | `CMD_DB_DIALECT` | **`undefined`**, `postgres`, `mariadb` | [Dialect](https://sequelize.org/v5/manual/dialects.html) / protocol used to connect to the database. |
|
||||
| `dbURL` | `CMD_DB_URL` | **`undefined`**, `postgres://username:password@localhost:5432/hedgedoc`, `mysql://username:password@localhost:3306/hedgedoc`, or `sqlite:///path/to/hedgedoc.db` | Set the db in URL style. If set, then the relevant `db` config entries will be overridden. |
|
||||
| `loglevel` | `CMD_LOGLEVEL` | **`info`**, `debug` ... | Defines what kind of logs are provided to stdout. Available options: `debug`, `verbose`, `info`, `warn`, `error` |
|
||||
| `forbiddenNoteIDs` | `CMD_FORBIDDEN_NOTE_IDS` | **`['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views']`**, `['robots.txt']` or `'robots.txt'` | disallow creation of notes, even if `allowFreeUrl` or `CMD_ALLOW_FREEURL` is `true` |
|
||||
| `imageUploadType` | `CMD_IMAGE_UPLOAD_TYPE` | **`filesystem`**, `imgur`, `s3`, `minio`, `azure`, `lutim` | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/s3-image-upload.md) or [Minio](guides/minio-image-upload.md), also there's a whole section on their respective env vars below. |
|
||||
| `sourceURL` | `CMD_SOURCE_URL` | **no default**, `https://github.com/hedgedoc/hedgedoc/tree/<current commit>` | Provides the link to the source code of HedgeDoc on the entry page (Please, make sure you change this when you run a modified version) |
|
||||
| `tooBusyLag` | `CMD_TOOBUSY_LAG` | **`70`** | CPU time for one event loop tick until node throttles connections. (milliseconds) |
|
||||
| `staticCacheTime` | | **`1 * 24 * 60 * 60 * 1000`** | static file cache time |
|
||||
| `heartbeatInterval` | | **`5000`** | socket.io heartbeat interval |
|
||||
| `heartbeatTimeout` | | **`10000`** | socket.io heartbeat timeout |
|
||||
| `documentMaxLength` | `CMD_DOCUMENT_MAX_LENGTH` | **`100000`** | note max length |
|
||||
| `linkifyHeaderStyle` | | **`keep-case`**, `lower-case`, `gfm` | how is a header text converted into a link id |
|
||||
| `enableStatsApi` | `CMD_ENABLE_STATS_API` | **`true`**, `false` | Enables or disables the /status and /metrics endpoint. |
|
||||
| config file | environment | **default** and example value | description |
|
||||
|-------------------------|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| | `CMD_CONFIG_FILE` | **no default**, `/path/to/config.json` | optional override for the path to HedgeDoc's config file |
|
||||
| `db` | | **`undefined`**, `{ "dialect": "sqlite", "storage": "./db.hedgedoc.sqlite" }` | set the db configs, [see more here](https://sequelize.org/v5/manual/dialects.html) |
|
||||
| `db.username` | `CMD_DB_USERNAME` | **`undefined`**, `hedgedoc-db-user` | Username used to authenticate to the database (host). |
|
||||
| `db.password` | `CMD_DB_PASSWORD` | **`undefined`** | Password used to authenticate to the database (host). |
|
||||
| `db.database` | `CMD_DB_DATABASE` | **`undefined`**, `hedgedoc` | Name of the database used to store hedgedoc data. |
|
||||
| `db.host` | `CMD_DB_HOST` | **`undefined`**, `db-host.example.com` | Hostname used to connect the database server. |
|
||||
| `db.post` | `CMD_DB_PORT` | **`undefined`**, `5432` | Port used to connect the database server. |
|
||||
| `db.dialect` | `CMD_DB_DIALECT` | **`undefined`**, `postgres`, `mariadb` | [Dialect](https://sequelize.org/v5/manual/dialects.html) / protocol used to connect to the database. |
|
||||
| `dbURL` | `CMD_DB_URL` | **`undefined`**, `postgres://username:password@localhost:5432/hedgedoc`, `mysql://username:password@localhost:3306/hedgedoc`, or `sqlite:///path/to/hedgedoc.db` | Set the db in URL style. If set, then the relevant `db` config entries will be overridden. |
|
||||
| `loglevel` | `CMD_LOGLEVEL` | **`info`**, `debug` ... | Defines what kind of logs are provided to stdout. Available options: `debug`, `verbose`, `info`, `warn`, `error` |
|
||||
| `forbiddenNoteIDs` | `CMD_FORBIDDEN_NOTE_IDS` | **`['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views']`**, `['robots.txt']` or `'robots.txt'` | disallow creation of notes, even if `allowFreeUrl` or `CMD_ALLOW_FREEURL` is `true` |
|
||||
| `externalLinkWarning` | `CMD_EXTERNAL_LINK_WARNING` | **`true`**, `false` | When set to `false`, disables the external link warning page entirely. All external links open directly without being routed through the `/_link` redirect. |
|
||||
| `externalLinkWhitelist` | `CMD_EXTERNAL_LINK_WHITELIST` | **`[]`**, `example.com,trusted.org,*.example.org` | Comma-separated list of hosts that bypass the external link warning page. An entry like `example.com` matches that host only; prefix with `*.` (e.g. `*.example.com`) to also match all of its subdomains. |
|
||||
| `imageUploadType` | `CMD_IMAGE_UPLOAD_TYPE` | **`filesystem`**, `imgur`, `s3`, `minio`, `azure`, `lutim` | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/media/s3-image-upload.md) or [Minio](guides/media/minio-image-upload.md), also there's a whole section on their respective env vars below. |
|
||||
| `sourceURL` | `CMD_SOURCE_URL` | **no default**, `https://github.com/hedgedoc/hedgedoc/tree/<current commit>` | Provides the link to the source code of HedgeDoc on the entry page (Please, make sure you change this when you run a modified version) |
|
||||
| `tooBusyLag` | `CMD_TOOBUSY_LAG` | **`70`** | CPU time for one event loop tick until node throttles connections. (milliseconds) |
|
||||
| `staticCacheTime` | | **`1 * 24 * 60 * 60 * 1000`** | static file cache time |
|
||||
| `heartbeatInterval` | | **`5000`** | socket.io heartbeat interval |
|
||||
| `heartbeatTimeout` | | **`10000`** | socket.io heartbeat timeout |
|
||||
| `documentMaxLength` | `CMD_DOCUMENT_MAX_LENGTH` | **`100000`** | note max length |
|
||||
| `linkifyHeaderStyle` | | **`keep-case`**, `lower-case`, `gfm` | how is a header text converted into a link id |
|
||||
| `enableStatsApi` | `CMD_ENABLE_STATS_API` | **`true`**, `false` | Enables or disables the /status and /metrics endpoint. |
|
||||
|
||||
## HedgeDoc paths stuff
|
||||
|
||||
@@ -80,33 +82,34 @@ these are rarely used for various reasons.
|
||||
| `urlPath` | `CMD_URL_PATH` | **`null`**, `hedgedoc` | If HedgeDoc is run from a subdirectory like `www.example.com/<urlpath>` |
|
||||
| `host` | `CMD_HOST` | **`0.0.0.0`**, `localhost` | interface/ip to listen on |
|
||||
| `port` | `CMD_PORT` | **`3000`**, `80` | port to listen on |
|
||||
| `path` | `CMD_PATH` | **no default**, `/var/run/hedgedoc.sock` | path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored) |
|
||||
| `path` | `CMD_PATH` | **no default**, `/var/run/hedgedoc.sock` | path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored for listening; however, `port` or `CMD_PORT` is still used for URL generation when `urlAddPort` is enabled) |
|
||||
| `protocolUseSSL` | `CMD_PROTOCOL_USESSL` | **`false`** or `true` | set to use SSL protocol for resources path (only applied when domain is set) |
|
||||
| `useSSL` | | **`false`** or `true` | set to use SSL server (if `true`, will auto turn on `protocolUseSSL`) |
|
||||
| `urlAddPort` | `CMD_URL_ADDPORT` | **`false`** or `true` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) |
|
||||
| `allowOrigin` | `CMD_ALLOW_ORIGIN` | **`['localhost']`**, `['hedgedoc.org']`, `['localhost', 'hedgedoc.org']` | domain name whitelist (use comma to separate) |
|
||||
|
||||
## Web security aspects
|
||||
|
||||
| config file | environment | **default** and example value | description |
|
||||
|-------------------------------|--------------------------------|-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `hsts` | | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) |
|
||||
| | `CMD_HSTS_ENABLE` | **`true`** or `false` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) |
|
||||
| | `CMD_HSTS_INCLUDE_SUBDOMAINS` | **`true`** or `false` | set to include subdomains in HSTS (default is `false`) |
|
||||
| | `CMD_HSTS_MAX_AGE` | **`31536000`**, `60 * 60 * 24 * 365` | max duration in seconds to tell clients to keep HSTS status (default is a year) |
|
||||
| | `CMD_HSTS_PRELOAD` | `true` or **`false`** | whether to allow preloading of the site's HSTS status (e.g. into browsers) |
|
||||
| `csp` | | `{"enable": true, "addDefaults": true}` | Nested object to configure the Content Security Policy |
|
||||
| `csp.enable` | `CMD_CSP_ENABLE` | **`true`** or `false` | Whether to apply a `Content-Security-Policy` header to responses. We don't recommend disabling this option, as it significantly reduces the security of the application. |
|
||||
| `csp.addDefaults` | | **`true`** or `false` | Disable to not include the default CSP. Be careful, this will break the application if the correct directives are not set manually. |
|
||||
| `csp.directives` | | **no default**, `{"scriptSrc": "trustworthy-scripts.example.com"}` | Custom CSP directives. These are passed to Helmet - see [their documentation](https://helmetjs.github.io/docs/csp/) for more information on the format. |
|
||||
| `csp.addDisqus` | `CMD_CSP_ADD_DISQUS` | **`false`** or `true` | Enable to allow users to add Disqus comments to their notes or presentations. We don't recommend enabling this option, as it increases the attack surface of XSS attacks. |
|
||||
| `csp.addGoogleAnalytics` | `CMD_CSP_ADD_GOOGLE_ANALYTICS` | **`false`** or `true` | Enable to allow users to add Google Analytics to their notes. We don't recommend enabling this option, as it increases the attack surface of XSS attacks. |
|
||||
| `csp.upgradeInsecureRequests` | | **`auto`** or `true` or `false` | By default (`auto`), insecure (HTTP) requests are upgraded to HTTPS via CSP if `useSSL` is on. To change this behaviour, set to either `true` or `false`. |
|
||||
| `csp.reportUri` | `CMD_CSP_REPORTURI` | **`undefined`**, `https://<someid>.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations. |
|
||||
| `csp.allowFraming` | `CMD_CSP_ALLOW_FRAMING` | **`true`** or `false` | Disable to disallow embedding of the instance via iframe. We **strongly recommend disabling** this option, as it increases the attack surface of XSS attacks. |
|
||||
| `csp.allowPDFEmbed` | `CMD_CSP_ALLOW_PDF_EMBED` | **`true`** or `false` | Disable to disallow embedding PDFs. We recommend disabling this option, as it increases the attack surface of XSS attacks. |
|
||||
| `cookiePolicy` | `CMD_COOKIE_POLICY` | **`lax`**, `strict` or `none` | Set a SameSite policy whether cookies are send from cross-origin. Be careful: setting a SameSite value of none without https breaks the editor. |
|
||||
| `rateLimitNewNotes` | `CMD_RATE_LIMIT_NEW_NOTES` | **`20`**, `0` or any positive number | Sets the maximum amount of new note creations per 5-minute window per user. Can be disabled by setting to `0`. |
|
||||
| config file | environment | **default** and example value | description |
|
||||
|-------------------------------|-----------------------------------|-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `hsts` | | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) |
|
||||
| | `CMD_HSTS_ENABLE` | **`true`** or `false` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) |
|
||||
| | `CMD_HSTS_INCLUDE_SUBDOMAINS` | **`true`** or `false` | set to include subdomains in HSTS (default is `false`) |
|
||||
| | `CMD_HSTS_MAX_AGE` | **`31536000`**, `60 * 60 * 24 * 365` | max duration in seconds to tell clients to keep HSTS status (default is a year) |
|
||||
| | `CMD_HSTS_PRELOAD` | `true` or **`false`** | whether to allow preloading of the site's HSTS status (e.g. into browsers) |
|
||||
| `csp` | | `{"enable": true, "addDefaults": true}` | Nested object to configure the Content Security Policy |
|
||||
| `csp.enable` | `CMD_CSP_ENABLE` | **`true`** or `false` | Whether to apply a `Content-Security-Policy` header to responses. We don't recommend disabling this option, as it significantly reduces the security of the application. |
|
||||
| `csp.addDefaults` | | **`true`** or `false` | Disable to not include the default CSP. Be careful, this will break the application if the correct directives are not set manually. |
|
||||
| `csp.directives` | | **no default**, `{"scriptSrc": "trustworthy-scripts.example.com"}` | Custom CSP directives. These are passed to Helmet - see [their documentation](https://helmetjs.github.io/docs/csp/) for more information on the format. |
|
||||
| `csp.addDisqus` | `CMD_CSP_ADD_DISQUS` | **`false`** or `true` | Enable to allow users to add Disqus comments to their notes or presentations. We don't recommend enabling this option, as it increases the attack surface of XSS attacks. |
|
||||
| `csp.addGoogleAnalytics` | `CMD_CSP_ADD_GOOGLE_ANALYTICS` | **`false`** or `true` | Enable to allow users to add Google Analytics to their notes. We don't recommend enabling this option, as it increases the attack surface of XSS attacks. |
|
||||
| `csp.upgradeInsecureRequests` | | **`auto`** or `true` or `false` | By default (`auto`), insecure (HTTP) requests are upgraded to HTTPS via CSP if `useSSL` is on. To change this behaviour, set to either `true` or `false`. |
|
||||
| `csp.reportUri` | `CMD_CSP_REPORTURI` | **`undefined`**, `https://<someid>.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations. |
|
||||
| `csp.allowFraming` | `CMD_CSP_ALLOW_FRAMING` | **`true`** or `false` | Disable to disallow embedding of the instance via iframe. We **strongly recommend disabling** this option, as it increases the attack surface of XSS attacks. |
|
||||
| `csp.allowPDFEmbed` | `CMD_CSP_ALLOW_PDF_EMBED` | **`true`** or `false` | Disable to disallow embedding PDFs. We recommend disabling this option, as it increases the attack surface of XSS attacks. |
|
||||
| `cookiePolicy` | `CMD_COOKIE_POLICY` | **`lax`**, `strict` or `none` | Set a SameSite policy whether cookies are send from cross-origin. Be careful: setting a SameSite value of none without https breaks the editor. |
|
||||
| `rateLimitNewNotes` | `CMD_RATE_LIMIT_NEW_NOTES` | **`20`**, `0` or any positive number | Sets the maximum amount of new note creations per 5-minute window per user. Can be disabled by setting to `0`. |
|
||||
| `rateLimitUser` | `CMD_RATE_LIMIT_USER` | **`10`**, `0` or any positive number | Sets the maximum amount of user auth interactions (login, signup) per 5-minute window per user. Can be disabled by setting to `0`. |
|
||||
| `rateLimitUsingCloudflare` | `CMD_RATE_LIMIT_USING_CLOUDFLARE` | **`false`** or `true` | Activate specialy cloudflare ip handling. If you use cloudflare for your instance you want this to be true. |
|
||||
|
||||
## Privacy and External Requests
|
||||
|
||||
@@ -123,7 +126,7 @@ these are rarely used for various reasons.
|
||||
| `allowFreeURL` | `CMD_ALLOW_FREEURL` | **`false`** or `true` | Set to allow new note creation by accessing a nonexistent note URL. This is the behavior familiar from [Etherpad](https://github.com/ether/etherpad-lite). |
|
||||
| `requireFreeURLAuthentication` | `CMD_REQUIRE_FREEURL_AUTHENTICATION` | **`false`** or `true` | Set to require authentication for FreeURL mode style note creation. |
|
||||
| `disableNoteCreation` | `CMD_DISABLE_NOTE_CREATION` | **`false`** or `true` | Set to `true` to disallow any person to create notes. |
|
||||
| `defaultPermission` | `CMD_DEFAULT_PERMISSION` | **`editable`**, `freely`, `limited`, `locked`, `protected` or `private` | Set notes default permission (only applied on signed-in users). |
|
||||
| `defaultPermission` | `CMD_DEFAULT_PERMISSION` | **`editable`**, `freely`, `limited`, `locked`, `protected` or `private` | Set notes default permission (only applied on signed-in users). See the [permissions documentation](./references/permissions.md) |
|
||||
| `enableUploads` | `CMD_ENABLE_UPLOADS` | **`all`** , `registered`, `none` | Set to specify who can upload images (`all` includes guest users, `registered` restricts to registered users, `none` disables uploads completely). If not set, but both `allowAnonymous` and `allowAnonymousEdits` are `false`, this will be set to `registered`. |
|
||||
| `sessionName` | | **`connect.sid`** | Cookie session name. |
|
||||
| `sessionLife` | `CMD_SESSION_LIFE` | **`14 * 24 * 60 * 60 * 1000`**, `1209600000` (14 days) | Cookie session life time in milliseconds. |
|
||||
@@ -273,7 +276,7 @@ you don't have to use either of these.
|
||||
|
||||
| config file | environment | **default** and example value | description |
|
||||
|-----------------|----------------------------|-------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `s3` | | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION" }` | When `imageuploadtype` be set to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](guides/s3-image-upload.md) |
|
||||
| `s3` | | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION" }` | When `imageuploadtype` be set to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](guides/media/s3-image-upload.md) |
|
||||
| | `CMD_S3_ACCESS_KEY_ID` | **no default** | AWS access key id |
|
||||
| | `CMD_S3_SECRET_ACCESS_KEY` | **no default** | AWS secret key |
|
||||
| | `CMD_S3_REGION` | **no default**, `ap-northeast-1` | AWS S3 region |
|
||||
@@ -299,7 +302,7 @@ you don't have to use either of these.
|
||||
|
||||
| config file | environment | **default** and example value | description |
|
||||
|-------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `minio` | | `{ "accessKey": "YOUR_MINIO_ACCESS_KEY", "secretKey": "YOUR_MINIO_SECRET_KEY", "endpoint": "YOUR_MINIO_HOST", port: 9000, secure: true }` | When `imageUploadType` is set to `minio`, you need to set this key. Also check out our [Minio Image Upload Guide](guides/minio-image-upload.md) |
|
||||
| `minio` | | `{ "accessKey": "YOUR_MINIO_ACCESS_KEY", "secretKey": "YOUR_MINIO_SECRET_KEY", "endpoint": "YOUR_MINIO_HOST", port: 9000, secure: true }` | When `imageUploadType` is set to `minio`, you need to set this key. Also check out our [Minio Image Upload Guide](guides/media/minio-image-upload.md) |
|
||||
| | `CMD_MINIO_ACCESS_KEY` | **no default** | Minio access key |
|
||||
| | `CMD_MINIO_SECRET_KEY` | **no default** | Minio secret key |
|
||||
| | `CMD_MINIO_ENDPOINT` | **no default**, `minio.example.org` | Address of your Minio endpoint/instance |
|
||||
|
||||
@@ -3,7 +3,7 @@ openapi: 3.0.1
|
||||
info:
|
||||
title: HedgeDoc
|
||||
description: HedgeDoc is an open source collaborative note editor. Several tasks of HedgeDoc can be automated through this API.
|
||||
version: 1.10.5
|
||||
version: 1.11.1
|
||||
contact:
|
||||
name: HedgeDoc on GitHub
|
||||
url: https://github.com/hedgedoc/hedgedoc
|
||||
|
||||
@@ -11,3 +11,10 @@ And even more in this 2010 article series:
|
||||
- <https://drive.googleblog.com/2010/09/whats-different-about-new-google-docs_21.html>
|
||||
- <https://drive.googleblog.com/2010/09/whats-different-about-new-google-docs_22.html>
|
||||
- <https://drive.googleblog.com/2010/09/whats-different-about-new-google-docs.html>
|
||||
|
||||
## OT library
|
||||
|
||||
HedgeDoc uses a vendored version of OT.js. The source code lives under `public/vendor/ot/`.
|
||||
The HedgeDoc frontend uses a minified build `public/vendor/ot/ot.min.js`.
|
||||
|
||||
This build is generated by `yarn run build:ot`. This needs to be done after changes to the OT code.
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# Release Checklist:
|
||||
|
||||
To copy this checklist please go to [this link](https://github.com/hedgedoc/hedgedoc/raw/refs/heads/master/docs/content/dev/release_checklist.md).
|
||||
|
||||
## Preparations:
|
||||
|
||||
- [ ] Create release PR(s)
|
||||
- [ ] In the main repo (actually only create a local branch to include all the fixes, additions and so on and only push and create a PR after testing)
|
||||
- [ ] Release PR in <https://github.com/hedgedoc/container>
|
||||
- [ ] Update version in `.github/workflows/release.yml`
|
||||
- [ ] Update version in `docker-compose.yml`
|
||||
- [ ] Release PR in <https://github.com/hedgedoc/social-media>
|
||||
- [ ] Security fixes (make sure all available undisclosed security fixes are merged)
|
||||
- [ ] Bump the version
|
||||
@@ -48,6 +52,7 @@
|
||||
mkdir /tmp/hedgedoc && cd /tmp/hedgedoc
|
||||
git clone -b master https://github.com/hedgedoc/hedgedoc.git .
|
||||
yarn install
|
||||
yarn build:ot
|
||||
yarn build
|
||||
cd ..
|
||||
tar cvzf hedgedoc-x.y.z.tar.gz --sort=name --exclude hedgedoc/node_modules --exclude hedgedoc/.git --exclude hedgedoc/.github --exclude hedgedoc/.yarn/cache hedgedoc
|
||||
@@ -55,11 +60,36 @@ tar cvzf hedgedoc-x.y.z.tar.gz --sort=name --exclude hedgedoc/node_modules --exc
|
||||
|
||||
Use this tar ball to test the following things:
|
||||
|
||||
### bin/scripts
|
||||
|
||||
- [ ] `bin/setup`
|
||||
- [ ] `bin/cleanup`
|
||||
- [ ] `bin/manage_users`
|
||||
- [ ] `bin/manage_users --add test@example.com`
|
||||
- [ ] `bin/manage_users --reset test@example.com`
|
||||
- [ ] `bin/manage_users --del test@example.com`
|
||||
- [ ] `bin/migrate_from_fs_to_minio`
|
||||
1. Clear the uploads directory.
|
||||
2. Start HedgeDoc 1 with filesystem uploads and a Postgres DB locally.
|
||||
3. Create a new note and upload an image. The image should be now in the uploads directory.
|
||||
4. Stop HedgeDoc. Update the config to configure a Minio instance (minio and s3bucket keys in config.json). Ensure the config on the Minio instance is existing (valid access key and secret, bucket existing and empty).
|
||||
5. Run `bin/migrate_fs_to_minio`.
|
||||
6. Start the HedgeDoc again. The note with the previously uploaded image should now contain the new image URL. The image should load.
|
||||
7. Check that the bucket in Minio contains the uploaded image.
|
||||
|
||||
### Config
|
||||
|
||||
- [ ] Setting `allowAnonymous: false` disables guest note creation
|
||||
- [ ] Setting `enableStatsApi: false` disables /metrics and /status endpoint
|
||||
|
||||
|
||||
### Account system
|
||||
|
||||
- [ ] User registration works
|
||||
- [ ] User login works
|
||||
- [ ] User self-deletion works
|
||||
- [ ] User self-deletion works (notes are deleted as well)
|
||||
- [ ] User data export works
|
||||
- [ ] Rate-limiting for basic user/password (`curl -X POST --data email=test@example.com%40password=123456 http://localhost:3000/login`)
|
||||
|
||||
### Notes
|
||||
|
||||
@@ -74,6 +104,7 @@ Use this tar ball to test the following things:
|
||||
- [ ] Working with 2 (or more) devices on a page works and results in the same document
|
||||
- [ ] Uploads work for images
|
||||
- [ ] Uploads fail for other data (e.g. binaries)
|
||||
- [ ] Opening uploads in new tab downloads them instead of displaying
|
||||
|
||||
### Database
|
||||
|
||||
@@ -86,7 +117,7 @@ Use this tar ball to test the following things:
|
||||
|
||||
- [ ] Postgres works
|
||||
|
||||
Run `docker run -d --name=hd1-pg -p 5432:5432 -e POSTGRES_USER=hd1db -e POSTGRES_PASSWORD=hd1db -e POSTGRES_DB=hd1db postgres:latest`
|
||||
Run `docker run -d --name=hd1-pg -p 5432:5432 -e POSTGRES_USER=hd1db -e POSTGRES_PASSWORD=hd1db -e POSTGRES_DB=hd1db --rm postgres:latest`
|
||||
and put this into your config:
|
||||
```
|
||||
"db": {
|
||||
@@ -165,19 +196,21 @@ opengraph:
|
||||
---
|
||||
```
|
||||
|
||||
### GDPR features
|
||||
### Slides
|
||||
|
||||
- [ ] Delete account works
|
||||
- [ ] When account is deleted, verify notes are gone as well
|
||||
- [ ] Data export works
|
||||
<http://localhost:3000/slide-example>
|
||||
|
||||
- [ ] Make sure the slide mode works as intended
|
||||
- [ ] Speaker view is toggled by `S`
|
||||
|
||||
### Auth
|
||||
|
||||
- [ ] SAML
|
||||
- config
|
||||
- Download certificate from Authentik Admin -> Certificates -> auth.hedgedoc.cloud to `/tmp/auth.hedgedoc.cloud.pem`
|
||||
- add to config.json:
|
||||
```
|
||||
"saml": {
|
||||
"idpSsoUrl": "https://auth.hedgedoc.cloud/application/saml/test-hd1/sso/binding/redirect/",
|
||||
"idpSsoUrl": "https://auth.hedgedoc.cloud/application/saml/test-hd1-saml/sso/binding/redirect/",
|
||||
"idpCert": "/tmp/auth.hedgedoc.cloud.pem",
|
||||
"identifierFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
|
||||
"attribute": {
|
||||
@@ -204,9 +237,10 @@ opengraph:
|
||||
}
|
||||
```
|
||||
- [ ] OAuth2
|
||||
- add to config.json and replace `REDACTED` with the proper client ID and secret
|
||||
```
|
||||
"oauth2": {
|
||||
"baseURL": "https://auth.hedgedoc.cloud/application/o/test-hedgedoc/",
|
||||
"baseURL": "https://auth.hedgedoc.cloud/application/o/test-hd1-oauth2/",
|
||||
"userProfileURL": "https://auth.hedgedoc.cloud/application/o/userinfo/",
|
||||
"tokenURL": "https://auth.hedgedoc.cloud/application/o/token/",
|
||||
"authorizationURL": "https://auth.hedgedoc.cloud/application/o/authorize/",
|
||||
@@ -220,7 +254,63 @@ opengraph:
|
||||
}
|
||||
```
|
||||
- [ ] GitHub
|
||||
- [ ] Rate-limiting for basic user/password (try to login with e.g. test@example.com and invalid password about 10 to 15 times in a row -> you should receive a message "Too many requests" at some point)
|
||||
```
|
||||
"github": {
|
||||
"clientID": "CHANGE_THIS",
|
||||
"clientSecret": "CHANGE_THIS"
|
||||
}
|
||||
```
|
||||
- [ ] GitLab
|
||||
```
|
||||
"gitlab": {
|
||||
"baseURL": "https://gitlab.fachschaften.org",
|
||||
"clientID": "CHANGE_THIS",
|
||||
"clientSecret": "CHANGE_THIS",
|
||||
"scope": "read_user"
|
||||
}
|
||||
```
|
||||
|
||||
### Docker images
|
||||
|
||||
1. You should push the release branch to Github
|
||||
2. You need to clone the [container repo](https://github.com/hedgedoc/container) and use the commands in the root there.
|
||||
3. Edit `debian/Dockerfile` and `alpine/Dockerfile` to use the release branch instead of the `master` branch for building
|
||||
|
||||
- [ ] debian
|
||||
|
||||
```
|
||||
docker buildx build -f debian/Dockerfile -t hedgedoc-local:1.x.y-debian .
|
||||
docker run --rm -p 3000:3000 -e CMD_DOMAIN="localhost" -e CMD_URL_ADDPORT=true -e "CMD_DB_URL=sqlite://:memory:" hedgedoc-local:1.x.y-debian
|
||||
```
|
||||
The server is then accessable via <http://localhost:3000>. The log should inlcude a line like
|
||||
|
||||
```
|
||||
127.0.0.1 - - [06/Dec/2025:16:17:51 +0000] "GET /_health HTTP/1.1" 200 14 "-" "hedgedoc-container-healthcheck/1.2"
|
||||
```
|
||||
|
||||
You should also check `docker ps` for
|
||||
|
||||
```
|
||||
hedgedoc-local:1.x.y-debian "/usr/local/bin/dock…" 29 seconds ago Up 28 seconds (healthy)
|
||||
```
|
||||
|
||||
- [ ] alpine
|
||||
|
||||
```
|
||||
docker buildx build -f alpine/Dockerfile -t hedgedoc-local:1.x.y-alpine .
|
||||
docker run --rm -p 3000:3000 -e CMD_DOMAIN="localhost" -e CMD_URL_ADDPORT=true -e "CMD_DB_URL=sqlite://:memory:" hedgedoc-local:1.x.y-alpine
|
||||
```
|
||||
The server is then accessable via <http://localhost:3000>. The log should inlcude a line like
|
||||
|
||||
```
|
||||
127.0.0.1 - - [06/Dec/2025:16:17:51 +0000] "GET /_health HTTP/1.1" 200 14 "-" "hedgedoc-container-healthcheck/1.2"
|
||||
```
|
||||
|
||||
You should also check `docker ps` for
|
||||
|
||||
```
|
||||
hedgedoc-local:1.x.y-alpine "/usr/local/bin/dock…" 29 seconds ago Up 28 seconds (healthy)
|
||||
```
|
||||
|
||||
|
||||
## Release:
|
||||
@@ -228,12 +318,14 @@ opengraph:
|
||||
- [ ] Merge Release PR in main repo
|
||||
- [ ] Tag commit with `git tag 1.x.y` and push it
|
||||
- [ ] Create release in GitHub and upload tar ball to GitHub
|
||||
- [ ] Update placeholders in Security Advisories (if they exist)
|
||||
- [ ] Publish Security Advisories (if they exist)
|
||||
- [ ] Merge Release PR in <https://github.com/hedgedoc/container>
|
||||
- Wait for the images to be available at <https://quay.io/repository/hedgedoc/hedgedoc?tab=tags>
|
||||
- [ ] Update website by running the ["deploy" workflow](https://github.com/hedgedoc/hedgedoc.github.io/actions?query=workflow%3A%22Deploy+to+github+actions+branch%22) in hedgedoc/hedgedoc.github.io
|
||||
- [ ] Update website by running the ["deploy" workflow](https://github.com/hedgedoc/hedgedoc.github.io/actions/workflows/deploy.yml) in hedgedoc/hedgedoc.github.io
|
||||
- [ ] Update docs.hedgedoc.org by running the ["build" workflow](https://github.com/hedgedoc/docs.hedgedoc.org/actions/workflows/build.yml)
|
||||
- [ ] Merge Release PR in <https://github.com/hedgedoc/social-media>
|
||||
- (optional) All people doing the release boost the post
|
||||
- [ ] Share the good news in the Matrix-Chatroom
|
||||
- (optional) All people doing the release boost the post
|
||||
- [ ] Share the good news in the Matrix-Chatroom
|
||||
- [ ] Request CVEs (if applicable)
|
||||
- [ ] Change this release checklist if necessary
|
||||
|
||||
@@ -47,17 +47,17 @@ If you still want to allow embedding via iframe, ensure that:
|
||||
- `cookiePolicy` / `CMD_COOKIE_POLICY` is set to `none` (Otherwise you will get a `AUTH failed: No cookie transmitted` error.)
|
||||
- `csp.allowFraming` / `CMD_CSP_ALLOW_FRAMING` is set to `true`
|
||||
|
||||
See also the [configuration docs](/configuration/#web-security-aspects) for more information about these options.
|
||||
See also the [configuration docs](configuration.md#web-security-aspects) for more information about these options.
|
||||
|
||||
## I can't upload images or the upload gets stuck
|
||||
This problem is typically accompanied by the error `Invalid URL: /uploads/` in the log and is often caused by a missing
|
||||
`domain` / `CMD_DOMAIN` config option or an incorrect reverse proxy config.
|
||||
Have a look at our [reverse proxy documentation](https://docs.hedgedoc.org/guides/reverse-proxy/)
|
||||
Have a look at our [reverse proxy documentation](guides/reverse-proxy.md)
|
||||
and make sure that `protocolUseSSL` / `CMD_PROTOCOL_USESSL` is set to `true` if you serve HedgeDoc via HTTPS.
|
||||
|
||||
## HedgeDoc fails executing migrations and does not start
|
||||
Unfortunately, older versions of HedgeDoc had some bugs regarding migrations and didn't always record that a migration was executed.
|
||||
Have a look at the *[Troubleshooting Migrations](/guides/migration-troubleshooting/)* guide for more information.
|
||||
Have a look at the *[Troubleshooting Migrations](guides/migration-troubleshooting.md)* guide for more information.
|
||||
|
||||
## Why does my interface look weird?
|
||||
|
||||
@@ -76,4 +76,4 @@ You should check the following settings (or their corresponding environment vari
|
||||
- `protocolUseSSL` (env: `CMD_PROTOCOL_USESSL`)
|
||||
- `urlAddPort` (env: `CMD_URL_ADDPORT`)
|
||||
|
||||
Check [the reverse proxy guide](../guides/reverse-proxy/#configure-asset-link-generation) for an explanation.
|
||||
Check [the reverse proxy guide](guides/reverse-proxy.md#configure-asset-link-generation) for an explanation.
|
||||
|
||||
@@ -22,12 +22,11 @@ identity_providers:
|
||||
refresh_token_lifespan: 90m
|
||||
enable_client_debug_messages: false
|
||||
clients:
|
||||
- id: HedgeDoc # this should be changed to something more secure
|
||||
description: HedgeDoc SSO
|
||||
- client_id: <random client id>
|
||||
client_name: HedgeDoc
|
||||
secret: <client secret here>
|
||||
public: false
|
||||
authorization_policy: two_factor
|
||||
audience: []
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
@@ -43,7 +42,7 @@ identity_providers:
|
||||
- form_post
|
||||
- query
|
||||
- fragment
|
||||
userinfo_signing_algorithm: none
|
||||
token_endpoint_auth_method: 'client_secret_post'
|
||||
```
|
||||
|
||||
4. Restart Authelia to apply to new configuration and check for any errors in the log
|
||||
@@ -53,7 +52,7 @@ identity_providers:
|
||||
- CMD_URL_ADDPORT=false
|
||||
- CMD_PROTOCOL_USESSL=true
|
||||
- CMD_OAUTH2_PROVIDERNAME=Authelia
|
||||
- CMD_OAUTH2_CLIENT_ID=HedgeDoc
|
||||
- CMD_OAUTH2_CLIENT_ID=<client id here>
|
||||
- CMD_OAUTH2_CLIENT_SECRET=<client secret here>
|
||||
- CMD_OAUTH2_SCOPE=openid email profile
|
||||
- CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=sub
|
||||
@@ -61,7 +60,7 @@ identity_providers:
|
||||
- CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email
|
||||
- CMD_OAUTH2_USER_PROFILE_URL=https://<your-authelia-url>/api/oidc/userinfo
|
||||
- CMD_OAUTH2_TOKEN_URL=https://<your-authelia-url>/api/oidc/token
|
||||
- CMD_OAUTH2_AUTHORIZATION_URL=https://<your-authelia-url>/api/oidc/authorize
|
||||
- CMD_OAUTH2_AUTHORIZATION_URL=https://<your-authelia-url>/api/oidc/authorization
|
||||
```
|
||||
|
||||
6. Run `docker-compose up -d` on HedgeDoc to apply your settings.
|
||||
|
||||
@@ -21,22 +21,22 @@
|
||||
docker logs test-minio
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
3. Open <http://localhost:9000> and login with the shown credentials.
|
||||
|
||||

|
||||

|
||||
|
||||
4. Create a bucket for HedgeDoc
|
||||
|
||||

|
||||

|
||||
|
||||
5. Add a policy for the prefix `uploads` and make it read-only.
|
||||
|
||||

|
||||

|
||||
*Open policy editor*
|
||||
|
||||

|
||||

|
||||
*Add policy for uploads*
|
||||
|
||||
6. Set credentials and configs for Minio in HedgeDoc's `config.json`
|
||||
@@ -63,7 +63,7 @@ In order to view the images uploaded to HedgeDoc the storage bucket needs to be
|
||||
|
||||
## Configure HedgeDoc
|
||||
|
||||
Set the following environment variables (or the [corresponding config.json values](/configuration/#amazon-s3)):
|
||||
Set the following environment variables (or the [corresponding config.json values](../../configuration.md#amazon-s3)):
|
||||
|
||||
```env
|
||||
CMD_IMAGE_UPLOAD_TYPE="s3"
|
||||
@@ -75,4 +75,4 @@ CMD_S3_BUCKET="your-bucket-name"
|
||||
|
||||
The access key and secret key are the values you received when you enabled the interoperability feature.
|
||||
|
||||
After restarting HedgeDoc you should now be able to add images to your pages.
|
||||
After restarting HedgeDoc you should now be able to add images to your pages.
|
||||
@@ -3,13 +3,13 @@
|
||||
**Note:** *This guide was written before the renaming. Just replace `HackMD` with `HedgeDoc` in your mind 😃 thanks!*
|
||||
|
||||
1. Go to [AWS S3 console](https://console.aws.amazon.com/s3/home) and create a new bucket.
|
||||

|
||||

|
||||
|
||||
2. Click on bucket, select **Properties** on the side panel, and find **Permission** section. Click **Edit bucket policy**.
|
||||

|
||||

|
||||
|
||||
3. Enter the following policy, replace `bucket_name` with your bucket name:
|
||||

|
||||

|
||||
|
||||
```json
|
||||
{
|
||||
@@ -28,13 +28,13 @@
|
||||
4. Go to IAM console and create a new IAM user. Remember your user credentials(`key`/`access token`)
|
||||
|
||||
5. Enter user page, select **Permission** tab, look at **Inline Policies** section, and click **Create User Policy**
|
||||

|
||||

|
||||
|
||||
6. Select **Custom Policy**
|
||||

|
||||

|
||||
|
||||
7. Enter the following policy, replace `bucket_name` with your bucket name:
|
||||

|
||||

|
||||
|
||||
```json
|
||||
{
|
||||
@@ -70,7 +70,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
9. In additional to edit `config.json` directly, you could also try [environment variables](../configuration.md).
|
||||
9. In additional to edit `config.json` directly, you could also try [environment variables](../../configuration.md).
|
||||
|
||||
## Related Tools
|
||||
|
||||
49
docs/content/guides/production-ready.md
Normal file
49
docs/content/guides/production-ready.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# How to make your HedgeDoc instance production ready
|
||||
|
||||
If you run your own HedgeDoc instance at one point you should probably consider a few things and how you want to handle them.
|
||||
In this guide we try to show you a few of those considerations and want to discuss what's best to do.
|
||||
|
||||
## Node Env
|
||||
|
||||
You want to start HedgeDoc with `NODE_ENV=production`. This tells HedgeDoc to run in production, log a bit less and use the production configuration in your config.json.
|
||||
|
||||
## HTTPS
|
||||
|
||||
You want to configure a reverse proxy that handles your HedgeDoc TLS termination. For this you should consult the [reverse proxy guide](reverse-proxy.md).
|
||||
Look also at the hsts options in our [configuration](../configuration.md#web-security-aspects) to specify what works for your setup.
|
||||
|
||||
## Uploads
|
||||
|
||||
Depending on your instance and your expected amount of usage you may want to configure a different upload backend or at the very least configure external monitoring for the filesystem folder where HedgeDoc should put uploads to account for any sudden increases in usage.
|
||||
|
||||
## Database
|
||||
|
||||
We strongly recommend you use a different database than SQLite for your production instance. If you're a single user instance this is probably fine, but a database server is typically more robust than a single sqlite.db file.
|
||||
|
||||
## Login
|
||||
|
||||
You should configure a production ready user backend. This can either be via LDAP, SAML or OAuth2 or you use one of HedgeDoc preset configurations for e.g. GitHub. Our username / password system works, but lacks many functionalities you probably want like two-factor-auth or self-service password resets.
|
||||
|
||||
## Guest Users
|
||||
|
||||
When HegdeDoc was first written the internet was a different place. Back then allow anonymous usage of a service like HedgeDoc seemed like a cool idea.
|
||||
Unfortunatly we don't live in that time anymore and anonymous usage is probably not the best idea anymore.
|
||||
To prevent anonyomous usage you should set `allowAnonymous` or `CMD_ALLOW_ANONYMOUS` to `false`. After that anonymous users are not able to create any notes.
|
||||
You could set `allowAnonymousEdits` or `CMD_ALLOW_ANONYMOUS_EDITS` to `true` to allow anonymous users to edit pages where the note owner explicity allowed this.
|
||||
|
||||
## Default Note Permission
|
||||
|
||||
Depending on your needs to may want to change the default permissions of notes to something more strict. Per default everybody expect guests can write to the notes, but you may want to have a different default. See [this table](../references/permissions.md) to understand the different options.
|
||||
Regardless of what you chose here, the users will be able to change this behavior.
|
||||
|
||||
## Metrics
|
||||
|
||||
Please be aware that HedgeDoc exposes a few metric endpoints `/status`, `/_health` and `/metrics`. You either want to disable the more verbose of these endpoints (`/status` and `/metrics`) via `enableStatsApi` or `CMD_ENABLE_STATS_API` or configure your reverse proxy to only route these endpoints to a private network that your metrics grabber has access to.
|
||||
|
||||
## Backup
|
||||
|
||||
We recommend keeping backups of your instance in case something fails allong the lines. These backups should include:
|
||||
|
||||
- Your database
|
||||
- Your uploads
|
||||
- Your configuration
|
||||
@@ -19,7 +19,7 @@ This documentation will cover HTTPS setup, with comments for HTTP setup.
|
||||
| `domain` | `CMD_DOMAIN` | The full domain where your instance will be available | `hedgedoc.example.com` |
|
||||
| `host` | `CMD_HOST` | An ip or domain name that is only available to HedgeDoc and your reverse proxy | `localhost` |
|
||||
| `port` | `CMD_PORT` | An available port number on that IP | `3000` |
|
||||
| `path` | `CMD_PATH` | path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored) | `/var/run/hedgedoc.sock` |
|
||||
| `path` | `CMD_PATH` | path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored for listening; however, `port` or `CMD_PORT` is still used for URL generation when `urlAddPort` is enabled) | `/var/run/hedgedoc.sock` |
|
||||
| `protocolUseSSL` | `CMD_PROTOCOL_USESSL` | `true` if you want to serve your instance over SSL (HTTPS), `false` if you want to use plain HTTP | `true` |
|
||||
| `useSSL` | | `false`, the communications between HedgeDoc and the proxy are unencrypted | `false` |
|
||||
| `urlAddPort` | `CMD_URL_ADDPORT` | `false`, HedgeDoc should not append its port to the URLs it links | `false` |
|
||||
|
||||
3
docs/content/images/icons/hand-paper-o.svg
Normal file
3
docs/content/images/icons/hand-paper-o.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1650" height="1780" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M880 128c-62 0-112 50-112 112v656h-32V368c0-62-50-112-112-112s-112 50-112 112v784L358 947c-24-32-62-51-102-51-71 0-128 58-128 128 0 28 9 55 26 77l384 512c24 32 62 51 102 51h688c46 0 86-33 95-78l76-405c3-19 5-39 5-59V624c0-62-50-112-112-112s-112 50-112 112v272h-32V368c0-62-50-112-112-112s-112 50-112 112v528h-32V240c0-62-50-112-112-112zm0-128c90 0 173 51 214 132 14-3 28-4 42-4 132 0 240 108 240 240v17c139-8 256 99 256 239v498c0 28-3 56-8 83l-76 404c-19 106-112 183-220 183H640c-80 0-157-39-205-102L51 1178c-33-44-51-99-51-154 0-141 114-256 256-256 42 0 93 10 128 34V368c0-132 108-240 240-240 14 0 28 1 42 4C707 51 790 0 880 0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 720 B |
3
docs/content/images/icons/id-card.svg
Normal file
3
docs/content/images/icons/id-card.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="2050" height="1680" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M896 1212c0-132-32-284-164-284-40 40-95 64-156 64s-116-24-156-64c-132 0-164 152-164 284 0 73 48 132 107 132h426c59 0 107-59 107-132zM768 768c0-106-86-192-192-192s-192 86-192 192 86 192 192 192 192-86 192-192zm1024 480v-64c0-18-14-32-32-32h-704c-18 0-32 14-32 32v64c0 18 14 32 32 32h704c18 0 32-14 32-32zm-384-256v-64c0-18-14-32-32-32h-320c-18 0-32 14-32 32v64c0 18 14 32 32 32h320c18 0 32-14 32-32zm384 0v-64c0-18-14-32-32-32h-192c-18 0-32 14-32 32v64c0 18 14 32 32 32h192c18 0 32-14 32-32zm0-256v-64c0-18-14-32-32-32h-704c-18 0-32 14-32 32v64c0 18 14 32 32 32h704c18 0 32-14 32-32zM128 384h1792v-96c0-18-14-32-32-32H160c-18 0-32 14-32 32v96zm1920-96v1216c0 88-72 160-160 160H160c-88 0-160-72-160-160V288c0-88 72-160 160-160h1728c88 0 160 72 160 160z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 842 B |
3
docs/content/images/icons/leaf.svg
Normal file
3
docs/content/images/icons/leaf.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1900" height="1700" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1280 704c0-35-29-64-64-64-354 0-582 148-813 403-12 13-19 27-19 45 0 35 29 64 64 64 18 0 32-7 45-19 49-44 93-92 141-137 181-163 336-228 582-228 35 0 64-29 64-64zm512-198c0 64-7 129-20 193-64 311-264 513-542 651-135 68-286 108-438 108-96 0-195-16-286-47-48-16-144-79-184-79-50 0-110 204-197 204-63 0-82-31-109-77-9-17-16-23-16-44 0-104 198-185 198-243 0-9-26-62-30-82-6-34-9-69-9-104 0-318 253-545 537-639 205-68 641 11 780-121 55-51 82-98 166-98 113 0 150 293 150 378z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 560 B |
3
docs/content/images/icons/lock.svg
Normal file
3
docs/content/images/icons/lock.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1550" height="1550" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M320 768h512V576c0-141-115-256-256-256S320 435 320 576v192zm832 96v576c0 53-43 96-96 96H96c-53 0-96-43-96-96V864c0-53 43-96 96-96h32V576c0-246 202-448 448-448s448 202 448 448v192h32c53 0 96 43 96 96z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 291 B |
3
docs/content/images/icons/pencil.svg
Normal file
3
docs/content/images/icons/pencil.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1550" height="2100" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M363 2000l91-91-235-235-91 91v107h128v128h107zm523-928c0-13-9-22-22-22-6 0-12 2-17 7l-542 542c-5 5-7 11-7 17 0 13 9 22 22 22 6 0 12-2 17-7l542-542c5-5 7-11 7-17zm-54-192l416 416-832 832H0v-416zm683 96c0 34-14 67-37 90l-166 166-416-416 166-165c23-24 56-38 90-38s67 14 91 38l235 234c23 24 37 57 37 91z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 391 B |
3
docs/content/images/icons/umbrella.svg
Normal file
3
docs/content/images/icons/umbrella.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1750" height="1750" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M896 828v580c0 139-117 256-256 256s-256-117-256-256c0-35 29-64 64-64s64 29 64 64c0 67 61 128 128 128s128-61 128-128V828c21-7 42-11 64-11s43 4 64 11zm768 27c0 17-15 32-32 32-9 0-16-4-23-10-58-54-112-92-195-92-95 0-177 59-231 134-12 17-21 35-32 52-7 11-15 17-28 17-14 0-22-6-29-17-11-17-20-35-32-52-54-75-135-134-230-134s-176 59-230 134c-12 17-21 35-32 52-7 11-15 17-29 17-13 0-21-6-28-17-11-17-20-35-32-52-54-75-136-134-231-134-83 0-137 38-195 92-7 6-14 10-23 10-17 0-32-15-32-32 0-3 0-5 1-7 92-378 457-592 831-592 372 0 741 214 831 592 1 2 1 4 1 7zM896 128v98c-21-1-43-2-64-2s-43 1-64 2v-98c0-35 29-64 64-64s64 29 64 64z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 712 B |
@@ -16,6 +16,6 @@ If you have any questions that aren't answered here, feel free to ask us on [Mat
|
||||
[hedgedoc-demo]: https://demo.hedgedoc.org
|
||||
[hedgedoc-history]: https://hedgedoc.org/history
|
||||
[hedgedoc-history-details]: https://hedgedoc.org/history/details
|
||||
[hedgedoc-faq]: /faq
|
||||
[hedgedoc-faq]: faq.md
|
||||
[matrix.org-url]: https://chat.hedgedoc.org
|
||||
[hedgedoc-community]: https://community.hedgedoc.org
|
||||
|
||||
23
docs/content/references/permissions.md
Normal file
23
docs/content/references/permissions.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Permissions
|
||||
|
||||
HedgeDoc 1.x uses a basic permission system that differentiates between the owner of a note (the account who initially
|
||||
created it), logged-in users and guests.
|
||||
|
||||
It is possible to change the access permission of a note through the little button on the top right of the view pane
|
||||
in the editor. However, only the note owner can change the permission of a note.
|
||||
|
||||
The permissions are named **Freely**, **Editable**, **Limited**, **Locked**, **Protected** and **Private**.
|
||||
|
||||
You can configure the default permission for new notes using the `defaultPermission` option in the `config.json` file or
|
||||
`CMD_DEFAULT_PERMISSION` environment variable.
|
||||
|
||||
For more information, see the [configuration documentation](../configuration.md#users-and-privileges).
|
||||
|
||||
| | Permission | Owner read/write | Signed-in read | Signed-in write | Guest read | Guest write |
|
||||
|:---------------------------------------------------------------------------------:|:--------------:|:----------------:|:--------------:|:---------------:|:----------:|:-----------:|
|
||||
| <img src="/images/icons/leaf.svg" alt="leaf icon" height="16" width="16"> | **Freely** | ✔ | ✔ | ✔ | ✔ | ✔ |
|
||||
| <img src="/images/icons/pencil.svg" alt="pencil icon" height="16" width="16"> | **Editable** | ✔ | ✔ | ✔ | ✔ | ✖ |
|
||||
| <img src="/images/icons/id-card.svg" alt="id card icon" height="16" width="16"> | **Limited** | ✔ | ✔ | ✔ | ✖ | ✖ |
|
||||
| <img src="/images/icons/lock.svg" alt="lock icon" height="16" width="16"> | **Locked** | ✔ | ✔ | ✖ | ✔ | ✖ |
|
||||
| <img src="/images/icons/umbrella.svg" alt="umbrella icon" height="16" width="16"> | **Protected** | ✔ | ✔ | ✖ | ✖ | ✖ |
|
||||
| <img src="/images/icons/hand-paper-o.svg" alt="hand icon" height="16" width="16"> | **Private** | ✔ | ✖ | ✖ | ✖ | ✖ |
|
||||
@@ -161,3 +161,29 @@ custom background image:
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Speaker notes and speaker view
|
||||
|
||||
You can add speaker notes to your slides.
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: slide
|
||||
...
|
||||
|
||||
# This is a test slide
|
||||
|
||||
I am on your slide
|
||||
|
||||
Note: Find me in the notes section in the down right corner in speaker view
|
||||
|
||||
---
|
||||
|
||||
# Another slide
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The notes are not displayed in the presentation view, but you can press `S` there to open a new window with the speaker view.
|
||||
|
||||
Both speaker notes and speaker view are features of reveal.js, you can read more in their documentation: https://revealjs.com/speaker-view/
|
||||
|
||||
@@ -25,7 +25,7 @@ title: meta title
|
||||
|
||||
## description
|
||||
|
||||
This option will set the note description as a `<meta name="description">` tag. This only affects the [Publish](../features#Share-Notes) function.
|
||||
This option will set the note description as a `<meta name="description">` tag. This only affects the Publish function.
|
||||
|
||||
> default: not set
|
||||
|
||||
|
||||
@@ -65,6 +65,12 @@ using the button below. This will run the official Docker image from [quay.io](h
|
||||
|
||||
[](https://www.pikapods.com/pods?run=hedgedoc)
|
||||
|
||||
### Zenith
|
||||
|
||||
[Zenith](https://zenith.hosting/) offers one-click managed hosting for open source apps. Deploy a fully managed HedgeDoc instance in minutes using the button below.
|
||||
|
||||
[](https://zenith.hosting/host/hedgedoc)
|
||||
|
||||
### SelfPrivacy
|
||||
|
||||
HedgeDoc is available as a 1-click install on [SelfPrivacy](https://selfprivacy.org/). SelfPrivacy app allows you to set up self-hosted services on your automatically configured and managed NixOS-based server. Because HedgeDoc is officialy supported by SelfPrivacy, all you need to do is add it from the Services catalog on the Services page.
|
||||
@@ -79,10 +85,11 @@ The source code of the module can be found [here](https://git.selfprivacy.org/Se
|
||||
|
||||
HedgeDoc is available in the Arch Linux _community_ repository.
|
||||
|
||||
[Link to the package](https://archlinux.org/packages/community/any/hedgedoc/)
|
||||
[Link to the package](https://archlinux.org/packages/extra/any/hedgedoc/)
|
||||
|
||||
### FreeBSD
|
||||
|
||||
HedgeDoc is available in the FreeBSD _ports_ repository. After installation, customise your `config.json` file, referring to the official HedgeDoc documentation.
|
||||
HedgeDoc was available in the FreeBSD _ports_ repository.
|
||||
However, it has been removed due to lack of maintenance.
|
||||
|
||||
[Ports Repository](https://freshports.org/www/hedgedoc)
|
||||
|
||||
@@ -12,7 +12,8 @@ We currently support the `amd64` and `arm64` architectures.
|
||||
The easiest way to get started with HedgeDoc and Docker is to use the following `docker-compose.yml`:
|
||||
|
||||
!!! warning
|
||||
This is a minimal example to get started quickly and not intended for production use.
|
||||
This is a minimal example to get started quickly and not intended for production use.
|
||||
For more information about production use see [this guide](../guides/production-ready.md).
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
@@ -28,7 +29,7 @@ services:
|
||||
restart: always
|
||||
app:
|
||||
# Make sure to use the latest release from https://hedgedoc.org/latest-release
|
||||
image: quay.io/hedgedoc/hedgedoc:1.10.5
|
||||
image: quay.io/hedgedoc/hedgedoc:1.11.1
|
||||
environment:
|
||||
- CMD_DB_URL=postgres://hedgedoc:password@database:5432/hedgedoc
|
||||
- CMD_DOMAIN=localhost
|
||||
@@ -46,7 +47,7 @@ volumes:
|
||||
```
|
||||
After executing `docker-compose up`, HedgeDoc should be available at [http://localhost:3000](http://localhost:3000).
|
||||
You can now continue to configure your container with environment variables.
|
||||
Check out [the configuration docs](/configuration) for more details.
|
||||
Check out [the configuration docs](../configuration.md) for more details.
|
||||
|
||||
## File Permissions
|
||||
|
||||
@@ -82,7 +83,7 @@ Then run `docker-compose up` to start HedgeDoc again.
|
||||
### Migrating from CodiMD & HackMD
|
||||
|
||||
If you currently use CodiMD or HackMD, you should be able to swap the docker image for ours.
|
||||
See [the general migration hints](/setup/getting-started/#migrating-from-codimd-hackmd) for compatibility details.
|
||||
See [the general migration hints](../setup/getting-started.md#migrating-from-codimd-hackmd) for compatibility details.
|
||||
|
||||
|
||||
## Backup
|
||||
|
||||
@@ -5,8 +5,8 @@ To set up your instance follow these steps:
|
||||
|
||||
1. Choose an installation method and follow the instructions
|
||||
2. [Configure your reverse proxy](https://docs.hedgedoc.org/guides/reverse-proxy/)
|
||||
3. [Configure HedgeDoc](https://docs.hedgedoc.org/configuration/)
|
||||
4. If you didn't disable [local accounts](/configuration/#email-local-account), you can use the "Sign In" button to
|
||||
3. [Configure HedgeDoc](../configuration.md)
|
||||
4. If you didn't disable [local accounts](../configuration.md#email-local-account), you can use the "Sign In" button to
|
||||
create an account, login and start using HedgeDoc.
|
||||
|
||||
Follow us on <a href="https://social.hedgedoc.org/" target="_blank" rel="noreferer noopener">:fontawesome-brands-mastodon:{: .mastodon }Mastodon</a> for updates.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Manual Installation
|
||||
|
||||
!!! info "Requirements on your server"
|
||||
- Node.js 18 or later
|
||||
- Node.js 20 or later
|
||||
We recommend to run HedgeDoc with the latest LTS release of Node.js.
|
||||
- Database (PostgreSQL, MySQL, MariaDB, SQLite)
|
||||
The database must use charset `utf8`. This is typically the default in PostgreSQL and SQLite.
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
1. Check if you meet the [requirements at the top of this document](#manual-installation).
|
||||
2. Download the [latest release](https://hedgedoc.org/latest-release/) and extract it.
|
||||
<small>Alternatively, you can use Git to clone the repository and checkout a release, e.g. with `git clone -b 1.10.5 https://github.com/hedgedoc/hedgedoc.git`.</small>
|
||||
<small>Alternatively, you can use Git to clone the repository and checkout a release, e.g. with `git clone -b 1.11.1 https://github.com/hedgedoc/hedgedoc.git`.</small>
|
||||
3. Enter the directory and execute `bin/setup`, which will install the dependencies and create example configs.
|
||||
4. Configure HedgeDoc: To get started, you can use this minimal `config.json`:
|
||||
```json
|
||||
@@ -56,12 +56,12 @@
|
||||
|
||||
If you want to upgrade HedgeDoc from an older version, follow these steps:
|
||||
|
||||
1. Check if you still meet the [requirements at the top of this document](#requirements-on-your-server).
|
||||
1. Check if you still meet the [requirements at the top of this document](#manual-installation).
|
||||
2. Ensure you read the [release notes](https://hedgedoc.org/releases/) of all versions between your current version
|
||||
and the latest release.
|
||||
2. Fully stop your old HedgeDoc server.
|
||||
3. [Download](https://hedgedoc.org/latest-release/) the new release and extract it over the old directory.
|
||||
<small>If you use Git, you can check out the new tag with e.g. `git fetch origin && git checkout 1.10.5`</small>
|
||||
<small>If you use Git, you can check out the new tag with e.g. `git fetch origin && git checkout 1.11.1`</small>
|
||||
5. Run `bin/setup`. This will take care of installing dependencies. It is safe to run on an existing installation.
|
||||
6. *:octicons-light-bulb-16: If you used the release tarball for 1.7.0 or newer, this step can be skipped.*
|
||||
Build the frontend bundle by running `yarn install --immutable` and `yarn build`. The extra `yarn install --immutable` is necessary as `bin/setup` does not install the build dependencies.
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
.mastodon {
|
||||
color: #2b90d9;
|
||||
color: #6364FF;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .light-mode-only {
|
||||
|
||||
@@ -14,6 +14,7 @@ nav:
|
||||
- 'Manual Installation': setup/manual-setup.md
|
||||
- 'Community Installation Methods': setup/community.md
|
||||
- Guides:
|
||||
- 'Production Ready': guides/production-ready.md
|
||||
- 'Reverse Proxy': guides/reverse-proxy.md
|
||||
- Authentication:
|
||||
- LDAP: guides/auth/ldap-ad.md
|
||||
@@ -30,9 +31,9 @@ nav:
|
||||
- Authelia: guides/auth/authelia.md
|
||||
- Microsoft Entra ID (Azure AD): guides/auth/entra-id.md
|
||||
- Media Backend:
|
||||
- MinIO: guides/minio-image-upload.md
|
||||
- S3: guides/s3-image-upload.md
|
||||
- Google Cloud Storage: guides/s3-google-cloud-storage.md
|
||||
- MinIO: guides/media/minio-image-upload.md
|
||||
- S3: guides/media/s3-image-upload.md
|
||||
- Google Cloud Storage: guides/media/s3-google-cloud-storage.md
|
||||
- Migrate from Etherpad: guides/migrate-etherpad.md
|
||||
- Migration Troubleshooting: guides/migration-troubleshooting.md
|
||||
- Terms of Use Setup: guides/providing-terms.md
|
||||
@@ -42,6 +43,7 @@ nav:
|
||||
- 'Slide Options': references/slide-options.md
|
||||
- 'URL Scheme': references/url-scheme.md
|
||||
- 'YAML Metadata': references/yaml-metadata.md
|
||||
- 'Permissions': references/permissions.md
|
||||
- Developer:
|
||||
- 'Getting Started': dev/getting-started.md
|
||||
- API: dev/api.md
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.7.0
|
||||
pymdown-extensions==10.17.2
|
||||
mkdocs-material==9.7.7
|
||||
pymdown-extensions==11.0.1
|
||||
mdx_truly_sane_lists==1.3
|
||||
mkdocs-redirects==1.2.2
|
||||
mkdocs-redirects==1.2.3
|
||||
|
||||
@@ -9,7 +9,6 @@ module.exports = {
|
||||
port: 3000,
|
||||
loglevel: 'info',
|
||||
urlAddPort: false,
|
||||
allowOrigin: ['localhost'],
|
||||
useSSL: false,
|
||||
hsts: {
|
||||
enable: true,
|
||||
@@ -30,6 +29,8 @@ module.exports = {
|
||||
allowPDFEmbed: true
|
||||
},
|
||||
rateLimitNewNotes: 20,
|
||||
rateLimitUser: 10,
|
||||
rateLimitUsingCloudflare: false,
|
||||
cookiePolicy: 'lax',
|
||||
protocolUseSSL: false,
|
||||
// permissions
|
||||
@@ -190,5 +191,7 @@ module.exports = {
|
||||
// 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1"
|
||||
// 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2"
|
||||
linkifyHeaderStyle: 'keep-case',
|
||||
enableStatsApi: true
|
||||
enableStatsApi: true,
|
||||
externalLinkWarning: true,
|
||||
externalLinkWhitelist: []
|
||||
}
|
||||
|
||||
@@ -27,9 +27,10 @@ module.exports = {
|
||||
allowPDFEmbed: toBooleanConfig(process.env.CMD_CSP_ALLOW_PDF_EMBED)
|
||||
},
|
||||
rateLimitNewNotes: toIntegerConfig(process.env.CMD_RATE_LIMIT_NEW_NOTES),
|
||||
rateLimitUser: toIntegerConfig(process.env.CMD_RATE_LIMIT_USER),
|
||||
rateLimitUsingCloudflare: toBooleanConfig(process.env.CMD_RATE_LIMIT_USING_CLOUDFLARE),
|
||||
cookiePolicy: process.env.CMD_COOKIE_POLICY,
|
||||
protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL),
|
||||
allowOrigin: toArrayConfig(process.env.CMD_ALLOW_ORIGIN),
|
||||
allowAnonymous: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS),
|
||||
allowAnonymousEdits: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_EDITS),
|
||||
allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL),
|
||||
@@ -164,5 +165,7 @@ module.exports = {
|
||||
allowGravatar: toBooleanConfig(process.env.CMD_ALLOW_GRAVATAR),
|
||||
openID: toBooleanConfig(process.env.CMD_OPENID),
|
||||
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE,
|
||||
enableStatsApi: toBooleanConfig(process.env.CMD_ENABLE_STATS_API)
|
||||
enableStatsApi: toBooleanConfig(process.env.CMD_ENABLE_STATS_API),
|
||||
externalLinkWarning: toBooleanConfig(process.env.CMD_EXTERNAL_LINK_WARNING, true),
|
||||
externalLinkWhitelist: toArrayConfig(process.env.CMD_EXTERNAL_LINK_WHITELIST)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ module.exports = {
|
||||
reportURI: process.env.HMD_CSP_REPORTURI
|
||||
},
|
||||
protocolUseSSL: toBooleanConfig(process.env.HMD_PROTOCOL_USESSL),
|
||||
allowOrigin: toArrayConfig(process.env.HMD_ALLOW_ORIGIN),
|
||||
allowAnonymous: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS),
|
||||
allowAnonymousEdits: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS_EDITS),
|
||||
allowFreeURL: toBooleanConfig(process.env.HMD_ALLOW_FREEURL),
|
||||
|
||||
@@ -193,7 +193,8 @@ switch (config.imageUploadType) {
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/svg+xml'
|
||||
'image/svg+xml',
|
||||
'image/webp'
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const CspStrategy = {}
|
||||
const defaultDirectives = {
|
||||
defaultSrc: ['\'none\''],
|
||||
baseUri: ['\'self\''],
|
||||
connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws'), 'https://vimeo.com/api/v2/video/', 'https://hedgeagent.sa6anw.se/add_note'],
|
||||
connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws'), 'https://vimeo.com/api/v2/video/'],
|
||||
fontSrc: ['\'self\''],
|
||||
manifestSrc: ['\'self\''],
|
||||
frameSrc: ['\'self\'', 'https://player.vimeo.com', 'https://www.youtube.com', 'https://gist.github.com'],
|
||||
|
||||
@@ -8,6 +8,7 @@ const config = require('./config')
|
||||
exports.generateAvatar = function (name) {
|
||||
// use darker colors for better contrast
|
||||
const color = new Chance(name).color({
|
||||
format: 'hex',
|
||||
max_red: 150,
|
||||
max_green: 150,
|
||||
max_blue: 150
|
||||
|
||||
24
lib/links.js
Normal file
24
lib/links.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
serveLinkWarningPage: function (req, res) {
|
||||
let targetURL = typeof req.query.url === 'string' ? req.query.url : ''
|
||||
let valid = false
|
||||
try {
|
||||
targetURL = decodeURIComponent(targetURL)
|
||||
const parsed = new URL(targetURL)
|
||||
valid = ['http:', 'https:'].includes(parsed.protocol)
|
||||
targetURL = parsed.href
|
||||
} catch (err) {
|
||||
valid = false
|
||||
}
|
||||
res.set({
|
||||
'Cache-Control': 'no-store'
|
||||
})
|
||||
res.render('link.ejs', {
|
||||
title: 'External link',
|
||||
valid,
|
||||
targetURL,
|
||||
cspNonce: res.locals.nonce,
|
||||
opengraph: []
|
||||
})
|
||||
}
|
||||
}
|
||||
19
lib/migrations/20260622225700-remove-temp.js
Normal file
19
lib/migrations/20260622225700-remove-temp.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
up: function (queryInterface, Sequelize) {
|
||||
return queryInterface.dropTable('Temp')
|
||||
},
|
||||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
return queryInterface.createTable('Temp', {
|
||||
id: {
|
||||
type: Sequelize.STRING,
|
||||
primaryKey: true
|
||||
},
|
||||
date: Sequelize.TEXT,
|
||||
createdAt: Sequelize.DATE,
|
||||
updatedAt: Sequelize.DATE
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const Sequelize = require('sequelize')
|
||||
const { cloneDeep } = require('lodash')
|
||||
const Umzug = require('umzug')
|
||||
const { SequelizeStorage, Umzug } = require('umzug')
|
||||
|
||||
// core
|
||||
const config = require('../config')
|
||||
@@ -66,21 +66,24 @@ Object.keys(db).forEach(function (modelName) {
|
||||
|
||||
const umzug = new Umzug({
|
||||
migrations: {
|
||||
path: path.resolve(__dirname, '..', 'migrations'),
|
||||
params: [
|
||||
sequelize.getQueryInterface(),
|
||||
Sequelize.DataTypes
|
||||
]
|
||||
glob: path.resolve(__dirname, '..', 'migrations', '*.js'),
|
||||
resolve: ({ name, path, context }) => {
|
||||
// Adjust the migration from the new signature to the v2 signature, making easier to upgrade to v3
|
||||
const migration = require(path)
|
||||
return {
|
||||
name,
|
||||
up: async () => migration.up(context, Sequelize),
|
||||
down: async () => migration.down(context, Sequelize)
|
||||
}
|
||||
}
|
||||
},
|
||||
context: sequelize.getQueryInterface(),
|
||||
// Required wrapper function required to prevent winstion issue
|
||||
// https://github.com/winstonjs/winston/issues/1577
|
||||
logging: message => {
|
||||
logger.info(message)
|
||||
},
|
||||
storage: 'sequelize',
|
||||
storageOptions: {
|
||||
sequelize
|
||||
}
|
||||
storage: new SequelizeStorage({ sequelize })
|
||||
})
|
||||
|
||||
db.runMigrations = async function runMigrations () {
|
||||
|
||||
@@ -5,12 +5,13 @@ const path = require('path')
|
||||
const LZString = require('lz-string')
|
||||
const base64url = require('base64url')
|
||||
const md = require('markdown-it')()
|
||||
const metaMarked = require('@hedgedoc/meta-marked')
|
||||
const yaml = require('js-yaml')
|
||||
const cheerio = require('cheerio')
|
||||
const nanoid = require('nanoid')
|
||||
const Sequelize = require('sequelize')
|
||||
const async = require('async')
|
||||
const moment = require('moment')
|
||||
const { constrainObject } = require('constrain-object')
|
||||
const DiffMatchPatch = require('diff-match-patch')
|
||||
const dmp = new DiffMatchPatch()
|
||||
|
||||
@@ -397,12 +398,41 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
return tags
|
||||
}
|
||||
function splitFrontmatter (str) {
|
||||
if (str.slice(0, 3) !== '---') {
|
||||
return null
|
||||
}
|
||||
const matcher = /\n(\.{3}|-{3})/g
|
||||
const metaEnd = matcher.exec(str)
|
||||
if (!metaEnd) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
yaml: str.slice(0, metaEnd.index),
|
||||
markdown: str.slice(matcher.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Note.extractMeta = function (content) {
|
||||
let obj = null
|
||||
try {
|
||||
obj = metaMarked(content)
|
||||
if (!obj.markdown) obj.markdown = ''
|
||||
if (!obj.meta) obj.meta = {}
|
||||
const parts = splitFrontmatter(content)
|
||||
if (parts) {
|
||||
const parsed = yaml.load(parts.yaml)
|
||||
const constrained = constrainObject(parsed, {
|
||||
onCycle: 'omit',
|
||||
maxDepth: 4
|
||||
})
|
||||
obj = {
|
||||
meta: constrained || {},
|
||||
markdown: parts.markdown || ''
|
||||
}
|
||||
} else {
|
||||
obj = {
|
||||
meta: {},
|
||||
markdown: content
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
obj = {
|
||||
markdown: content,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
'use strict'
|
||||
// external modules
|
||||
const nanoid = require('nanoid')
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
const Temp = sequelize.define('Temp', {
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
defaultValue: nanoid.nanoid
|
||||
},
|
||||
data: {
|
||||
type: DataTypes.TEXT
|
||||
}
|
||||
})
|
||||
|
||||
return Temp
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
'use strict'
|
||||
|
||||
import Scrypt from 'scrypt-kdf'
|
||||
|
||||
import { createRequire } from 'module'
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
// external modules
|
||||
const Sequelize = require('sequelize')
|
||||
const scrypt = require('scrypt-kdf')
|
||||
const filterXSS = require('xss')
|
||||
|
||||
// core
|
||||
const logger = require('../logger')
|
||||
const { generateAvatarURL } = require('../letter-avatars')
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
function UserModel (sequelize, DataTypes) {
|
||||
const User = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -46,8 +51,13 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
})
|
||||
|
||||
User.prototype.verifyPassword = function (attempt) {
|
||||
return scrypt.verify(Buffer.from(this.password, 'hex'), attempt)
|
||||
User.prototype.verifyPassword = async function (attempt) {
|
||||
try {
|
||||
return (await Scrypt.verify(Buffer.from(this.password, 'hex'), attempt))
|
||||
} catch (error) {
|
||||
logger.error(`Error during login: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
User.associate = function (models) {
|
||||
@@ -143,14 +153,15 @@ module.exports = function (sequelize, DataTypes) {
|
||||
return filterXSS(photo)
|
||||
}
|
||||
User.parseProfileByEmail = function (email) {
|
||||
const sanitizedEmail = email.replaceAll('"', '').replaceAll("'", '').replaceAll('<', '').replaceAll('>', '')
|
||||
return {
|
||||
name: email.substring(0, email.lastIndexOf('@')),
|
||||
photo: generateAvatarURL('', email, false),
|
||||
biggerphoto: generateAvatarURL('', email, true)
|
||||
name: sanitizedEmail.split('@')[0],
|
||||
photo: generateAvatarURL('', sanitizedEmail, false),
|
||||
biggerphoto: generateAvatarURL('', sanitizedEmail, true)
|
||||
}
|
||||
}
|
||||
|
||||
function updatePasswordHashHook (user, options) {
|
||||
async function updatePasswordHashHook (user, options) {
|
||||
// suggested way to hash passwords to be able to do this asynchronously:
|
||||
// @see https://github.com/sequelize/sequelize/issues/1821#issuecomment-44265819
|
||||
|
||||
@@ -158,7 +169,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 }).then(keyBuf => {
|
||||
user.setDataValue('password', keyBuf.toString('hex'))
|
||||
})
|
||||
}
|
||||
@@ -168,3 +179,5 @@ module.exports = function (sequelize, DataTypes) {
|
||||
|
||||
return User
|
||||
}
|
||||
|
||||
export default UserModel
|
||||
|
||||
@@ -219,19 +219,26 @@ ot.Client = (function (global) {
|
||||
this.acknowlaged = acknowlaged;
|
||||
this.client = client;
|
||||
this.revision = revision;
|
||||
this.pendingOps = [];
|
||||
}
|
||||
Client.Stale = Stale;
|
||||
|
||||
Stale.prototype.applyClient = function (client, operation) {
|
||||
return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
|
||||
var swb = new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
|
||||
swb.pendingOps = this.pendingOps;
|
||||
return swb;
|
||||
};
|
||||
|
||||
Stale.prototype.applyServer = function (client, revision, operation) {
|
||||
throw new Error("Ignored server-side change.");
|
||||
// Buffer incoming operations while waiting for the missing operations
|
||||
// response. These will be applied sequentially after the missing ops.
|
||||
this.pendingOps.push({ revision: revision, operation: operation });
|
||||
return this;
|
||||
};
|
||||
|
||||
Stale.prototype.applyOperations = function (client, head, operations) {
|
||||
var transform = this.acknowlaged.constructor.transform;
|
||||
// Apply the requested missing operations (concurrent with acknowlaged)
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
var op = ot.TextOperation.fromJSON(operations[i]);
|
||||
var pair = transform(this.acknowlaged, op);
|
||||
@@ -239,6 +246,14 @@ ot.Client = (function (global) {
|
||||
this.acknowlaged = pair[0];
|
||||
}
|
||||
client.revision = this.revision;
|
||||
// Apply any operations that arrived while we were waiting for the
|
||||
// missing operations. These are sequential (not concurrent), so they
|
||||
// can be applied directly like Synchronized.applyServer would.
|
||||
for (var j = 0; j < this.pendingOps.length; j++) {
|
||||
var pending = this.pendingOps[j];
|
||||
client.revision = pending.revision;
|
||||
client.applyOperation(pending.operation);
|
||||
}
|
||||
return synchronized_;
|
||||
};
|
||||
|
||||
@@ -261,20 +276,27 @@ ot.Client = (function (global) {
|
||||
this.buffer = buffer;
|
||||
this.client = client;
|
||||
this.revision = revision;
|
||||
this.pendingOps = [];
|
||||
}
|
||||
Client.StaleWithBuffer = StaleWithBuffer;
|
||||
|
||||
StaleWithBuffer.prototype.applyClient = function (client, operation) {
|
||||
var buffer = this.buffer.compose(operation);
|
||||
return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
|
||||
var swb = new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
|
||||
swb.pendingOps = this.pendingOps;
|
||||
return swb;
|
||||
};
|
||||
|
||||
StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
|
||||
throw new Error("Ignored server-side change.");
|
||||
// Buffer incoming operations while waiting for the missing operations
|
||||
// response. These will be applied sequentially after the missing ops.
|
||||
this.pendingOps.push({ revision: revision, operation: operation });
|
||||
return this;
|
||||
};
|
||||
|
||||
StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
|
||||
var transform = this.acknowlaged.constructor.transform;
|
||||
// Apply the requested missing operations (concurrent with acknowlaged and buffer)
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
var op = ot.TextOperation.fromJSON(operations[i]);
|
||||
var pair1 = transform(this.acknowlaged, op);
|
||||
@@ -284,6 +306,16 @@ ot.Client = (function (global) {
|
||||
this.buffer = pair2[0];
|
||||
}
|
||||
client.revision = this.revision;
|
||||
// Apply any operations that arrived while we were waiting for the
|
||||
// missing operations. These are sequential (not concurrent), so we
|
||||
// transform the buffer against them and apply them to the document.
|
||||
for (var j = 0; j < this.pendingOps.length; j++) {
|
||||
var pending = this.pendingOps[j];
|
||||
var pair3 = transform(this.buffer, pending.operation);
|
||||
client.applyOperation(pair3[1]);
|
||||
this.buffer = pair3[0];
|
||||
client.revision = pending.revision;
|
||||
}
|
||||
client.sendOperation(client.revision, this.buffer);
|
||||
return new AwaitingConfirm(this.buffer);
|
||||
};
|
||||
|
||||
@@ -51,14 +51,23 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
|
||||
self.onOperation(socket, revision, operation, selection);
|
||||
if (typeof self.operationCallback === 'function')
|
||||
self.operationCallback(socket, operation);
|
||||
socket._opRetries = 0;
|
||||
} catch (err) {
|
||||
logger.error("Operation error from client " + socket.id + ": " + err.message);
|
||||
if (!socket._opRetries) socket._opRetries = 0;
|
||||
socket._opRetries++;
|
||||
setTimeout(function() {
|
||||
var docOut = {
|
||||
str: self.document,
|
||||
revision: self.operations.length,
|
||||
clients: self.users,
|
||||
force: true
|
||||
clients: self.users
|
||||
};
|
||||
// Only force-reset after 3 consecutive failures as a last resort.
|
||||
// Without force:true, the client will use its Stale recovery to resync.
|
||||
if (socket._opRetries >= 3) {
|
||||
docOut.force = true;
|
||||
socket._opRetries = 0;
|
||||
}
|
||||
socket.emit('doc', docOut);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use strict'
|
||||
const util = require('util')
|
||||
// realtime
|
||||
// external modules
|
||||
const cookie = require('cookie')
|
||||
@@ -29,6 +30,8 @@ const realtime = {
|
||||
maintenance: true
|
||||
}
|
||||
|
||||
const allowedPermissionValues = ['freely', 'editable', 'limited', 'locked', 'protected', 'private']
|
||||
|
||||
function onAuthorizeSuccess (data, accept) {
|
||||
accept()
|
||||
}
|
||||
@@ -363,13 +366,25 @@ function connectNextSocket () {
|
||||
}
|
||||
|
||||
function checkViewPermission (req, note) {
|
||||
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') {
|
||||
if (req.user && req.user.logged_in) { return true } else { return false }
|
||||
} else {
|
||||
const permission = note.permission
|
||||
|
||||
// Private notes should only be readable to their owner
|
||||
if (permission === 'private') {
|
||||
return req.user && req.user.logged_in && req.user.id === note.owner
|
||||
}
|
||||
|
||||
// Limited and protected notes should be readable by all logged-in users
|
||||
if (permission === 'limited' || permission === 'protected') {
|
||||
return req.user && req.user.logged_in
|
||||
}
|
||||
|
||||
// Freely, Editable and Locked notes should be readable by everyone
|
||||
if (permission === 'freely' || permission === 'editable' || permission === 'locked') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Invalid permission
|
||||
return false
|
||||
}
|
||||
|
||||
let isConnectionBusy = false
|
||||
@@ -384,6 +399,9 @@ function finishConnection (socket, noteId, socketId) {
|
||||
}
|
||||
const note = notes[noteId]
|
||||
const user = users[socketId]
|
||||
if (!user) {
|
||||
return failConnection(500, 'user not found', socket)
|
||||
}
|
||||
// update user color to author color
|
||||
if (note.authors[user.userid]) {
|
||||
user.color = users[socket.id].color = note.authors[user.userid].color
|
||||
@@ -552,16 +570,13 @@ function disconnect (socket) {
|
||||
if (note.server.isDirty) {
|
||||
updateNote(note, function (err, _note) {
|
||||
if (err) return logger.error('disconnect note failed: ' + err)
|
||||
// clear server before delete to avoid memory leaks
|
||||
note.server.document = ''
|
||||
note.server.operations = []
|
||||
delete note.server
|
||||
delete notes[noteId]
|
||||
if (config.debug) {
|
||||
logger.debug(notes)
|
||||
getStatus(function (data) {
|
||||
logger.debug(JSON.stringify(data))
|
||||
})
|
||||
// Check again for online users before clear, because users could reconnect during the async updateNote
|
||||
if (Object.keys(note.users).length <= 0) {
|
||||
// clear server before delete to avoid memory leaks
|
||||
note.server.document = ''
|
||||
note.server.operations = []
|
||||
delete note.server
|
||||
delete notes[noteId]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -579,7 +594,7 @@ function disconnect (socket) {
|
||||
if (disconnectSocketQueue.length > 0) { disconnect(disconnectSocketQueue[0]) }
|
||||
|
||||
if (config.debug) {
|
||||
logger.debug(notes)
|
||||
logger.debug(util.inspect(notes))
|
||||
getStatus(function (data) {
|
||||
logger.debug(JSON.stringify(data))
|
||||
})
|
||||
@@ -633,6 +648,10 @@ function ifMayEdit (socket, callback) {
|
||||
// only owner can change
|
||||
if (!note.owner || note.owner !== socket.request.user.id) { mayEdit = false }
|
||||
break
|
||||
default:
|
||||
// invalid permission
|
||||
mayEdit = false
|
||||
break
|
||||
}
|
||||
// if user may edit and this is a text operation
|
||||
if (socket.origin === 'operation' && mayEdit) {
|
||||
@@ -710,7 +729,7 @@ function connection (socket) {
|
||||
|
||||
// initialize user data
|
||||
// random color
|
||||
let color = chance.color()
|
||||
let color = chance.color({ format: 'hex' })
|
||||
// make sure color not duplicated or reach max random count
|
||||
if (notes[noteId]) {
|
||||
let randomcount = 0
|
||||
@@ -723,7 +742,7 @@ function connection (socket) {
|
||||
}
|
||||
})
|
||||
if (found) {
|
||||
color = chance.color()
|
||||
color = chance.color({ format: 'hex' })
|
||||
randomcount++
|
||||
}
|
||||
} while (found && randomcount < maxrandomcount)
|
||||
@@ -775,7 +794,14 @@ function connection (socket) {
|
||||
const note = notes[noteId]
|
||||
// Only owner can change permission
|
||||
if (note.owner && note.owner === socket.request.user.id) {
|
||||
if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousEdits) return
|
||||
// If config disallows freely permission, do not set it
|
||||
if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousEdits) {
|
||||
return
|
||||
}
|
||||
// Ignore invalid permission values
|
||||
if (!allowedPermissionValues.includes(permission)) {
|
||||
return
|
||||
}
|
||||
note.permission = permission
|
||||
models.Note.update({
|
||||
permission
|
||||
|
||||
114
lib/response.js
114
lib/response.js
@@ -65,69 +65,73 @@ function githubActions (req, res, next) {
|
||||
function githubActionGist (req, res, note) {
|
||||
const code = req.query.code
|
||||
const state = req.query.state
|
||||
if (!code || !state) {
|
||||
const sessionsState = req.session.githubGistState
|
||||
const redirectUri = req.session.githubGistRedirectUri
|
||||
delete req.session.githubGistState
|
||||
delete req.session.githubGistRedirectUri
|
||||
if (!code || !state || sessionsState !== state) {
|
||||
return errors.errorForbidden(res)
|
||||
} else {
|
||||
const data = {
|
||||
client_id: config.github.clientID,
|
||||
client_secret: config.github.clientSecret,
|
||||
code,
|
||||
state
|
||||
}
|
||||
|
||||
const data = {
|
||||
client_id: config.github.clientID,
|
||||
client_secret: config.github.clientSecret,
|
||||
code,
|
||||
redirect_uri: redirectUri
|
||||
}
|
||||
const authUrl = 'https://github.com/login/oauth/access_token'
|
||||
fetch(authUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
}
|
||||
const authUrl = 'https://github.com/login/oauth/access_token'
|
||||
fetch(authUrl, {
|
||||
}).then(resp => {
|
||||
if (!resp.ok) {
|
||||
throw new Error('forbidden')
|
||||
}
|
||||
return resp.json()
|
||||
}).then(body => {
|
||||
const accessToken = body.access_token
|
||||
if (!accessToken) {
|
||||
throw new Error('forbidden')
|
||||
}
|
||||
const content = note.content
|
||||
const title = models.Note.decodeTitle(note.title)
|
||||
const filename = title.replace('/', ' ') + '.md'
|
||||
const gist = {
|
||||
files: {}
|
||||
}
|
||||
gist.files[filename] = {
|
||||
content
|
||||
}
|
||||
const gistUrl = 'https://api.github.com/gists'
|
||||
return fetch(gistUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
body: JSON.stringify(gist),
|
||||
headers: {
|
||||
'User-Agent': 'HedgeDoc',
|
||||
Authorization: 'token ' + accessToken,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
}
|
||||
}).then(resp => {
|
||||
if (!resp.ok) {
|
||||
throw new Error('forbidden')
|
||||
}
|
||||
return resp.json()
|
||||
}).then(body => {
|
||||
const accessToken = body.access_token
|
||||
if (!accessToken) {
|
||||
throw new Error('forbidden')
|
||||
}
|
||||
const content = note.content
|
||||
const title = models.Note.decodeTitle(note.title)
|
||||
const filename = title.replace('/', ' ') + '.md'
|
||||
const gist = {
|
||||
files: {}
|
||||
}
|
||||
gist.files[filename] = {
|
||||
content
|
||||
}
|
||||
const gistUrl = 'https://api.github.com/gists'
|
||||
return fetch(gistUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(gist),
|
||||
headers: {
|
||||
'User-Agent': 'HedgeDoc',
|
||||
Authorization: 'token ' + accessToken,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
}
|
||||
})
|
||||
}).then(resp => {
|
||||
if (resp.status !== 201) {
|
||||
throw new Error('forbidden')
|
||||
}
|
||||
return resp.json()
|
||||
}).then(body => {
|
||||
res.setHeader('referer', '')
|
||||
res.redirect(body.html_url)
|
||||
}).catch(error => {
|
||||
if (error.message === 'forbidden') {
|
||||
return errors.errorForbidden(res)
|
||||
}
|
||||
logger.error('GitHub Gist auth failed: ' + error)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
}
|
||||
}).then(resp => {
|
||||
if (resp.status !== 201) {
|
||||
throw new Error('forbidden')
|
||||
}
|
||||
return resp.json()
|
||||
}).then(body => {
|
||||
res.setHeader('referer', '')
|
||||
res.redirect(body.html_url)
|
||||
}).catch(error => {
|
||||
if (error.message === 'forbidden') {
|
||||
return errors.errorForbidden(res)
|
||||
}
|
||||
logger.error('GitHub Gist auth failed: ' + error)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
}
|
||||
|
||||
function gitlabActions (req, res, next) {
|
||||
|
||||
@@ -8,6 +8,8 @@ const baseRouter = module.exports = Router()
|
||||
|
||||
const errors = require('../errors')
|
||||
|
||||
const links = require('../links')
|
||||
|
||||
// get index
|
||||
baseRouter.get('/', response.showIndex)
|
||||
// get 403 forbidden
|
||||
@@ -22,3 +24,5 @@ baseRouter.get('/404', function (req, res) {
|
||||
baseRouter.get('/500', function (req, res) {
|
||||
errors.errorInternalError(res)
|
||||
})
|
||||
// get external link warning page
|
||||
baseRouter.get('/_link', links.serveLinkWarningPage)
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
'use strict'
|
||||
|
||||
import isSvg from 'is-svg'
|
||||
import formidable from 'formidable'
|
||||
|
||||
import { createRequire } from 'module'
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
const Router = require('express').Router
|
||||
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')
|
||||
const isSvg = require('is-svg')
|
||||
const { JSDOM } = require('jsdom')
|
||||
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const errors = require('../../errors')
|
||||
|
||||
const imageRouter = (module.exports = Router())
|
||||
let DOMPurify
|
||||
try {
|
||||
const window = new JSDOM('').window
|
||||
global.window = window
|
||||
const createDOMPurify = require('dompurify')
|
||||
DOMPurify = createDOMPurify(window)
|
||||
} catch (err) {
|
||||
logger.error('Failed to initialize DOMPurify for SVG sanitization:', err)
|
||||
}
|
||||
|
||||
const imageRouter = Router()
|
||||
export default imageRouter
|
||||
|
||||
async function checkUploadType (filePath) {
|
||||
const extension = path.extname(filePath).toLowerCase()
|
||||
@@ -21,7 +37,7 @@ async function checkUploadType (filePath) {
|
||||
let typeFromMagic = await FileType.fileTypeFromFile(filePath)
|
||||
if (extension === '.svg' && (typeFromMagic === undefined || typeFromMagic.mime === 'application/xml')) {
|
||||
const fileContent = fs.readFileSync(filePath)
|
||||
if (isSvg(fileContent)) {
|
||||
if (isSvg(fileContent.toString())) {
|
||||
typeFromMagic = {
|
||||
ext: 'svg',
|
||||
mime: 'image/svg+xml'
|
||||
@@ -55,6 +71,31 @@ async function checkUploadType (filePath) {
|
||||
return true
|
||||
}
|
||||
|
||||
function sanitizeSvg (filePath) {
|
||||
if (!DOMPurify) {
|
||||
logger.error('SVG sanitization failed: DOMPurify not initialized')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const svgContent = fs.readFileSync(filePath, 'utf8')
|
||||
const cleanSvg = DOMPurify.sanitize(svgContent, {
|
||||
USE_PROFILES: { svg: true, svgFilters: true }
|
||||
})
|
||||
|
||||
if (!cleanSvg) {
|
||||
logger.error('SVG sanitization resulted in empty content')
|
||||
return false
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, cleanSvg, 'utf8')
|
||||
return true
|
||||
} catch (err) {
|
||||
logger.error('SVG sanitization error:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// upload image
|
||||
imageRouter.post('/uploadimage', function (req, res) {
|
||||
const uploadsEnabled = config.enableUploads
|
||||
@@ -84,28 +125,36 @@ imageRouter.post('/uploadimage', function (req, res) {
|
||||
}
|
||||
})
|
||||
|
||||
form.parse(req, async function (err, fields, files) {
|
||||
form.parse(req, async function (err, _fields, files) {
|
||||
if (err) {
|
||||
logger.error(`Image upload error: formidable error: ${err}`)
|
||||
rimraf.sync(tmpDir)
|
||||
return errors.errorForbidden(res)
|
||||
} else if (!files.image || !files.image.filepath) {
|
||||
} else if (!files.image || !files.image.length < 1 || !files.image[0].filepath) {
|
||||
logger.debug(`${JSON.stringify(files)}`)
|
||||
logger.error('Image upload error: Upload didn\'t contain file)')
|
||||
rimraf.sync(tmpDir)
|
||||
return errors.errorBadRequest(res)
|
||||
} else if (!(await checkUploadType(files.image.filepath))) {
|
||||
} else if (!(await checkUploadType(files.image[0].filepath))) {
|
||||
rimraf.sync(tmpDir)
|
||||
return errors.errorBadRequest(res)
|
||||
} else {
|
||||
const extension = path.extname(files.image[0].filepath).toLowerCase()
|
||||
if (extension === '.svg' && !sanitizeSvg(files.image[0].filepath)) {
|
||||
logger.error('Image upload error: SVG sanitization failed')
|
||||
rimraf.sync(tmpDir)
|
||||
return errors.errorBadRequest(res)
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`SERVER received uploadimage: ${JSON.stringify(files.image)}`
|
||||
)
|
||||
|
||||
const uploadProvider = require('./' + config.imageUploadType)
|
||||
logger.debug(
|
||||
`imageRouter: Uploading ${files.image.filepath} using ${config.imageUploadType}`
|
||||
`imageRouter: Uploading ${files.image[0].filepath} using ${config.imageUploadType}`
|
||||
)
|
||||
uploadProvider.uploadImage(files.image.filepath, function (err, url) {
|
||||
uploadProvider.uploadImage(files.image[0].filepath, function (err, url) {
|
||||
rimraf.sync(tmpDir)
|
||||
if (err !== null) {
|
||||
logger.error(err)
|
||||
|
||||
@@ -8,13 +8,16 @@ const determineKey = (req) => {
|
||||
if (req.user) {
|
||||
return req.user.id
|
||||
}
|
||||
return ipKeyGenerator(req.header('cf-connecting-ip') || req.ip)
|
||||
if (config.rateLimitUsingCloudflare) {
|
||||
return ipKeyGenerator(req.header('cf-connecting-ip') || req.ip)
|
||||
}
|
||||
return ipKeyGenerator(req.ip)
|
||||
}
|
||||
|
||||
// limits requests to user endpoints (login, signup) to 10 requests per 5 minutes
|
||||
const userEndpoints = rateLimit({
|
||||
windowMs: 5 * 60 * 1000,
|
||||
limit: 10,
|
||||
limit: config.rateLimitUser,
|
||||
keyGenerator: determineKey,
|
||||
handler: (req, res) => errors.errorTooManyRequests(res)
|
||||
})
|
||||
|
||||
@@ -32,11 +32,15 @@ exports.getInfo = function getInfo (req, res, note) {
|
||||
}
|
||||
|
||||
exports.createGist = function createGist (req, res, note) {
|
||||
const state = nanoid.nanoid()
|
||||
const redirectUri = config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist'
|
||||
req.session.githubGistState = state
|
||||
req.session.githubGistRedirectUri = redirectUri
|
||||
const data = {
|
||||
client_id: config.github.clientID,
|
||||
redirect_uri: config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist',
|
||||
redirect_uri: redirectUri,
|
||||
scope: 'gist',
|
||||
state: nanoid.nanoid()
|
||||
state
|
||||
}
|
||||
const query = querystring.stringify(data)
|
||||
res.redirect('https://github.com/login/oauth/authorize?' + query)
|
||||
|
||||
@@ -5,10 +5,6 @@ const Router = require('express').Router
|
||||
const errors = require('../errors')
|
||||
const realtime = require('../realtime')
|
||||
const config = require('../config')
|
||||
const models = require('../models')
|
||||
const logger = require('../logger')
|
||||
|
||||
const { urlencodedParser } = require('./utils')
|
||||
|
||||
const statusRouter = module.exports = Router()
|
||||
|
||||
@@ -36,71 +32,6 @@ statusRouter.get('/status', function (req, res, next) {
|
||||
res.send(data)
|
||||
})
|
||||
})
|
||||
// get status
|
||||
statusRouter.get('/temp', function (req, res) {
|
||||
const host = req.get('host')
|
||||
if (config.allowOrigin.indexOf(host) === -1) {
|
||||
errors.errorForbidden(res)
|
||||
} else {
|
||||
const tempid = req.query.tempid
|
||||
if (!tempid) {
|
||||
errors.errorForbidden(res)
|
||||
} else {
|
||||
models.Temp.findOne({
|
||||
where: {
|
||||
id: tempid
|
||||
}
|
||||
}).then(function (temp) {
|
||||
if (!temp) {
|
||||
errors.errorNotFound(res)
|
||||
} else {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
res.send({
|
||||
temp: temp.data
|
||||
})
|
||||
temp.destroy().catch(function (err) {
|
||||
if (err) {
|
||||
logger.error('remove temp failed: ' + err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}).catch(function (err) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
// post status
|
||||
statusRouter.post('/temp', urlencodedParser, function (req, res) {
|
||||
const host = req.get('host')
|
||||
if (config.allowOrigin.indexOf(host) === -1) {
|
||||
errors.errorForbidden(res)
|
||||
} else {
|
||||
const data = req.body.data
|
||||
if (!data) {
|
||||
errors.errorForbidden(res)
|
||||
} else {
|
||||
logger.debug(`SERVER received temp from [${host}]: ${req.body.data}`)
|
||||
models.Temp.create({
|
||||
data
|
||||
}).then(function (temp) {
|
||||
if (temp) {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
res.send({
|
||||
status: 'ok',
|
||||
id: temp.id
|
||||
})
|
||||
} else {
|
||||
errors.errorInternalError(res)
|
||||
}
|
||||
}).catch(function (err) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
statusRouter.get('/config', function (req, res) {
|
||||
const data = {
|
||||
@@ -113,6 +44,8 @@ statusRouter.get('/config', function (req, res) {
|
||||
linkifyHeaderStyle: config.linkifyHeaderStyle,
|
||||
cookiePolicy: config.cookiePolicy,
|
||||
enableUploads: config.enableUploads,
|
||||
externalLinkWarning: config.externalLinkWarning,
|
||||
externalLinkWhitelist: config.externalLinkWhitelist,
|
||||
userToken: req.user ? req.user.deleteToken : ''
|
||||
}
|
||||
res.set({
|
||||
|
||||
@@ -126,5 +126,13 @@
|
||||
"Protected - Only owner can edit (forbid guests)": "Geschützt - Nur Besitzer kann bearbeiten (Gäste verboten)",
|
||||
"Private - Only owner can view & edit": "Privat - Nur Besitzer kann anschauen & bearbeiten",
|
||||
"changed": "verändert",
|
||||
"created": "erstellt"
|
||||
"created": "erstellt",
|
||||
"External link": "Externer Link",
|
||||
"You've clicked a link that points to an external page.": "Es wurde ein Link zu einer externen Seite angeklickt.",
|
||||
"The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there.": "Der Inhalt dieser Seite ist unabhängig von HedgeDoc. Dort keine HedgeDoc-Zugangsdaten eingeben.",
|
||||
"Do you want to continue?": "Fortfahren?",
|
||||
"Continue to external page": "Weiter zur externen Seite",
|
||||
"Close": "Schließen",
|
||||
"Invalid link": "Ungültiger Link",
|
||||
"The provided link is not a valid URL.": "Die angegebene URL ist nicht gültig."
|
||||
}
|
||||
@@ -40,7 +40,6 @@
|
||||
"Extra": "Extra",
|
||||
"Revision": "Revision",
|
||||
"Slide Mode": "Slide Mode",
|
||||
"Invite Agent": "Invite Agent",
|
||||
"Export": "Export",
|
||||
"Import": "Import",
|
||||
"Clipboard": "Clipboard",
|
||||
@@ -127,5 +126,13 @@
|
||||
"Protected - Only owner can edit (forbid guests)": "Protected - Only owner can edit (forbid guests)",
|
||||
"Private - Only owner can view & edit": "Private - Only owner can view & edit",
|
||||
"changed": "changed",
|
||||
"created": "created"
|
||||
}
|
||||
"created": "created",
|
||||
"You've clicked a link that points to an external page.": "You've clicked a link that points to an external page.",
|
||||
"External link": "External link",
|
||||
"Invalid link": "Invalid link",
|
||||
"Do you want to continue?": "Do you want to continue?",
|
||||
"Continue to external page": "Continue to external page",
|
||||
"Close": "Close",
|
||||
"The provided link is not a valid URL.": "The provided link is not a valid URL.",
|
||||
"The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there.": "The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there."
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
"Extra": "Extra",
|
||||
"Revision": "Revision",
|
||||
"Slide Mode": "Slide Mode",
|
||||
"Invite Agent": "Invite Agent",
|
||||
"Export": "Exportera",
|
||||
"Import": "Importera",
|
||||
"Clipboard": "Urklipp",
|
||||
|
||||
124
package.json
124
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "HedgeDoc",
|
||||
"version": "1.10.5",
|
||||
"version": "1.11.1",
|
||||
"description": "The best platform to write and share markdown.",
|
||||
"main": "app.js",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -14,65 +14,68 @@
|
||||
"dev": "webpack --config webpack.dev.js --progress --watch",
|
||||
"heroku-prebuild": "bin/heroku",
|
||||
"build": "webpack --config webpack.prod.js --progress",
|
||||
"build:ot": "bash -c \"cd public/vendor/ot && ./compress.sh\"",
|
||||
"start": "node app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hedgedoc/meta-marked": "14.1.0",
|
||||
"@node-saml/passport-saml": "5.1.0",
|
||||
"@passport-next/passport-openid": "1.0.0",
|
||||
"Idle.Js": "git+https://github.com/shawnmclean/Idle.js#commit=2b57cc6e49d177b7ddce0cca00ef5cbe07453541",
|
||||
"archiver": "7.0.1",
|
||||
"async": "3.2.6",
|
||||
"aws-sdk": "2.1692.0",
|
||||
"aws-sdk": "2.1693.0",
|
||||
"azure-storage": "2.10.7",
|
||||
"base64url": "3.0.1",
|
||||
"body-parser": "2.2.1",
|
||||
"body-parser": "2.3.0",
|
||||
"chance": "1.1.13",
|
||||
"cheerio": "0.22.0",
|
||||
"cheerio": "1.2.0",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"compression": "1.8.1",
|
||||
"connect-flash": "0.1.1",
|
||||
"connect-session-sequelize": "8.0.4",
|
||||
"connect-session-sequelize": "8.0.6",
|
||||
"constrain-object": "1.1.0",
|
||||
"cookie": "1.1.1",
|
||||
"cookie-parser": "1.4.7",
|
||||
"deep-freeze": "0.0.1",
|
||||
"diff-match-patch": "git+https://github.com/hackmdio/diff-match-patch.git#commit=59a9395ad9fe143e601e7ae5765ed943bdd2b11e",
|
||||
"dompurify": "3.4.14",
|
||||
"ejs": "3.1.10",
|
||||
"express": "4.22.1",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"express-session": "1.18.2",
|
||||
"file-type": "21.1.1",
|
||||
"formidable": "2.1.5",
|
||||
"express": "4.22.2",
|
||||
"express-rate-limit": "8.6.2",
|
||||
"express-session": "1.19.0",
|
||||
"file-type": "21.3.4",
|
||||
"formidable": "3.5.4",
|
||||
"graceful-fs": "4.2.11",
|
||||
"helmet": "8.1.0",
|
||||
"helmet": "8.3.0",
|
||||
"i18n": "0.15.3",
|
||||
"is-svg": "4.4.0",
|
||||
"jsdom-nogyp": "0.8.3",
|
||||
"lodash": "4.17.21",
|
||||
"is-svg": "6.1.0",
|
||||
"js-yaml": "4.3.1",
|
||||
"jsdom": "24.1.3",
|
||||
"lodash": "4.18.1",
|
||||
"lutim": "1.0.3",
|
||||
"lz-string": "git+https://github.com/hackmdio/lz-string.git#commit=6edfccb79cd8c210f03fd3bf18e41ca144fbeefb",
|
||||
"mariadb": "3.4.5",
|
||||
"markdown-it": "13.0.2",
|
||||
"markdown-it-abbr": "1.0.4",
|
||||
"markdown-it-container": "3.0.0",
|
||||
"markdown-it-deflist": "2.1.0",
|
||||
"markdown-it-emoji": "2.0.2",
|
||||
"markdown-it-footnote": "3.0.3",
|
||||
"mariadb": "3.5.3",
|
||||
"markdown-it": "14.1.1",
|
||||
"markdown-it-abbr": "2.0.0",
|
||||
"markdown-it-container": "4.0.0",
|
||||
"markdown-it-deflist": "4.0.0",
|
||||
"markdown-it-emoji": "3.1.0",
|
||||
"markdown-it-footnote": "4.0.0",
|
||||
"markdown-it-imsize": "2.0.1",
|
||||
"markdown-it-ins": "3.0.1",
|
||||
"markdown-it-mark": "3.0.1",
|
||||
"markdown-it-ins": "4.0.0",
|
||||
"markdown-it-mark": "4.0.0",
|
||||
"markdown-it-mathjax": "2.0.0",
|
||||
"markdown-it-regexp": "0.4.0",
|
||||
"markdown-it-sub": "1.0.0",
|
||||
"markdown-it-sup": "1.0.0",
|
||||
"markdown-it-sub": "2.0.0",
|
||||
"markdown-it-sup": "2.0.0",
|
||||
"mattermost": "3.4.0",
|
||||
"method-override": "3.0.0",
|
||||
"minimist": "1.2.8",
|
||||
"minio": "8.0.6",
|
||||
"minio": "8.0.7",
|
||||
"moment": "2.30.1",
|
||||
"morgan": "1.10.1",
|
||||
"mysql2": "3.15.3",
|
||||
"nanoid": "3.3.11",
|
||||
"morgan": "1.11.0",
|
||||
"mysql2": "3.23.4",
|
||||
"nanoid": "3.3.18",
|
||||
"passport": "patch:passport@npm%3A0.7.0#~/.yarn/patches/passport-npm-0.7.0-df02531736.patch",
|
||||
"passport-dropbox-oauth2": "1.1.0",
|
||||
"passport-facebook": "3.0.0",
|
||||
@@ -85,23 +88,23 @@
|
||||
"passport-twitter": "1.0.4",
|
||||
"passport.socketio": "3.7.0",
|
||||
"pdfobject": "2.3.1",
|
||||
"pg": "8.16.3",
|
||||
"pg": "8.23.0",
|
||||
"pg-hstore": "2.3.4",
|
||||
"prom-client": "15.1.3",
|
||||
"prometheus-api-metrics": "4.0.0",
|
||||
"readline-sync": "1.4.10",
|
||||
"rimraf": "6.1.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"scrypt-kdf": "2.0.1",
|
||||
"rimraf": "6.1.3",
|
||||
"sanitize-filename": "1.6.4",
|
||||
"scrypt-kdf": "3.0.0",
|
||||
"sequelize": "5.22.5",
|
||||
"socket.io": "4.8.1",
|
||||
"socket.io": "4.8.3",
|
||||
"sqlite3": "5.1.7",
|
||||
"store": "2.0.12",
|
||||
"toobusy-js": "0.5.1",
|
||||
"umzug": "2.3.0",
|
||||
"uuid": "11.1.0",
|
||||
"validator": "13.15.23",
|
||||
"winston": "3.18.3",
|
||||
"umzug": "3.8.3",
|
||||
"uuid": "11.1.1",
|
||||
"validator": "13.15.35",
|
||||
"winston": "3.19.0",
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"engines": {
|
||||
@@ -134,10 +137,10 @@
|
||||
"url": "https://github.com/hedgedoc/hedgedoc.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "3.3.3",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/eslintrc": "3.3.6",
|
||||
"@eslint/js": "9.39.5",
|
||||
"@hedgedoc/codemirror-5": "5.65.12",
|
||||
"abcjs": "6.5.2",
|
||||
"abcjs": "6.7.0",
|
||||
"babel-cli": "6.26.0",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-loader": "7.1.5",
|
||||
@@ -150,12 +153,12 @@
|
||||
"copy-webpack-plugin": "6.4.1",
|
||||
"css-loader": "5.2.7",
|
||||
"emojify.js": "1.1.0",
|
||||
"esbuild-loader": "4.4.0",
|
||||
"eslint": "9.39.1",
|
||||
"esbuild-loader": "4.5.0",
|
||||
"eslint": "9.39.5",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-n": "17.23.1",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-n": "17.24.0",
|
||||
"eslint-plugin-promise": "7.3.0",
|
||||
"eslint-plugin-standard": "5.0.0",
|
||||
"exports-loader": "1.1.1",
|
||||
"expose-loader": "1.0.3",
|
||||
@@ -163,50 +166,49 @@
|
||||
"file-saver": "2.0.5",
|
||||
"flowchart.js": "1.18.0",
|
||||
"fork-awesome": "1.2.0",
|
||||
"globals": "16.5.0",
|
||||
"highlight.js": "10.7.3",
|
||||
"globals": "17.11.0",
|
||||
"highlight.js": "11.12.0",
|
||||
"html-webpack-plugin": "4.5.2",
|
||||
"imports-loader": "1.2.0",
|
||||
"ionicons": "2.0.1",
|
||||
"jquery": "3.7.1",
|
||||
"jquery-mousewheel": "3.2.2",
|
||||
"jquery-ui": "1.14.1",
|
||||
"js-cookie": "3.0.5",
|
||||
"jquery-ui": "1.14.2",
|
||||
"js-cookie": "3.0.8",
|
||||
"js-sequence-diagrams": "git+https://github.com/hedgedoc/js-sequence-diagrams.git",
|
||||
"js-yaml": "3.14.2",
|
||||
"jsonlint": "1.6.3",
|
||||
"keymaster": "1.6.2",
|
||||
"less": "4.4.2",
|
||||
"less": "4.9.0",
|
||||
"less-loader": "7.3.0",
|
||||
"list.js": "2.3.1",
|
||||
"mathjax": "2.7.9",
|
||||
"mermaid": "9.1.7",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"mocha": "11.7.5",
|
||||
"mocha": "11.8.0",
|
||||
"mock-require": "3.0.3",
|
||||
"optimize-css-assets-webpack-plugin": "6.0.1",
|
||||
"prismjs": "1.30.0",
|
||||
"raphael": "2.3.0",
|
||||
"remark-cli": "12.0.1",
|
||||
"remark-preset-lint-markdown-style-guide": "5.1.3",
|
||||
"remark-preset-lint-markdown-style-guide": "6.0.1",
|
||||
"reveal.js": "3.9.2",
|
||||
"select2": "3.5.2-browserify",
|
||||
"socket.io-client": "4.8.1",
|
||||
"socket.io-client": "4.8.3",
|
||||
"spin.js": "4.1.2",
|
||||
"string-loader": "0.0.1",
|
||||
"turndown": "7.2.2",
|
||||
"turndown": "7.2.4",
|
||||
"uglify-js": "3.19.3",
|
||||
"url-loader": "4.1.1",
|
||||
"velocity-animate": "1.5.2",
|
||||
"visibilityjs": "2.0.2",
|
||||
"viz.js": "1.8.2",
|
||||
"webpack": "4.47.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-merge": "5.10.0",
|
||||
"wurl": "2.5.4"
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "4.0.9",
|
||||
"utf-8-validate": "6.0.5"
|
||||
"bufferutil": "4.1.0",
|
||||
"utf-8-validate": "6.0.6"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0"
|
||||
"packageManager": "yarn@4.13.0"
|
||||
}
|
||||
|
||||
3
public/css/highlightjs.css
Normal file
3
public/css/highlightjs.css
Normal file
@@ -0,0 +1,3 @@
|
||||
pre code.hljs {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -44,10 +44,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-body.slides section[data-markdown] > ul {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.markdown-body.slides > section > section + section::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -61,7 +57,8 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown-body.slides ul, .markdown-body.slides ol {
|
||||
.markdown-body.slides section[data-markdown] > div:first-child > ul,
|
||||
.markdown-body.slides section[data-markdown] > div:first-child > ol {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
margin: 0 0 0 1em;
|
||||
|
||||
@@ -30,9 +30,9 @@ Or meet us on [**Matrix**](https://chat.hedgedoc.org) for dev-talk and interacti
|
||||
|
||||
### Night Mode
|
||||
|
||||
When you are tired of a white screen and like a night mode, click on the little moon <i class="fa fa-moon-o"></i> and turn on the night view of HedgeDoc.
|
||||
When you are tired of a white screen and like a night mode, click on the little moon <i class="fa fa-moon-o"></i> in the header bar and turn on the night view of HedgeDoc.
|
||||
|
||||
The editor view, which is in night mode by default, can also be toggled between night and day view using the the little sun<i class="fa fa-sun-o fa-fw"></i>.
|
||||
The editor view, which is in night mode by default, can also be toggled between night and day view using the the little sun<i class="fa fa-sun-o fa-fw"></i>. It is below the editor in the bottom bar.
|
||||
|
||||
### Image Upload
|
||||
|
||||
@@ -58,7 +58,7 @@ Similarly to the *save* feature, you can also import a Markdown file from **Drop
|
||||
### Permissions
|
||||
|
||||
It is possible to change the access permission of a note through the little button on the top right of the view.
|
||||
There are four possible options:
|
||||
There are six possible options:
|
||||
|
||||
| | Owner read/write | Signed-in read | Signed-in write | Guest read | Guest write |
|
||||
|:------------------------------------------------------------------------------------ |:----------------:|:--------------:|:---------------:|:----------:|:-----------:|
|
||||
|
||||
@@ -1,5 +1,104 @@
|
||||
# Release Notes
|
||||
|
||||
## <i class="fa fa-tag"></i> 1.x.x <i class="fa fa-calendar-o"></i> UNRELEASED
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Automatically skip the login modal if no form-based login and exactly one external login provider is configured
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- All links will be opened in new tabs instead of the same tab as the note. This was changed when the external link warning was implemented and now the old behavior is restored.
|
||||
- Fixed multiple bugs in the realtime connection handling that could lead to data-loss of users with a flaky connection
|
||||
|
||||
## <i class="fa fa-tag"></i> 1.11.1 <i class="fa fa-calendar-o"></i> 2026-07-24
|
||||
|
||||
### Security fixes
|
||||
|
||||
- [GHSA-93w7-49m2-cqwg](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-93w7-49m2-cqwg) reports possible corruption of permission values due to missing validation. This should not impact permission checks, except when the note owner intentionally set their note permission to something invalid.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Added external link warning setting (`externalLinkWarning` in config.json or `CMD_EXTERNAL_LINK_WARNING`) to disable the external link warning page entirely
|
||||
- Added external link whitelist setting (`externalLinkWhitelist` in config.json or `CMD_EXTERNAL_LINK_WHITELIST`) to skip warning page for certain domains
|
||||
- Added support for webp file uploads (for all backends except `imgur`, since that does not support these)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Fixed external link warning for subpath instances
|
||||
- Restore native browser zoom-in keyboard shortcuts in the editor
|
||||
- Nested list items render properly again in the preview pane for slides
|
||||
|
||||
### Refactoring / Clean-up
|
||||
|
||||
- Removed the old Temp database object and related API endpoints. This was used by a very old way of migrating the history and is no longer needed.
|
||||
- Removed unused `allowOrigin` (environment variable `CMD_ALLOW_ORIGIN`) config option.
|
||||
- Removed unnecessary/duplicate entries in the XSS filtering whitelist
|
||||
|
||||
### Contributors
|
||||
|
||||
- [Shivam Shukla](https://github.com/Shivam-Shukla0)
|
||||
- [Tobi](https://github.com/tldev-de)
|
||||
|
||||
## <i class="fa fa-tag"></i> 1.11.0 <i class="fa fa-calendar-o"></i> 2026-06-18
|
||||
|
||||
### Security fixes
|
||||
|
||||
This release contains four security fixes:
|
||||
- [CVE-2026-58487](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-6c2w-8w96-3pcv) reports a possible HTML injection via the localpart of an email address.
|
||||
- [CVE-2026-58486](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-qj78-mjch-wwrv) reports a possible Denial-of-Service attack using the YAML frontmatter parsing.
|
||||
- [CVE-2026-58489](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-8v9p-5j95-826j) reports a possible CSRF attack vector in the GitHub Gist export.
|
||||
- [CVE-2026-58488](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-2f9f-w8xq-276v) reports a rate-limiting bypass by abusing the CF-Connecting-IP header.
|
||||
|
||||
Thanks to [Chandler Johnson](https://github.com/chndlrx), [taylorodell](https://github.com/taylorodell) and [alanturing881](https://github.com/alanturing881) for reporting!
|
||||
|
||||
### Important notices
|
||||
|
||||
- When using Cloudflare in front of HedgeDoc, you should set `rateLimitUsingCloudflare` in the config.json or `CMD_RATE_LIMIT_USING_CLOUDFLARE` as environment variable to `true`.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Added a warning page when clicking external links
|
||||
- Improve the config.json.example file, which is used by `bin/setup`
|
||||
- Allow configuration of login / signup rate-limits
|
||||
- Allow configuration of Cloudflare usage in regards of rate-limits
|
||||
- Several improvements in the documentation at <https://docs.hedgedoc.org>
|
||||
|
||||
## <i class="fa fa-tag"></i> 1.10.8 <i class="fa fa-calendar-o"></i> 2026-04-15
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Fix data loss when 5+ users edit a document concurrently, caused by the OT client discarding operations during revision gap recovery ([#6342](https://github.com/hedgedoc/hedgedoc/issues/6342))
|
||||
- Add defensive null checks to `hex2rgb` to prevent crashes from non-hex color values
|
||||
|
||||
### Contributors
|
||||
- [Dex Devlon](https://github.com/bxff)
|
||||
|
||||
## <i class="fa fa-tag"></i> 1.10.7 <i class="fa fa-calendar-o"></i> 2026-02-24
|
||||
|
||||
### Bugfixes
|
||||
- Random colors for user's cursors and selections are now always in hex format to avoid conversion errors
|
||||
- Correctly close realtime connections if they disconnect during connection creation
|
||||
- manage_users CLI does not silently drop errors
|
||||
|
||||
## <i class="fa fa-tag"></i> 1.10.6 <i class="fa fa-calendar-o"></i> 2026-02-04
|
||||
|
||||
### Security fixes
|
||||
|
||||
This release contains two medium severity security fixes:
|
||||
- [GHSA-x74j-jmf9-534w](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-x74j-jmf9-534w) reports a bug where security headers for upload files were not set correctly.
|
||||
- [GHSA-672m-p72w-gw28](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-672m-p72w-gw28) reports potential security issues with limited script execution in uploaded SVG files.
|
||||
|
||||
Thanks to [@HUSEYNKHANLI](https://github.com/HUSEYNKHANLI) and [@drkim-dev](https://github.com/drkim-dev) for reporting!
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Dependency updates
|
||||
- Enhancements in the documentation at [docs.hedgedoc.org](https://docs.hedgedoc.org)
|
||||
|
||||
### Contributors
|
||||
- [xenein](https://github.com/xenein) (#6322)
|
||||
|
||||
## <i class="fa fa-tag"></i> 1.10.5 <i class="fa fa-calendar-o"></i> 2025-12-06
|
||||
|
||||
This release is just a fix for the docker container. It does not contain any
|
||||
|
||||
@@ -61,7 +61,6 @@ const options = {
|
||||
}
|
||||
const historyList = new List('history', options)
|
||||
|
||||
window.migrateHistoryFromTempCallback = pageInit
|
||||
setloginStateChangeEvent(pageInit)
|
||||
|
||||
pageInit()
|
||||
|
||||
@@ -11,9 +11,17 @@ import { escapeHtml, unescapeHtml } from './utils'
|
||||
|
||||
import markdownit from 'markdown-it'
|
||||
import markdownitContainer from 'markdown-it-container'
|
||||
|
||||
/* Defined regex markdown it plugins */
|
||||
import Plugin from 'markdown-it-regexp'
|
||||
import MarkdownItRegexpPlugin from 'markdown-it-regexp'
|
||||
import MarkdownItAbbrPlugin from 'markdown-it-abbr'
|
||||
import MarkdownItFootnotePlugin from 'markdown-it-footnote'
|
||||
import MarkdownItDeflistPlugin from 'markdown-it-deflist'
|
||||
import MarkdownItMarkPlugin from 'markdown-it-mark'
|
||||
import MarkdownItInsPlugin from 'markdown-it-ins'
|
||||
import MarkdownItSubPlugin from 'markdown-it-sub'
|
||||
import MarkdownItSupPlugin from 'markdown-it-sup'
|
||||
import MarkdownItMathjaxPlugin from 'markdown-it-mathjax'
|
||||
import { full as MarkdownItEmojiPlugin } from 'markdown-it-emoji'
|
||||
import MarkdownItImsizePlugin from 'markdown-it-imsize'
|
||||
|
||||
require('prismjs/themes/prism.css')
|
||||
require('prismjs/components/prism-wiki')
|
||||
@@ -517,7 +525,9 @@ export function finishView (view) {
|
||||
if (!languages.includes(reallang)) {
|
||||
result = hljs.highlightAuto(code)
|
||||
} else {
|
||||
result = hljs.highlight(reallang, code)
|
||||
result = hljs.highlight(code, {
|
||||
language: reallang
|
||||
})
|
||||
}
|
||||
if (codeDiv.length > 0) codeDiv.html(result.value)
|
||||
else langDiv.html(result.value)
|
||||
@@ -557,10 +567,6 @@ export function postProcess (code) {
|
||||
html = html.replace(/@import url\(([^)]*)\);?/gi, '')
|
||||
$(value).html(html)
|
||||
})
|
||||
// link should open in new window or tab
|
||||
// also add noopener to prevent clickjacking
|
||||
// See details: https://mathiasbynens.github.io/rel-noopener/
|
||||
result.find('a:not([href^="#"]):not([target])').attr('target', '_blank').attr('rel', 'noopener')
|
||||
|
||||
// If it's hashtag link then make it base uri independent
|
||||
result.find('a[href^="#"]').each((index, linkTag) => {
|
||||
@@ -568,6 +574,7 @@ export function postProcess (code) {
|
||||
currentLocation.hash = linkTag.hash
|
||||
linkTag.href = currentLocation.toString()
|
||||
})
|
||||
rewriteExternalLinks(result)
|
||||
|
||||
// update continue line numbers
|
||||
const linenumberdivs = result.find('.gutter.linenumber').toArray()
|
||||
@@ -593,6 +600,54 @@ export function postProcess (code) {
|
||||
}
|
||||
window.postProcess = postProcess
|
||||
|
||||
// rewrite external links to go through the /_link warning page
|
||||
export function rewriteExternalLinks (view) {
|
||||
if (window.externalLinkWarning === false) {
|
||||
view.find('a:not([href^="#"]):not([target])').attr('target', '_blank').attr('rel', 'noopener')
|
||||
return
|
||||
}
|
||||
const whitelist = window.externalLinkWhitelist || []
|
||||
view.find('a[href]').each((index, linkTag) => {
|
||||
const anchor = $(linkTag)
|
||||
const href = anchor.attr('href')
|
||||
if (!href || href.startsWith('#')) {
|
||||
return
|
||||
}
|
||||
let parsed
|
||||
try {
|
||||
parsed = new URL(href)
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
// only rewrite links that have an absolute http(s) URL on a different origin
|
||||
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin === window.location.origin) {
|
||||
return
|
||||
}
|
||||
// skip rewriting for whitelisted domains while differentiating between wildcard entries and plain host entries
|
||||
const hostname = parsed.hostname.toLowerCase()
|
||||
if (whitelist.some(domain => {
|
||||
const lowercaseDomain = domain.toLowerCase()
|
||||
if (lowercaseDomain.startsWith('*.')) {
|
||||
return hostname.endsWith('.' + lowercaseDomain.slice(2))
|
||||
}
|
||||
return hostname === lowercaseDomain
|
||||
})) {
|
||||
if (!anchor.attr('target')) {
|
||||
anchor.attr('target', '_blank').attr('rel', 'noopener')
|
||||
}
|
||||
return
|
||||
}
|
||||
const urlPath = window.urlpath ? `/${window.urlpath}` : ''
|
||||
const warningURL = `${window.location.origin}${urlPath}/_link?url=${encodeURIComponent(parsed.href)}`
|
||||
anchor.attr('href', warningURL)
|
||||
anchor.on('click', event => {
|
||||
event.preventDefault()
|
||||
window.open(warningURL, '_blank')
|
||||
})
|
||||
})
|
||||
}
|
||||
window.rewriteExternalLinks = rewriteExternalLinks
|
||||
|
||||
const domevents = Object.getOwnPropertyNames(document).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(document)))).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(window))).filter(function (i) {
|
||||
return !i.indexOf('on') && (document[i] === null || typeof document[i] === 'function')
|
||||
}).filter(function (elem, pos, self) {
|
||||
@@ -1014,14 +1069,14 @@ export const md = markdownit('default', {
|
||||
})
|
||||
window.md = md
|
||||
|
||||
md.use(require('markdown-it-abbr'))
|
||||
md.use(require('markdown-it-footnote'))
|
||||
md.use(require('markdown-it-deflist'))
|
||||
md.use(require('markdown-it-mark'))
|
||||
md.use(require('markdown-it-ins'))
|
||||
md.use(require('markdown-it-sub'))
|
||||
md.use(require('markdown-it-sup'))
|
||||
md.use(require('markdown-it-mathjax')({
|
||||
md.use(MarkdownItAbbrPlugin)
|
||||
md.use(MarkdownItFootnotePlugin)
|
||||
md.use(MarkdownItDeflistPlugin)
|
||||
md.use(MarkdownItMarkPlugin)
|
||||
md.use(MarkdownItInsPlugin)
|
||||
md.use(MarkdownItSubPlugin)
|
||||
md.use(MarkdownItSupPlugin)
|
||||
md.use(MarkdownItMathjaxPlugin({
|
||||
beforeMath: '<span class="mathjax raw">',
|
||||
afterMath: '</span>',
|
||||
beforeInlineMath: '<span class="mathjax raw">\\(',
|
||||
@@ -1029,9 +1084,9 @@ md.use(require('markdown-it-mathjax')({
|
||||
beforeDisplayMath: '<span class="mathjax raw">\\[',
|
||||
afterDisplayMath: '\\]</span>'
|
||||
}))
|
||||
md.use(require('markdown-it-imsize'))
|
||||
md.use(MarkdownItImsizePlugin)
|
||||
|
||||
md.use(require('markdown-it-emoji'), {
|
||||
md.use(MarkdownItEmojiPlugin, {
|
||||
shortcuts: {}
|
||||
})
|
||||
|
||||
@@ -1102,7 +1157,7 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
|
||||
}
|
||||
|
||||
// youtube
|
||||
const youtubePlugin = new Plugin(
|
||||
const youtubePlugin = new MarkdownItRegexpPlugin(
|
||||
// regexp to match
|
||||
/{%youtube\s*([\w-]{11})\s*%}/,
|
||||
|
||||
@@ -1120,7 +1175,7 @@ const youtubePlugin = new Plugin(
|
||||
}
|
||||
)
|
||||
// vimeo
|
||||
const vimeoPlugin = new Plugin(
|
||||
const vimeoPlugin = new MarkdownItRegexpPlugin(
|
||||
// regexp to match
|
||||
/{%vimeo\s*(\d{6,11})\s*%}/,
|
||||
|
||||
@@ -1135,7 +1190,7 @@ const vimeoPlugin = new Plugin(
|
||||
}
|
||||
)
|
||||
// gist
|
||||
const gistPlugin = new Plugin(
|
||||
const gistPlugin = new MarkdownItRegexpPlugin(
|
||||
// regexp to match
|
||||
/{%gist\s*(\w+\/\w+)\s*%}/,
|
||||
|
||||
@@ -1145,14 +1200,14 @@ const gistPlugin = new Plugin(
|
||||
}
|
||||
)
|
||||
// TOC
|
||||
const tocPlugin = new Plugin(
|
||||
const tocPlugin = new MarkdownItRegexpPlugin(
|
||||
// regexp to match
|
||||
/^\[TOC\]$/i,
|
||||
|
||||
(match, utils) => '<div class="toc"></div>'
|
||||
)
|
||||
// slideshare
|
||||
const slidesharePlugin = new Plugin(
|
||||
const slidesharePlugin = new MarkdownItRegexpPlugin(
|
||||
// regexp to match
|
||||
/{%slideshare\s*(\w+\/[\w-]+)\s*%}/,
|
||||
|
||||
@@ -1164,7 +1219,7 @@ const slidesharePlugin = new Plugin(
|
||||
}
|
||||
)
|
||||
// speakerdeck
|
||||
const speakerdeckPlugin = new Plugin(
|
||||
const speakerdeckPlugin = new MarkdownItRegexpPlugin(
|
||||
// regexp to match
|
||||
/{%speakerdeck\s*(\w+\/[\w-]+)\s*%}/,
|
||||
|
||||
@@ -1176,7 +1231,7 @@ const speakerdeckPlugin = new Plugin(
|
||||
}
|
||||
)
|
||||
// pdf
|
||||
const pdfPlugin = new Plugin(
|
||||
const pdfPlugin = new MarkdownItRegexpPlugin(
|
||||
// regexp to match
|
||||
/{%pdf\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
@@ -1189,7 +1244,7 @@ const pdfPlugin = new Plugin(
|
||||
}
|
||||
)
|
||||
|
||||
const emojijsPlugin = new Plugin(
|
||||
const emojijsPlugin = new MarkdownItRegexpPlugin(
|
||||
// regexp to match emoji shortcodes :something:
|
||||
// We generate an universal regex that guaranteed only contains the
|
||||
// emojies we have available. This should prevent all false-positives
|
||||
@@ -1226,7 +1281,11 @@ function meta (state, start, end, silent) {
|
||||
if (line >= end) return false
|
||||
|
||||
try {
|
||||
md.meta = window.jsyaml.safeLoad(data.join('\n')) || {}
|
||||
const parsed = window.jsyaml.load(data.join('\n')) || {}
|
||||
md.meta = window.ConstrainObject.constrainObject(parsed, {
|
||||
onCycle: 'omit',
|
||||
maxDepth: 4
|
||||
})
|
||||
delete md.metaError
|
||||
} catch (err) {
|
||||
md.metaError = err
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import store from 'store'
|
||||
import LZString from 'lz-string'
|
||||
import url from 'wurl'
|
||||
|
||||
import {
|
||||
checkNoteIdValid,
|
||||
@@ -19,40 +18,6 @@ import {
|
||||
urlpath
|
||||
} from './lib/config'
|
||||
|
||||
window.migrateHistoryFromTempCallback = null
|
||||
|
||||
migrateHistoryFromTemp()
|
||||
|
||||
function migrateHistoryFromTemp () {
|
||||
if (url('#tempid')) {
|
||||
$.get(`${serverurl}/temp`, {
|
||||
tempid: url('#tempid')
|
||||
})
|
||||
.done(data => {
|
||||
if (data && data.temp) {
|
||||
getStorageHistory(olddata => {
|
||||
if (!olddata || olddata.length === 0) {
|
||||
saveHistoryToStorage(JSON.parse(data.temp))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
let hash = location.hash.split('#')[1]
|
||||
hash = hash.split('&')
|
||||
for (let i = 0; i < hash.length; i++) {
|
||||
if (hash[i].indexOf('tempid') === 0) {
|
||||
hash.splice(i, 1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
hash = hash.join('&')
|
||||
location.hash = hash
|
||||
if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function saveHistory (notehistory) {
|
||||
checkIfAuth(
|
||||
() => {
|
||||
|
||||
@@ -10,7 +10,8 @@ require('prismjs/components/prism-typescript')
|
||||
require('prismjs/components/prism-jsx')
|
||||
require('prismjs/components/prism-makefile')
|
||||
require('prismjs/components/prism-gherkin')
|
||||
require('highlight.js/styles/github-gist.css')
|
||||
require('highlight.js/styles/github.css')
|
||||
require('../css/highlightjs.css')
|
||||
require('emojify.js/dist/css/basic/emojify.min.css')
|
||||
require('../css/github-extract.css')
|
||||
require('../css/markdown.css')
|
||||
|
||||
@@ -18,7 +18,6 @@ import hex2rgb from '../vendor/ot/hex2rgb'
|
||||
import { saveAs } from 'file-saver'
|
||||
import chance from 'chance'
|
||||
import store from 'store'
|
||||
import url from 'wurl'
|
||||
import { Spinner } from 'spin.js'
|
||||
|
||||
import _ from 'lodash'
|
||||
@@ -55,6 +54,7 @@ import {
|
||||
renderTOC,
|
||||
renderTags,
|
||||
renderTitle,
|
||||
rewriteExternalLinks,
|
||||
scrollToHash,
|
||||
smoothHashScroll,
|
||||
updateLastChange,
|
||||
@@ -92,7 +92,8 @@ require('../css/extra.css')
|
||||
require('../css/slide-preview.css')
|
||||
require('../css/site.css')
|
||||
|
||||
require('highlight.js/styles/github-gist.css')
|
||||
require('highlight.js/styles/github.css')
|
||||
require('../css/highlightjs.css')
|
||||
require('./fix-aria-hidden-for-modals')
|
||||
|
||||
let defaultTextHeight = 20
|
||||
@@ -427,7 +428,7 @@ const supportExtraTags = [
|
||||
text: '[random color tag]',
|
||||
search: '[]',
|
||||
command: function () {
|
||||
const color = chance().color()
|
||||
const color = chance().color({ format: 'hex' })
|
||||
return '[color=' + color + ']'
|
||||
}
|
||||
}
|
||||
@@ -1206,23 +1207,6 @@ ui.toolbar.publish.attr('href', noteurl + '/publish')
|
||||
// extra
|
||||
// slide
|
||||
ui.toolbar.extra.slide.attr('href', noteurl + '/slide')
|
||||
|
||||
// invite agent
|
||||
ui.toolbar.extra.inviteAgent.click(function (e) {
|
||||
e.preventDefault()
|
||||
|
||||
$.ajax({
|
||||
url: 'https://hedgeagent.sa6anw.se/add_note',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ note_id: noteid }),
|
||||
error: function (xhr, status, err) {
|
||||
console.error('Invite agent failed:', status, err)
|
||||
alert('Failed to invite agent.')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// download
|
||||
// markdown
|
||||
ui.toolbar.download.markdown.click(function (e) {
|
||||
@@ -1814,7 +1798,7 @@ $('#gistImportModalConfirm').click(function () {
|
||||
false
|
||||
)
|
||||
} else {
|
||||
const hostname = url('hostname', gisturl)
|
||||
const hostname = new URL(gisturl).hostname
|
||||
if (hostname !== 'gist.github.com') {
|
||||
showMessageModal(
|
||||
'<i class="fa fa-github"></i> Import from Gist',
|
||||
@@ -1825,7 +1809,8 @@ $('#gistImportModalConfirm').click(function () {
|
||||
)
|
||||
} else {
|
||||
ui.spinner.show()
|
||||
$.get('https://api.github.com/gists/' + url('-1', gisturl))
|
||||
const gistId = new URL(gisturl).pathname.split('/').pop()
|
||||
$.get('https://api.github.com/gists/' + gistId)
|
||||
.done(function (data) {
|
||||
if (data.files) {
|
||||
let contents = ''
|
||||
@@ -2249,7 +2234,6 @@ socket.on('delete', function () {
|
||||
})
|
||||
}
|
||||
})
|
||||
let retryTimer = null
|
||||
socket.on('maintenance', function () {
|
||||
cmClient.revision = -1
|
||||
})
|
||||
@@ -2262,21 +2246,17 @@ socket.on('disconnect', function (data) {
|
||||
if (!editor.getOption('readOnly')) {
|
||||
editor.setOption('readOnly', true)
|
||||
}
|
||||
if (!retryTimer) {
|
||||
retryTimer = setInterval(function () {
|
||||
if (!needRefresh) socket.connect()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
socket.on('reconnect', function (data) {
|
||||
// sync back any change in offline
|
||||
emitUserStatus(true)
|
||||
cursorActivity(editor)
|
||||
socket.emit('online users')
|
||||
})
|
||||
socket.on('connect', function (data) {
|
||||
clearInterval(retryTimer)
|
||||
retryTimer = null
|
||||
if (socket.recovered) {
|
||||
// sync back any change in offline
|
||||
console.debug('Reconnected client')
|
||||
emitUserStatus(true)
|
||||
cursorActivity(editor)
|
||||
socket.emit('online users')
|
||||
return
|
||||
}
|
||||
console.debug('Client connected (no reconnect)')
|
||||
personalInfo.id = socket.id
|
||||
showStatus(statusType.connected)
|
||||
socket.emit('version')
|
||||
@@ -3515,6 +3495,7 @@ function updateViewInner () {
|
||||
}
|
||||
removeDOMEvents(ui.area.markdown)
|
||||
finishView(ui.area.markdown)
|
||||
rewriteExternalLinks(ui.area.markdown)
|
||||
autoLinkify(ui.area.markdown)
|
||||
deduplicatedHeaderId(ui.area.markdown)
|
||||
renderTOC(ui.area.markdown)
|
||||
|
||||
@@ -8,6 +8,9 @@ window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %>
|
||||
|
||||
window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'
|
||||
|
||||
window.externalLinkWarning = <%- JSON.stringify(externalLinkWarning) %>
|
||||
window.externalLinkWhitelist = <%- JSON.stringify(externalLinkWhitelist) %>
|
||||
|
||||
window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>'
|
||||
|
||||
window.cookiePolicy = '<%- cookiePolicy %>'
|
||||
|
||||
@@ -197,15 +197,6 @@ export default class Editor {
|
||||
'Shift-Ctrl-6': cm => {
|
||||
utils.wrapTextWith(this.editor, cm, '^')
|
||||
},
|
||||
'Ctrl-+': cm => {
|
||||
utils.wrapTextWith(this.editor, cm, '+')
|
||||
},
|
||||
'Shift-Ctrl-=': cm => {
|
||||
utils.wrapTextWith(this.editor, cm, '+')
|
||||
},
|
||||
'Ctrl-=': cm => {
|
||||
utils.wrapTextWith(this.editor, cm, '=')
|
||||
},
|
||||
'Shift-Ctrl-Backspace': cm => {
|
||||
utils.wrapTextWith(this.editor, cm, 'Backspace')
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ export const getUIElements = () => ({
|
||||
publish: $('.ui-publish'),
|
||||
extra: {
|
||||
revision: $('.ui-extra-revision'),
|
||||
slide: $('.ui-extra-slide'),
|
||||
inviteAgent: $('.ui-invite-agent')
|
||||
slide: $('.ui-extra-slide')
|
||||
},
|
||||
download: {
|
||||
markdown: $('.ui-download-markdown'),
|
||||
|
||||
@@ -21,7 +21,8 @@ require('../css/extra.css')
|
||||
require('../css/slide-preview.css')
|
||||
require('../css/site.css')
|
||||
|
||||
require('highlight.js/styles/github-gist.css')
|
||||
require('highlight.js/styles/github.css')
|
||||
require('../css/highlightjs.css')
|
||||
|
||||
const markdown = $('#doc.markdown-body')
|
||||
const text = markdown.text()
|
||||
|
||||
@@ -18,22 +18,14 @@ whiteList.ol = ['start']
|
||||
whiteList.li = ['value']
|
||||
// allow style tag
|
||||
whiteList.style = []
|
||||
// allow kbd tag
|
||||
whiteList.kbd = []
|
||||
// allow ifram tag with some safe attributes
|
||||
whiteList.iframe = ['allowfullscreen', 'name', 'referrerpolicy', 'src', 'width', 'height']
|
||||
// allow summary tag
|
||||
whiteList.summary = []
|
||||
// allow ruby tag
|
||||
whiteList.ruby = []
|
||||
// allow rp tag for ruby
|
||||
whiteList.rp = []
|
||||
// allow rt tag for ruby
|
||||
whiteList.rt = []
|
||||
// allow figure tag
|
||||
whiteList.figure = []
|
||||
// allow figcaption tag
|
||||
whiteList.figcaption = []
|
||||
|
||||
const filterXSSOptions = {
|
||||
allowCommentTag: true,
|
||||
|
||||
40
public/vendor/ot/client.js
vendored
40
public/vendor/ot/client.js
vendored
@@ -219,19 +219,26 @@ ot.Client = (function (global) {
|
||||
this.acknowlaged = acknowlaged;
|
||||
this.client = client;
|
||||
this.revision = revision;
|
||||
this.pendingOps = [];
|
||||
}
|
||||
Client.Stale = Stale;
|
||||
|
||||
Stale.prototype.applyClient = function (client, operation) {
|
||||
return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
|
||||
var swb = new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
|
||||
swb.pendingOps = this.pendingOps;
|
||||
return swb;
|
||||
};
|
||||
|
||||
Stale.prototype.applyServer = function (client, revision, operation) {
|
||||
throw new Error("Ignored server-side change.");
|
||||
// Buffer incoming operations while waiting for the missing operations
|
||||
// response. These will be applied sequentially after the missing ops.
|
||||
this.pendingOps.push({ revision: revision, operation: operation });
|
||||
return this;
|
||||
};
|
||||
|
||||
Stale.prototype.applyOperations = function (client, head, operations) {
|
||||
var transform = this.acknowlaged.constructor.transform;
|
||||
// Apply the requested missing operations (concurrent with acknowlaged)
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
var op = ot.TextOperation.fromJSON(operations[i]);
|
||||
var pair = transform(this.acknowlaged, op);
|
||||
@@ -239,6 +246,14 @@ ot.Client = (function (global) {
|
||||
this.acknowlaged = pair[0];
|
||||
}
|
||||
client.revision = this.revision;
|
||||
// Apply any operations that arrived while we were waiting for the
|
||||
// missing operations. These are sequential (not concurrent), so they
|
||||
// can be applied directly like Synchronized.applyServer would.
|
||||
for (var j = 0; j < this.pendingOps.length; j++) {
|
||||
var pending = this.pendingOps[j];
|
||||
client.revision = pending.revision;
|
||||
client.applyOperation(pending.operation);
|
||||
}
|
||||
return synchronized_;
|
||||
};
|
||||
|
||||
@@ -261,20 +276,27 @@ ot.Client = (function (global) {
|
||||
this.buffer = buffer;
|
||||
this.client = client;
|
||||
this.revision = revision;
|
||||
this.pendingOps = [];
|
||||
}
|
||||
Client.StaleWithBuffer = StaleWithBuffer;
|
||||
|
||||
StaleWithBuffer.prototype.applyClient = function (client, operation) {
|
||||
var buffer = this.buffer.compose(operation);
|
||||
return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
|
||||
var swb = new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
|
||||
swb.pendingOps = this.pendingOps;
|
||||
return swb;
|
||||
};
|
||||
|
||||
StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
|
||||
throw new Error("Ignored server-side change.");
|
||||
// Buffer incoming operations while waiting for the missing operations
|
||||
// response. These will be applied sequentially after the missing ops.
|
||||
this.pendingOps.push({ revision: revision, operation: operation });
|
||||
return this;
|
||||
};
|
||||
|
||||
StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
|
||||
var transform = this.acknowlaged.constructor.transform;
|
||||
// Apply the requested missing operations (concurrent with acknowlaged and buffer)
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
var op = ot.TextOperation.fromJSON(operations[i]);
|
||||
var pair1 = transform(this.acknowlaged, op);
|
||||
@@ -284,6 +306,16 @@ ot.Client = (function (global) {
|
||||
this.buffer = pair2[0];
|
||||
}
|
||||
client.revision = this.revision;
|
||||
// Apply any operations that arrived while we were waiting for the
|
||||
// missing operations. These are sequential (not concurrent), so we
|
||||
// transform the buffer against them and apply them to the document.
|
||||
for (var j = 0; j < this.pendingOps.length; j++) {
|
||||
var pending = this.pendingOps[j];
|
||||
var pair3 = transform(this.buffer, pending.operation);
|
||||
client.applyOperation(pair3[1]);
|
||||
this.buffer = pair3[0];
|
||||
client.revision = pending.revision;
|
||||
}
|
||||
client.sendOperation(client.revision, this.buffer);
|
||||
return new AwaitingConfirm(this.buffer);
|
||||
};
|
||||
|
||||
5
public/vendor/ot/compress.sh
vendored
Normal file → Executable file
5
public/vendor/ot/compress.sh
vendored
Normal file → Executable file
@@ -1,4 +1,4 @@
|
||||
uglifyjs --compress --mangle --output ot.min.js \
|
||||
yarn uglifyjs --compress --mangle --output ot.min.js \
|
||||
./text-operation.js \
|
||||
./selection.js \
|
||||
./wrapped-operation.js \
|
||||
@@ -7,4 +7,5 @@ uglifyjs --compress --mangle --output ot.min.js \
|
||||
./codemirror-adapter.js \
|
||||
./socketio-adapter.js \
|
||||
./ajax-adapter.js \
|
||||
./editor-client.js
|
||||
./editor-client.js
|
||||
echo "Built new ot.min.js"
|
||||
|
||||
11
public/vendor/ot/hex2rgb.js
vendored
11
public/vendor/ot/hex2rgb.js
vendored
@@ -1,12 +1,19 @@
|
||||
const defaultColor = { red: 0, green: 0, blue: 0 }
|
||||
|
||||
function hex2rgb (hex) {
|
||||
if (!hex || typeof hex !== 'string') return defaultColor
|
||||
if (hex[0] == '#') hex = hex.substr(1)
|
||||
if (hex.length == 3) {
|
||||
var temp = hex
|
||||
hex = ''
|
||||
temp = /^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(temp).slice(1)
|
||||
var match = /^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(temp)
|
||||
if (!match) return defaultColor
|
||||
temp = match.slice(1)
|
||||
for (var i = 0; i < 3; i++) hex += temp[i] + temp[i]
|
||||
}
|
||||
var triplets = /^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex).slice(1)
|
||||
var result = /^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex)
|
||||
if (!result) return defaultColor
|
||||
var triplets = result.slice(1)
|
||||
return {
|
||||
red: parseInt(triplets[0], 16),
|
||||
green: parseInt(triplets[1], 16),
|
||||
|
||||
2
public/vendor/ot/ot.min.js
vendored
2
public/vendor/ot/ot.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -6,9 +6,9 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%- include('../includes/favicon') %>
|
||||
<% for (var og in opengraph) { %>
|
||||
<% if (opengraph.hasOwnProperty(og) && String(opengraph[og]).trim() !== '') { %>
|
||||
<% if (Object.keys(opengraph).includes(og) && typeof opengraph[og] === 'string' && opengraph[og].trim() !== '') { %>
|
||||
<meta property="og:<%= og %>" content="<%= String(opengraph[og]) %>">
|
||||
<% }} if (!opengraph.hasOwnProperty('image')) { %>
|
||||
<% }} if (!Object.keys(opengraph).includes('image')) { %>
|
||||
<meta property="og:image" content="<%- serverURL %>/icons/android-chrome-512x512.png">
|
||||
<meta property="og:image:alt" content="HedgeDoc logo">
|
||||
<meta property="og:image:type" content="image/png">
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
</li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank" rel="noopener"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a>
|
||||
</li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-invite-agent" tabindex="-1" href="#"><i class="fa fa-robot fa-fw"></i> <%= __('Invite Agent') %></a>
|
||||
</li>
|
||||
<% if(enableGitHubGist || enableDropBoxSave || enableGitlabSnippets) { %>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header"><%= __('Export') %></li>
|
||||
@@ -138,8 +136,6 @@
|
||||
</li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank" rel="noopener"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a>
|
||||
</li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-invite-agent" tabindex="-1" href="#"><i class="fa fa-robot fa-fw"></i> <%= __('Invite Agent') %></a>
|
||||
</li>
|
||||
<% if(enableGitHubGist || enableDropBoxSave || enableGitlabSnippets) { %>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header"><%= __('Export') %></li>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script src="config"></script>
|
||||
<script src="build/constrain-object.min.js"></script>
|
||||
<% for (var js in htmlWebpackPlugin.files.js) { %>
|
||||
<script src="<%= htmlWebpackPlugin.files.js[js] %>" defer></script>
|
||||
<% } %>
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
<% if (allowAnonymous && !disableNoteCreation) { %>
|
||||
<a type="button" href="<%- serverURL %>/new" class="btn btn-sm btn-primary"><i class="fa fa-plus"></i> <%= __('New guest note') %></a>
|
||||
<% } %>
|
||||
<% if (authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.saml || authProviders.oauth2 || authProviders.email) { %>
|
||||
<% if (authProviders.autoLoginProvider) { %>
|
||||
<a type="button" href="<%- serverURL %>/auth/<%- authProviders.autoLoginProvider %>" class="btn btn-sm btn-success ui-signin"><%= __('Sign In') %></a>
|
||||
<% } else if (authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.saml || authProviders.oauth2 || authProviders.email) { %>
|
||||
<button class="btn btn-sm btn-success ui-signin" data-toggle="modal" data-target=".signin-modal"><%= __('Sign In') %></button>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -52,7 +54,13 @@
|
||||
<% if (errorMessage && errorMessage.length > 0) { %>
|
||||
<div class="alert alert-danger" style="max-width: 400px; margin: 0 auto;"><%= errorMessage %></div>
|
||||
<% } %>
|
||||
<% if (authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.saml || authProviders.oauth2 || authProviders.email) { %>
|
||||
<% if (authProviders.autoLoginProvider) { %>
|
||||
<span class="ui-signin">
|
||||
<br>
|
||||
<a type="button" href="<%- serverURL %>/auth/<%- authProviders.autoLoginProvider %>" class="btn btn-lg btn-success ui-signin" style="min-width: 200px;"><%= __('Sign In') %></a>
|
||||
</span>
|
||||
<span class="ui-or"><%= __('or') %></span>
|
||||
<% } else if (authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.saml || authProviders.oauth2 || authProviders.email) { %>
|
||||
<span class="ui-signin">
|
||||
<br>
|
||||
<a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="min-width: 200px;"><%= __('Sign In') %></a>
|
||||
|
||||
44
public/views/link.ejs
Normal file
44
public/views/link.ejs
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<%- include('hedgedoc/head') %>
|
||||
<link rel="stylesheet" href="<%- serverURL %>/css/center.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('hedgedoc/header') %>
|
||||
<div class="container-fluid text-center">
|
||||
<div class="vertical-center-row">
|
||||
<h1>
|
||||
<% if (valid) { %>
|
||||
<%= __('External link') %>
|
||||
<% } else { %>
|
||||
<%= __('Invalid link') %>
|
||||
<% } %>
|
||||
</h1>
|
||||
<p>
|
||||
<%= __('You\'ve clicked a link that points to an external page.') %>
|
||||
<br>
|
||||
<%= __('The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there.') %>
|
||||
</p>
|
||||
<% if (valid) { %>
|
||||
<p><code class="text-break"><%- targetURL %></code></p>
|
||||
<p><%= __('Do you want to continue?') %></p>
|
||||
<p>
|
||||
<a class="btn btn-primary" href="<%- targetURL %>"><%= __('Continue to external page') %></a>
|
||||
<a class="btn btn-default" id="close-button"><%= __('Close') %></a>
|
||||
</p>
|
||||
<% } else { %>
|
||||
<p class="text-danger"><strong><%= __('The provided link is not a valid URL.') %></strong></p>
|
||||
<p>
|
||||
<a class="btn btn-default" id="close-button"><%= __('Close') %></a>
|
||||
</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script nonce="<%= cspNonce %>">
|
||||
document.getElementById("close-button").addEventListener('click', _ => window.close())
|
||||
</script>
|
||||
</html>
|
||||
@@ -15,9 +15,9 @@
|
||||
<meta name="description" content="<%= description %>">
|
||||
<% } %>
|
||||
<% for (var og in opengraph) { %>
|
||||
<% if (opengraph.hasOwnProperty(og) && opengraph[og].trim() !== '') { %>
|
||||
<% if (Object.keys(opengraph).includes(og) && typeof opengraph[og] === 'string' && opengraph[og].trim() !== '') { %>
|
||||
<meta property="og:<%= og %>" content="<%= opengraph[og] %>">
|
||||
<% }} if (!opengraph.hasOwnProperty('image')) { %>
|
||||
<% }} if (!Object.keys(opengraph).includes('image')) { %>
|
||||
<meta property="og:image" content="<%- serverURL %>/icons/android-chrome-512x512.png">
|
||||
<meta property="og:image:alt" content="HedgeDoc logo">
|
||||
<meta property="og:image:type" content="image/png">
|
||||
@@ -34,7 +34,7 @@
|
||||
<small>
|
||||
<span>
|
||||
<% if(lastchangeuserprofile) { %>
|
||||
<span class="ui-lastchangeuser"> <i class="ui-user-icon small" style="background-image: url(<%- lastchangeuserprofile.photo %>);" data-toggle="tooltip" data-placement="right" title="<%- lastchangeuserprofile.name %>"></i></span>
|
||||
<span class="ui-lastchangeuser"> <i class="ui-user-icon small" style="background-image: url(<%- lastchangeuserprofile.photo %>);" data-toggle="tooltip" data-placement="right" title="<%= lastchangeuserprofile.name %>"></i></span>
|
||||
<% } else { %>
|
||||
<span class="ui-no-lastchangeuser"> <i class="fa fa-clock-o fa-fw" style="width: 18px;"></i></span>
|
||||
<% } %>
|
||||
@@ -45,7 +45,7 @@
|
||||
<br>
|
||||
<% if(ownerprofile && owner !== lastchangeuser) { %>
|
||||
<span class="ui-owner">
|
||||
 <i class="ui-user-icon small" style="background-image: url(<%- ownerprofile.photo %>);" data-toggle="tooltip" data-placement="right" title="<%- ownerprofile.name %>"></i>
|
||||
 <i class="ui-user-icon small" style="background-image: url(<%- ownerprofile.photo %>);" data-toggle="tooltip" data-placement="right" title="<%= ownerprofile.name %>"></i>
|
||||
<span class="text-uppercase">owned this note</span>
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
138
test/note.js
Normal file
138
test/note.js
Normal file
@@ -0,0 +1,138 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
|
||||
const models = require('../lib/models')
|
||||
const Note = models.Note
|
||||
|
||||
describe('Note.extractMeta', function () {
|
||||
it('parses standard frontmatter with title and description', function () {
|
||||
const content = '---\ntitle: My Note\ndescription: A test note\n---\n# Hello World'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.strictEqual(result.meta.title, 'My Note')
|
||||
assert.strictEqual(result.meta.description, 'A test note')
|
||||
assert.strictEqual(result.markdown, '\n# Hello World')
|
||||
})
|
||||
|
||||
it('parses frontmatter with tags as array', function () {
|
||||
const content = '---\ntitle: Tagged\ntags:\n - foo\n - bar\n - baz\n---\nBody'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.strictEqual(result.meta.title, 'Tagged')
|
||||
assert.deepStrictEqual(result.meta.tags, ['foo', 'bar', 'baz'])
|
||||
})
|
||||
|
||||
it('parses frontmatter with tags as string', function () {
|
||||
const content = '---\ntags: foo, bar, baz\n---\nBody'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.strictEqual(result.meta.tags, 'foo, bar, baz')
|
||||
})
|
||||
|
||||
it('parses frontmatter with opengraph object', function () {
|
||||
const content = '---\nopengraph:\n title: OG Title\n description: OG Description\n image: https://example.com/img.png\n---\nBody'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.strictEqual(result.meta.opengraph.title, 'OG Title')
|
||||
assert.strictEqual(result.meta.opengraph.description, 'OG Description')
|
||||
assert.strictEqual(result.meta.opengraph.image, 'https://example.com/img.png')
|
||||
})
|
||||
|
||||
it('parses frontmatter with slideOptions', function () {
|
||||
const content = '---\nslideOptions:\n theme: moon\n transition: slide\n---\n# Slide 1'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.strictEqual(result.meta.slideOptions.theme, 'moon')
|
||||
assert.strictEqual(result.meta.slideOptions.transition, 'slide')
|
||||
})
|
||||
|
||||
it('parses frontmatter with boolean and numeric values', function () {
|
||||
const content = '---\nbreaks: false\nGA: UA-12345\nlang: en\n---\nBody'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.strictEqual(result.meta.breaks, false)
|
||||
assert.strictEqual(result.meta.GA, 'UA-12345')
|
||||
assert.strictEqual(result.meta.lang, 'en')
|
||||
})
|
||||
|
||||
it('returns empty meta and full content when no frontmatter present', function () {
|
||||
const content = '# Just a heading\n\nSome text here.'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.deepStrictEqual(result.meta, {})
|
||||
assert.strictEqual(result.markdown, content)
|
||||
})
|
||||
|
||||
it('returns empty meta for empty frontmatter', function () {
|
||||
const content = '---\n---\nBody text'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.deepStrictEqual(result.meta, {})
|
||||
assert.strictEqual(result.markdown, '\nBody text')
|
||||
})
|
||||
|
||||
it('handles frontmatter closed with ...', function () {
|
||||
const content = '---\ntitle: Dot Delimiter\n...\nBody'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.strictEqual(result.meta.title, 'Dot Delimiter')
|
||||
assert.strictEqual(result.markdown, '\nBody')
|
||||
})
|
||||
|
||||
it('returns empty meta for malformed YAML', function () {
|
||||
const content = '---\ntitle: [invalid yaml\n---\nBody'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.deepStrictEqual(result.meta, {})
|
||||
assert.strictEqual(result.markdown, content)
|
||||
})
|
||||
|
||||
it('handles empty content', function () {
|
||||
const result = Note.extractMeta('')
|
||||
assert.deepStrictEqual(result.meta, {})
|
||||
assert.strictEqual(result.markdown, '')
|
||||
})
|
||||
|
||||
it('handles content that starts with --- but has no closing delimiter', function () {
|
||||
const content = '---\ntitle: No closing\nNo delimiter here'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.deepStrictEqual(result.meta, {})
|
||||
assert.strictEqual(result.markdown, content)
|
||||
})
|
||||
|
||||
it('neutralizes YAML alias bomb (10 levels of 10-way branching)', function () {
|
||||
const content = [
|
||||
'---',
|
||||
'a0: &a0 1',
|
||||
'a1: &a1 [*a0,*a0,*a0,*a0,*a0,*a0,*a0,*a0,*a0,*a0]',
|
||||
'a2: &a2 [*a1,*a1,*a1,*a1,*a1,*a1,*a1,*a1,*a1,*a1]',
|
||||
'a3: &a3 [*a2,*a2,*a2,*a2,*a2,*a2,*a2,*a2,*a2,*a2]',
|
||||
'a4: &a4 [*a3,*a3,*a3,*a3,*a3,*a3,*a3,*a3,*a3,*a3]',
|
||||
'a5: &a5 [*a4,*a4,*a4,*a4,*a4,*a4,*a4,*a4,*a4,*a4]',
|
||||
'a6: &a6 [*a5,*a5,*a5,*a5,*a5,*a5,*a5,*a5,*a5,*a5]',
|
||||
'a7: &a7 [*a6,*a6,*a6,*a6,*a6,*a6,*a6,*a6,*a6,*a6]',
|
||||
'a8: &a8 [*a7,*a7,*a7,*a7,*a7,*a7,*a7,*a7,*a7,*a7]',
|
||||
'a9: &a9 [*a8,*a8,*a8,*a8,*a8,*a8,*a8,*a8,*a8,*a8]',
|
||||
'a10: &a10 [*a9,*a9,*a9,*a9,*a9,*a9,*a9,*a9,*a9,*a9]',
|
||||
'opengraph: *a10',
|
||||
'---',
|
||||
'x'
|
||||
].join('\n')
|
||||
|
||||
const start = Date.now()
|
||||
const result = Note.extractMeta(content)
|
||||
const elapsed = Date.now() - start
|
||||
|
||||
assert.ok(elapsed < 1000, `extractMeta took ${elapsed}ms, should complete in under 1000ms`)
|
||||
assert.ok(result.meta !== null, 'meta should not be null')
|
||||
assert.ok(typeof result.meta === 'object', 'meta should be an object')
|
||||
assert.strictEqual(result.markdown, '\nx')
|
||||
|
||||
const json = JSON.stringify(result.meta)
|
||||
assert.ok(json.length < 100000, `stringified meta should be small, got ${json.length} bytes`)
|
||||
})
|
||||
|
||||
it('truncates excessively long string values', function () {
|
||||
const longString = 'a'.repeat(20000)
|
||||
const content = `---\ntitle: ${longString}\n---\nBody`
|
||||
const result = Note.extractMeta(content)
|
||||
assert.ok(result.meta.title.length <= 10000, `title should be truncated, got ${result.meta.title.length}`)
|
||||
})
|
||||
|
||||
it('preserves normal nested objects within depth limit', function () {
|
||||
const content = '---\nlevel1:\n level2:\n level3: deep value\n---\nBody'
|
||||
const result = Note.extractMeta(content)
|
||||
assert.strictEqual(result.meta.level1.level2.level3, 'deep value')
|
||||
})
|
||||
})
|
||||
@@ -137,6 +137,11 @@ module.exports = {
|
||||
context: path.join(__dirname, 'node_modules/reveal.js'),
|
||||
from: 'plugin',
|
||||
to: 'reveal.js/plugin'
|
||||
},
|
||||
{
|
||||
context: path.join(__dirname, 'node_modules/constrain-object/dist'),
|
||||
from: 'index.iife.js',
|
||||
to: 'constrain-object.min.js'
|
||||
}
|
||||
]
|
||||
}),
|
||||
@@ -301,11 +306,16 @@ module.exports = {
|
||||
'socket.io-client': 'io',
|
||||
'jquery': '$',
|
||||
'moment': 'moment',
|
||||
'select2': 'select2'
|
||||
'select2': 'select2',
|
||||
'constrain-object': 'ConstrainObject'
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
type: 'javascript/auto'
|
||||
},
|
||||
{
|
||||
test: require.resolve(path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.js')),
|
||||
use: {
|
||||
|
||||
@@ -18,7 +18,7 @@ module.exports = [
|
||||
new EsbuildPlugin({
|
||||
target: 'es2015',
|
||||
format: 'cjs',
|
||||
exclude: ['MathJax/extensions/a11y/mathmaps', 'reveal.js/plugin/markdown/marked.js']
|
||||
exclude: ['MathJax/extensions/a11y/mathmaps', 'reveal.js/plugin/markdown/marked.js', 'constrain-object.min.js']
|
||||
})
|
||||
],
|
||||
splitChunks: {
|
||||
|
||||
Reference in New Issue
Block a user