Merge commit 'c6e2cefa8f6f2075f356b25f98b69004a3442929' into LailaTheElf

This commit is contained in:
2025-11-25 22:59:42 +01:00
50 changed files with 3393 additions and 2900 deletions

View File

@@ -19,14 +19,14 @@ runs:
shell: bash
- name: Cache yarn cache
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-master
- name: Set up NodeJS
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: ${{ inputs.NODEJS_VERSION }}

View File

@@ -19,7 +19,7 @@ jobs:
name: Node ${{ matrix.node }}
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node
uses: ./.github/actions/setup-node
@@ -33,7 +33,7 @@ jobs:
run: yarn run mocha-suite
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: github.ref == 'refs/heads/master' && matrix.node-version == '22'
with:
name: Prebuild with Node.js ${{ matrix.node-version }}

View File

@@ -12,7 +12,7 @@ jobs:
name: Lint files
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup node
uses: ./.github/actions/setup-node

942
.yarn/releases/yarn-4.12.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,4 +4,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.6.0.cjs
yarnPath: .yarn/releases/yarn-4.12.0.cjs

View File

@@ -25,7 +25,7 @@ RUN rm -rf /hedgedoc/.git/*
# Install app dependencies and build
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
FROM docker.io/library/node:20.17.0-alpine@sha256:2d07db07a2df6830718ae2a47db6fedce6745f5bcd174c398f2acdda90a11c03 AS modules-installer

47
app.js
View File

@@ -12,6 +12,7 @@ const session = require('express-session')
const SequelizeStore = require('connect-session-sequelize')(session.Store)
const fs = require('fs')
const path = require('path')
const { Server } = require('socket.io')
const morgan = require('morgan')
const passportSocketIo = require('passport.socketio')
@@ -81,7 +82,16 @@ if (config.enableStatsApi) {
}
// 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
const realtime = require('./lib/realtime.js')
@@ -147,10 +157,29 @@ app.use(i18n.init)
// routes without sessions
// static files
app.use('/', express.static(path.join(__dirname, '/public'), { maxAge: config.staticCacheTime, index: false, redirect: false }))
app.use('/docs', express.static(path.resolve(__dirname, config.docsPath), { maxAge: config.staticCacheTime, redirect: false }))
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 }))
app.use('/', express.static(path.join(__dirname, '/public'), {
maxAge: config.staticCacheTime,
index: false,
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
app.use(useUnless(['/status', '/metrics', '/_health'], session({
@@ -253,9 +282,6 @@ io.use(passportSocketIo.authorize({
success: realtime.onAuthorizeSuccess,
fail: realtime.onAuthorizeFail
}))
// socket.io heartbeat
io.set('heartbeat interval', config.heartbeatInterval)
io.set('heartbeat timeout', config.heartbeatTimeout)
// socket.io connection
io.sockets.on('connection', realtime.connection)
@@ -330,8 +356,9 @@ function handleTermSignals () {
alreadyHandlingTermSignals = true
realtime.maintenance = true
// disconnect all socket.io clients
Object.keys(io.sockets.sockets).forEach(function (key) {
const socket = io.sockets.sockets[key]
Array.from(io.sockets.sockets.keys()).forEach(function (key) {
const socket = io.sockets.sockets.get(key)
if (!socket) return
// notify client server going into maintenance status
socket.emit('maintenance')
setTimeout(function () {

View File

@@ -95,6 +95,8 @@
"issuer": "change or delete: identity of the service provider (default: config.serverURL)",
"identifierFormat": "change or delete: name identifier format (default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')",
"disableRequestedAuthnContext": "change or delete: true to allow any authentication method, false restricts to password authentication method (default: false)",
"wantAssertionsSigned": "change or delete: true to enforce signed assertions, false allows unsigned assertions (default: true)",
"wantAuthnResponseSigned": "change or delete: true to enforce signed response, false allows unsigned response (default: true)",
"groupAttribute": "change or delete: attribute name for group list (ex: memberOf)",
"requiredGroups": [ "change or delete: group names that allowed" ],
"externalGroups": [ "change or delete: group names that not allowed" ],

View File

@@ -2,19 +2,30 @@
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
to `config.json` before filling in your own details.
- 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
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
| config file | environment | **default** and example value | description |
| ----------- | ----------- | ----------------------------- | -------------------------------------------------------------------------------- |
|-------------|-------------|-------------------------------|----------------------------------------------------------------------------------|
| | `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 |
@@ -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.post` | `CMD_DB_PORT` | **`undefined`**, `5432` | Port used to connect the database server. |
| `db.dialect` | `CMD_DB_DIALECT` | **`undefined`**, `postgres`, `mariadb` | [Dialect](https://sequelize.org/v5/manual/dialects.html) / protocol used to connect to the database. |
| `dbURL` | `CMD_DB_URL` | **`undefined`**, `postgres://username:password@localhost:5432/hedgedoc` 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` |
| `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. |
@@ -48,7 +59,7 @@ to `config.json` before filling in your own details.
these are rarely used for various reasons.
| 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. |
| `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`) |
@@ -64,7 +75,7 @@ these are rarely used for various reasons.
## HedgeDoc Location
| config file | environment | **default** and example value | description |
| ---------------- | --------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|------------------|-----------------------|--------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `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>` |
| `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
| 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. |
## Users and Privileges
| config file | environment | **default** and example value | description |
|--------------------------------|--------------------------------------|-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|--------------------------------|--------------------------------------|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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`). |
| `allowFreeURL` | `CMD_ALLOW_FREEURL` | **`false`** or `true` | Set to allow new note creation by accessing a nonexistent note URL. This is the behavior familiar from [Etherpad](https://github.com/ether/etherpad-lite). |
| `requireFreeURLAuthentication` | `CMD_REQUIRE_FREEURL_AUTHENTICATION` | **`false`** or `true` | Set to require authentication for FreeURL mode style note creation. |
| `disableNoteCreation` | `CMD_DISABLE_NOTE_CREATION` | **`false`** or `true` | Set to `true` to disallow any person to create notes. |
| `defaultPermission` | `CMD_DEFAULT_PERMISSION` | **`editable`**, `freely`, `limited`, `locked`, `protected` or `private` | Set notes default permission (only applied on signed-in users). |
| `enableUploads` | `CMD_ENABLE_UPLOADS` | **`all`** , `registered`, `none` | Set to specify who can upload images (`all` includes guest users, `registered` restricts to registered users, `none` disables uploads completely). If not set, but both `allowAnonymous` and `allowAnonymousEdits` are `false`, this will be set to `registered`. |
| `sessionName` | | **`connect.sid`** | Cookie session name. |
| `sessionLife` | `CMD_SESSION_LIFE` | **`14 * 24 * 60 * 60 * 1000`**, `1209600000` (14 days) | Cookie session life time in milliseconds. |
| `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)
| config file | environment | **default** and example value | description |
| -------------------- | -------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|----------------------|----------------------------|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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`. |
### Dropbox Login
| 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) |
| | `CMD_DROPBOX_CLIENTID` | **no default** | Dropbox API client id |
| | `CMD_DROPBOX_CLIENTSECRET` | **no default** | Dropbox API client secret |
@@ -137,7 +149,7 @@ these are rarely used for various reasons.
### Facebook Login
| 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) |
| | `CMD_FACEBOOK_CLIENTID` | **no default** | Facebook API client id |
| | `CMD_FACEBOOK_CLIENTSECRET` | **no default** | Facebook API client secret |
@@ -145,7 +157,7 @@ these are rarely used for various reasons.
### GitHub Login
| 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). |
| | `CMD_GITHUB_CLIENTID` | **no default** | GitHub API client id |
| | `CMD_GITHUB_CLIENTSECRET` | **no default** | GitHub API client secret |
@@ -153,7 +165,7 @@ these are rarely used for various reasons.
### GitLab Login
| 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! |
| | `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) |
@@ -164,7 +176,7 @@ these are rarely used for various reasons.
### Google Login
| 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) |
| | `CMD_GOOGLE_CLIENTID` | **no default** | Google API client id |
| | `CMD_GOOGLE_CLIENTSECRET` | **no default** | Google API client secret |
@@ -173,7 +185,7 @@ these are rarely used for various reasons.
### LDAP Login
| 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! |
| | `CMD_LDAP_URL` | **no default**, `ldap://example.com` | URL of LDAP server |
| | `CMD_LDAP_BINDDN` | **no default** | bindDn for LDAP access |
@@ -189,7 +201,7 @@ these are rarely used for various reasons.
### Mattermost Login
| 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! |
| | `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 |
@@ -198,7 +210,7 @@ these are rarely used for various reasons.
### OAuth2 Login
| 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! |
| | `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) |
@@ -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 |
!!! 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.
Remember to also make the file available inside the Docker container, if you're running HedgeDoc in Docker container.
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.
Remember to also make the file available inside the Docker container, if you're running HedgeDoc in Docker container.
### SAML Login
| 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! |
| | `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_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_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_GROUPATTRIBUTE` | **no default**, `memberOf` | attribute name for group list (optional) |
| | `CMD_SAML_WANT_ASSERTIONS_SIGNED` | **`true`** or `false` | true to enforce signed assertions, false allows unsigned assertions |
| | `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_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_USERNAME` | **no default**, `mailNickname` | attribute map for `username` (optional, default: NameID of SAML response) |
| | `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_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` | **`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`** | attribute map for `username` (optional) |
| | `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 |
### Twitter Login
| 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) |
| | `CMD_TWITTER_CONSUMERKEY` | **no default** | Twitter API consumer key |
| | `CMD_TWITTER_CONSUMERSECRET` | **no default** | Twitter API consumer secret |
@@ -255,7 +271,7 @@ you don't have to use either of these.
### Amazon S3
| 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) |
| | `CMD_S3_ACCESS_KEY_ID` | **no default** | AWS access key id |
| | `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
| config file | environment | **default** and example value | description |
| ----------- | ----------------------------- | ----------------------------- | ------------------------------------------------------------------------- |
|-------------|-------------------------------|-------------------------------|---------------------------------------------------------------------------|
| | `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) |
### imgur
| config file | environment | **default** and example value | description |
| ----------- | -------------------- | ------------------------------ | ------------------- |
|-------------|----------------------|-------------------------------|---------------------|
| | `CMD_IMGUR_CLIENTID` | **no default** | Imgur API client id |
### Minio
| 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) |
| | `CMD_MINIO_ACCESS_KEY` | **no default** | Minio access key |
| | `CMD_MINIO_SECRET_KEY` | **no default** | Minio secret key |
@@ -292,6 +308,6 @@ you don't have to use either of these.
### Lutim
| 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 |
| | `CMD_LUTIM_URL` | **`https://framapic.org/`** | When `CMD_IMAGE_UPLOAD_TYPE` is set to `lutim`, you can setup the lutim url |

View File

@@ -3,7 +3,7 @@ openapi: 3.0.1
info:
title: HedgeDoc
description: HedgeDoc is an open source collaborative note editor. Several tasks of HedgeDoc can be automated through this API.
version: 1.10.1
version: 1.10.3
contact:
name: HedgeDoc on GitHub
url: https://github.com/hedgedoc/hedgedoc

View 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

View File

@@ -2,22 +2,22 @@
## 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.
## 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)
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.
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.
4. Click **Save**
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
@@ -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.
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
CMD_OAUTH2_USER_PROFILE_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/userinfo
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=preferred_username
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=name
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email
CMD_OAUTH2_TOKEN_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/token
CMD_OAUTH2_AUTHORIZATION_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/auth
CMD_OAUTH2_CLIENT_ID=<your client ID>
CMD_OAUTH2_CLIENT_SECRET=<your client secret, which you can find under the Credentials tab for your client>
CMD_OAUTH2_PROVIDERNAME=Keycloak
CMD_OAUTH2_SCOPE=openid email profile
CMD_DOMAIN=<hedgedoc.example.com>
CMD_PROTOCOL_USESSL=true
CMD_URL_ADDPORT=false
app:
environment:
- CMD_OAUTH2_USER_PROFILE_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/userinfo
- CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=preferred_username
- CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=name
- CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email
- CMD_OAUTH2_TOKEN_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/token
- CMD_OAUTH2_AUTHORIZATION_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/auth
- CMD_OAUTH2_CLIENT_ID=<your client ID>
- CMD_OAUTH2_CLIENT_SECRET=<your client secret, which you can find under the Credentials tab for your client>
- CMD_OAUTH2_PROVIDERNAME=Keycloak
- CMD_OAUTH2_SCOPE=openid email profile
- CMD_DOMAIN=<hedgedoc.example.com>
- CMD_PROTOCOL_USESSL=true
- CMD_URL_ADDPORT=false
```
6. Run `docker-compose up -d` to apply your settings.
7. Sign in to your HedgeDoc using your Keycloak ID
After running `docker-compose up -d` to apply your settings,
you should now be able to sign in to your HedgeDoc using your Keycloak.

View File

@@ -7,8 +7,8 @@ This documentation will cover HTTPS setup, with comments for HTTP setup.
## Cloudflare
!!! 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.
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-).
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://developers.cloudflare.com/speed/optimization/content/rocket-loader).
## HedgeDoc config
@@ -81,8 +81,9 @@ server {
proxy_set_header Connection $connection_upgrade;
}
listen [::]:443 ssl http2;
listen 443 ssl http2;
listen [::]:443 ssl;
listen 443 ssl;
http2 on;
ssl_certificate fullchain.pem;
ssl_certificate_key privkey.pem;
include options-ssl-nginx.conf;
@@ -97,6 +98,66 @@ server {
connection to the server, and the editor interface will display an endless loading
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
You will need these modules enabled: `proxy`, `proxy_http` and `proxy_wstunnel`.
Here is an example config snippet:

View File

@@ -65,6 +65,14 @@ using the button below. This will run the official Docker image from [quay.io](h
[![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](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
### Arch Linux

View File

@@ -6,7 +6,7 @@
- [Docker Compose](https://docs.docker.com/compose/install/)
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`:
@@ -28,7 +28,7 @@ services:
restart: always
app:
# Make sure to use the latest release from https://hedgedoc.org/latest-release
image: quay.io/hedgedoc/hedgedoc:1.10.1
image: quay.io/hedgedoc/hedgedoc:1.10.3
environment:
- CMD_DB_URL=postgres://hedgedoc:password@database:5432/hedgedoc
- CMD_DOMAIN=localhost

View File

@@ -19,7 +19,7 @@
1. Check if you meet the [requirements at the top of this document](#manual-installation).
2. Download the [latest release](https://hedgedoc.org/latest-release/) and extract it.
<small>Alternatively, you can use Git to clone the repository and checkout a release, e.g. with `git clone -b 1.10.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.
4. Configure HedgeDoc: To get started, you can use this minimal `config.json`:
```json
@@ -61,7 +61,7 @@ If you want to upgrade HedgeDoc from an older version, follow these steps:
and the latest release.
2. Fully stop your old HedgeDoc server.
3. [Download](https://hedgedoc.org/latest-release/) the new release and extract it over the old directory.
<small>If you use Git, you can check out the new tag with e.g. `git fetch origin && git checkout 1.10.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.
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.
@@ -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`!
- If you use an external database like PostgreSQL or MariaDB, make sure to add a corresponding
`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
[Unit]
@@ -125,7 +133,7 @@ ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=@system-service pkey_alloc pkey_mprotect
# You may have to adjust these settings
User=hedgedoc

View File

@@ -48,6 +48,7 @@ nav:
- 'Operational Transformation': dev/ot.md
- Webpack: dev/webpack.md
- 'Documentation': dev/documentation.md
- 'Release Checklist': dev/release_checklist.md
- FAQ: faq.md
markdown_extensions:
- toc:

View File

@@ -1,5 +1,5 @@
mkdocs==1.6.1
mkdocs-material==9.6.1
pymdown-extensions==10.14.3
mkdocs-material==9.7.0
pymdown-extensions==10.17.1
mdx_truly_sane_lists==1.3
mkdocs-redirects==1.2.2

View File

@@ -32,6 +32,7 @@ module.exports = {
rateLimitNewNotes: 20,
cookiePolicy: 'lax',
protocolUseSSL: false,
// permissions
allowAnonymous: true,
allowAnonymousEdits: false,
allowFreeURL: false,
@@ -39,6 +40,10 @@ module.exports = {
disableNoteCreation: false,
forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views'],
defaultPermission: 'editable',
enableUploads: undefined, // 'all', 'registered', 'none' are valid options.
// This is undefined by default and set during runtime based on allowAnonymous and allowAnonymousEdits for backwards-compatibility unless explicitly set.
dbURL: '',
db: {},
// ssl path
@@ -160,9 +165,11 @@ module.exports = {
requiredGroups: [],
attribute: {
id: undefined,
username: undefined,
email: undefined
}
username: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
},
wantAssertionsSigned: true,
wantAuthnResponseSigned: true
},
email: true,
allowEmailRegister: true,

View File

@@ -35,6 +35,7 @@ module.exports = {
allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL),
requireFreeURLAuthentication: toBooleanConfig(process.env.CMD_REQUIRE_FREEURL_AUTHENTICATION),
disableNoteCreation: toBooleanConfig(process.env.CMD_DISABLE_NOTE_CREATION),
enableUploads: process.env.CMD_ENABLE_UPLOADS,
forbiddenNoteIDs: toArrayConfig(process.env.CMD_FORBIDDEN_NOTE_IDS),
defaultPermission: process.env.CMD_DEFAULT_PERMISSION,
dbURL: process.env.CMD_DB_URL,
@@ -146,6 +147,8 @@ module.exports = {
issuer: process.env.CMD_SAML_ISSUER,
identifierFormat: process.env.CMD_SAML_IDENTIFIERFORMAT,
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,
externalGroups: toArrayConfig(process.env.CMD_SAML_EXTERNALGROUPS, '|', []),
requiredGroups: toArrayConfig(process.env.CMD_SAML_REQUIREDGROUPS, '|', []),

View File

@@ -79,6 +79,17 @@ if (!config.allowAnonymous && !config.allowAnonymousEdits) {
if (!(config.defaultPermission in config.permission)) {
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
if (config.useSSL === true) {
@@ -164,6 +175,10 @@ if (['filesystem', 's3', 'minio', 'imgur', 'azure', 'lutim'].indexOf(config.imag
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
switch (config.imageUploadType) {
case 'imgur':

View File

@@ -7,21 +7,18 @@ const CspStrategy = {}
const defaultDirectives = {
defaultSrc: ['\'none\''],
baseUri: ['\'self\''],
connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws')],
connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws'), 'https://vimeo.com/api/v2/video/'],
fontSrc: ['\'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
scriptSrc: [
config.serverURL + '/build/',
config.serverURL + '/js/',
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
],
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 :/
formAction: ['\'self\''],
mediaSrc: ['*']

View File

@@ -1,14 +1,16 @@
'use strict'
// external modules
const crypto = require('crypto')
const randomcolor = require('randomcolor')
const Chance = require('chance')
const config = require('./config')
// core
exports.generateAvatar = function (name) {
const color = randomcolor({
seed: name,
luminosity: 'dark'
// use darker colors for better contrast
const color = new Chance(name).color({
max_red: 150,
max_green: 150,
max_blue: 150
})
const letter = name.substring(0, 1).toUpperCase()

View File

@@ -7,13 +7,12 @@ const base64url = require('base64url')
const md = require('markdown-it')()
const metaMarked = require('@hedgedoc/meta-marked')
const cheerio = require('cheerio')
const shortId = require('shortid')
const nanoid = require('nanoid')
const Sequelize = require('sequelize')
const async = require('async')
const moment = require('moment')
const DiffMatchPatch = require('diff-match-patch')
const dmp = new DiffMatchPatch()
const S = require('string')
// core
const config = require('../config')
@@ -37,7 +36,7 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.STRING,
unique: true,
allowNull: false,
defaultValue: shortId.generate
defaultValue: () => nanoid.nanoid(10)
},
alias: {
type: DataTypes.STRING,
@@ -297,8 +296,12 @@ module.exports = function (sequelize, DataTypes) {
parseNoteIdByShortId: function (_callback) {
// try to parse note id by shortId
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({
// 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)
? sequelize.where(sequelize.fn('BINARY', sequelize.col('shortid')), noteId)
: {
@@ -344,7 +347,9 @@ module.exports = function (sequelize, DataTypes) {
title = meta.title
} else {
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'
return title
@@ -374,7 +379,7 @@ module.exports = function (sequelize, DataTypes) {
if (/^tags/gmi.test($(value).text())) {
const codes = $(value).find('code')
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)
}
}

View File

@@ -4,7 +4,7 @@ const Sequelize = require('sequelize')
const async = require('async')
const moment = require('moment')
const childProcess = require('child_process')
const shortId = require('shortid')
const nanoid = require('nanoid')
const path = require('path')
const Op = Sequelize.Op
@@ -44,7 +44,7 @@ function createDmpWorker () {
function sendDmpWorker (data, callback) {
if (!dmpWorker) dmpWorker = createDmpWorker()
const cacheKey = Date.now() + '_' + shortId.generate()
const cacheKey = Date.now() + '_' + nanoid.nanoid()
dmpCallbackCache[cacheKey] = callback
data = Object.assign(data, {
cacheKey

View File

@@ -1,13 +1,13 @@
'use strict'
// external modules
const shortId = require('shortid')
const nanoid = require('nanoid')
module.exports = function (sequelize, DataTypes) {
const Temp = sequelize.define('Temp', {
id: {
type: DataTypes.STRING,
primaryKey: true,
defaultValue: shortId.generate
defaultValue: nanoid.nanoid
},
data: {
type: DataTypes.TEXT

View File

@@ -4,7 +4,6 @@
const cookie = require('cookie')
const cookieParser = require('cookie-parser')
const async = require('async')
const randomcolor = require('randomcolor')
const Chance = require('chance')
const chance = new Chance()
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
setInterval(function () {
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]) ||
(socket && (!socket.rooms || socket.rooms.length <= 0))) {
(socket && (!socket.rooms || socket.rooms.size <= 0))) {
logger.debug(`cleaner found redundant user: ${key}`)
if (!socket) {
socket = {
@@ -711,7 +710,7 @@ function connection (socket) {
// initialize user data
// random color
let color = randomcolor()
let color = chance.color()
// make sure color not duplicated or reach max random count
if (notes[noteId]) {
let randomcount = 0
@@ -724,7 +723,7 @@ function connection (socket) {
}
})
if (found) {
color = randomcolor()
color = chance.color()
randomcount++
}
} while (found && randomcount < maxrandomcount)

View File

@@ -3,7 +3,6 @@
// external modules
const fs = require('fs')
const path = require('path')
const fetch = require('node-fetch')
// core
const config = require('./config')
const logger = require('./logger')

View File

@@ -62,6 +62,11 @@ function parseProfile (data) {
const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
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 {
id: id || username,
username,

View File

@@ -2,7 +2,7 @@
const Router = require('express').Router
const passport = require('passport')
const SamlStrategy = require('passport-saml').Strategy
const SamlStrategy = require('@node-saml/passport-saml').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
@@ -12,7 +12,9 @@ const intersection = function (array1, array2) { return array1.filter((n) => arr
const samlAuth = module.exports = Router()
passport.use(new SamlStrategy({
passport.use(
new SamlStrategy(
{
callbackUrl: config.serverURL + '/auth/saml/callback',
entryPoint: config.saml.idpSsoUrl,
issuer: config.saml.issuer || config.serverURL,
@@ -25,7 +27,7 @@ passport.use(new SamlStrategy({
logger.error(`SAML client certificate: ${e.message}`)
}
}()),
cert: (function () {
idpCert: (function () {
try {
return fs.readFileSync(config.saml.idpCert, 'utf-8')
} catch (e) {
@@ -34,8 +36,12 @@ passport.use(new SamlStrategy({
}
}()),
identifierFormat: config.saml.identifierFormat,
disableRequestedAuthnContext: config.saml.disableRequestedAuthnContext
}, function (user, done) {
disableRequestedAuthnContext: config.saml.disableRequestedAuthnContext,
wantAssertionsSigned: config.saml.wantAssertionsSigned,
wantAuthnResponseSigned: config.saml.wantAuthnResponseSigned
},
// sign-in
function (user, done) {
// check authorization if needed
if (config.saml.externalGroups && config.saml.groupAttribute) {
const externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute])
@@ -52,14 +58,15 @@ passport.use(new SamlStrategy({
}
// user creation
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 = {
provider: 'saml',
id: 'SAML-' + uuid,
username: user[config.saml.attribute.username] || user.nameID,
emails: user[config.saml.attribute.email] ? [user[config.saml.attribute.email]] : []
}
if (profile.emails.length === 0 && config.saml.identifierFormat === 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress') {
profile.emails.push(user.nameID)
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] : []
}
const stringifiedProfile = JSON.stringify(profile)
models.User.findOrCreate({
@@ -90,20 +97,38 @@ passport.use(new SamlStrategy({
logger.error('saml auth failed: ' + err.message)
return done(err, null)
})
}))
},
// logout
function (profile, done) {
return done(null, profile)
}
)
)
samlAuth.get('/auth/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', {
successReturnToOrRedirect: config.serverURL + '/',
failureRedirect: config.serverURL + '/'
})
}),
function (req, res) {
res.redirect('/')
}
)
samlAuth.get('/auth/saml/metadata', function (req, res) {

View File

@@ -2,7 +2,6 @@
const config = require('../../config')
const logger = require('../../logger')
const fs = require('fs')
const fetch = require('node-fetch')
exports.uploadImage = function (imagePath, callback) {
if (!callback || typeof callback !== 'function') {

View File

@@ -57,13 +57,17 @@ async function checkUploadType (filePath) {
// upload image
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 (
!req.isAuthenticated() &&
!config.allowAnonymous &&
!config.allowAnonymousEdits
uploadsEnabled === 'registered' &&
!req.isAuthenticated()
) {
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)
}

View File

@@ -1,6 +1,6 @@
'use strict'
const { rateLimit } = require('express-rate-limit')
const { rateLimit, ipKeyGenerator } = require('express-rate-limit')
const errors = require('../../errors')
const config = require('../../config')
@@ -8,7 +8,7 @@ const determineKey = (req) => {
if (req.user) {
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

View File

@@ -8,6 +8,11 @@ const config = require('../../config')
toobusy.maxLag(config.tooBusyLag)
module.exports = function (req, res, next) {
// We dont want to return "toobusy" errors for healthchecks, as that
// will cause the process to be restarted
if (req.baseUrl === '/_health') {
next()
}
if (toobusy()) {
errors.errorServiceUnavailable(res)
} else {

View File

@@ -2,7 +2,7 @@ const models = require('../../models')
const logger = require('../../logger')
const config = require('../../config')
const errors = require('../../errors')
const shortId = require('shortid')
const nanoid = require('nanoid')
const moment = require('moment')
const querystring = require('querystring')
@@ -36,7 +36,7 @@ exports.createGist = function createGist (req, res, note) {
client_id: config.github.clientID,
redirect_uri: config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist',
scope: 'gist',
state: shortId.generate()
state: nanoid.nanoid()
}
const query = querystring.stringify(data)
res.redirect('https://github.com/login/oauth/authorize?' + query)

View File

@@ -111,7 +111,8 @@ statusRouter.get('/config', function (req, res) {
DROPBOX_APP_KEY: config.dropbox.appKey,
allowedUploadMimeTypes: config.allowedUploadMimeTypes,
linkifyHeaderStyle: config.linkifyHeaderStyle,
cookiePolicy: config.cookiePolicy
cookiePolicy: config.cookiePolicy,
enableUploads: config.enableUploads
}
res.set({
'Cache-Control': 'private', // only cache by client

View File

@@ -1,6 +1,6 @@
{
"name": "the_hedgedoc_elf",
"version": "1.10.2",
"version": "1.10.3_LailaTheElf",
"description": "The best platform to write and share markdown.",
"main": "app.js",
"license": "AGPL-3.0",
@@ -18,40 +18,40 @@
},
"dependencies": {
"@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",
"Idle.Js": "git+https://github.com/shawnmclean/Idle.js#commit=2b57cc6e49d177b7ddce0cca00ef5cbe07453541",
"archiver": "6.0.2",
"archiver": "7.0.1",
"async": "3.2.6",
"aws-sdk": "2.1692.0",
"azure-storage": "2.10.7",
"base64url": "3.0.1",
"body-parser": "1.20.3",
"chance": "1.1.12",
"body-parser": "2.2.1",
"chance": "1.1.13",
"cheerio": "0.22.0",
"clean-webpack-plugin": "4.0.0",
"compression": "1.7.5",
"compression": "1.8.1",
"connect-flash": "0.1.1",
"connect-session-sequelize": "7.1.7",
"connect-session-sequelize": "8.0.2",
"cookie": "1.0.2",
"cookie-parser": "1.4.7",
"deep-freeze": "0.0.1",
"diff-match-patch": "git+https://github.com/hackmdio/diff-match-patch.git#commit=59a9395ad9fe143e601e7ae5765ed943bdd2b11e",
"ejs": "3.1.10",
"express": "4.21.2",
"express-rate-limit": "7.5.0",
"express-session": "1.18.1",
"file-type": "20.0.1",
"formidable": "2.1.2",
"express-rate-limit": "8.2.1",
"express-session": "1.18.2",
"file-type": "21.1.1",
"formidable": "3.5.4",
"graceful-fs": "4.2.11",
"helmet": "8.0.0",
"i18n": "0.15.1",
"helmet": "8.1.0",
"i18n": "0.15.3",
"is-svg": "4.4.0",
"jsdom-nogyp": "0.8.3",
"lodash": "4.17.21",
"lutim": "1.0.3",
"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-abbr": "1.0.4",
"markdown-it-container": "3.0.0",
@@ -68,11 +68,11 @@
"mattermost": "3.4.0",
"method-override": "3.0.0",
"minimist": "1.2.8",
"minio": "7.1.3",
"minio": "8.0.6",
"moment": "2.30.1",
"morgan": "1.10.0",
"mysql2": "3.12.0",
"node-fetch": "2.7.0",
"morgan": "1.10.1",
"mysql2": "3.15.3",
"nanoid": "3.3.11",
"passport": "patch:passport@npm%3A0.7.0#~/.yarn/patches/passport-npm-0.7.0-df02531736.patch",
"passport-dropbox-oauth2": "1.1.0",
"passport-facebook": "3.0.0",
@@ -84,27 +84,24 @@
"passport-oauth2": "1.8.0",
"passport-twitter": "1.0.4",
"passport.socketio": "3.7.0",
"pdfobject": "2.3.0",
"pg": "8.13.1",
"pdfobject": "2.3.1",
"pg": "8.16.3",
"pg-hstore": "2.3.4",
"prom-client": "15.1.3",
"prometheus-api-metrics": "3.2.2",
"randomcolor": "0.6.2",
"prometheus-api-metrics": "4.0.0",
"readline-sync": "1.4.10",
"rimraf": "5.0.10",
"rimraf": "6.1.2",
"sanitize-filename": "1.6.3",
"scrypt-kdf": "2.0.1",
"sequelize": "5.22.5",
"shortid": "2.2.17",
"socket.io": "2.5.1",
"socket.io": "4.8.1",
"sqlite3": "5.1.7",
"store": "2.0.12",
"string": "3.3.3",
"toobusy-js": "0.5.1",
"umzug": "2.3.0",
"uuid": "11.0.5",
"validator": "13.12.0",
"winston": "3.17.0",
"uuid": "11.1.0",
"validator": "13.15.23",
"winston": "3.18.3",
"xss": "1.0.15"
},
"engines": {
@@ -141,10 +138,10 @@
"url": "https://gitea.finnvanreenen.nl/LailaTheElf/hedgedoc.git"
},
"devDependencies": {
"@eslint/eslintrc": "3.2.0",
"@eslint/js": "9.19.0",
"@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.39.1",
"@hedgedoc/codemirror-5": "5.65.12",
"abcjs": "6.4.4",
"abcjs": "6.5.2",
"babel-cli": "6.26.0",
"babel-core": "6.26.3",
"babel-loader": "7.1.5",
@@ -157,12 +154,11 @@
"copy-webpack-plugin": "6.4.1",
"css-loader": "5.2.7",
"emojify.js": "1.1.0",
"esbuild-loader": "4.2.2",
"escape-html": "1.0.3",
"eslint": "9.19.0",
"esbuild-loader": "4.4.0",
"eslint": "9.39.1",
"eslint-config-standard": "17.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-n": "17.15.1",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.23.1",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-standard": "5.0.0",
"exports-loader": "1.1.1",
@@ -171,39 +167,38 @@
"file-saver": "2.0.5",
"flowchart.js": "1.18.0",
"fork-awesome": "1.2.0",
"gist-embed": "2.6.0",
"globals": "15.14.0",
"globals": "16.5.0",
"highlight.js": "10.7.3",
"html-webpack-plugin": "4.5.2",
"imports-loader": "1.2.0",
"ionicons": "2.0.1",
"jquery": "3.7.1",
"jquery-mousewheel": "3.1.13",
"jquery-mousewheel": "3.2.2",
"jquery-ui": "1.14.1",
"js-cookie": "3.0.5",
"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",
"keymaster": "1.6.2",
"less": "4.2.2",
"less": "4.4.2",
"less-loader": "7.3.0",
"list.js": "2.3.1",
"mathjax": "2.7.9",
"mermaid": "11.4.1",
"mini-css-extract-plugin": "1.6.2",
"mocha": "11.1.0",
"mocha": "11.7.5",
"mock-require": "3.0.3",
"optimize-css-assets-webpack-plugin": "6.0.1",
"prismjs": "1.29.0",
"prismjs": "1.30.0",
"raphael": "2.3.0",
"remark-cli": "12.0.1",
"remark-preset-lint-markdown-style-guide": "5.1.3",
"reveal.js": "3.9.2",
"select2": "3.5.2-browserify",
"socket.io-client": "2.5.0",
"socket.io-client": "4.8.1",
"spin.js": "4.1.2",
"string-loader": "0.0.1",
"turndown": "7.2.0",
"turndown": "7.2.2",
"url-loader": "4.1.1",
"velocity-animate": "1.5.2",
"visibilityjs": "2.0.2",
@@ -217,5 +212,5 @@
"bufferutil": "4.0.9",
"utf-8-validate": "6.0.5"
},
"packageManager": "yarn@4.6.0"
"packageManager": "yarn@4.12.0"
}

View File

@@ -63,6 +63,11 @@
-webkit-transition: opacity 0.2s; /* Safari */
transition: opacity 0.2s;
}
iframe.github-gist-frame {
width: 100%;
border: none;
height: 32rem;
}
.slideshare .inner,
.speakerdeck .inner {

View File

@@ -238,10 +238,6 @@ When youre a carpenter making a beautiful chest of drawers, youre not goin
{%gist schacon/4277%}
#### SlideShare
{%slideshare briansolis/26-disruptive-technology-trends-2016-2018-56796196 %}
#### PDF
**Caution: this might be blocked by your browser if not using an `https` URL.**

View File

@@ -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.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.
We recommend upgrading as soon as possible, if you use local logins.

View File

@@ -24,7 +24,7 @@ import {
import { saveAs } from 'file-saver'
import List from 'list.js'
import S from 'string'
import { unescapeHtml } from './utils'
require('./locale')
@@ -398,7 +398,7 @@ function buildTagsFilter (tags) {
for (let i = 0; i < tags.length; i++) {
tags[i] = {
id: i,
text: S(tags[i]).unescapeHTML().s
text: unescapeHtml(tags[i])
}
}
filtertags = tags

View File

@@ -3,11 +3,11 @@
import Prism from 'prismjs'
import PDFObject from 'pdfobject'
import S from 'string'
import { saveAs } from 'file-saver'
import escapeHTML from 'escape-html'
import filterXSS from 'xss'
import getUIElements from './lib/editor/ui-elements'
import { escapeHtml, unescapeHtml } from './utils'
import markdownit from 'markdown-it'
import markdownitContainer from 'markdown-it-container'
@@ -15,8 +15,6 @@ import markdownitContainer from 'markdown-it-container'
/* Defined regex markdown it plugins */
import Plugin from 'markdown-it-regexp'
import 'gist-embed'
require('prismjs/themes/prism.css')
require('prismjs/components/prism-wiki')
require('prismjs/components/prism-haskell')
@@ -168,7 +166,11 @@ export function renderTags (view) {
function slugifyWithUTF8 (text) {
// 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
newText = newText.replace(/\s+/g, '-')
// slugify string to make it valid as an attribute
@@ -291,22 +293,14 @@ export function finishView (view) {
imgPlayiframe(this, 'https://player.vimeo.com/video/')
})
.each((key, value) => {
const vimeoLink = `https://vimeo.com/${$(value).attr('data-videoid')}`
$.ajax({
type: 'GET',
url: `https://vimeo.com/api/oembed.json?url=${encodeURIComponent(vimeoLink)}`,
jsonp: 'callback',
dataType: 'jsonp',
success (data) {
const image = `<img src="${data.thumbnail_url}" />`
fetch(`https://vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`)
.then(response => response.json())
.then(data => {
const image = `<img src="${data[0].thumbnail_large}" />`
$(value).prepend(image)
if (window.viewAjaxCallback) window.viewAjaxCallback()
}
})
})
// gist
view.find('code[data-gist-id]').each((key, value) => {
if ($(value).children().length === 0) { $(value).gist(window.viewAjaxCallback) }
.catch(console.error)
})
// sequence diagram
const sequences = view.find('div.sequence-diagram.raw').removeClass('raw')
@@ -328,7 +322,7 @@ export function finishView (view) {
svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet')
} catch (err) {
$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)
}
})
@@ -353,7 +347,7 @@ export function finishView (view) {
$value.children().unwrap().unwrap()
} catch (err) {
$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)
}
})
@@ -375,7 +369,7 @@ export function finishView (view) {
})
} catch (err) {
$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)
}
})
@@ -398,7 +392,7 @@ export function finishView (view) {
errormessage = err.str
}
$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)
}
})
@@ -421,7 +415,7 @@ export function finishView (view) {
})
} catch (err) {
$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)
}
})
@@ -450,24 +444,12 @@ export function finishView (view) {
// slideshare
view.find('div.slideshare.raw').removeClass('raw')
.each((key, value) => {
$.ajax({
type: 'GET',
url: `https://www.slideshare.net/api/oembed/2?url=https://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`,
jsonp: 'callback',
dataType: 'jsonp',
success (data) {
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()
}
})
const url = `https://slideshare.com/${$(value).attr('data-slideshareid')}`
const inner = $('<a>Slideshare</a>')
inner.attr('href', url)
inner.attr('rel', 'noopener noreferrer')
inner.attr('target', '_blank')
$(value).append(inner)
})
// speakerdeck
view.find('div.speakerdeck.raw').removeClass('raw')
@@ -508,24 +490,24 @@ export function finishView (view) {
value: code
}
} else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') {
code = S(code).unescapeHTML().s
code = unescapeHtml(code)
result = {
value: Prism.highlight(code, Prism.languages[reallang])
}
} else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') {
code = S(code).unescapeHTML().s
code = unescapeHtml(code)
result = {
value: Prism.highlight(code, Prism.languages.wiki)
}
} else if (reallang === 'cmake') {
code = S(code).unescapeHTML().s
code = unescapeHtml(code)
result = {
value: Prism.highlight(code, Prism.languages.makefile)
}
} else {
require.ensure([], function (require) {
const hljs = require('highlight.js')
code = S(code).unescapeHTML().s
code = unescapeHtml(code)
const languages = hljs.listLanguages()
if (!languages.includes(reallang)) {
result = hljs.highlightAuto(code)
@@ -598,7 +580,7 @@ export function postProcess (code) {
if (warning && warning.length > 0) {
warning.text(md.metaError)
} 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)
}
}
@@ -639,8 +621,6 @@ function generateCleanHTML (view) {
src.find('*[class=""]').removeAttr('class')
eles.removeAttr('data-startline data-endline')
src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
// remove gist content
src.find('code[data-gist-id]').children().remove()
// disable todo list
src.find('input.task-list-item-checkbox').attr('disabled', '')
// replace emoji image path
@@ -836,7 +816,7 @@ export function smoothHashScroll () {
function imgPlayiframe (element, src) {
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`)
$(element).find('img').css('visibility', 'hidden')
$(element).append(iframe)
@@ -987,7 +967,7 @@ export function scrollToHash () {
function highlightRender (code, lang) {
if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return }
code = S(code).escapeHTML().s
code = escapeHtml(code)
if (lang === 'sequence') {
return `<div class="sequence-diagram raw">${code}</div>`
} else if (lang === 'flow') {
@@ -1156,8 +1136,7 @@ const gistPlugin = new Plugin(
(match, utils) => {
const gistid = match[1]
const code = `<code data-gist-id="${gistid}"></code>`
return code
return `<iframe sandbox class="github-gist-frame" src="https://gist.github.com/${gistid}.pibb"></iframe>`
}
)
// TOC

View File

@@ -2,13 +2,13 @@
/* global serverurl, moment */
import store from 'store'
import S from 'string'
import LZString from 'lz-string'
import url from 'wurl'
import {
checkNoteIdValid,
encodeNoteId
encodeNoteId,
escapeHtml
} from './utils'
import {
@@ -275,8 +275,8 @@ function parseToHistory (list, notehistory, callback) {
notehistory[i].fromNow = timestamp.fromNow()
notehistory[i].time = timestamp.format('llll')
// prevent XSS
notehistory[i].text = S(notehistory[i].text).escapeHTML().s
notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? S(notehistory[i].tags).escapeHTML().s.split(',') : []
notehistory[i].text = escapeHtml(notehistory[i].text)
notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? escapeHtml(notehistory[i].tags).split(',') : []
// add to list
if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) { list.add(notehistory[i]) }
}

View File

@@ -22,4 +22,3 @@ const $ = require('jquery')
window.jQuery = $
window.$ = $
require('bootstrap')
require('gist-embed/gist-embed.min')

View File

@@ -16,7 +16,7 @@ import { ot } from '../vendor/ot/ot.min.js'
import hex2rgb from '../vendor/ot/hex2rgb'
import { saveAs } from 'file-saver'
import randomColor from 'randomcolor'
import chance from 'chance'
import store from 'store'
import url from 'wurl'
import { Spinner } from 'spin.js'
@@ -427,7 +427,7 @@ const supportExtraTags = [
text: '[random color tag]',
search: '[]',
command: function () {
const color = randomColor()
const color = chance().color()
return '[color=' + color + ']'
}
}
@@ -1079,6 +1079,10 @@ function changeMode (type) {
// add and update tool bar
if (!editorInstance.toolBar) {
editorInstance.addToolBar()
const uploadButtonVisible = window.enableUploads === 'all' || (window.enableUploads === 'registered' && personalInfo.login)
if (!uploadButtonVisible) {
$('#uploadImage').remove()
}
}
// work around foldGutter might not init properly
editor.setOption('foldGutter', false)
@@ -2111,11 +2115,11 @@ function updatePermission (newPermission) {
break
case 'editable':
label = '<i class="fa fa-shield"></i> Editable'
title = 'Signed people can edit'
title = 'Signed-in people can edit'
break
case '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
case 'locked':
label = '<i class="fa fa-lock"></i> Locked'
@@ -2123,7 +2127,7 @@ function updatePermission (newPermission) {
break
case 'protected':
label = '<i class="fa fa-umbrella"></i> Protected'
title = 'Only owner can edit (forbid guest)'
title = 'Only owner can edit (forbid guests)'
break
case 'private':
label = '<i class="fa fa-hand-stop-o"></i> Private'

View File

@@ -3,6 +3,7 @@ window.urlpath = '<%- urlpath %>'
window.debug = <%- debug %>
window.version = '<%- version %>'
window.enableUploads = '<%- enableUploads %>'
window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %>
window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'

View File

@@ -4,8 +4,10 @@ const filterXSS = require('xss')
const whiteListAttr = ['id', 'class', 'style']
window.whiteListAttr = whiteListAttr
// allow link starts with '.', '/' and custom protocol with '://', exclude link starts with javascript://
const linkRegex = /^(?!javascript:\/\/)([\w|-]+:\/\/)|^([.|/])+/i
// allow links starting with '.', '/', '#', '?', 'http://', 'https://' and protocols supported by the navigator.registerProtocolHandler API
// 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
const dataUriRegex = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)\s*$/i
// custom white list

View File

@@ -30,3 +30,21 @@ export function decodeNoteId (encodedId) {
idParts.push(id.substr(20, 12))
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 || ''
}

3260
yarn.lock

File diff suppressed because it is too large Load Diff