Merge commit 'c6e2cefa8f6f2075f356b25f98b69004a3442929' into LailaTheElf
This commit is contained in:
4
.github/actions/setup-node/action.yml
vendored
4
.github/actions/setup-node/action.yml
vendored
@@ -19,14 +19,14 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Cache yarn cache
|
- name: Cache yarn cache
|
||||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
key: ${{ runner.os }}-yarn-master
|
key: ${{ runner.os }}-yarn-master
|
||||||
|
|
||||||
- name: Set up NodeJS
|
- name: Set up NodeJS
|
||||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.NODEJS_VERSION }}
|
node-version: ${{ inputs.NODEJS_VERSION }}
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/build-and-test.yml
vendored
4
.github/workflows/build-and-test.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
name: Node ${{ matrix.node }}
|
name: Node ${{ matrix.node }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
run: yarn run mocha-suite
|
run: yarn run mocha-suite
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
if: github.ref == 'refs/heads/master' && matrix.node-version == '22'
|
if: github.ref == 'refs/heads/master' && matrix.node-version == '22'
|
||||||
with:
|
with:
|
||||||
name: Prebuild with Node.js ${{ matrix.node-version }}
|
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
|
name: Lint files
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|||||||
942
.yarn/releases/yarn-4.12.0.cjs
vendored
Executable file
942
.yarn/releases/yarn-4.12.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
934
.yarn/releases/yarn-4.6.0.cjs
vendored
934
.yarn/releases/yarn-4.6.0.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -4,4 +4,4 @@ enableGlobalCache: false
|
|||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.6.0.cjs
|
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ RUN rm -rf /hedgedoc/.git/*
|
|||||||
|
|
||||||
# Install app dependencies and build
|
# Install app dependencies and build
|
||||||
WORKDIR /hedgedoc
|
WORKDIR /hedgedoc
|
||||||
RUN --mount=type=cache,sharing=locked,target=/tmp/.yarn yarn install --immutable
|
RUN --mount=type=cache,sharing=locked,target=/tmp/.yarn yarn install
|
||||||
RUN yarn run build
|
RUN yarn run build
|
||||||
|
|
||||||
FROM docker.io/library/node:20.17.0-alpine@sha256:2d07db07a2df6830718ae2a47db6fedce6745f5bcd174c398f2acdda90a11c03 AS modules-installer
|
FROM docker.io/library/node:20.17.0-alpine@sha256:2d07db07a2df6830718ae2a47db6fedce6745f5bcd174c398f2acdda90a11c03 AS modules-installer
|
||||||
|
|||||||
47
app.js
47
app.js
@@ -12,6 +12,7 @@ const session = require('express-session')
|
|||||||
const SequelizeStore = require('connect-session-sequelize')(session.Store)
|
const SequelizeStore = require('connect-session-sequelize')(session.Store)
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const { Server } = require('socket.io')
|
||||||
|
|
||||||
const morgan = require('morgan')
|
const morgan = require('morgan')
|
||||||
const passportSocketIo = require('passport.socketio')
|
const passportSocketIo = require('passport.socketio')
|
||||||
@@ -81,7 +82,16 @@ if (config.enableStatsApi) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// socket io
|
// socket io
|
||||||
const io = require('socket.io')(server, { cookie: false })
|
const io = new Server(server, {
|
||||||
|
pingInterval: config.heartbeatInterval,
|
||||||
|
pingTimeout: config.heartbeatTimeout,
|
||||||
|
cookie: false,
|
||||||
|
cors: {
|
||||||
|
origin: config.serverURL,
|
||||||
|
methods: ['GET', 'POST'],
|
||||||
|
credentials: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// others
|
// others
|
||||||
const realtime = require('./lib/realtime.js')
|
const realtime = require('./lib/realtime.js')
|
||||||
@@ -147,10 +157,29 @@ app.use(i18n.init)
|
|||||||
|
|
||||||
// routes without sessions
|
// routes without sessions
|
||||||
// static files
|
// static files
|
||||||
app.use('/', express.static(path.join(__dirname, '/public'), { maxAge: config.staticCacheTime, index: false, redirect: false }))
|
app.use('/', express.static(path.join(__dirname, '/public'), {
|
||||||
app.use('/docs', express.static(path.resolve(__dirname, config.docsPath), { maxAge: config.staticCacheTime, redirect: false }))
|
maxAge: config.staticCacheTime,
|
||||||
app.use('/uploads', express.static(path.resolve(__dirname, config.uploadsPath), { maxAge: config.staticCacheTime, redirect: false }))
|
index: false,
|
||||||
app.use('/default.md', express.static(path.resolve(__dirname, config.defaultNotePath), { maxAge: config.staticCacheTime }))
|
redirect: false
|
||||||
|
}))
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
app.use('/default.md', express.static(path.resolve(__dirname, config.defaultNotePath), {
|
||||||
|
maxAge: config.staticCacheTime
|
||||||
|
}))
|
||||||
|
|
||||||
// session
|
// session
|
||||||
app.use(useUnless(['/status', '/metrics', '/_health'], session({
|
app.use(useUnless(['/status', '/metrics', '/_health'], session({
|
||||||
@@ -253,9 +282,6 @@ io.use(passportSocketIo.authorize({
|
|||||||
success: realtime.onAuthorizeSuccess,
|
success: realtime.onAuthorizeSuccess,
|
||||||
fail: realtime.onAuthorizeFail
|
fail: realtime.onAuthorizeFail
|
||||||
}))
|
}))
|
||||||
// socket.io heartbeat
|
|
||||||
io.set('heartbeat interval', config.heartbeatInterval)
|
|
||||||
io.set('heartbeat timeout', config.heartbeatTimeout)
|
|
||||||
// socket.io connection
|
// socket.io connection
|
||||||
io.sockets.on('connection', realtime.connection)
|
io.sockets.on('connection', realtime.connection)
|
||||||
|
|
||||||
@@ -330,8 +356,9 @@ function handleTermSignals () {
|
|||||||
alreadyHandlingTermSignals = true
|
alreadyHandlingTermSignals = true
|
||||||
realtime.maintenance = true
|
realtime.maintenance = true
|
||||||
// disconnect all socket.io clients
|
// disconnect all socket.io clients
|
||||||
Object.keys(io.sockets.sockets).forEach(function (key) {
|
Array.from(io.sockets.sockets.keys()).forEach(function (key) {
|
||||||
const socket = io.sockets.sockets[key]
|
const socket = io.sockets.sockets.get(key)
|
||||||
|
if (!socket) return
|
||||||
// notify client server going into maintenance status
|
// notify client server going into maintenance status
|
||||||
socket.emit('maintenance')
|
socket.emit('maintenance')
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
|
|||||||
@@ -95,6 +95,8 @@
|
|||||||
"issuer": "change or delete: identity of the service provider (default: config.serverURL)",
|
"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')",
|
"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)",
|
"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)",
|
"groupAttribute": "change or delete: attribute name for group list (ex: memberOf)",
|
||||||
"requiredGroups": [ "change or delete: group names that allowed" ],
|
"requiredGroups": [ "change or delete: group names that allowed" ],
|
||||||
"externalGroups": [ "change or delete: group names that not allowed" ],
|
"externalGroups": [ "change or delete: group names that not allowed" ],
|
||||||
|
|||||||
@@ -2,19 +2,30 @@
|
|||||||
|
|
||||||
You can choose to configure HedgeDoc with either a config file or with environment variables.
|
You can choose to configure HedgeDoc with either a config file or with environment variables.
|
||||||
|
|
||||||
Environment variables take precedence over configurations from the config files. They generally start with `CMD_` for our own options, but we also list node-specific options you can configure this way.
|
Environment variables take precedence over configurations from the config files. They generally start with `CMD_` for
|
||||||
|
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/default.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/default.js), in case you wonder if you even need to override it.
|
- 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/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/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
|
- The config file is processed in [
|
||||||
to `config.json` before filling in your own details.
|
`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/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
|
||||||
|
to `config.json` before filling in your own details.
|
||||||
|
|
||||||
**Note:** *Due to the rename process we renamed all `HMD_`-prefix variables to be `CMD_`-prefixed. The old ones continue to work.*
|
**Note:** *Due to the rename process we renamed all `HMD_`-prefix variables to be `CMD_`-prefixed. The old ones continue
|
||||||
|
to work.*
|
||||||
|
|
||||||
## Node.JS
|
## Node.JS
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | ----------- | ----------------------------- | -------------------------------------------------------------------------------- |
|
|-------------|-------------|-------------------------------|----------------------------------------------------------------------------------|
|
||||||
| | `NODE_ENV` | `production` or `development` | set current environment (will apply corresponding settings in the `config.json`) |
|
| | `NODE_ENV` | `production` or `development` | set current environment (will apply corresponding settings in the `config.json`) |
|
||||||
| `debug` | `DEBUG` | `true` or `false` | set debug mode, show more logs |
|
| `debug` | `DEBUG` | `true` or `false` | set debug mode, show more logs |
|
||||||
|
|
||||||
@@ -30,7 +41,7 @@ to `config.json` before filling in your own details.
|
|||||||
| `db.host` | `CMD_DB_HOST` | **`undefined`**, `db-host.example.com` | Hostname used to connect the database server. |
|
| `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.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. |
|
| `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` or `mysql://username:password@localhost:3306/hedgedoc` | Set the db in URL style. If set, then the relevant `db` config entries will be overridden. |
|
| `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` |
|
| `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` |
|
| `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. |
|
| `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. |
|
||||||
@@ -48,7 +59,7 @@ to `config.json` before filling in your own details.
|
|||||||
these are rarely used for various reasons.
|
these are rarely used for various reasons.
|
||||||
|
|
||||||
| config file | environment | **default** and example values | description |
|
| config file | environment | **default** and example values | description |
|
||||||
| ----------------- | ----------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|-------------------|-------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------|
|
||||||
| `defaultNotePath` | | **`./public/default.md`** | default note file path<sup>1</sup>, empty notes will be created with this template. |
|
| `defaultNotePath` | | **`./public/default.md`** | default note file path<sup>1</sup>, empty notes will be created with this template. |
|
||||||
| `dhParamPath` | | **`undefined`**, `./cert/dhparam.pem` | SSL dhparam path<sup>1</sup> (only need when you set `useSSL`) |
|
| `dhParamPath` | | **`undefined`**, `./cert/dhparam.pem` | SSL dhparam path<sup>1</sup> (only need when you set `useSSL`) |
|
||||||
| `sslCAPath` | | **`undefined`**, `['./cert/COMODORSAAddTrustCA.crt']` | SSL ca chain<sup>1</sup> (only need when you set `useSSL`) |
|
| `sslCAPath` | | **`undefined`**, `['./cert/COMODORSAAddTrustCA.crt']` | SSL ca chain<sup>1</sup> (only need when you set `useSSL`) |
|
||||||
@@ -64,7 +75,7 @@ these are rarely used for various reasons.
|
|||||||
## HedgeDoc Location
|
## HedgeDoc Location
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ---------------- | --------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|------------------|-----------------------|--------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
|
||||||
| `domain` | `CMD_DOMAIN` | **`null`**, `localhost`, `hedgedoc.org` | domain name |
|
| `domain` | `CMD_DOMAIN` | **`null`**, `localhost`, `hedgedoc.org` | domain name |
|
||||||
| `urlPath` | `CMD_URL_PATH` | **`null`**, `hedgedoc` | If HedgeDoc is run from a subdirectory like `www.example.com/<urlpath>` |
|
| `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 |
|
| `host` | `CMD_HOST` | **`0.0.0.0`**, `localhost` | interface/ip to listen on |
|
||||||
@@ -100,19 +111,20 @@ these are rarely used for various reasons.
|
|||||||
## Privacy and External Requests
|
## Privacy and External Requests
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| --------------- | -------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|-----------------|----------------------|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `allowGravatar` | `CMD_ALLOW_GRAVATAR` | **`true`** or `false` | set to `false` to disable [Libravatar](https://www.libravatar.org/) as profile picture source on your instance. Libravatar is a federated open-source alternative to Gravatar. |
|
| `allowGravatar` | `CMD_ALLOW_GRAVATAR` | **`true`** or `false` | set to `false` to disable [Libravatar](https://www.libravatar.org/) as profile picture source on your instance. Libravatar is a federated open-source alternative to Gravatar. |
|
||||||
|
|
||||||
## Users and Privileges
|
## Users and Privileges
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
|--------------------------------|--------------------------------------|-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|--------------------------------|--------------------------------------|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `allowAnonymous` | `CMD_ALLOW_ANONYMOUS` | **`true`** or `false` | Set to allow anonymous usage (default is `true`). |
|
| `allowAnonymous` | `CMD_ALLOW_ANONYMOUS` | **`true`** or `false` | Set to allow anonymous usage (default is `true`). |
|
||||||
| `allowAnonymousEdits` | `CMD_ALLOW_ANONYMOUS_EDITS` | **`false`** or `true` | If `allowAnonymous` is `false`: allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`). |
|
| `allowAnonymousEdits` | `CMD_ALLOW_ANONYMOUS_EDITS` | **`false`** or `true` | If `allowAnonymous` is `false`: allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`). |
|
||||||
| `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). |
|
| `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. |
|
| `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. |
|
| `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). |
|
||||||
|
| `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. |
|
| `sessionName` | | **`connect.sid`** | Cookie session name. |
|
||||||
| `sessionLife` | `CMD_SESSION_LIFE` | **`14 * 24 * 60 * 60 * 1000`**, `1209600000` (14 days) | Cookie session life time in milliseconds. |
|
| `sessionLife` | `CMD_SESSION_LIFE` | **`14 * 24 * 60 * 60 * 1000`**, `1209600000` (14 days) | Cookie session life time in milliseconds. |
|
||||||
| `sessionSecret` | `CMD_SESSION_SECRET` | **`secret`** | Cookie session secret used to sign the session cookie. If none is set, one will randomly generated on each startup, meaning all your users will be logged out. Can be generated with e.g. `pwgen -s 64 1`. |
|
| `sessionSecret` | `CMD_SESSION_SECRET` | **`secret`** | Cookie session secret used to sign the session cookie. If none is set, one will randomly generated on each startup, meaning all your users will be logged out. Can be generated with e.g. `pwgen -s 64 1`. |
|
||||||
@@ -122,14 +134,14 @@ these are rarely used for various reasons.
|
|||||||
### Email (local account)
|
### Email (local account)
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| -------------------- | -------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|----------------------|----------------------------|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `email` | `CMD_EMAIL` | **`true`** or `false` | Set to allow email sign-in. The default is `true`. |
|
| `email` | `CMD_EMAIL` | **`true`** or `false` | Set to allow email sign-in. The default is `true`. |
|
||||||
| `allowEmailRegister` | `CMD_ALLOW_EMAIL_REGISTER` | **`true`** or `false` | Set to allow registration of new accounts using an email address. If set to `false`, you can still create accounts using the command line - see `bin/manage_users` for details (In production mode, remember to run it with `NODE_ENV` set as `production` in the enviroment). This setting has no effect if `email` or `CMD_EMAIL` is `false`. The default for `allowEmailRegister` or `CMD_ALLOW_EMAIL_REGISTER` is `true`. |
|
| `allowEmailRegister` | `CMD_ALLOW_EMAIL_REGISTER` | **`true`** or `false` | Set to allow registration of new accounts using an email address. If set to `false`, you can still create accounts using the command line - see `bin/manage_users` for details (In production mode, remember to run it with `NODE_ENV` set as `production` in the enviroment). This setting has no effect if `email` or `CMD_EMAIL` is `false`. The default for `allowEmailRegister` or `CMD_ALLOW_EMAIL_REGISTER` is `true`. |
|
||||||
|
|
||||||
### Dropbox Login
|
### Dropbox Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | -------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|-------------|----------------------------|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `dropbox` | | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret obtained by the [Dropbox developer tools](https://www.dropbox.com/developers/apps) |
|
| `dropbox` | | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret obtained by the [Dropbox developer tools](https://www.dropbox.com/developers/apps) |
|
||||||
| | `CMD_DROPBOX_CLIENTID` | **no default** | Dropbox API client id |
|
| | `CMD_DROPBOX_CLIENTID` | **no default** | Dropbox API client id |
|
||||||
| | `CMD_DROPBOX_CLIENTSECRET` | **no default** | Dropbox API client secret |
|
| | `CMD_DROPBOX_CLIENTSECRET` | **no default** | Dropbox API client secret |
|
||||||
@@ -137,7 +149,7 @@ these are rarely used for various reasons.
|
|||||||
### Facebook Login
|
### Facebook Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | --------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|-------------|-----------------------------|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `facebook` | | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret obtained by the [Facebook app console](https://developers.facebook.com/apps) |
|
| `facebook` | | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret obtained by the [Facebook app console](https://developers.facebook.com/apps) |
|
||||||
| | `CMD_FACEBOOK_CLIENTID` | **no default** | Facebook API client id |
|
| | `CMD_FACEBOOK_CLIENTID` | **no default** | Facebook API client id |
|
||||||
| | `CMD_FACEBOOK_CLIENTSECRET` | **no default** | Facebook API client secret |
|
| | `CMD_FACEBOOK_CLIENTSECRET` | **no default** | Facebook API client secret |
|
||||||
@@ -145,7 +157,7 @@ these are rarely used for various reasons.
|
|||||||
### GitHub Login
|
### GitHub Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | ------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|-------------|---------------------------|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `github` | | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret obtained by the GitHub developer page. For more details have a look at the [GitHub auth guide](guides/auth/github.md). |
|
| `github` | | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret obtained by the GitHub developer page. For more details have a look at the [GitHub auth guide](guides/auth/github.md). |
|
||||||
| | `CMD_GITHUB_CLIENTID` | **no default** | GitHub API client id |
|
| | `CMD_GITHUB_CLIENTID` | **no default** | GitHub API client id |
|
||||||
| | `CMD_GITHUB_CLIENTSECRET` | **no default** | GitHub API client secret |
|
| | `CMD_GITHUB_CLIENTSECRET` | **no default** | GitHub API client secret |
|
||||||
@@ -153,7 +165,7 @@ these are rarely used for various reasons.
|
|||||||
### GitLab Login
|
### GitLab Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | ------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|-------------|---------------------------|------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `gitlab` | | `{baseURL: ..., scope: ..., version: ..., clientID: ..., clientSecret: ...}` | An object containing your GitLab application data. Refer to the [GitLab guide](guides/auth/gitlab-self-hosted.md) for more details! |
|
| `gitlab` | | `{baseURL: ..., scope: ..., version: ..., clientID: ..., clientSecret: ...}` | An object containing your GitLab application data. Refer to the [GitLab guide](guides/auth/gitlab-self-hosted.md) for more details! |
|
||||||
| | `CMD_GITLAB_SCOPE` | **no default**, `read_user` or `api` | GitLab API requested scope (default is `api`) (GitLab snippet import/export need `api` scope) |
|
| | `CMD_GITLAB_SCOPE` | **no default**, `read_user` or `api` | GitLab API requested scope (default is `api`) (GitLab snippet import/export need `api` scope) |
|
||||||
| | `CMD_GITLAB_BASEURL` | **no default** | GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional) |
|
| | `CMD_GITLAB_BASEURL` | **no default** | GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional) |
|
||||||
@@ -164,7 +176,7 @@ these are rarely used for various reasons.
|
|||||||
### Google Login
|
### Google Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | ------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|-------------|---------------------------|---------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `google` | | `{clientID: ..., clientSecret: ..., hostedDomain: ...}` | An object containing the client ID and the client secret obtained by the [Google API console](https://console.cloud.google.com/apis) |
|
| `google` | | `{clientID: ..., clientSecret: ..., hostedDomain: ...}` | An object containing the client ID and the client secret obtained by the [Google API console](https://console.cloud.google.com/apis) |
|
||||||
| | `CMD_GOOGLE_CLIENTID` | **no default** | Google API client id |
|
| | `CMD_GOOGLE_CLIENTID` | **no default** | Google API client id |
|
||||||
| | `CMD_GOOGLE_CLIENTSECRET` | **no default** | Google API client secret |
|
| | `CMD_GOOGLE_CLIENTSECRET` | **no default** | Google API client secret |
|
||||||
@@ -173,7 +185,7 @@ these are rarely used for various reasons.
|
|||||||
### LDAP Login
|
### LDAP Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|-------------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
|
||||||
| `ldap` | | `{providerName: ..., url: ..., bindDn: ..., bindCredentials: ..., searchBase: ..., searchFilter: ..., searchAttributes: ..., usernameField: ..., useridField: ..., tlsca: ...}` | An object detailing the LDAP connection. Refer to the [LDAP-AD guide](guides/auth/ldap-ad.md) for more details! |
|
| `ldap` | | `{providerName: ..., url: ..., bindDn: ..., bindCredentials: ..., searchBase: ..., searchFilter: ..., searchAttributes: ..., usernameField: ..., useridField: ..., tlsca: ...}` | An object detailing the LDAP connection. Refer to the [LDAP-AD guide](guides/auth/ldap-ad.md) for more details! |
|
||||||
| | `CMD_LDAP_URL` | **no default**, `ldap://example.com` | URL of LDAP server |
|
| | `CMD_LDAP_URL` | **no default**, `ldap://example.com` | URL of LDAP server |
|
||||||
| | `CMD_LDAP_BINDDN` | **no default** | bindDn for LDAP access |
|
| | `CMD_LDAP_BINDDN` | **no default** | bindDn for LDAP access |
|
||||||
@@ -189,7 +201,7 @@ these are rarely used for various reasons.
|
|||||||
### Mattermost Login
|
### Mattermost Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ------------ | ----------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|--------------|-------------------------------|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `mattermost` | | `{baseURL: ..., clientID: ..., clientSecret: ...}` | An object containing the base URL of your Mattermost application data. Refer to the [Mattermost guide](guides/auth/mattermost-self-hosted.md) for more details! |
|
| `mattermost` | | `{baseURL: ..., clientID: ..., clientSecret: ...}` | An object containing the base URL of your Mattermost application data. Refer to the [Mattermost guide](guides/auth/mattermost-self-hosted.md) for more details! |
|
||||||
| | `CMD_MATTERMOST_BASEURL` | **no default** | Mattermost authentication endpoint for versions below 5.0. For Mattermost version 5.0 and above, see [guide](guides/auth/mattermost-self-hosted.md). |
|
| | `CMD_MATTERMOST_BASEURL` | **no default** | Mattermost authentication endpoint for versions below 5.0. For Mattermost version 5.0 and above, see [guide](guides/auth/mattermost-self-hosted.md). |
|
||||||
| | `CMD_MATTERMOST_CLIENTID` | **no default** | Mattermost API client id |
|
| | `CMD_MATTERMOST_CLIENTID` | **no default** | Mattermost API client id |
|
||||||
@@ -198,7 +210,7 @@ these are rarely used for various reasons.
|
|||||||
### OAuth2 Login
|
### OAuth2 Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|-------------|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `oauth2` | | `{baseURL: ..., userProfileURL: ..., userProfileUsernameAttr: ..., userProfileDisplayNameAttr: ..., userProfileEmailAttr: ..., tokenURL: ..., authorizationURL: ..., clientID: ..., clientSecret: ..., scope: ...}` | An object detailing your OAuth2 provider. Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details! |
|
| `oauth2` | | `{baseURL: ..., userProfileURL: ..., userProfileUsernameAttr: ..., userProfileDisplayNameAttr: ..., userProfileEmailAttr: ..., tokenURL: ..., authorizationURL: ..., clientID: ..., clientSecret: ..., scope: ...}` | An object detailing your OAuth2 provider. Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details! |
|
||||||
| | `CMD_OAUTH2_USER_PROFILE_URL` | **no default**, `https://example.com` | Where to retrieve information about a user after successful login. Needs to output JSON. (no default value) Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details on all of the `CMD_OAUTH2...` options. |
|
| | `CMD_OAUTH2_USER_PROFILE_URL` | **no default**, `https://example.com` | Where to retrieve information about a user after successful login. Needs to output JSON. (no default value) Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details on all of the `CMD_OAUTH2...` options. |
|
||||||
| | `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR` | **no default**, `name` | where to find the username in the JSON from the user profile URL. (no default value) |
|
| | `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR` | **no default**, `name` | where to find the username in the JSON from the user profile URL. (no default value) |
|
||||||
@@ -215,33 +227,37 @@ these are rarely used for various reasons.
|
|||||||
| | `CMD_OAUTH2_ACCESS_ROLE` | **no default**, `role/hedgedoc` | The role which should be included in the ID token roles claim to grant access |
|
| | `CMD_OAUTH2_ACCESS_ROLE` | **no default**, `role/hedgedoc` | The role which should be included in the ID token roles claim to grant access |
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
If you are using a [CA not trusted by Node.js](https://github.com/nodejs/node/issues/4175) (like Let's Encrypt e.g) for your OAuth2 provider you can set the [`NODE_EXTRA_CA_CERTS`](https://nodejs.org/api/cli.html#cli_node_extra_ca_certs_file) environment variable to the CA certificate file path of your CA.
|
If you are using a [CA not trusted by Node.js](https://github.com/nodejs/node/issues/4175) (like Let's Encrypt e.g) for
|
||||||
Remember to also make the file available inside the Docker container, if you're running HedgeDoc in Docker container.
|
your OAuth2 provider you can set the [
|
||||||
|
`NODE_EXTRA_CA_CERTS`](https://nodejs.org/api/cli.html#cli_node_extra_ca_certs_file) environment variable to the CA
|
||||||
|
certificate file path of your CA.
|
||||||
|
Remember to also make the file available inside the Docker container, if you're running HedgeDoc in Docker container.
|
||||||
|
|
||||||
### SAML Login
|
### SAML Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|-------------|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `saml` | | `{idpSsoUrl: ..., idpCert: ..., clientCert: ..., issuer: ..., identifierFormat: ..., disableRequestedAuthnContext: ..., groupAttribute: ..., externalGroups: [], requiredGroups: [], attribute: {id: ..., username: ..., email: ...}}` | An object detailing your SAML provider. Refer to the [OneLogin](guides/auth/saml-onelogin.md) and [SAML](guides/auth/saml.md) guides for more details! |
|
| `saml` | | `{idpSsoUrl: ..., idpCert: ..., clientCert: ..., issuer: ..., identifierFormat: ..., disableRequestedAuthnContext: ..., groupAttribute: ..., externalGroups: [], requiredGroups: [], attribute: {id: ..., username: ..., email: ...}}` | An object detailing your SAML provider. Refer to the [OneLogin](guides/auth/saml-onelogin.md) and [SAML](guides/auth/saml.md) guides for more details! |
|
||||||
| | `CMD_SAML_IDPSSOURL` | **no default**, `https://idp.example.com/sso` | authentication endpoint of IdP. for details, see [guide](guides/auth/saml-onelogin.md). |
|
| | `CMD_SAML_IDPSSOURL` | **no default**, `https://idp.example.com/sso` | authentication endpoint of IdP. for details, see [guide](guides/auth/saml-onelogin.md). |
|
||||||
| | `CMD_SAML_IDPCERT` | **no default**, `/path/to/cert.pem` | certificate file path of IdP in PEM format |
|
| | `CMD_SAML_IDPCERT` | **no default**, `/path/to/cert.pem` | certificate file path of IdP in PEM format |
|
||||||
| | `CMD_SAML_CLIENTCERT` | **no default**, `/path/to/privatecert.pem` | certificate file path for the client in PEM format (optional) |
|
| | `CMD_SAML_CLIENTCERT` | **no default**, `/path/to/privatecert.pem` | certificate file path for the client in PEM format (optional) |
|
||||||
| | `CMD_SAML_ISSUER` | **no default** | Issuer to supply to identity provider (optional, default: `serverURL` config)" |
|
| | `CMD_SAML_ISSUER` | **no default** | Issuer to supply to identity provider (optional, default: `serverURL` config)" |
|
||||||
| | `CMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT` | **no default**, `true` or `false` | true to allow any authentication method, false restricts to password authentication (PasswordProtectedTransport) method (default: false) |
|
| | `CMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT` | **no default**, `true` or `false` | true to allow any authentication method, false restricts to password authentication (PasswordProtectedTransport) method (default: false) |
|
||||||
| | `CMD_SAML_IDENTIFIERFORMAT` | **`urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`** | name identifier format (optional, default: `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) |
|
| | `CMD_SAML_WANT_ASSERTIONS_SIGNED` | **`true`** or `false` | true to enforce signed assertions, false allows unsigned assertions |
|
||||||
| | `CMD_SAML_GROUPATTRIBUTE` | **no default**, `memberOf` | attribute name for group list (optional) |
|
| | `CMD_SAML_WANT_AUTHN_RESPONSE_SIGNED` | **`true`** or `false` | true to enforce signed response, false allows unsigned response |
|
||||||
|
| | `CMD_SAML_IDENTIFIERFORMAT` | **`urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`** | name identifier format (optional, if using `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`, ensure that the IdP returns an unique email address for each user) |
|
||||||
|
| | `CMD_SAML_GROUPATTRIBUTE` | **no default**, `http://schemas.xmlsoap.org/claims/Group` | attribute name for group list (optional) |
|
||||||
| | `CMD_SAML_REQUIREDGROUPS` | **no default**, `hedgedoc-users` | group names that allowed (use vertical bar to separate) (optional) |
|
| | `CMD_SAML_REQUIREDGROUPS` | **no default**, `hedgedoc-users` | group names that allowed (use vertical bar to separate) (optional) |
|
||||||
| | `CMD_SAML_EXTERNALGROUPS` | **no default**, `Temporary-staff` | group names that not allowed (use vertical bar to separate) (optional) |
|
| | `CMD_SAML_EXTERNALGROUPS` | **no default**, `Temporary-staff` | group names that not allowed (use vertical bar to separate) (optional) |
|
||||||
| | `CMD_SAML_ATTRIBUTE_ID` | **no default**, `sAMAccountName` | attribute map for `id` (optional, default: NameID of SAML response) |
|
| | `CMD_SAML_ATTRIBUTE_ID` | *defaults to NameID as configured per requested NameID-Format*, `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn` | attribute map for `id` (optional, default: NameID of SAML response) |
|
||||||
| | `CMD_SAML_ATTRIBUTE_USERNAME` | **no default**, `mailNickname` | attribute map for `username` (optional, default: NameID of SAML response) |
|
| | `CMD_SAML_ATTRIBUTE_USERNAME` | **`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`** | attribute map for `username` (optional) |
|
||||||
| | `CMD_SAML_ATTRIBUTE_EMAIL` | **no default**, `mail` | attribute map for `email` (optional, default: NameID of SAML response if `CMD_SAML_IDENTIFIERFORMAT` is default) |
|
| | `CMD_SAML_ATTRIBUTE_EMAIL` | **`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`** | attribute map for `email` (optional) |
|
||||||
| | `CMD_SAML_PROVIDERNAME` | **no default**, `My institution` | Optional name to be displayed at login form indicating the SAML provider |
|
| | `CMD_SAML_PROVIDERNAME` | **no default**, `My institution` | Optional name to be displayed at login form indicating the SAML provider |
|
||||||
|
|
||||||
### Twitter Login
|
### Twitter Login
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | ---------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|-------------|------------------------------|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `twitter` | | `{consumerKey: ..., consumerSecret: ...}` | An object containing the consumer key and secret obtained by the [Twitter developer tools](https://developer.twitter.com/apps). For more details have a look at the [Twitter auth guide](guides/auth/twitter.md) |
|
| `twitter` | | `{consumerKey: ..., consumerSecret: ...}` | An object containing the consumer key and secret obtained by the [Twitter developer tools](https://developer.twitter.com/apps). For more details have a look at the [Twitter auth guide](guides/auth/twitter.md) |
|
||||||
| | `CMD_TWITTER_CONSUMERKEY` | **no default** | Twitter API consumer key |
|
| | `CMD_TWITTER_CONSUMERKEY` | **no default** | Twitter API consumer key |
|
||||||
| | `CMD_TWITTER_CONSUMERSECRET` | **no default** | Twitter API consumer secret |
|
| | `CMD_TWITTER_CONSUMERSECRET` | **no default** | Twitter API consumer secret |
|
||||||
@@ -255,7 +271,7 @@ you don't have to use either of these.
|
|||||||
### Amazon S3
|
### Amazon S3
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| 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/s3-image-upload.md) |
|
||||||
| | `CMD_S3_ACCESS_KEY_ID` | **no default** | AWS access key id |
|
| | `CMD_S3_ACCESS_KEY_ID` | **no default** | AWS access key id |
|
||||||
| | `CMD_S3_SECRET_ACCESS_KEY` | **no default** | AWS secret key |
|
| | `CMD_S3_SECRET_ACCESS_KEY` | **no default** | AWS secret key |
|
||||||
@@ -268,20 +284,20 @@ you don't have to use either of these.
|
|||||||
### Azure Blob Storage
|
### Azure Blob Storage
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | ----------------------------- | ----------------------------- | ------------------------------------------------------------------------- |
|
|-------------|-------------------------------|-------------------------------|---------------------------------------------------------------------------|
|
||||||
| | `CMD_AZURE_CONNECTION_STRING` | **no default** | Azure Blob Storage connection string |
|
| | `CMD_AZURE_CONNECTION_STRING` | **no default** | Azure Blob Storage connection string |
|
||||||
| | `CMD_AZURE_CONTAINER` | **no default** | Azure Blob Storage container name (automatically created if non existent) |
|
| | `CMD_AZURE_CONTAINER` | **no default** | Azure Blob Storage container name (automatically created if non existent) |
|
||||||
|
|
||||||
### imgur
|
### imgur
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | -------------------- | ------------------------------ | ------------------- |
|
|-------------|----------------------|-------------------------------|---------------------|
|
||||||
| | `CMD_IMGUR_CLIENTID` | **no default** | Imgur API client id |
|
| | `CMD_IMGUR_CLIENTID` | **no default** | Imgur API client id |
|
||||||
|
|
||||||
### Minio
|
### Minio
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| 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/minio-image-upload.md) |
|
||||||
| | `CMD_MINIO_ACCESS_KEY` | **no default** | Minio access key |
|
| | `CMD_MINIO_ACCESS_KEY` | **no default** | Minio access key |
|
||||||
| | `CMD_MINIO_SECRET_KEY` | **no default** | Minio secret key |
|
| | `CMD_MINIO_SECRET_KEY` | **no default** | Minio secret key |
|
||||||
@@ -292,6 +308,6 @@ you don't have to use either of these.
|
|||||||
### Lutim
|
### Lutim
|
||||||
|
|
||||||
| config file | environment | **default** and example value | description |
|
| config file | environment | **default** and example value | description |
|
||||||
| ----------- | --------------- | ----------------------------- | --------------------------------------------------------------------------- |
|
|-------------|-----------------|-------------------------------|-----------------------------------------------------------------------------|
|
||||||
| `lutim` | | `{"url": "YOUR_LUTIM_URL"}` | When `imageUploadType` is set to `lutim`, you can setup the lutim url |
|
| `lutim` | | `{"url": "YOUR_LUTIM_URL"}` | When `imageUploadType` is set to `lutim`, you can setup the lutim url |
|
||||||
| | `CMD_LUTIM_URL` | **`https://framapic.org/`** | When `CMD_IMAGE_UPLOAD_TYPE` is set to `lutim`, you can setup the lutim url |
|
| | `CMD_LUTIM_URL` | **`https://framapic.org/`** | When `CMD_IMAGE_UPLOAD_TYPE` is set to `lutim`, you can setup the lutim url |
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ openapi: 3.0.1
|
|||||||
info:
|
info:
|
||||||
title: HedgeDoc
|
title: HedgeDoc
|
||||||
description: HedgeDoc is an open source collaborative note editor. Several tasks of HedgeDoc can be automated through this API.
|
description: HedgeDoc is an open source collaborative note editor. Several tasks of HedgeDoc can be automated through this API.
|
||||||
version: 1.10.1
|
version: 1.10.3
|
||||||
contact:
|
contact:
|
||||||
name: HedgeDoc on GitHub
|
name: HedgeDoc on GitHub
|
||||||
url: https://github.com/hedgedoc/hedgedoc
|
url: https://github.com/hedgedoc/hedgedoc
|
||||||
|
|||||||
217
docs/content/dev/release_checklist.md
Normal file
217
docs/content/dev/release_checklist.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# Release Checklist:
|
||||||
|
|
||||||
|
## 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>
|
||||||
|
- [ ] Release PR in <https://github.com/hedgedoc/social-media>
|
||||||
|
- [ ] Security fixes (make sure all available undisclosed security fixes are merged)
|
||||||
|
- [ ] Bump the version
|
||||||
|
- [ ] docs/content/dev/openapi.yml
|
||||||
|
- version number image
|
||||||
|
- [ ] docs/content/setup/manual-setup.md
|
||||||
|
- `git clone` branch
|
||||||
|
- `git checkout` branch
|
||||||
|
- [ ] package.json
|
||||||
|
- `version`
|
||||||
|
- [ ] docs/content/setup/docker.md
|
||||||
|
- Update docker-compose in the docs
|
||||||
|
- [ ] Make sure `yarn.lock` is up to date
|
||||||
|
- [ ] Make sure translations are up to date
|
||||||
|
- We use [poeditor_pull](https://github.com/costales/poeditor_pull) to download all language files from POEditor.
|
||||||
|
|
||||||
|
1. change the following line in the script
|
||||||
|
```python
|
||||||
|
- r_lang = requests.post('https://api.poeditor.com/v2/projects/export', dict(api_token=project_api, id=project_id, language=lang['code'], type="po"))
|
||||||
|
+ r_lang = requests.post('https://api.poeditor.com/v2/projects/export', dict(api_token=project_api, id=project_id, language=lang['code'], type="key_value_json"))
|
||||||
|
```
|
||||||
|
2. run the script.
|
||||||
|
3. update the json files in the `locales` directory.
|
||||||
|
- any languages with 0% translations should not be included
|
||||||
|
4. If any languages are new, they need to be added to `locales/_supported.json`
|
||||||
|
- [ ] Write release notes (`public/docs/release-notes.md`)
|
||||||
|
- [ ] Update date
|
||||||
|
- [ ] General description
|
||||||
|
- [ ] Things requiring special action beside updating the software
|
||||||
|
- [ ] New features
|
||||||
|
- [ ] Bug fixes
|
||||||
|
- [ ] Enhancements
|
||||||
|
- [ ] Add all contributors
|
||||||
|
- sort alphabetically
|
||||||
|
- [ ] Update `AUTHORS` file
|
||||||
|
|
||||||
|
## Final Testing:
|
||||||
|
|
||||||
|
- [ ] Create release tar ball
|
||||||
|
```bash
|
||||||
|
mkdir /tmp/hedgedoc && cd /tmp/hedgedoc
|
||||||
|
git clone -b master https://github.com/hedgedoc/hedgedoc.git .
|
||||||
|
yarn install
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this tar ball to test the following things:
|
||||||
|
|
||||||
|
### Account system
|
||||||
|
|
||||||
|
- [ ] User registration works
|
||||||
|
- [ ] User login works
|
||||||
|
- [ ] User self-deletion works
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- [ ] Create new note works
|
||||||
|
- [ ] Create new note with custom alias works when FreeURL-mode is enabled
|
||||||
|
- [ ] Create new note with custom alias fails when FreeURL-mode is disabled
|
||||||
|
- [ ] New note keeps content (visit, write something, leave, visit again after a minute)
|
||||||
|
- [ ] API `POST /new` works
|
||||||
|
- `curl -i -d '# hello world' -H "Content-Type: text/markdown" http://localhost:3000/new`
|
||||||
|
- [ ] API `POST /new/some-test-note` works when FreeURL-mode is enabled
|
||||||
|
- [ ] API `POST /new/some-test-note` fails when FreeURL-mode is disabled
|
||||||
|
- [ ] 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)
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
#### Sqlite
|
||||||
|
|
||||||
|
- [ ] Sqlite works
|
||||||
|
- [ ] Keeps content of already existing SQLite file from older version
|
||||||
|
|
||||||
|
#### Postgres
|
||||||
|
|
||||||
|
- [ ] 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`
|
||||||
|
and put this into your config:
|
||||||
|
```
|
||||||
|
"db": {
|
||||||
|
"username": "hd1db",
|
||||||
|
"password": "hd1db",
|
||||||
|
"database": "hd1db",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"dialect": "postgres"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
#### MariaDB
|
||||||
|
|
||||||
|
- [ ] MariaDB works
|
||||||
|
|
||||||
|
Run `docker run --name=hd1-mysql -p 3306:3306 -e MARIADB_USER=hd1db --env MARIADB_PASSWORD=hd1db --env MARIADB_DATABASE=hd1db -e MARIADB_RANDOM_ROOT_PASSWORD=true --rm -d mariadb:latest`
|
||||||
|
and put this into your config:
|
||||||
|
```
|
||||||
|
"db": {
|
||||||
|
"username": "hd1db",
|
||||||
|
"password": "hd1db",
|
||||||
|
"database": "hd1db",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"dialect": "mariadb"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features page
|
||||||
|
|
||||||
|
- [ ] Loading `/features` results in no browser console errors (they may appear for iframed code)
|
||||||
|
- [ ] Diagrams render without error
|
||||||
|
- [ ] MathJAX rendering works for inline `$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$`
|
||||||
|
- [ ] MathJAX rendering works for multi-line (see features page)
|
||||||
|
- [ ] Codeblocks areas are highlighted and have line numbers in front.
|
||||||
|
|
||||||
|
### Table of content (TOC) tests
|
||||||
|
|
||||||
|
- [ ] TOC renders in the document as content
|
||||||
|
- [ ] TOC renders in the lower right corner of the document in `both`-view
|
||||||
|
- [ ] TOC renders besides the document in the `view`-view
|
||||||
|
- [ ] TOC renders besides the document in `published`-view
|
||||||
|
- [ ] Interactive TOC follows the header while scrolling in `both`-view
|
||||||
|
- [ ] Interactive TOC follows the header while scrolling in `view`-view
|
||||||
|
- [ ] Interactive TOC follows the header while scrolling in `published`-view
|
||||||
|
|
||||||
|
### Embeddings
|
||||||
|
|
||||||
|
Click in them an try to play around with them. Don't just check they exist and show up.
|
||||||
|
|
||||||
|
- [ ] Youtube embedding works
|
||||||
|
- [ ] Vimeo embedding works
|
||||||
|
- [ ] Gist embedding works
|
||||||
|
|
||||||
|
### Working YAML-Meta
|
||||||
|
|
||||||
|
- [ ] Testing each option if it works
|
||||||
|
|
||||||
|
### GDPR features
|
||||||
|
|
||||||
|
- [ ] Delete account works
|
||||||
|
- [ ] When account is deleted, verify notes are gone as well
|
||||||
|
- [ ] Data export works
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
- [ ] SAML
|
||||||
|
- config
|
||||||
|
```
|
||||||
|
"saml": {
|
||||||
|
"idpSsoUrl": "https://auth.hedgedoc.cloud/application/saml/test-hd1/sso/binding/redirect/",
|
||||||
|
"idpCert": "/tmp/auth.hedgedoc.cloud.pem",
|
||||||
|
"identifierFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
|
||||||
|
"attribute": {
|
||||||
|
"id": "http://schemas.goauthentik.io/2021/02/saml/uid",
|
||||||
|
"username": "http://schemas.goauthentik.io/2021/02/saml/username",
|
||||||
|
"email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] LDAP
|
||||||
|
- docker image: `docker run --rm -p 10389:10389 -p 10636:10636 -d ghcr.io/rroemhild/docker-test-openldap:master`
|
||||||
|
- config
|
||||||
|
```
|
||||||
|
"ldap": {
|
||||||
|
"url": "ldap://localhost:10389",
|
||||||
|
"bindDn": "cn=admin,dc=planetexpress,dc=com",
|
||||||
|
"bindCredentials": "GoodNewsEveryone",
|
||||||
|
"searchBase": "ou=people,dc=planetexpress,dc=com",
|
||||||
|
"searchFilter": "(&(uid={{username}})(objectClass=inetOrgPerson))",
|
||||||
|
"searchAttributes": ["uid", "cn"],
|
||||||
|
"usernameField": "cn",
|
||||||
|
"useridField": "uid",
|
||||||
|
"tlsOptions": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] OAuth2
|
||||||
|
```
|
||||||
|
"oauth2": {
|
||||||
|
"baseURL": "https://auth.hedgedoc.cloud/application/o/test-hedgedoc/",
|
||||||
|
"userProfileURL": "https://auth.hedgedoc.cloud/application/o/userinfo/",
|
||||||
|
"tokenURL": "https://auth.hedgedoc.cloud/application/o/token/",
|
||||||
|
"authorizationURL": "https://auth.hedgedoc.cloud/application/o/authorize/",
|
||||||
|
"clientID": "REDACTED",
|
||||||
|
"clientSecret": "REDACTED",
|
||||||
|
"scope": "openid profile user",
|
||||||
|
"userProfileUsernameAttr": "preferred_username",
|
||||||
|
"userProfileEmailAttr": "email",
|
||||||
|
"userProfileDisplayNameAttr": "name"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] GitHub
|
||||||
|
- [ ] Rate-limiting for basic user/password
|
||||||
|
|
||||||
|
## Release:
|
||||||
|
|
||||||
|
- [ ] 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
|
||||||
|
- [ ] 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 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
|
||||||
|
- [ ] Change this release checklist if necessary
|
||||||
@@ -2,22 +2,22 @@
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
This guide assumes you have run and configured Keycloak. If you'd like to meet this prerequisite quickly, it can be achieved by running a `jboss/keycloak` container and attaching it to your network. Set the environment variables KEYCLOAK_USER and `KEYCLOAK_PASSWORD`, and expose port 8080.
|
This guide assumes you have run and configured Keycloak.
|
||||||
|
If you'd like to meet this prerequisite quickly, it can be achieved by running a `quay.io/keycloak/keycloak` container and attaching it to your network.
|
||||||
|
For details and quick-start command, take a look at [the Keycloak Docker documentation](https://www.keycloak.org/getting-started/getting-started-docker).
|
||||||
Where HTTPS is specified throughout, use HTTP instead. You may also have to specify the exposed port, 8080.
|
Where HTTPS is specified throughout, use HTTP instead. You may also have to specify the exposed port, 8080.
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. Sign in to the administration portal for your Keycloak instance at <https://keycloak.example.com/auth/admin/master/console>
|
1. Sign in to the administration portal for your Keycloak instance at <https://keycloak.example.com/admin/master/console>.
|
||||||
|
|
||||||
You may note that a separate realm is specified throughout this tutorial. It is best practice not to use the master realm, as it normally contains the realm-management client that federates access using the policies and permissions you can create.
|
> You may note that a separate realm is specified throughout this tutorial. It is best practice not to use the `master` realm, as it normally contains the realm-management client that federates access using the policies and permissions you can create.
|
||||||
|
|
||||||
2. Navigate to the client management page at `https://keycloak.example.com/auth/admin/master/console/#/realms/your-realm/clients` (admin permissions required)
|
2. Navigate to the client management page at <https://keycloak.example.com/admin/master/console/#/your-realm/clients> (admin permissions required)
|
||||||
3. Click **Create** to create a new client and fill out the registration form. You should set the Root URL to the fully qualified public URL of your HedgeDoc instance.
|
3. Click **Create** to create a new client and fill out the registration form. You should set the “Root URL” to the fully qualified public URL of your HedgeDoc instance.
|
||||||
4. Click **Save**
|
4. Click **Save**
|
||||||
5. Set the **Access Type** of the client to `confidential`. This will make your client require a client secret upon authentication.
|
5. Set the **Access Type** of the client to `confidential`. This will make your client require a client secret upon authentication.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Additional steps to circumvent generic OAuth2 issue
|
### Additional steps to circumvent generic OAuth2 issue
|
||||||
|
|
||||||
@@ -26,25 +26,27 @@ You may note that a separate realm is specified throughout this tutorial. It is
|
|||||||
3. Create a new mapper under the Mappers tab. This should reference the User Property `id`. `Claim JSON Type` should be String and all switches below should be enabled. Save the mapper.
|
3. Create a new mapper under the Mappers tab. This should reference the User Property `id`. `Claim JSON Type` should be String and all switches below should be enabled. Save the mapper.
|
||||||
4. Go to the client you set up in the previous steps using the Clients page, then choose the Client Scopes tab. Apply the scope you've created. This should mitigate errors as seen in [hedgedoc/hedgedoc#56](https://github.com/hedgedoc/hedgedoc/issues/56), as the `/userinfo` endpoint should now bring back the user's ID under the `id` key as well as `sub`.
|
4. Go to the client you set up in the previous steps using the Clients page, then choose the Client Scopes tab. Apply the scope you've created. This should mitigate errors as seen in [hedgedoc/hedgedoc#56](https://github.com/hedgedoc/hedgedoc/issues/56), as the `/userinfo` endpoint should now bring back the user's ID under the `id` key as well as `sub`.
|
||||||
|
|
||||||
---
|
## Container Configuration
|
||||||
|
|
||||||
5. In the `docker-compose.yml` add the following environment variables to `app:` `environment:`
|
In the `docker-compose.yml` add the following environment variables to `app.environment:`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
CMD_OAUTH2_USER_PROFILE_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/userinfo
|
app:
|
||||||
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=preferred_username
|
environment:
|
||||||
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=name
|
- CMD_OAUTH2_USER_PROFILE_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/userinfo
|
||||||
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email
|
- CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=preferred_username
|
||||||
CMD_OAUTH2_TOKEN_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/token
|
- CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=name
|
||||||
CMD_OAUTH2_AUTHORIZATION_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/auth
|
- CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email
|
||||||
CMD_OAUTH2_CLIENT_ID=<your client ID>
|
- CMD_OAUTH2_TOKEN_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/token
|
||||||
CMD_OAUTH2_CLIENT_SECRET=<your client secret, which you can find under the Credentials tab for your client>
|
- CMD_OAUTH2_AUTHORIZATION_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/auth
|
||||||
CMD_OAUTH2_PROVIDERNAME=Keycloak
|
- CMD_OAUTH2_CLIENT_ID=<your client ID>
|
||||||
CMD_OAUTH2_SCOPE=openid email profile
|
- CMD_OAUTH2_CLIENT_SECRET=<your client secret, which you can find under the Credentials tab for your client>
|
||||||
CMD_DOMAIN=<hedgedoc.example.com>
|
- CMD_OAUTH2_PROVIDERNAME=Keycloak
|
||||||
CMD_PROTOCOL_USESSL=true
|
- CMD_OAUTH2_SCOPE=openid email profile
|
||||||
CMD_URL_ADDPORT=false
|
- CMD_DOMAIN=<hedgedoc.example.com>
|
||||||
|
- CMD_PROTOCOL_USESSL=true
|
||||||
|
- CMD_URL_ADDPORT=false
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Run `docker-compose up -d` to apply your settings.
|
After running `docker-compose up -d` to apply your settings,
|
||||||
7. Sign in to your HedgeDoc using your Keycloak ID
|
you should now be able to sign in to your HedgeDoc using your Keycloak.
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ This documentation will cover HTTPS setup, with comments for HTTP setup.
|
|||||||
|
|
||||||
## Cloudflare
|
## Cloudflare
|
||||||
!!! warning
|
!!! warning
|
||||||
If you use Cloudflare as reverse proxy then you **MUST** disable the minify features for HTML, CSS and JS, or your HedgeDoc instance may be broken.
|
If you use Cloudflare as reverse proxy, then you **MUST** disable Rocket Loader, or your HedgeDoc instance may be broken.
|
||||||
For more information please read the [Cloudflare documentation](https://support.cloudflare.com/hc/en-us/articles/200168196-How-do-I-minify-HTML-CSS-and-JavaScript-to-optimize-my-site-).
|
For more information please read the [Cloudflare documentation](https://developers.cloudflare.com/speed/optimization/content/rocket-loader).
|
||||||
|
|
||||||
## HedgeDoc config
|
## HedgeDoc config
|
||||||
|
|
||||||
@@ -81,8 +81,9 @@ server {
|
|||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen [::]:443 ssl http2;
|
listen [::]:443 ssl;
|
||||||
listen 443 ssl http2;
|
listen 443 ssl;
|
||||||
|
http2 on;
|
||||||
ssl_certificate fullchain.pem;
|
ssl_certificate fullchain.pem;
|
||||||
ssl_certificate_key privkey.pem;
|
ssl_certificate_key privkey.pem;
|
||||||
include options-ssl-nginx.conf;
|
include options-ssl-nginx.conf;
|
||||||
@@ -97,6 +98,66 @@ server {
|
|||||||
connection to the server, and the editor interface will display an endless loading
|
connection to the server, and the editor interface will display an endless loading
|
||||||
animation.
|
animation.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Starting with NGINX Version [1.25.1](https://nginx.org/en/CHANGES) (released on 13
|
||||||
|
Jun 2023) the `http2`-**parameter** for the `listen`-directive has been deprecated!
|
||||||
|
|
||||||
|
NGINX Version 1.25.1 introduces [`http2` as a standalone directive](https://nginx.org/en/docs/http/ngx_http_v2_module.html)
|
||||||
|
which can be enabled as can be seen in the example above.
|
||||||
|
|
||||||
|
If you are running on an older NGINX version you can delete the `http2 on;`-line and
|
||||||
|
add the `http2`-parameter to both `listen`-directive lines.
|
||||||
|
|
||||||
|
```
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
listen 443 ssl http2;
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! information
|
||||||
|
|
||||||
|
If you do not want to expose the `/metrics` and `/status` HTTP-endpoints to the whole
|
||||||
|
internet but you need to (for example) monitor `/metrics` using your Prometheus
|
||||||
|
installation (so disabling `enableStatsApi` in the HedgeDoc config is not a viable
|
||||||
|
option) you can add the following location blocks to your NGINX-server-block to limit
|
||||||
|
access to trusted (monitoring) networks / ip-literals.
|
||||||
|
|
||||||
|
```
|
||||||
|
location /metrics {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
allow 2001:db8::/64;
|
||||||
|
allow 192.0.2.0/24;
|
||||||
|
[...]
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /status {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
allow 2001:db8::/64;
|
||||||
|
allow 192.0.2.0/24;
|
||||||
|
[...]
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
While it is certainly not a security issue itself to keep these public to the internet
|
||||||
|
it could give attackers additional information and help them exploit your HedgeDoc installation.
|
||||||
|
|
||||||
|
Therefore if you do not have a monitoring setup (like Prometheus) it's likely you do not
|
||||||
|
need to expose this information at all and can simply set `enableStatsApi` to false (default
|
||||||
|
is true) in your HedgeDoc `config.json`.
|
||||||
|
|
||||||
|
|
||||||
### Apache
|
### Apache
|
||||||
You will need these modules enabled: `proxy`, `proxy_http` and `proxy_wstunnel`.
|
You will need these modules enabled: `proxy`, `proxy_http` and `proxy_wstunnel`.
|
||||||
Here is an example config snippet:
|
Here is an example config snippet:
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ using the button below. This will run the official Docker image from [quay.io](h
|
|||||||
|
|
||||||
[](https://www.pikapods.com/pods?run=hedgedoc)
|
[](https://www.pikapods.com/pods?run=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.
|
||||||
|
|
||||||
|
The source code of the module can be found [here](https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config/src/branch/flakes/sp-modules/hedgedoc).
|
||||||
|
|
||||||
|
[Install SelfPrivacy](https://selfprivacy.org/download/)
|
||||||
|
|
||||||
## Distribution Packages
|
## Distribution Packages
|
||||||
|
|
||||||
### Arch Linux
|
### Arch Linux
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- [Docker Compose](https://docs.docker.com/compose/install/)
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
|
|
||||||
The official docker images are [available on quay.io](https://quay.io/repository/hedgedoc/hedgedoc).
|
The official docker images are [available on quay.io](https://quay.io/repository/hedgedoc/hedgedoc).
|
||||||
We currently only support the `amd64` architecture.
|
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`:
|
The easiest way to get started with HedgeDoc and Docker is to use the following `docker-compose.yml`:
|
||||||
@@ -28,7 +28,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
app:
|
app:
|
||||||
# Make sure to use the latest release from https://hedgedoc.org/latest-release
|
# Make sure to use the latest release from https://hedgedoc.org/latest-release
|
||||||
image: quay.io/hedgedoc/hedgedoc:1.10.1
|
image: quay.io/hedgedoc/hedgedoc:1.10.3
|
||||||
environment:
|
environment:
|
||||||
- CMD_DB_URL=postgres://hedgedoc:password@database:5432/hedgedoc
|
- CMD_DB_URL=postgres://hedgedoc:password@database:5432/hedgedoc
|
||||||
- CMD_DOMAIN=localhost
|
- CMD_DOMAIN=localhost
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
1. Check if you meet the [requirements at the top of this document](#manual-installation).
|
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.
|
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.1 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.10.3 https://github.com/hedgedoc/hedgedoc.git`.</small>
|
||||||
3. Enter the directory and execute `bin/setup`, which will install the dependencies and create example configs.
|
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`:
|
4. Configure HedgeDoc: To get started, you can use this minimal `config.json`:
|
||||||
```json
|
```json
|
||||||
@@ -61,7 +61,7 @@ If you want to upgrade HedgeDoc from an older version, follow these steps:
|
|||||||
and the latest release.
|
and the latest release.
|
||||||
2. Fully stop your old HedgeDoc server.
|
2. Fully stop your old HedgeDoc server.
|
||||||
3. [Download](https://hedgedoc.org/latest-release/) the new release and extract it over the old directory.
|
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.1`</small>
|
<small>If you use Git, you can check out the new tag with e.g. `git fetch origin && git checkout 1.10.3`</small>
|
||||||
5. Run `bin/setup`. This will take care of installing dependencies. It is safe to run on an existing installation.
|
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.*
|
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.
|
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.
|
||||||
@@ -88,6 +88,14 @@ Using the unit file below, you can run HedgeDoc as a systemd service.
|
|||||||
file in the root directory of the HedgeDoc installation**, but create a subfolder like `db`!
|
file in the root directory of the HedgeDoc installation**, but create a subfolder like `db`!
|
||||||
- If you use an external database like PostgreSQL or MariaDB, make sure to add a corresponding
|
- If you use an external database like PostgreSQL or MariaDB, make sure to add a corresponding
|
||||||
`After` statement.
|
`After` statement.
|
||||||
|
- `SystemCallFilter=`
|
||||||
|
- More about filtering system calls can be read in the [systemd.exec documentation](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#System%20Call%20Filtering).
|
||||||
|
- If the service does not start please have a look at your systemd-journal (`journalctl -f`) and then try to `systemctl start hedgedoc.service`.
|
||||||
|
- In the systemd-journal you will then see a line with `... kernel: audit: ...`. The important part of this line is `syscall=` (example `syscall=330`).
|
||||||
|
- You can lookup the name of the syscall for the numer on a website like <https://filippo.io/linux-syscall-table/>. Example: 330 is `pkey_alloc`.
|
||||||
|
- Add the name of the syscall at the end of the line of `SystemCallFilter=` (separated by spaces), `systemctl daemon-reload` and then `systemctl restart hedgedoc.service`.
|
||||||
|
- If it does not work have another look at the systemd-journal and repeat the previous steps (add/allow additional needed syscalls).
|
||||||
|
- You can also use groups of syscalls (starting with `@`). See the systemd.exec documentation as it contains a table of `Currently predefined system call sets` you can use. Of course as HedgeDoc is usually exposed to the internet it might be wise to only allow syscalls HedgeDoc really needs depending on your own paranoia. ;-)
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
@@ -125,7 +133,7 @@ ProtectSystem=strict
|
|||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
SystemCallArchitectures=native
|
SystemCallArchitectures=native
|
||||||
SystemCallFilter=@system-service
|
SystemCallFilter=@system-service pkey_alloc pkey_mprotect
|
||||||
|
|
||||||
# You may have to adjust these settings
|
# You may have to adjust these settings
|
||||||
User=hedgedoc
|
User=hedgedoc
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ nav:
|
|||||||
- 'Operational Transformation': dev/ot.md
|
- 'Operational Transformation': dev/ot.md
|
||||||
- Webpack: dev/webpack.md
|
- Webpack: dev/webpack.md
|
||||||
- 'Documentation': dev/documentation.md
|
- 'Documentation': dev/documentation.md
|
||||||
|
- 'Release Checklist': dev/release_checklist.md
|
||||||
- FAQ: faq.md
|
- FAQ: faq.md
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- toc:
|
- toc:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-material==9.6.1
|
mkdocs-material==9.7.0
|
||||||
pymdown-extensions==10.14.3
|
pymdown-extensions==10.17.1
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
mkdocs-redirects==1.2.2
|
mkdocs-redirects==1.2.2
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ module.exports = {
|
|||||||
rateLimitNewNotes: 20,
|
rateLimitNewNotes: 20,
|
||||||
cookiePolicy: 'lax',
|
cookiePolicy: 'lax',
|
||||||
protocolUseSSL: false,
|
protocolUseSSL: false,
|
||||||
|
// permissions
|
||||||
allowAnonymous: true,
|
allowAnonymous: true,
|
||||||
allowAnonymousEdits: false,
|
allowAnonymousEdits: false,
|
||||||
allowFreeURL: false,
|
allowFreeURL: false,
|
||||||
@@ -39,6 +40,10 @@ module.exports = {
|
|||||||
disableNoteCreation: false,
|
disableNoteCreation: false,
|
||||||
forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views'],
|
forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views'],
|
||||||
defaultPermission: 'editable',
|
defaultPermission: 'editable',
|
||||||
|
|
||||||
|
enableUploads: undefined, // 'all', 'registered', 'none' are valid options.
|
||||||
|
// This is undefined by default and set during runtime based on allowAnonymous and allowAnonymousEdits for backwards-compatibility unless explicitly set.
|
||||||
|
|
||||||
dbURL: '',
|
dbURL: '',
|
||||||
db: {},
|
db: {},
|
||||||
// ssl path
|
// ssl path
|
||||||
@@ -160,9 +165,11 @@ module.exports = {
|
|||||||
requiredGroups: [],
|
requiredGroups: [],
|
||||||
attribute: {
|
attribute: {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
username: undefined,
|
username: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
|
||||||
email: undefined
|
email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
|
||||||
}
|
},
|
||||||
|
wantAssertionsSigned: true,
|
||||||
|
wantAuthnResponseSigned: true
|
||||||
},
|
},
|
||||||
email: true,
|
email: true,
|
||||||
allowEmailRegister: true,
|
allowEmailRegister: true,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ module.exports = {
|
|||||||
allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL),
|
allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL),
|
||||||
requireFreeURLAuthentication: toBooleanConfig(process.env.CMD_REQUIRE_FREEURL_AUTHENTICATION),
|
requireFreeURLAuthentication: toBooleanConfig(process.env.CMD_REQUIRE_FREEURL_AUTHENTICATION),
|
||||||
disableNoteCreation: toBooleanConfig(process.env.CMD_DISABLE_NOTE_CREATION),
|
disableNoteCreation: toBooleanConfig(process.env.CMD_DISABLE_NOTE_CREATION),
|
||||||
|
enableUploads: process.env.CMD_ENABLE_UPLOADS,
|
||||||
forbiddenNoteIDs: toArrayConfig(process.env.CMD_FORBIDDEN_NOTE_IDS),
|
forbiddenNoteIDs: toArrayConfig(process.env.CMD_FORBIDDEN_NOTE_IDS),
|
||||||
defaultPermission: process.env.CMD_DEFAULT_PERMISSION,
|
defaultPermission: process.env.CMD_DEFAULT_PERMISSION,
|
||||||
dbURL: process.env.CMD_DB_URL,
|
dbURL: process.env.CMD_DB_URL,
|
||||||
@@ -146,6 +147,8 @@ module.exports = {
|
|||||||
issuer: process.env.CMD_SAML_ISSUER,
|
issuer: process.env.CMD_SAML_ISSUER,
|
||||||
identifierFormat: process.env.CMD_SAML_IDENTIFIERFORMAT,
|
identifierFormat: process.env.CMD_SAML_IDENTIFIERFORMAT,
|
||||||
disableRequestedAuthnContext: toBooleanConfig(process.env.CMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT),
|
disableRequestedAuthnContext: toBooleanConfig(process.env.CMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT),
|
||||||
|
wantAssertionsSigned: toBooleanConfig(process.env.CMD_SAML_WANT_ASSERTIONS_SIGNED),
|
||||||
|
wantAuthnResponseSigned: toBooleanConfig(process.env.CMD_SAML_WANT_AUTHN_RESPONSE_SIGNED),
|
||||||
groupAttribute: process.env.CMD_SAML_GROUPATTRIBUTE,
|
groupAttribute: process.env.CMD_SAML_GROUPATTRIBUTE,
|
||||||
externalGroups: toArrayConfig(process.env.CMD_SAML_EXTERNALGROUPS, '|', []),
|
externalGroups: toArrayConfig(process.env.CMD_SAML_EXTERNALGROUPS, '|', []),
|
||||||
requiredGroups: toArrayConfig(process.env.CMD_SAML_REQUIREDGROUPS, '|', []),
|
requiredGroups: toArrayConfig(process.env.CMD_SAML_REQUIREDGROUPS, '|', []),
|
||||||
|
|||||||
@@ -79,6 +79,17 @@ if (!config.allowAnonymous && !config.allowAnonymousEdits) {
|
|||||||
if (!(config.defaultPermission in config.permission)) {
|
if (!(config.defaultPermission in config.permission)) {
|
||||||
config.defaultPermission = config.permission.editable
|
config.defaultPermission = config.permission.editable
|
||||||
}
|
}
|
||||||
|
if (config.enableUploads === undefined) {
|
||||||
|
if (!config.allowAnonymousEdits && !config.allowAnonymous) {
|
||||||
|
config.enableUploads = 'registered'
|
||||||
|
} else {
|
||||||
|
config.enableUploads = 'all'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!['all', 'registered', 'none'].includes(config.enableUploads)) {
|
||||||
|
logger.error('Config option "enableUploads"/CMD_ENABLE_UPLOADS is not correctly set. Please use "all", "registered" or "none". Defaulting to "all"')
|
||||||
|
config.enableUploads = 'all'
|
||||||
|
}
|
||||||
|
|
||||||
// Use HTTPS protocol if the internal TLS server is enabled
|
// Use HTTPS protocol if the internal TLS server is enabled
|
||||||
if (config.useSSL === true) {
|
if (config.useSSL === true) {
|
||||||
@@ -164,6 +175,10 @@ if (['filesystem', 's3', 'minio', 'imgur', 'azure', 'lutim'].indexOf(config.imag
|
|||||||
config.imageUploadType = 'filesystem'
|
config.imageUploadType = 'filesystem'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.isSAMLEnable && !config.saml.wantAssertionsSigned && !config.saml.wantAuthnResponseSigned) {
|
||||||
|
logger.error('You can only deactivate one of "saml.wantAssertionsSigned" and "saml.wantAuthnResponseSigned"')
|
||||||
|
}
|
||||||
|
|
||||||
// figure out mime types for image uploads
|
// figure out mime types for image uploads
|
||||||
switch (config.imageUploadType) {
|
switch (config.imageUploadType) {
|
||||||
case 'imgur':
|
case 'imgur':
|
||||||
|
|||||||
@@ -7,21 +7,18 @@ const CspStrategy = {}
|
|||||||
const defaultDirectives = {
|
const defaultDirectives = {
|
||||||
defaultSrc: ['\'none\''],
|
defaultSrc: ['\'none\''],
|
||||||
baseUri: ['\'self\''],
|
baseUri: ['\'self\''],
|
||||||
connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws')],
|
connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws'), 'https://vimeo.com/api/v2/video/'],
|
||||||
fontSrc: ['\'self\''],
|
fontSrc: ['\'self\''],
|
||||||
manifestSrc: ['\'self\''],
|
manifestSrc: ['\'self\''],
|
||||||
frameSrc: ['\'self\'', 'https://player.vimeo.com', 'https://www.slideshare.net/slideshow/embed_code/key/', 'https://www.youtube.com'],
|
frameSrc: ['\'self\'', 'https://player.vimeo.com', 'https://www.youtube.com', 'https://gist.github.com'],
|
||||||
imgSrc: ['*', 'data:'], // we allow using arbitrary images & explicit data for mermaid
|
imgSrc: ['*', 'data:'], // we allow using arbitrary images & explicit data for mermaid
|
||||||
scriptSrc: [
|
scriptSrc: [
|
||||||
config.serverURL + '/build/',
|
config.serverURL + '/build/',
|
||||||
config.serverURL + '/js/',
|
config.serverURL + '/js/',
|
||||||
config.serverURL + '/config',
|
config.serverURL + '/config',
|
||||||
'https://gist.github.com/',
|
|
||||||
'https://vimeo.com/api/oembed.json',
|
|
||||||
'https://www.slideshare.net/api/oembed/2',
|
|
||||||
'\'unsafe-inline\'' // this is ignored by browsers supporting nonces/hashes
|
'\'unsafe-inline\'' // this is ignored by browsers supporting nonces/hashes
|
||||||
],
|
],
|
||||||
styleSrc: [config.serverURL + '/build/', config.serverURL + '/css/', '\'unsafe-inline\'', 'https://github.githubassets.com'], // unsafe-inline is required for some libs, plus used in views
|
styleSrc: [config.serverURL + '/build/', config.serverURL + '/css/', '\'unsafe-inline\''], // unsafe-inline is required for some libs, plus used in views
|
||||||
objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/
|
objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/
|
||||||
formAction: ['\'self\''],
|
formAction: ['\'self\''],
|
||||||
mediaSrc: ['*']
|
mediaSrc: ['*']
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
// external modules
|
// external modules
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const randomcolor = require('randomcolor')
|
const Chance = require('chance')
|
||||||
const config = require('./config')
|
const config = require('./config')
|
||||||
|
|
||||||
// core
|
// core
|
||||||
exports.generateAvatar = function (name) {
|
exports.generateAvatar = function (name) {
|
||||||
const color = randomcolor({
|
// use darker colors for better contrast
|
||||||
seed: name,
|
const color = new Chance(name).color({
|
||||||
luminosity: 'dark'
|
max_red: 150,
|
||||||
|
max_green: 150,
|
||||||
|
max_blue: 150
|
||||||
})
|
})
|
||||||
const letter = name.substring(0, 1).toUpperCase()
|
const letter = name.substring(0, 1).toUpperCase()
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ const base64url = require('base64url')
|
|||||||
const md = require('markdown-it')()
|
const md = require('markdown-it')()
|
||||||
const metaMarked = require('@hedgedoc/meta-marked')
|
const metaMarked = require('@hedgedoc/meta-marked')
|
||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const shortId = require('shortid')
|
const nanoid = require('nanoid')
|
||||||
const Sequelize = require('sequelize')
|
const Sequelize = require('sequelize')
|
||||||
const async = require('async')
|
const async = require('async')
|
||||||
const moment = require('moment')
|
const moment = require('moment')
|
||||||
const DiffMatchPatch = require('diff-match-patch')
|
const DiffMatchPatch = require('diff-match-patch')
|
||||||
const dmp = new DiffMatchPatch()
|
const dmp = new DiffMatchPatch()
|
||||||
const S = require('string')
|
|
||||||
|
|
||||||
// core
|
// core
|
||||||
const config = require('../config')
|
const config = require('../config')
|
||||||
@@ -37,7 +36,7 @@ module.exports = function (sequelize, DataTypes) {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
unique: true,
|
unique: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: shortId.generate
|
defaultValue: () => nanoid.nanoid(10)
|
||||||
},
|
},
|
||||||
alias: {
|
alias: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
@@ -297,8 +296,12 @@ module.exports = function (sequelize, DataTypes) {
|
|||||||
parseNoteIdByShortId: function (_callback) {
|
parseNoteIdByShortId: function (_callback) {
|
||||||
// try to parse note id by shortId
|
// try to parse note id by shortId
|
||||||
try {
|
try {
|
||||||
if (shortId.isValid(noteId)) {
|
// old short ids generated by the `shortid` package could be from 7 to 14 characters long
|
||||||
|
// new ones generated by the `nanoid` package are always 10 characters long
|
||||||
|
if (noteId && noteId.length >= 7 && noteId.length <= 14) {
|
||||||
Note.findOne({
|
Note.findOne({
|
||||||
|
// MariaDB and MySQL do case-insensitive comparison by default (unless a collation charset like utf8mb4 is used)
|
||||||
|
// The binary conversion ensures, case-sensitive comparison.
|
||||||
where: utils.isMySQL(sequelize)
|
where: utils.isMySQL(sequelize)
|
||||||
? sequelize.where(sequelize.fn('BINARY', sequelize.col('shortid')), noteId)
|
? sequelize.where(sequelize.fn('BINARY', sequelize.col('shortid')), noteId)
|
||||||
: {
|
: {
|
||||||
@@ -344,7 +347,9 @@ module.exports = function (sequelize, DataTypes) {
|
|||||||
title = meta.title
|
title = meta.title
|
||||||
} else {
|
} else {
|
||||||
const h1s = $('h1')
|
const h1s = $('h1')
|
||||||
if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { title = S(h1s.first().text()).stripTags().s }
|
if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) {
|
||||||
|
title = h1s.first().text().trim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!title) title = 'Untitled'
|
if (!title) title = 'Untitled'
|
||||||
return title
|
return title
|
||||||
@@ -374,7 +379,7 @@ module.exports = function (sequelize, DataTypes) {
|
|||||||
if (/^tags/gmi.test($(value).text())) {
|
if (/^tags/gmi.test($(value).text())) {
|
||||||
const codes = $(value).find('code')
|
const codes = $(value).find('code')
|
||||||
for (let i = 0; i < codes.length; i++) {
|
for (let i = 0; i < codes.length; i++) {
|
||||||
const text = S($(codes[i]).text().trim()).stripTags().s
|
const text = $(codes[i]).text().trim()
|
||||||
if (text) rawtags.push(text)
|
if (text) rawtags.push(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const Sequelize = require('sequelize')
|
|||||||
const async = require('async')
|
const async = require('async')
|
||||||
const moment = require('moment')
|
const moment = require('moment')
|
||||||
const childProcess = require('child_process')
|
const childProcess = require('child_process')
|
||||||
const shortId = require('shortid')
|
const nanoid = require('nanoid')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const Op = Sequelize.Op
|
const Op = Sequelize.Op
|
||||||
@@ -44,7 +44,7 @@ function createDmpWorker () {
|
|||||||
|
|
||||||
function sendDmpWorker (data, callback) {
|
function sendDmpWorker (data, callback) {
|
||||||
if (!dmpWorker) dmpWorker = createDmpWorker()
|
if (!dmpWorker) dmpWorker = createDmpWorker()
|
||||||
const cacheKey = Date.now() + '_' + shortId.generate()
|
const cacheKey = Date.now() + '_' + nanoid.nanoid()
|
||||||
dmpCallbackCache[cacheKey] = callback
|
dmpCallbackCache[cacheKey] = callback
|
||||||
data = Object.assign(data, {
|
data = Object.assign(data, {
|
||||||
cacheKey
|
cacheKey
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
// external modules
|
// external modules
|
||||||
const shortId = require('shortid')
|
const nanoid = require('nanoid')
|
||||||
|
|
||||||
module.exports = function (sequelize, DataTypes) {
|
module.exports = function (sequelize, DataTypes) {
|
||||||
const Temp = sequelize.define('Temp', {
|
const Temp = sequelize.define('Temp', {
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
defaultValue: shortId.generate
|
defaultValue: nanoid.nanoid
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: DataTypes.TEXT
|
type: DataTypes.TEXT
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
const cookie = require('cookie')
|
const cookie = require('cookie')
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require('cookie-parser')
|
||||||
const async = require('async')
|
const async = require('async')
|
||||||
const randomcolor = require('randomcolor')
|
|
||||||
const Chance = require('chance')
|
const Chance = require('chance')
|
||||||
const chance = new Chance()
|
const chance = new Chance()
|
||||||
const moment = require('moment')
|
const moment = require('moment')
|
||||||
@@ -178,9 +177,9 @@ function finishUpdateNote (note, _note, callback) {
|
|||||||
// clean when user not in any rooms or user not in connected list
|
// clean when user not in any rooms or user not in connected list
|
||||||
setInterval(function () {
|
setInterval(function () {
|
||||||
async.each(Object.keys(users), function (key, callback) {
|
async.each(Object.keys(users), function (key, callback) {
|
||||||
let socket = realtime.io.sockets.connected[key]
|
let socket = realtime.io.sockets.sockets.get(key)
|
||||||
if ((!socket && users[key]) ||
|
if ((!socket && users[key]) ||
|
||||||
(socket && (!socket.rooms || socket.rooms.length <= 0))) {
|
(socket && (!socket.rooms || socket.rooms.size <= 0))) {
|
||||||
logger.debug(`cleaner found redundant user: ${key}`)
|
logger.debug(`cleaner found redundant user: ${key}`)
|
||||||
if (!socket) {
|
if (!socket) {
|
||||||
socket = {
|
socket = {
|
||||||
@@ -711,7 +710,7 @@ function connection (socket) {
|
|||||||
|
|
||||||
// initialize user data
|
// initialize user data
|
||||||
// random color
|
// random color
|
||||||
let color = randomcolor()
|
let color = chance.color()
|
||||||
// make sure color not duplicated or reach max random count
|
// make sure color not duplicated or reach max random count
|
||||||
if (notes[noteId]) {
|
if (notes[noteId]) {
|
||||||
let randomcount = 0
|
let randomcount = 0
|
||||||
@@ -724,7 +723,7 @@ function connection (socket) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (found) {
|
if (found) {
|
||||||
color = randomcolor()
|
color = chance.color()
|
||||||
randomcount++
|
randomcount++
|
||||||
}
|
}
|
||||||
} while (found && randomcount < maxrandomcount)
|
} while (found && randomcount < maxrandomcount)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
// external modules
|
// external modules
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fetch = require('node-fetch')
|
|
||||||
// core
|
// core
|
||||||
const config = require('./config')
|
const config = require('./config')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ function parseProfile (data) {
|
|||||||
const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
|
const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
|
||||||
const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)
|
const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)
|
||||||
|
|
||||||
|
if (id === undefined && username === undefined) {
|
||||||
|
logger.error('oauth2 auth failed: id and username are undefined')
|
||||||
|
throw new Error('User ID or Username required')
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: id || username,
|
id: id || username,
|
||||||
username,
|
username,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const Router = require('express').Router
|
const Router = require('express').Router
|
||||||
const passport = require('passport')
|
const passport = require('passport')
|
||||||
const SamlStrategy = require('passport-saml').Strategy
|
const SamlStrategy = require('@node-saml/passport-saml').Strategy
|
||||||
const config = require('../../../config')
|
const config = require('../../../config')
|
||||||
const models = require('../../../models')
|
const models = require('../../../models')
|
||||||
const logger = require('../../../logger')
|
const logger = require('../../../logger')
|
||||||
@@ -12,7 +12,9 @@ const intersection = function (array1, array2) { return array1.filter((n) => arr
|
|||||||
|
|
||||||
const samlAuth = module.exports = Router()
|
const samlAuth = module.exports = Router()
|
||||||
|
|
||||||
passport.use(new SamlStrategy({
|
passport.use(
|
||||||
|
new SamlStrategy(
|
||||||
|
{
|
||||||
callbackUrl: config.serverURL + '/auth/saml/callback',
|
callbackUrl: config.serverURL + '/auth/saml/callback',
|
||||||
entryPoint: config.saml.idpSsoUrl,
|
entryPoint: config.saml.idpSsoUrl,
|
||||||
issuer: config.saml.issuer || config.serverURL,
|
issuer: config.saml.issuer || config.serverURL,
|
||||||
@@ -25,7 +27,7 @@ passport.use(new SamlStrategy({
|
|||||||
logger.error(`SAML client certificate: ${e.message}`)
|
logger.error(`SAML client certificate: ${e.message}`)
|
||||||
}
|
}
|
||||||
}()),
|
}()),
|
||||||
cert: (function () {
|
idpCert: (function () {
|
||||||
try {
|
try {
|
||||||
return fs.readFileSync(config.saml.idpCert, 'utf-8')
|
return fs.readFileSync(config.saml.idpCert, 'utf-8')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -34,8 +36,12 @@ passport.use(new SamlStrategy({
|
|||||||
}
|
}
|
||||||
}()),
|
}()),
|
||||||
identifierFormat: config.saml.identifierFormat,
|
identifierFormat: config.saml.identifierFormat,
|
||||||
disableRequestedAuthnContext: config.saml.disableRequestedAuthnContext
|
disableRequestedAuthnContext: config.saml.disableRequestedAuthnContext,
|
||||||
}, function (user, done) {
|
wantAssertionsSigned: config.saml.wantAssertionsSigned,
|
||||||
|
wantAuthnResponseSigned: config.saml.wantAuthnResponseSigned
|
||||||
|
},
|
||||||
|
// sign-in
|
||||||
|
function (user, done) {
|
||||||
// check authorization if needed
|
// check authorization if needed
|
||||||
if (config.saml.externalGroups && config.saml.groupAttribute) {
|
if (config.saml.externalGroups && config.saml.groupAttribute) {
|
||||||
const externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute])
|
const externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute])
|
||||||
@@ -52,14 +58,15 @@ passport.use(new SamlStrategy({
|
|||||||
}
|
}
|
||||||
// user creation
|
// user creation
|
||||||
const uuid = user[config.saml.attribute.id] || user.nameID
|
const uuid = user[config.saml.attribute.id] || user.nameID
|
||||||
|
if (!uuid) {
|
||||||
|
logger.error('saml auth failed: id not found')
|
||||||
|
return done('Permission denied', null)
|
||||||
|
}
|
||||||
const profile = {
|
const profile = {
|
||||||
provider: 'saml',
|
provider: 'saml',
|
||||||
id: 'SAML-' + uuid,
|
id: 'SAML-' + uuid,
|
||||||
username: user[config.saml.attribute.username] || user.nameID,
|
username: user[config.saml.attribute.username] || user.nameID,
|
||||||
emails: user[config.saml.attribute.email] ? [user[config.saml.attribute.email]] : []
|
emails: user[config.saml.attribute.email] ? [user[config.saml.attribute.email]] : config.saml.identifierFormat === 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' ? [user.nameID] : []
|
||||||
}
|
|
||||||
if (profile.emails.length === 0 && config.saml.identifierFormat === 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress') {
|
|
||||||
profile.emails.push(user.nameID)
|
|
||||||
}
|
}
|
||||||
const stringifiedProfile = JSON.stringify(profile)
|
const stringifiedProfile = JSON.stringify(profile)
|
||||||
models.User.findOrCreate({
|
models.User.findOrCreate({
|
||||||
@@ -90,20 +97,38 @@ passport.use(new SamlStrategy({
|
|||||||
logger.error('saml auth failed: ' + err.message)
|
logger.error('saml auth failed: ' + err.message)
|
||||||
return done(err, null)
|
return done(err, null)
|
||||||
})
|
})
|
||||||
}))
|
},
|
||||||
|
// logout
|
||||||
|
function (profile, done) {
|
||||||
|
return done(null, profile)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
samlAuth.get('/auth/saml',
|
samlAuth.get('/auth/saml',
|
||||||
passport.authenticate('saml', {
|
passport.authenticate('saml', {
|
||||||
successReturnToOrRedirect: config.serverURL + '/',
|
failureRedirect: config.serverURL + '/',
|
||||||
failureRedirect: config.serverURL + '/'
|
failureFlash: true
|
||||||
})
|
}),
|
||||||
|
function (req, res) {
|
||||||
|
res.redirect('/')
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
samlAuth.post('/auth/saml/callback', urlencodedParser,
|
samlAuth.use('/auth/saml/callback', urlencodedParser,
|
||||||
|
function (req, res, next) {
|
||||||
|
if (req.method !== 'GET' && req.method !== 'POST') {
|
||||||
|
return res.status(405).end()
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
},
|
||||||
passport.authenticate('saml', {
|
passport.authenticate('saml', {
|
||||||
successReturnToOrRedirect: config.serverURL + '/',
|
successReturnToOrRedirect: config.serverURL + '/',
|
||||||
failureRedirect: config.serverURL + '/'
|
failureRedirect: config.serverURL + '/'
|
||||||
})
|
}),
|
||||||
|
function (req, res) {
|
||||||
|
res.redirect('/')
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
samlAuth.get('/auth/saml/metadata', function (req, res) {
|
samlAuth.get('/auth/saml/metadata', function (req, res) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
const logger = require('../../logger')
|
const logger = require('../../logger')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const fetch = require('node-fetch')
|
|
||||||
|
|
||||||
exports.uploadImage = function (imagePath, callback) {
|
exports.uploadImage = function (imagePath, callback) {
|
||||||
if (!callback || typeof callback !== 'function') {
|
if (!callback || typeof callback !== 'function') {
|
||||||
|
|||||||
@@ -57,13 +57,17 @@ async function checkUploadType (filePath) {
|
|||||||
|
|
||||||
// upload image
|
// upload image
|
||||||
imageRouter.post('/uploadimage', function (req, res) {
|
imageRouter.post('/uploadimage', function (req, res) {
|
||||||
|
const uploadsEnabled = config.enableUploads
|
||||||
|
if (uploadsEnabled === 'none') {
|
||||||
|
logger.error('Image upload error: Uploads are disabled')
|
||||||
|
return errors.errorForbidden(res)
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!req.isAuthenticated() &&
|
uploadsEnabled === 'registered' &&
|
||||||
!config.allowAnonymous &&
|
!req.isAuthenticated()
|
||||||
!config.allowAnonymousEdits
|
|
||||||
) {
|
) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Image upload error: Anonymous edits and therefore uploads are not allowed'
|
'Image upload error: Anonymous uploads are not allowed'
|
||||||
)
|
)
|
||||||
return errors.errorForbidden(res)
|
return errors.errorForbidden(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const { rateLimit } = require('express-rate-limit')
|
const { rateLimit, ipKeyGenerator } = require('express-rate-limit')
|
||||||
const errors = require('../../errors')
|
const errors = require('../../errors')
|
||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ const determineKey = (req) => {
|
|||||||
if (req.user) {
|
if (req.user) {
|
||||||
return req.user.id
|
return req.user.id
|
||||||
}
|
}
|
||||||
return req.header('cf-connecting-ip') || req.ip
|
return ipKeyGenerator(req.header('cf-connecting-ip') || req.ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
// limits requests to user endpoints (login, signup) to 10 requests per 5 minutes
|
// limits requests to user endpoints (login, signup) to 10 requests per 5 minutes
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ const config = require('../../config')
|
|||||||
toobusy.maxLag(config.tooBusyLag)
|
toobusy.maxLag(config.tooBusyLag)
|
||||||
|
|
||||||
module.exports = function (req, res, next) {
|
module.exports = function (req, res, next) {
|
||||||
|
// We dont want to return "toobusy" errors for healthchecks, as that
|
||||||
|
// will cause the process to be restarted
|
||||||
|
if (req.baseUrl === '/_health') {
|
||||||
|
next()
|
||||||
|
}
|
||||||
if (toobusy()) {
|
if (toobusy()) {
|
||||||
errors.errorServiceUnavailable(res)
|
errors.errorServiceUnavailable(res)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const models = require('../../models')
|
|||||||
const logger = require('../../logger')
|
const logger = require('../../logger')
|
||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
const errors = require('../../errors')
|
const errors = require('../../errors')
|
||||||
const shortId = require('shortid')
|
const nanoid = require('nanoid')
|
||||||
const moment = require('moment')
|
const moment = require('moment')
|
||||||
const querystring = require('querystring')
|
const querystring = require('querystring')
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ exports.createGist = function createGist (req, res, note) {
|
|||||||
client_id: config.github.clientID,
|
client_id: config.github.clientID,
|
||||||
redirect_uri: config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist',
|
redirect_uri: config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist',
|
||||||
scope: 'gist',
|
scope: 'gist',
|
||||||
state: shortId.generate()
|
state: nanoid.nanoid()
|
||||||
}
|
}
|
||||||
const query = querystring.stringify(data)
|
const query = querystring.stringify(data)
|
||||||
res.redirect('https://github.com/login/oauth/authorize?' + query)
|
res.redirect('https://github.com/login/oauth/authorize?' + query)
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ statusRouter.get('/config', function (req, res) {
|
|||||||
DROPBOX_APP_KEY: config.dropbox.appKey,
|
DROPBOX_APP_KEY: config.dropbox.appKey,
|
||||||
allowedUploadMimeTypes: config.allowedUploadMimeTypes,
|
allowedUploadMimeTypes: config.allowedUploadMimeTypes,
|
||||||
linkifyHeaderStyle: config.linkifyHeaderStyle,
|
linkifyHeaderStyle: config.linkifyHeaderStyle,
|
||||||
cookiePolicy: config.cookiePolicy
|
cookiePolicy: config.cookiePolicy,
|
||||||
|
enableUploads: config.enableUploads
|
||||||
}
|
}
|
||||||
res.set({
|
res.set({
|
||||||
'Cache-Control': 'private', // only cache by client
|
'Cache-Control': 'private', // only cache by client
|
||||||
|
|||||||
89
package.json
89
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "the_hedgedoc_elf",
|
"name": "the_hedgedoc_elf",
|
||||||
"version": "1.10.2",
|
"version": "1.10.3_LailaTheElf",
|
||||||
"description": "The best platform to write and share markdown.",
|
"description": "The best platform to write and share markdown.",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
@@ -18,40 +18,40 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hedgedoc/meta-marked": "14.1.0",
|
"@hedgedoc/meta-marked": "14.1.0",
|
||||||
"@node-saml/passport-saml": "5.0.0",
|
"@node-saml/passport-saml": "5.1.0",
|
||||||
"@passport-next/passport-openid": "1.0.0",
|
"@passport-next/passport-openid": "1.0.0",
|
||||||
"Idle.Js": "git+https://github.com/shawnmclean/Idle.js#commit=2b57cc6e49d177b7ddce0cca00ef5cbe07453541",
|
"Idle.Js": "git+https://github.com/shawnmclean/Idle.js#commit=2b57cc6e49d177b7ddce0cca00ef5cbe07453541",
|
||||||
"archiver": "6.0.2",
|
"archiver": "7.0.1",
|
||||||
"async": "3.2.6",
|
"async": "3.2.6",
|
||||||
"aws-sdk": "2.1692.0",
|
"aws-sdk": "2.1692.0",
|
||||||
"azure-storage": "2.10.7",
|
"azure-storage": "2.10.7",
|
||||||
"base64url": "3.0.1",
|
"base64url": "3.0.1",
|
||||||
"body-parser": "1.20.3",
|
"body-parser": "2.2.1",
|
||||||
"chance": "1.1.12",
|
"chance": "1.1.13",
|
||||||
"cheerio": "0.22.0",
|
"cheerio": "0.22.0",
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"compression": "1.7.5",
|
"compression": "1.8.1",
|
||||||
"connect-flash": "0.1.1",
|
"connect-flash": "0.1.1",
|
||||||
"connect-session-sequelize": "7.1.7",
|
"connect-session-sequelize": "8.0.2",
|
||||||
"cookie": "1.0.2",
|
"cookie": "1.0.2",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"diff-match-patch": "git+https://github.com/hackmdio/diff-match-patch.git#commit=59a9395ad9fe143e601e7ae5765ed943bdd2b11e",
|
"diff-match-patch": "git+https://github.com/hackmdio/diff-match-patch.git#commit=59a9395ad9fe143e601e7ae5765ed943bdd2b11e",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"express-rate-limit": "7.5.0",
|
"express-rate-limit": "8.2.1",
|
||||||
"express-session": "1.18.1",
|
"express-session": "1.18.2",
|
||||||
"file-type": "20.0.1",
|
"file-type": "21.1.1",
|
||||||
"formidable": "2.1.2",
|
"formidable": "3.5.4",
|
||||||
"graceful-fs": "4.2.11",
|
"graceful-fs": "4.2.11",
|
||||||
"helmet": "8.0.0",
|
"helmet": "8.1.0",
|
||||||
"i18n": "0.15.1",
|
"i18n": "0.15.3",
|
||||||
"is-svg": "4.4.0",
|
"is-svg": "4.4.0",
|
||||||
"jsdom-nogyp": "0.8.3",
|
"jsdom-nogyp": "0.8.3",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lutim": "1.0.3",
|
"lutim": "1.0.3",
|
||||||
"lz-string": "git+https://github.com/hackmdio/lz-string.git#commit=6edfccb79cd8c210f03fd3bf18e41ca144fbeefb",
|
"lz-string": "git+https://github.com/hackmdio/lz-string.git#commit=6edfccb79cd8c210f03fd3bf18e41ca144fbeefb",
|
||||||
"mariadb": "3.4.0",
|
"mariadb": "3.4.5",
|
||||||
"markdown-it": "13.0.2",
|
"markdown-it": "13.0.2",
|
||||||
"markdown-it-abbr": "1.0.4",
|
"markdown-it-abbr": "1.0.4",
|
||||||
"markdown-it-container": "3.0.0",
|
"markdown-it-container": "3.0.0",
|
||||||
@@ -68,11 +68,11 @@
|
|||||||
"mattermost": "3.4.0",
|
"mattermost": "3.4.0",
|
||||||
"method-override": "3.0.0",
|
"method-override": "3.0.0",
|
||||||
"minimist": "1.2.8",
|
"minimist": "1.2.8",
|
||||||
"minio": "7.1.3",
|
"minio": "8.0.6",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"morgan": "1.10.0",
|
"morgan": "1.10.1",
|
||||||
"mysql2": "3.12.0",
|
"mysql2": "3.15.3",
|
||||||
"node-fetch": "2.7.0",
|
"nanoid": "3.3.11",
|
||||||
"passport": "patch:passport@npm%3A0.7.0#~/.yarn/patches/passport-npm-0.7.0-df02531736.patch",
|
"passport": "patch:passport@npm%3A0.7.0#~/.yarn/patches/passport-npm-0.7.0-df02531736.patch",
|
||||||
"passport-dropbox-oauth2": "1.1.0",
|
"passport-dropbox-oauth2": "1.1.0",
|
||||||
"passport-facebook": "3.0.0",
|
"passport-facebook": "3.0.0",
|
||||||
@@ -84,27 +84,24 @@
|
|||||||
"passport-oauth2": "1.8.0",
|
"passport-oauth2": "1.8.0",
|
||||||
"passport-twitter": "1.0.4",
|
"passport-twitter": "1.0.4",
|
||||||
"passport.socketio": "3.7.0",
|
"passport.socketio": "3.7.0",
|
||||||
"pdfobject": "2.3.0",
|
"pdfobject": "2.3.1",
|
||||||
"pg": "8.13.1",
|
"pg": "8.16.3",
|
||||||
"pg-hstore": "2.3.4",
|
"pg-hstore": "2.3.4",
|
||||||
"prom-client": "15.1.3",
|
"prom-client": "15.1.3",
|
||||||
"prometheus-api-metrics": "3.2.2",
|
"prometheus-api-metrics": "4.0.0",
|
||||||
"randomcolor": "0.6.2",
|
|
||||||
"readline-sync": "1.4.10",
|
"readline-sync": "1.4.10",
|
||||||
"rimraf": "5.0.10",
|
"rimraf": "6.1.2",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"scrypt-kdf": "2.0.1",
|
"scrypt-kdf": "2.0.1",
|
||||||
"sequelize": "5.22.5",
|
"sequelize": "5.22.5",
|
||||||
"shortid": "2.2.17",
|
"socket.io": "4.8.1",
|
||||||
"socket.io": "2.5.1",
|
|
||||||
"sqlite3": "5.1.7",
|
"sqlite3": "5.1.7",
|
||||||
"store": "2.0.12",
|
"store": "2.0.12",
|
||||||
"string": "3.3.3",
|
|
||||||
"toobusy-js": "0.5.1",
|
"toobusy-js": "0.5.1",
|
||||||
"umzug": "2.3.0",
|
"umzug": "2.3.0",
|
||||||
"uuid": "11.0.5",
|
"uuid": "11.1.0",
|
||||||
"validator": "13.12.0",
|
"validator": "13.15.23",
|
||||||
"winston": "3.17.0",
|
"winston": "3.18.3",
|
||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -141,10 +138,10 @@
|
|||||||
"url": "https://gitea.finnvanreenen.nl/LailaTheElf/hedgedoc.git"
|
"url": "https://gitea.finnvanreenen.nl/LailaTheElf/hedgedoc.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "3.2.0",
|
"@eslint/eslintrc": "3.3.1",
|
||||||
"@eslint/js": "9.19.0",
|
"@eslint/js": "9.39.1",
|
||||||
"@hedgedoc/codemirror-5": "5.65.12",
|
"@hedgedoc/codemirror-5": "5.65.12",
|
||||||
"abcjs": "6.4.4",
|
"abcjs": "6.5.2",
|
||||||
"babel-cli": "6.26.0",
|
"babel-cli": "6.26.0",
|
||||||
"babel-core": "6.26.3",
|
"babel-core": "6.26.3",
|
||||||
"babel-loader": "7.1.5",
|
"babel-loader": "7.1.5",
|
||||||
@@ -157,12 +154,11 @@
|
|||||||
"copy-webpack-plugin": "6.4.1",
|
"copy-webpack-plugin": "6.4.1",
|
||||||
"css-loader": "5.2.7",
|
"css-loader": "5.2.7",
|
||||||
"emojify.js": "1.1.0",
|
"emojify.js": "1.1.0",
|
||||||
"esbuild-loader": "4.2.2",
|
"esbuild-loader": "4.4.0",
|
||||||
"escape-html": "1.0.3",
|
"eslint": "9.39.1",
|
||||||
"eslint": "9.19.0",
|
|
||||||
"eslint-config-standard": "17.1.0",
|
"eslint-config-standard": "17.1.0",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-n": "17.15.1",
|
"eslint-plugin-n": "17.23.1",
|
||||||
"eslint-plugin-promise": "7.2.1",
|
"eslint-plugin-promise": "7.2.1",
|
||||||
"eslint-plugin-standard": "5.0.0",
|
"eslint-plugin-standard": "5.0.0",
|
||||||
"exports-loader": "1.1.1",
|
"exports-loader": "1.1.1",
|
||||||
@@ -171,39 +167,38 @@
|
|||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"flowchart.js": "1.18.0",
|
"flowchart.js": "1.18.0",
|
||||||
"fork-awesome": "1.2.0",
|
"fork-awesome": "1.2.0",
|
||||||
"gist-embed": "2.6.0",
|
"globals": "16.5.0",
|
||||||
"globals": "15.14.0",
|
|
||||||
"highlight.js": "10.7.3",
|
"highlight.js": "10.7.3",
|
||||||
"html-webpack-plugin": "4.5.2",
|
"html-webpack-plugin": "4.5.2",
|
||||||
"imports-loader": "1.2.0",
|
"imports-loader": "1.2.0",
|
||||||
"ionicons": "2.0.1",
|
"ionicons": "2.0.1",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery-mousewheel": "3.1.13",
|
"jquery-mousewheel": "3.2.2",
|
||||||
"jquery-ui": "1.14.1",
|
"jquery-ui": "1.14.1",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
"js-sequence-diagrams": "git+https://github.com/hedgedoc/js-sequence-diagrams.git",
|
"js-sequence-diagrams": "git+https://github.com/hedgedoc/js-sequence-diagrams.git",
|
||||||
"js-yaml": "3.14.1",
|
"js-yaml": "3.14.2",
|
||||||
"jsonlint": "1.6.3",
|
"jsonlint": "1.6.3",
|
||||||
"keymaster": "1.6.2",
|
"keymaster": "1.6.2",
|
||||||
"less": "4.2.2",
|
"less": "4.4.2",
|
||||||
"less-loader": "7.3.0",
|
"less-loader": "7.3.0",
|
||||||
"list.js": "2.3.1",
|
"list.js": "2.3.1",
|
||||||
"mathjax": "2.7.9",
|
"mathjax": "2.7.9",
|
||||||
"mermaid": "11.4.1",
|
"mermaid": "11.4.1",
|
||||||
"mini-css-extract-plugin": "1.6.2",
|
"mini-css-extract-plugin": "1.6.2",
|
||||||
"mocha": "11.1.0",
|
"mocha": "11.7.5",
|
||||||
"mock-require": "3.0.3",
|
"mock-require": "3.0.3",
|
||||||
"optimize-css-assets-webpack-plugin": "6.0.1",
|
"optimize-css-assets-webpack-plugin": "6.0.1",
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.30.0",
|
||||||
"raphael": "2.3.0",
|
"raphael": "2.3.0",
|
||||||
"remark-cli": "12.0.1",
|
"remark-cli": "12.0.1",
|
||||||
"remark-preset-lint-markdown-style-guide": "5.1.3",
|
"remark-preset-lint-markdown-style-guide": "5.1.3",
|
||||||
"reveal.js": "3.9.2",
|
"reveal.js": "3.9.2",
|
||||||
"select2": "3.5.2-browserify",
|
"select2": "3.5.2-browserify",
|
||||||
"socket.io-client": "2.5.0",
|
"socket.io-client": "4.8.1",
|
||||||
"spin.js": "4.1.2",
|
"spin.js": "4.1.2",
|
||||||
"string-loader": "0.0.1",
|
"string-loader": "0.0.1",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.2",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "4.1.1",
|
||||||
"velocity-animate": "1.5.2",
|
"velocity-animate": "1.5.2",
|
||||||
"visibilityjs": "2.0.2",
|
"visibilityjs": "2.0.2",
|
||||||
@@ -217,5 +212,5 @@
|
|||||||
"bufferutil": "4.0.9",
|
"bufferutil": "4.0.9",
|
||||||
"utf-8-validate": "6.0.5"
|
"utf-8-validate": "6.0.5"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.6.0"
|
"packageManager": "yarn@4.12.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,11 @@
|
|||||||
-webkit-transition: opacity 0.2s; /* Safari */
|
-webkit-transition: opacity 0.2s; /* Safari */
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
iframe.github-gist-frame {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
height: 32rem;
|
||||||
|
}
|
||||||
|
|
||||||
.slideshare .inner,
|
.slideshare .inner,
|
||||||
.speakerdeck .inner {
|
.speakerdeck .inner {
|
||||||
|
|||||||
@@ -238,10 +238,6 @@ When you’re a carpenter making a beautiful chest of drawers, you’re not goin
|
|||||||
|
|
||||||
{%gist schacon/4277%}
|
{%gist schacon/4277%}
|
||||||
|
|
||||||
#### SlideShare
|
|
||||||
|
|
||||||
{%slideshare briansolis/26-disruptive-technology-trends-2016-2018-56796196 %}
|
|
||||||
|
|
||||||
#### PDF
|
#### PDF
|
||||||
|
|
||||||
**Caution: this might be blocked by your browser if not using an `https` URL.**
|
**Caution: this might be blocked by your browser if not using an `https` URL.**
|
||||||
|
|||||||
@@ -2,7 +2,41 @@
|
|||||||
|
|
||||||
## <i class="fa fa-tag"></i> 1.x.x <i class="fa fa-calendar-o"></i> UNRELEASED
|
## <i class="fa fa-tag"></i> 1.x.x <i class="fa fa-calendar-o"></i> UNRELEASED
|
||||||
|
|
||||||
## <i class="fa fa-tag"></i> 1.10.1 <i class="fa fa-calendar-o"></i> 2024-02-02
|
### Enhancements
|
||||||
|
- Add `enableUploads` (`CMD_ENABLE_UPLOADS`) config option to restrict uploads to `registered` users, `all` users or
|
||||||
|
`none` to completely disable uploads.
|
||||||
|
- Allow links to protocols such as xmpp, webcal or geo
|
||||||
|
- Switch from deprecated shortid to nanoid module, with 10 character long aliases in "public" links
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- Ignore the healthcheck endpoint in the "too busy" limiter
|
||||||
|
|
||||||
|
## <i class="fa fa-tag"></i> 1.10.3 <i class="fa fa-calendar-o"></i> 2025-04-09
|
||||||
|
|
||||||
|
### Security fixes
|
||||||
|
|
||||||
|
This release fixes a security issue of a possible XSS exploit which can be planted via a malicous SVG file upload.
|
||||||
|
|
||||||
|
See [GHSA-3983-rrqh-mvx5](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-3983-rrqh-mvx5) for more details
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
- Add config options `CMD_SAML_WANT_ASSERTIONS_SIGNED` and `CMD_SAML_WANT_AUTHN_RESPONSE_SIGNED` for SAML auth, since
|
||||||
|
some instances didn't comply with the new defaults of `@node-saml/passport-saml`
|
||||||
|
|
||||||
|
## <i class="fa fa-tag"></i> 1.10.2 <i class="fa fa-calendar-o"></i> 2025-02-14
|
||||||
|
|
||||||
|
**PLEASE CHECK THIS IF YOU USE SAML AUTHENTICATION:**
|
||||||
|
This release had to set default values for the username and email address attribute mapping for SAML authentication for
|
||||||
|
security reasons.
|
||||||
|
If you use SAML authentication, please make sure to update your SAML configuration accordingly.
|
||||||
|
See: https://docs.hedgedoc.org/configuration/#saml-login `CMD_SAML_ATTRIBUTE_USERNAME` or `CMD_SAML_ATTRIBUTE_EMAIL`
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- Check if a valid user id is present when using OAuth2
|
||||||
|
- Abort SAML login if NameID is undefined instead of logging in with a user named "undefined" (Thanks [@Haanifee](https://github.com/Haanifee))
|
||||||
|
- Set default values for username and email attribute mapping in SAML configuration
|
||||||
|
|
||||||
|
## <i class="fa fa-tag"></i> 1.10.1 <i class="fa fa-calendar-o"></i> 2025-02-02
|
||||||
|
|
||||||
This release fixes a security issue where brute-forcing local email/passwords is possible because of missing rate-limits.
|
This release fixes a security issue where brute-forcing local email/passwords is possible because of missing rate-limits.
|
||||||
We recommend upgrading as soon as possible, if you use local logins.
|
We recommend upgrading as soon as possible, if you use local logins.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
|
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import List from 'list.js'
|
import List from 'list.js'
|
||||||
import S from 'string'
|
import { unescapeHtml } from './utils'
|
||||||
|
|
||||||
require('./locale')
|
require('./locale')
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ function buildTagsFilter (tags) {
|
|||||||
for (let i = 0; i < tags.length; i++) {
|
for (let i = 0; i < tags.length; i++) {
|
||||||
tags[i] = {
|
tags[i] = {
|
||||||
id: i,
|
id: i,
|
||||||
text: S(tags[i]).unescapeHTML().s
|
text: unescapeHtml(tags[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
filtertags = tags
|
filtertags = tags
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
import Prism from 'prismjs'
|
import Prism from 'prismjs'
|
||||||
import PDFObject from 'pdfobject'
|
import PDFObject from 'pdfobject'
|
||||||
import S from 'string'
|
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import escapeHTML from 'escape-html'
|
import filterXSS from 'xss'
|
||||||
|
|
||||||
import getUIElements from './lib/editor/ui-elements'
|
import getUIElements from './lib/editor/ui-elements'
|
||||||
|
import { escapeHtml, unescapeHtml } from './utils'
|
||||||
|
|
||||||
import markdownit from 'markdown-it'
|
import markdownit from 'markdown-it'
|
||||||
import markdownitContainer from 'markdown-it-container'
|
import markdownitContainer from 'markdown-it-container'
|
||||||
@@ -15,8 +15,6 @@ import markdownitContainer from 'markdown-it-container'
|
|||||||
/* Defined regex markdown it plugins */
|
/* Defined regex markdown it plugins */
|
||||||
import Plugin from 'markdown-it-regexp'
|
import Plugin from 'markdown-it-regexp'
|
||||||
|
|
||||||
import 'gist-embed'
|
|
||||||
|
|
||||||
require('prismjs/themes/prism.css')
|
require('prismjs/themes/prism.css')
|
||||||
require('prismjs/components/prism-wiki')
|
require('prismjs/components/prism-wiki')
|
||||||
require('prismjs/components/prism-haskell')
|
require('prismjs/components/prism-haskell')
|
||||||
@@ -168,7 +166,11 @@ export function renderTags (view) {
|
|||||||
|
|
||||||
function slugifyWithUTF8 (text) {
|
function slugifyWithUTF8 (text) {
|
||||||
// remove HTML tags and trim spaces
|
// remove HTML tags and trim spaces
|
||||||
let newText = S(text).trim().stripTags().s
|
let newText = filterXSS(text.trim(), {
|
||||||
|
whiteList: {},
|
||||||
|
stripIgnoreTag: true,
|
||||||
|
stripIgnoreTagBody: ['script', 'style']
|
||||||
|
})
|
||||||
// replace space between words with dashes
|
// replace space between words with dashes
|
||||||
newText = newText.replace(/\s+/g, '-')
|
newText = newText.replace(/\s+/g, '-')
|
||||||
// slugify string to make it valid as an attribute
|
// slugify string to make it valid as an attribute
|
||||||
@@ -291,22 +293,14 @@ export function finishView (view) {
|
|||||||
imgPlayiframe(this, 'https://player.vimeo.com/video/')
|
imgPlayiframe(this, 'https://player.vimeo.com/video/')
|
||||||
})
|
})
|
||||||
.each((key, value) => {
|
.each((key, value) => {
|
||||||
const vimeoLink = `https://vimeo.com/${$(value).attr('data-videoid')}`
|
fetch(`https://vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`)
|
||||||
$.ajax({
|
.then(response => response.json())
|
||||||
type: 'GET',
|
.then(data => {
|
||||||
url: `https://vimeo.com/api/oembed.json?url=${encodeURIComponent(vimeoLink)}`,
|
const image = `<img src="${data[0].thumbnail_large}" />`
|
||||||
jsonp: 'callback',
|
|
||||||
dataType: 'jsonp',
|
|
||||||
success (data) {
|
|
||||||
const image = `<img src="${data.thumbnail_url}" />`
|
|
||||||
$(value).prepend(image)
|
$(value).prepend(image)
|
||||||
if (window.viewAjaxCallback) window.viewAjaxCallback()
|
if (window.viewAjaxCallback) window.viewAjaxCallback()
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
.catch(console.error)
|
||||||
// gist
|
|
||||||
view.find('code[data-gist-id]').each((key, value) => {
|
|
||||||
if ($(value).children().length === 0) { $(value).gist(window.viewAjaxCallback) }
|
|
||||||
})
|
})
|
||||||
// sequence diagram
|
// sequence diagram
|
||||||
const sequences = view.find('div.sequence-diagram.raw').removeClass('raw')
|
const sequences = view.find('div.sequence-diagram.raw').removeClass('raw')
|
||||||
@@ -328,7 +322,7 @@ export function finishView (view) {
|
|||||||
svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet')
|
svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$value.unwrap()
|
$value.unwrap()
|
||||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
$value.parent().append(`<div class="alert alert-warning">${escapeHtml(err)}</div>`)
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -353,7 +347,7 @@ export function finishView (view) {
|
|||||||
$value.children().unwrap().unwrap()
|
$value.children().unwrap().unwrap()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$value.unwrap()
|
$value.unwrap()
|
||||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
$value.parent().append(`<div class="alert alert-warning">${escapeHtml(err)}</div>`)
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -375,7 +369,7 @@ export function finishView (view) {
|
|||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$value.unwrap()
|
$value.unwrap()
|
||||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
$value.parent().append(`<div class="alert alert-warning">${escapeHtml(err)}</div>`)
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -398,7 +392,7 @@ export function finishView (view) {
|
|||||||
errormessage = err.str
|
errormessage = err.str
|
||||||
}
|
}
|
||||||
$value.unwrap()
|
$value.unwrap()
|
||||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(errormessage)}</div>`)
|
$value.parent().append(`<div class="alert alert-warning">${escapeHtml(errormessage)}</div>`)
|
||||||
console.warn(errormessage)
|
console.warn(errormessage)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -421,7 +415,7 @@ export function finishView (view) {
|
|||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$value.unwrap()
|
$value.unwrap()
|
||||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
$value.parent().append(`<div class="alert alert-warning">${escapeHtml(err)}</div>`)
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -450,24 +444,12 @@ export function finishView (view) {
|
|||||||
// slideshare
|
// slideshare
|
||||||
view.find('div.slideshare.raw').removeClass('raw')
|
view.find('div.slideshare.raw').removeClass('raw')
|
||||||
.each((key, value) => {
|
.each((key, value) => {
|
||||||
$.ajax({
|
const url = `https://slideshare.com/${$(value).attr('data-slideshareid')}`
|
||||||
type: 'GET',
|
const inner = $('<a>Slideshare</a>')
|
||||||
url: `https://www.slideshare.net/api/oembed/2?url=https://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`,
|
inner.attr('href', url)
|
||||||
jsonp: 'callback',
|
inner.attr('rel', 'noopener noreferrer')
|
||||||
dataType: 'jsonp',
|
inner.attr('target', '_blank')
|
||||||
success (data) {
|
$(value).append(inner)
|
||||||
const $html = $(data.html)
|
|
||||||
const iframe = $html.closest('iframe')
|
|
||||||
const caption = $html.closest('div')
|
|
||||||
const inner = $('<div class="inner"></div>').append(iframe)
|
|
||||||
const height = iframe.attr('height')
|
|
||||||
const width = iframe.attr('width')
|
|
||||||
const ratio = (height / width) * 100
|
|
||||||
inner.css('padding-bottom', `${ratio}%`)
|
|
||||||
$(value).html(inner).append(caption)
|
|
||||||
if (window.viewAjaxCallback) window.viewAjaxCallback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
// speakerdeck
|
// speakerdeck
|
||||||
view.find('div.speakerdeck.raw').removeClass('raw')
|
view.find('div.speakerdeck.raw').removeClass('raw')
|
||||||
@@ -508,24 +490,24 @@ export function finishView (view) {
|
|||||||
value: code
|
value: code
|
||||||
}
|
}
|
||||||
} else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') {
|
} else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') {
|
||||||
code = S(code).unescapeHTML().s
|
code = unescapeHtml(code)
|
||||||
result = {
|
result = {
|
||||||
value: Prism.highlight(code, Prism.languages[reallang])
|
value: Prism.highlight(code, Prism.languages[reallang])
|
||||||
}
|
}
|
||||||
} else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') {
|
} else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') {
|
||||||
code = S(code).unescapeHTML().s
|
code = unescapeHtml(code)
|
||||||
result = {
|
result = {
|
||||||
value: Prism.highlight(code, Prism.languages.wiki)
|
value: Prism.highlight(code, Prism.languages.wiki)
|
||||||
}
|
}
|
||||||
} else if (reallang === 'cmake') {
|
} else if (reallang === 'cmake') {
|
||||||
code = S(code).unescapeHTML().s
|
code = unescapeHtml(code)
|
||||||
result = {
|
result = {
|
||||||
value: Prism.highlight(code, Prism.languages.makefile)
|
value: Prism.highlight(code, Prism.languages.makefile)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
require.ensure([], function (require) {
|
require.ensure([], function (require) {
|
||||||
const hljs = require('highlight.js')
|
const hljs = require('highlight.js')
|
||||||
code = S(code).unescapeHTML().s
|
code = unescapeHtml(code)
|
||||||
const languages = hljs.listLanguages()
|
const languages = hljs.listLanguages()
|
||||||
if (!languages.includes(reallang)) {
|
if (!languages.includes(reallang)) {
|
||||||
result = hljs.highlightAuto(code)
|
result = hljs.highlightAuto(code)
|
||||||
@@ -598,7 +580,7 @@ export function postProcess (code) {
|
|||||||
if (warning && warning.length > 0) {
|
if (warning && warning.length > 0) {
|
||||||
warning.text(md.metaError)
|
warning.text(md.metaError)
|
||||||
} else {
|
} else {
|
||||||
warning = $(`<div id="meta-error" class="alert alert-warning">${escapeHTML(md.metaError)}</div>`)
|
warning = $(`<div id="meta-error" class="alert alert-warning">${escapeHtml(md.metaError)}</div>`)
|
||||||
result.prepend(warning)
|
result.prepend(warning)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -639,8 +621,6 @@ function generateCleanHTML (view) {
|
|||||||
src.find('*[class=""]').removeAttr('class')
|
src.find('*[class=""]').removeAttr('class')
|
||||||
eles.removeAttr('data-startline data-endline')
|
eles.removeAttr('data-startline data-endline')
|
||||||
src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
|
src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
|
||||||
// remove gist content
|
|
||||||
src.find('code[data-gist-id]').children().remove()
|
|
||||||
// disable todo list
|
// disable todo list
|
||||||
src.find('input.task-list-item-checkbox').attr('disabled', '')
|
src.find('input.task-list-item-checkbox').attr('disabled', '')
|
||||||
// replace emoji image path
|
// replace emoji image path
|
||||||
@@ -836,7 +816,7 @@ export function smoothHashScroll () {
|
|||||||
|
|
||||||
function imgPlayiframe (element, src) {
|
function imgPlayiframe (element, src) {
|
||||||
if (!$(element).attr('data-videoid')) return
|
if (!$(element).attr('data-videoid')) return
|
||||||
const iframe = $("<iframe frameborder='0' webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>")
|
const iframe = $("<iframe style='border: none' allowfullscreen></iframe>")
|
||||||
$(iframe).attr('src', `${src + $(element).attr('data-videoid')}?autoplay=1`)
|
$(iframe).attr('src', `${src + $(element).attr('data-videoid')}?autoplay=1`)
|
||||||
$(element).find('img').css('visibility', 'hidden')
|
$(element).find('img').css('visibility', 'hidden')
|
||||||
$(element).append(iframe)
|
$(element).append(iframe)
|
||||||
@@ -987,7 +967,7 @@ export function scrollToHash () {
|
|||||||
|
|
||||||
function highlightRender (code, lang) {
|
function highlightRender (code, lang) {
|
||||||
if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return }
|
if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return }
|
||||||
code = S(code).escapeHTML().s
|
code = escapeHtml(code)
|
||||||
if (lang === 'sequence') {
|
if (lang === 'sequence') {
|
||||||
return `<div class="sequence-diagram raw">${code}</div>`
|
return `<div class="sequence-diagram raw">${code}</div>`
|
||||||
} else if (lang === 'flow') {
|
} else if (lang === 'flow') {
|
||||||
@@ -1156,8 +1136,7 @@ const gistPlugin = new Plugin(
|
|||||||
|
|
||||||
(match, utils) => {
|
(match, utils) => {
|
||||||
const gistid = match[1]
|
const gistid = match[1]
|
||||||
const code = `<code data-gist-id="${gistid}"></code>`
|
return `<iframe sandbox class="github-gist-frame" src="https://gist.github.com/${gistid}.pibb"></iframe>`
|
||||||
return code
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// TOC
|
// TOC
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
/* global serverurl, moment */
|
/* global serverurl, moment */
|
||||||
|
|
||||||
import store from 'store'
|
import store from 'store'
|
||||||
import S from 'string'
|
|
||||||
import LZString from 'lz-string'
|
import LZString from 'lz-string'
|
||||||
import url from 'wurl'
|
import url from 'wurl'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
checkNoteIdValid,
|
checkNoteIdValid,
|
||||||
encodeNoteId
|
encodeNoteId,
|
||||||
|
escapeHtml
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -275,8 +275,8 @@ function parseToHistory (list, notehistory, callback) {
|
|||||||
notehistory[i].fromNow = timestamp.fromNow()
|
notehistory[i].fromNow = timestamp.fromNow()
|
||||||
notehistory[i].time = timestamp.format('llll')
|
notehistory[i].time = timestamp.format('llll')
|
||||||
// prevent XSS
|
// prevent XSS
|
||||||
notehistory[i].text = S(notehistory[i].text).escapeHTML().s
|
notehistory[i].text = escapeHtml(notehistory[i].text)
|
||||||
notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? S(notehistory[i].tags).escapeHTML().s.split(',') : []
|
notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? escapeHtml(notehistory[i].tags).split(',') : []
|
||||||
// add to list
|
// add to list
|
||||||
if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) { list.add(notehistory[i]) }
|
if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) { list.add(notehistory[i]) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,4 +22,3 @@ const $ = require('jquery')
|
|||||||
window.jQuery = $
|
window.jQuery = $
|
||||||
window.$ = $
|
window.$ = $
|
||||||
require('bootstrap')
|
require('bootstrap')
|
||||||
require('gist-embed/gist-embed.min')
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { ot } from '../vendor/ot/ot.min.js'
|
|||||||
import hex2rgb from '../vendor/ot/hex2rgb'
|
import hex2rgb from '../vendor/ot/hex2rgb'
|
||||||
|
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import randomColor from 'randomcolor'
|
import chance from 'chance'
|
||||||
import store from 'store'
|
import store from 'store'
|
||||||
import url from 'wurl'
|
import url from 'wurl'
|
||||||
import { Spinner } from 'spin.js'
|
import { Spinner } from 'spin.js'
|
||||||
@@ -427,7 +427,7 @@ const supportExtraTags = [
|
|||||||
text: '[random color tag]',
|
text: '[random color tag]',
|
||||||
search: '[]',
|
search: '[]',
|
||||||
command: function () {
|
command: function () {
|
||||||
const color = randomColor()
|
const color = chance().color()
|
||||||
return '[color=' + color + ']'
|
return '[color=' + color + ']'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1079,6 +1079,10 @@ function changeMode (type) {
|
|||||||
// add and update tool bar
|
// add and update tool bar
|
||||||
if (!editorInstance.toolBar) {
|
if (!editorInstance.toolBar) {
|
||||||
editorInstance.addToolBar()
|
editorInstance.addToolBar()
|
||||||
|
const uploadButtonVisible = window.enableUploads === 'all' || (window.enableUploads === 'registered' && personalInfo.login)
|
||||||
|
if (!uploadButtonVisible) {
|
||||||
|
$('#uploadImage').remove()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// work around foldGutter might not init properly
|
// work around foldGutter might not init properly
|
||||||
editor.setOption('foldGutter', false)
|
editor.setOption('foldGutter', false)
|
||||||
@@ -2111,11 +2115,11 @@ function updatePermission (newPermission) {
|
|||||||
break
|
break
|
||||||
case 'editable':
|
case 'editable':
|
||||||
label = '<i class="fa fa-shield"></i> Editable'
|
label = '<i class="fa fa-shield"></i> Editable'
|
||||||
title = 'Signed people can edit'
|
title = 'Signed-in people can edit'
|
||||||
break
|
break
|
||||||
case 'limited':
|
case 'limited':
|
||||||
label = '<i class="fa fa-id-card"></i> Limited'
|
label = '<i class="fa fa-id-card"></i> Limited'
|
||||||
title = 'Signed people can edit (forbid guest)'
|
title = 'Signed-in people can edit (forbid guests)'
|
||||||
break
|
break
|
||||||
case 'locked':
|
case 'locked':
|
||||||
label = '<i class="fa fa-lock"></i> Locked'
|
label = '<i class="fa fa-lock"></i> Locked'
|
||||||
@@ -2123,7 +2127,7 @@ function updatePermission (newPermission) {
|
|||||||
break
|
break
|
||||||
case 'protected':
|
case 'protected':
|
||||||
label = '<i class="fa fa-umbrella"></i> Protected'
|
label = '<i class="fa fa-umbrella"></i> Protected'
|
||||||
title = 'Only owner can edit (forbid guest)'
|
title = 'Only owner can edit (forbid guests)'
|
||||||
break
|
break
|
||||||
case 'private':
|
case 'private':
|
||||||
label = '<i class="fa fa-hand-stop-o"></i> Private'
|
label = '<i class="fa fa-hand-stop-o"></i> Private'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ window.urlpath = '<%- urlpath %>'
|
|||||||
window.debug = <%- debug %>
|
window.debug = <%- debug %>
|
||||||
window.version = '<%- version %>'
|
window.version = '<%- version %>'
|
||||||
|
|
||||||
|
window.enableUploads = '<%- enableUploads %>'
|
||||||
window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %>
|
window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %>
|
||||||
|
|
||||||
window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'
|
window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ const filterXSS = require('xss')
|
|||||||
|
|
||||||
const whiteListAttr = ['id', 'class', 'style']
|
const whiteListAttr = ['id', 'class', 'style']
|
||||||
window.whiteListAttr = whiteListAttr
|
window.whiteListAttr = whiteListAttr
|
||||||
// allow link starts with '.', '/' and custom protocol with '://', exclude link starts with javascript://
|
// allow links starting with '.', '/', '#', '?', 'http://', 'https://' and protocols supported by the navigator.registerProtocolHandler API
|
||||||
const linkRegex = /^(?!javascript:\/\/)([\w|-]+:\/\/)|^([.|/])+/i
|
// These schemes can be considered safe-enough for linking to since these are the ones that can be opened using a browser.
|
||||||
|
// See: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
|
||||||
|
const linkRegex = /^(?:\?|#|\.|\/|https?:\/\/|(?:web\+[a-z]+|bitcoin|ftp|ftps|geo|im|irc|ircs|magnet|mailto|matrix|mms|news|nntp|openpgp4fpr|sftp|sip|sms|smsto|ssh|tel|urn|webcal|wtai|xmpp):)/i
|
||||||
// allow data uri, from https://gist.github.com/bgrins/6194623
|
// allow data uri, from https://gist.github.com/bgrins/6194623
|
||||||
const dataUriRegex = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)\s*$/i
|
const dataUriRegex = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)\s*$/i
|
||||||
// custom white list
|
// custom white list
|
||||||
|
|||||||
@@ -30,3 +30,21 @@ export function decodeNoteId (encodedId) {
|
|||||||
idParts.push(id.substr(20, 12))
|
idParts.push(id.substr(20, 12))
|
||||||
return idParts.join('-')
|
return idParts.join('-')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use browser's DOM APIs for escaping and unescaping HTML
|
||||||
|
export function escapeHtml (unsafe) {
|
||||||
|
if (!unsafe) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const tempDiv = document.createElement('div')
|
||||||
|
tempDiv.appendChild(document.createTextNode(String(unsafe)))
|
||||||
|
return tempDiv.innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unescapeHtml (escapedHtml) {
|
||||||
|
if (!escapedHtml) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const doc = new DOMParser().parseFromString(escapedHtml, 'text/html')
|
||||||
|
return doc.documentElement.textContent || ''
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user