Compare commits

201 Commits

Author SHA1 Message Date
34f6bb5d0f Update README.md 2026-09-14 20:06:04 +02:00
Joakim Svensson
1e2e22d238 Docker file och compose 2026-09-14 20:06:04 +02:00
Joakim Svensson
734171ded4 Docker file och compose 2026-09-14 20:06:04 +02:00
Joakim Svensson
0482e5b3c0 Add Invite Agent action to editor menu and allow CSP connect-src 2026-09-14 20:06:04 +02:00
Erik Michelson
b933504071 chore(release): bump version numbers, authors, etc
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 23:40:50 +02:00
renovate[bot]
47a7cdf3f3 chore(deps): lock file maintenance
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 22:37:37 +02:00
renovate[bot]
43f414adc4 fix(deps): update dependency markdown-it to v14.3.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 22:27:02 +02:00
Erik Michelson
02c6c08bd3 fix(upload): invalid check for upload content
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 22:25:12 +02:00
Erik Michelson
0e24145095 chore(deps): upgrade uuid to 14.0.2
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 22:25:12 +02:00
Erik Michelson
486fcfef67 chore(deps): upgrade archiver to 8.0.0
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 22:25:12 +02:00
Erik Michelson
dddd3a7ed5 fix(renderer): upgrade styles and API of highlight.js
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 22:06:36 +02:00
renovate[bot]
95ba7f6fb5 chore(deps): update dependency highlight.js to v11
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 22:06:36 +02:00
Philip Molares
788676aba4 refactor: umzug
allow umzug v3 to work with the umzug v2 migrations

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-08-21 20:30:00 +02:00
renovate[bot]
dcdf30b4c4 fix(deps): update dependency umzug to v3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 20:30:00 +02:00
Erik Michelson
074f7251e9 fix(deps): update scrypt import to ESM
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 20:05:02 +02:00
renovate[bot]
6ea80ef636 fix(deps): update dependency scrypt-kdf to v4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 20:05:02 +02:00
renovate[bot]
c66f4c2ec2 chore(deps): update dependency pymdown-extensions to v11
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 19:34:09 +02:00
renovate[bot]
10dd207ff4 chore(deps): update dependency remark-preset-lint-markdown-style-guide to v6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 19:15:25 +02:00
renovate[bot]
9a91b286ea chore(deps): update dependency globals to v17
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 19:10:33 +02:00
Philip Molares
65b39de212 refactor: formidable usage
Instead of a single image or an array formidable now always returns an array of files.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-08-21 18:31:13 +02:00
renovate[bot]
f2b50dd290 fix(deps): update dependency formidable to v3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 18:31:13 +02:00
Erik Michelson
b7c46b0a90 fix(image-upload): make imageRouter ESM ready
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 18:09:35 +02:00
renovate[bot]
01e1b6bd4d fix(deps): update dependency is-svg to v6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 18:09:35 +02:00
renovate[bot]
d62e095553 fix(deps): update dependency mariadb to v3.5.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 18:06:53 +02:00
Erik Michelson
8340e3f3a2 fix: update imports for updated markdown-it-plugins
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 17:48:11 +02:00
renovate[bot]
286add5d0d fix(deps): update dependency markdown-it-emoji to v3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 17:48:11 +02:00
renovate[bot]
5504ac52b2 fix(deps): update dependency cheerio to v1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 17:30:43 +02:00
renovate[bot]
9b008ccbab fix(deps): update dependency markdown-it to v14 [security]
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 17:15:31 +02:00
Philip Molares
37b23c86c5 ci: drop node 18
node 18 is deprecated and we already announce that HedgeDoc won't work with it anymore.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-08-21 16:34:00 +02:00
renovate[bot]
07d5997bb4 fix(deps): update dependency pg to v8.23.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 16:33:08 +02:00
renovate[bot]
03314ae9c4 chore(deps): update dependency mocha to v11.8.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 16:31:40 +02:00
renovate[bot]
a83e074ab1 chore(deps): update dependency less to v4.9.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 16:31:02 +02:00
Andreas Boesen
1464fc87cf update docs/content/setup/manual-setup.md: node 18 -> 20
The changelog 1.10.1 2025-02-02 deprecates Node 18 and says that future
releases will require Node 20.

<c02dc05bec/public/docs/release-notes.md (-1101--2025-02-02)>

Signed-off-by: Andreas Boesen <boesen@belwue.de>
2026-08-21 16:29:43 +02:00
renovate[bot]
f181334990 chore(deps): update dependency abcjs to v6.7.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 16:29:31 +02:00
renovate[bot]
28e8e34c79 fix(deps): update dependency nanoid to v3.3.18
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 16:24:07 +02:00
renovate[bot]
d9e2e6674b fix(deps): update dependency mysql2 to v3.23.4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 16:20:06 +02:00
renovate[bot]
9586121695 fix(deps): update dependency js-yaml to v4.3.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 16:19:07 +02:00
renovate[bot]
66732eff2d fix(deps): update dependency express-rate-limit to v8.6.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 15:59:06 +02:00
renovate[bot]
b2cb53143a fix(deps): update dependency dompurify to v3.4.14
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 13:25:14 +02:00
Erik Michelson
766d00c4d9 fix(realtime): proper reconnect handling for socket.io
Socket.IO seems to include an own reconnect mechanism for some time.
HedgeDoc however built an own reconnect mechanism on top of that.
Since the 'reconnect' event was dropped for socket.io 4.x, the
reconnect-cleanup code didn't run anymore.

This commit fixes this by:
- removing the manual reconnect interval,
- putting the reconnect handler into the correct place,
- setting reconnection options on the server side

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 13:22:17 +02:00
Erik Michelson
d1ad8d71fe fix(socket.io): remove deprecated cookie option from server-side
See docs at: https://socket.io/docs/v4/server-options/#cookie

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 13:22:17 +02:00
Erik Michelson
da2c065e59 fix(realtime): check for online users before clearing note
The disconnect handler for the realtime connection checks whether the
disconnecting socket was the last user for that note. If that's the
case and the note had unsaved changes, it persists them to the database
which is an async progress. In the callback of this async progress, the
note and all its pending changes (should be empty) are deleted.
However, when the user reconnects quickly because of a flaky
connection, it could happen that they create new pending changes before
the database saving finished. As there was no additional "no-users"
check before clearing the note, this could lead to content loss.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-08-21 13:22:17 +02:00
Tony Chau
ffcf79ef07 fix: open links in new tab
The addition of external link warning has changed the behavior of
opening links from opening in a new tab to opening in the same tab.
This restores the old behavior and opens all links in a new tab.

Signed-off-by: Tony Chau <90050474+fo5for@users.noreply.github.com>
2026-08-21 12:20:11 +02:00
Lars Kiesow
d05cfe4e9f feat: add auto login to skip sign-in method chooser
If you combine OIDC with local accounts or if your OIDC provider is an
external company (Facebook, GitHub, …) it makes sense for HedgeDoc to
show a additional login modal so users know that they will leave the
page.

However, if OAuth2/OIDC is a deployment's primary sign-in method and a
local OIDC provider is being used, users expect the common SSO method
being used, and he "Choose method" modal is a redundant extra click.
Essentially, you may have to click login thrice:

- Click login -> modal opens
- Click Login with provider -> provider login opens
- Log in (unless you are logged in already)

This simple patch adds an auto-login configuration, which makes the Sign
In button link straight to the provider's OAuth2 flow, improving the UX
of tHedgeDoc's login.

Signed-off-by: Lars Kiesow <lkiesow@uos.de>
2026-08-21 10:33:25 +02:00
Andy Piper
ed7f374434 docs: change mastodon color to brand purple 2026-07-31 20:10:05 +00:00
Philip Molares
16dd9a9faf docs: extend release-checklist from 1.11.1 release
We just released 1.11.1 and noticed some things that need to be changed in the release checklist.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-07-24 21:58:08 +02:00
Erik Michelson
0dda149c7f fix(permissions): ignore unknown permission values
The permission checks did not check for unknown permission values
submitted to the server. This allowed bypassing permission checks
under certain circumstances. However, this is not as critical as
it might sound at first, since checks guard that only the owner
can modify note permissions and the owner already has always
full permission to their note.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-07-24 21:39:28 +02:00
Erik Michelson
0a7fc28d8b chore(release): bump version and adapt release notes for 1.11.1
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-07-24 21:39:28 +02:00
renovate[bot]
95660a6665 chore(deps): lock file maintenance
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 22:23:42 +02:00
renovate[bot]
efcb6d472a fix(deps): update dependency mysql2 to v3.23.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:44:15 +02:00
renovate[bot]
a16f574283 chore(deps): update actions/setup-node action to v7
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:43:45 +02:00
renovate[bot]
3aae6316a7 fix(deps): update dependency helmet to v8.3.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:43:16 +02:00
renovate[bot]
88e73180fd fix(deps): update dependency express-rate-limit to v8.6.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:42:40 +02:00
renovate[bot]
7ee513b0cc chore(deps): update dependency less to v4.8.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:41:56 +02:00
renovate[bot]
c0cc5ff0bb fix(deps): update dependency nanoid to v3.3.16
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:36:22 +02:00
renovate[bot]
5e112e96df fix(deps): update dependency dompurify to v3.4.12
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:35:47 +02:00
Erik Michelson
9c1b36df95 fix(slide-preview): list items in correct order
This change fixes the displaying of (nested) list items in the preview
pane of the editor. Previously, list items were all treated as inline
blocks and therefore nested list items were placed next to the previous
item instead of below it. Now they align properly as expected.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-07-23 21:35:24 +02:00
renovate[bot]
844c390bbc chore(deps): update linters
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:35:13 +02:00
renovate[bot]
297f30ecba chore(deps): update dependency mkdocs-material to v9.7.7
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:34:28 +02:00
renovate[bot]
de588c19be chore(deps): update dependency abcjs to v6.6.4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:33:49 +02:00
renovate[bot]
44beea5d54 chore(deps): update actions/checkout action to v7.0.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 21:31:48 +02:00
Erik Michelson
2391e406cb chore(config): remove unused allowOrigin config option
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-07-22 22:54:36 +02:00
Philip Molares
b03489859a chore(changelog): explain Temp removal
Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-07-22 22:54:36 +02:00
Philip Molares
cda70628fa refactor: remove wurl dependency
This was mainly used in now removed code, any other uses were fixed with the URL constructor

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-07-22 22:54:36 +02:00
Philip Molares
0749c31100 refactor: remove Temp
This was a very old way of migrating the history and is no longer needed.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-07-22 22:54:36 +02:00
Philip Molares
3f50054609 refactor(xssWhitelist): remove unnecessary whitelist entries
All the deleted entries are in the default white contained and don't need to be added another time.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-07-22 20:20:54 +02:00
Philip Molares
1a7c27470e fix(externalLinks): make external link warning work with subpaths 2026-07-22 20:14:44 +02:00
Erik Michelson
c77d64ad6e refactor: wildcard and strict host link whitelisting
The previously contributed config option for whitelisting domains
treated all entries like wildcard entries and matched subdomains
too. Since some users don't want this behaviour, this changes the
config option to differentiate between wildcard entries like
"*.example.com" and strict host entries like "example.com".

Since this config option was not released yet, it is safe to
change it without breaking user setups.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-07-19 14:40:36 +02:00
Erik Michelson
0ebf0dbc6b feat: add config option to disable external link warning
Since it is heavily requested by users, this adds a new config option
to disable the previously introduced external link warning.
Be aware however, that disabling this config option weakens the
security of your HedgeDoc instance and is therefore only recommended
for closed trustworthy user groups.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-07-19 14:40:36 +02:00
Andre Julius
d20619315e docs: Update Arch Linux package link (community -> extra) (#6576)
Signed-off-by: Andre Julius <noromoron@gmail.com>
2026-07-19 14:34:54 +02:00
Shivam Shukla
a741859060 fix(editor): restore native browser zoom keyboard shortcuts
Signed-off-by: Shivam Shukla <lucifer84670@gmail.com>
2026-07-15 08:59:14 +02:00
Shivam Shukla
f6c1feb26f docs: clarify CMD_PORT is used for URL generation with CMD_PATH
The docs claimed that host and port are completely ignored when a
UNIX domain socket path is configured. However, the port is still
used for URL generation when urlAddPort is enabled.

Closes #6515

Signed-off-by: Shivam Shukla <lucifer84670@gmail.com>
2026-07-14 23:17:24 +02:00
odpay
2c5b38e3bd docs: add Zenith Hosting deploy option
Signed-off-by: odpay <odpay@odpay.net>
2026-07-10 16:27:37 +02:00
renovate[bot]
a48df18d9a fix(deps): update dependency pg to v8.22.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-04 23:53:46 +02:00
renovate[bot]
3c37dcfa26 fix(deps): update dependency js-yaml to v4.3.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-04 23:53:12 +02:00
renovate[bot]
7215f75624 fix(deps): update dependency nanoid to v3.3.15
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-04 23:52:42 +02:00
renovate[bot]
3889b84ac6 chore(deps): update dependency less to v4.6.7
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-04 23:50:44 +02:00
renovate[bot]
1a4ce7a33a chore(deps): update actions/cache action to v6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-04 23:49:44 +02:00
Erik Michelson
c02dc05bec docs: add CVE numbers to release-notes
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-07-04 22:27:16 +02:00
Tobias Dillig
1e75f7462d feat: add 'image/webp' to allowed upload MIME types
Signed-off-by: Tobias Dillig <tobias.dillig@frachtwerk.de>
2026-07-04 22:25:43 +02:00
Tobi
e4d5124211 feat(external-links): add whitelist to bypass redirect page (#6548)
Signed-off-by: Tobias Dillig <tobias.dillig@frachtwerk.de>
Signed-off-by: Tobi <tldev-de@users.noreply.github.com>
2026-06-25 17:16:27 +02:00
Will Hawkins
179719473c Minor Typos in Configuration Documentation
Fix minor typos in Configuration documentation to improve readability.

Signed-off-by: Will Hawkins <whh8b@obs.cr>
2026-06-23 00:13:32 +02:00
Philip Molares
e0351704c9 docs(release-checklist): add improvments from the 1.11.0 release
Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-06-19 00:13:47 +02:00
Erik Michelson
f115b2d084 fix(views): fail-safe opengraph tag HTML rendering
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-06-18 23:56:45 +02:00
Erik Michelson
4007201575 chore(deps): update lockfile
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-06-18 23:56:45 +02:00
Erik Michelson
fe15adaf31 chore(release): bump version, update release notes, update authors
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-06-18 23:56:45 +02:00
Erik Michelson
c489497e45 refactor(frontmatter): constrain frontmatter object after parsing
This change removes the meta-marked dependency which solely was
used for extracting the frontmatter, which is possible as well
with one function. Furthermore, this introduces constraints to
objects resulting from frontmatter parsing and enforces them in
order to prevent attacks like a yaml bomb (massive alias expansion).
This change should resolve a possible DoS attack.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-06-18 23:56:45 +02:00
Erik Michelson
fbd7307f16 fix(gist-export): validate OAuth2 state and redirect URI
The state of the OAuth2 response was not verified.
In theory an attacker could use this to extract the contents of a note.

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-06-18 23:56:45 +02:00
Erik Michelson
dd40a6c04b fix(auth): sanitize email addresses
Previously, maliciously crafted email addresses
could be used to sneak HTML code into the HedgeDoc
editor, since HedgeDoc uses the local part of the
address as a display name for users.
We're now sanitizing this using DOMPurify.

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-06-18 23:56:45 +02:00
renovate[bot]
291b9d592d chore(deps): update linters to v7.3.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 23:10:06 +02:00
renovate[bot]
16d426704f chore(deps): update test packages to v11.7.6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 23:09:32 +02:00
renovate[bot]
209391c7fc chore(deps): pin dependencies
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 23:01:54 +02:00
Erik Michelson
e9a2c514dd fix(ci): include node version in yarn cache
The dependency set may differ between node versions.
This fixed caching makes it harder for version-specific deps and
might introduce bugs therefore. We now include the node version
in the cache key.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-06-18 23:00:01 +02:00
Erik Michelson
6676d640ba revert: chore(deps): update yarn monorepo to v4.17.0"
This reverts commit 71a4d19a83.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-06-18 22:54:54 +02:00
renovate[bot]
71a4d19a83 chore(deps): update yarn monorepo to v4.17.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 22:13:45 +02:00
renovate[bot]
d80f26be9a fix(deps): update dependency helmet to v8.2.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 22:00:52 +02:00
renovate[bot]
1851f34d3f fix(deps): update dependency body-parser to v2.3.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:58:14 +02:00
renovate[bot]
32b433fa62 chore(deps): update dependency esbuild-loader to v4.5.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:57:16 +02:00
renovate[bot]
865e5b4180 fix(deps): update dependency pg to v8.21.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:51:05 +02:00
renovate[bot]
b940b52e71 chore(deps): update actions/checkout action to v7
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:50:25 +02:00
renovate[bot]
f443e5bbd4 chore(deps): update dependency pymdown-extensions to v10.21.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:50:12 +02:00
renovate[bot]
be6273ec8f fix(deps): update dependency dompurify to v3.4.11
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:49:36 +02:00
renovate[bot]
528530ebf3 fix(deps): update dependency morgan to v1.11.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:48:22 +02:00
renovate[bot]
0de5c5c1a9 fix(deps): update dependency express to v4.22.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:47:17 +02:00
renovate[bot]
86e20c9c48 fix(deps): update dependency nanoid to v3.3.13
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:44:01 +02:00
renovate[bot]
8046666943 fix(deps): update dependency uuid to v11.1.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:41:15 +02:00
renovate[bot]
7783aa7cc5 fix(deps): update dependency express-rate-limit to v8.5.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:40:46 +02:00
renovate[bot]
8b9463358d chore(deps): update dependency less to v4.6.6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:36:00 +02:00
renovate[bot]
72b3696af5 chore(deps): update dependency js-cookie to v3.0.8
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:31:06 +02:00
renovate[bot]
07505cdd61 chore(deps): update actions/checkout action to v6.0.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 21:30:19 +02:00
Philip Molares
8891d75baa feat(rate-limit): add more config options
Allow the admin to specify the rate-limits for user signup / login
Allow the admin to specify if Cloudflare specific rate-limit functionality should be used or not

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-06-17 21:08:07 +02:00
Philip Molares
d7c1e45814 refactor(docs): fix links
Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-06-17 19:35:24 +02:00
Philip Molares
523cb8ea1f chore(changelog): changes to config.json.example
Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-06-17 19:35:24 +02:00
Philip Molares
1fb904a137 feat(docs): add production ready page
This page explains what you need to consider and configure to make your HedgeDoc instance production ready.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-06-17 19:35:24 +02:00
Philip Molares
c77676faee refactor(config): improve config.json.example
Remove all the unnecessary config options, we know have a docs website for quite some time, we don't need this to document options.
Add some sane defaults in regard to anonymous usage and stats

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-06-17 19:35:24 +02:00
Philip Molares
f1070734b1 refactor(docs): move media guides
The media guides should also live in their own folder like the auth guides do as well

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-06-17 19:35:24 +02:00
Erik Michelson
814271dc0b feat(editor): add external link warning page
A malicious user could modify a valid looking link (for example of the editor UI)
to point to a malicous site, by using overlays and CSS. Since CSS should stay
enabled, we need to make the user aware of possible risks when leaving the
HedgeDoc instance, in order to protect them from credential-theft.

This commit adds a new interstitial page for external links, that shows the
target URL and asks the user, whether they really want to continue.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-06-10 20:15:09 +02:00
renovate[bot]
fa00e34635 chore(deps): update dependency abcjs to v6.6.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-09 19:30:55 +02:00
renovate[bot]
c145549af4 fix(deps): update dependency mysql2 to v3.22.5
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-09 11:41:01 +02:00
지지 ᚠד (Jiji Freya Daniel) Maslowski
193446daed docs(setup): note that the FreeBSD port is unmaintained
Signed-off-by: 지지 ᚠד (Jiji Freya Daniel) Maslowski <info@orangecms.org>
2026-05-30 11:10:30 +02:00
renovate[bot]
cc9194bfca fix(deps): update dependency dompurify to v3.4.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 14:15:41 +02:00
renovate[bot]
2abfdb9628 chore(deps): update actions/setup-node action to v6.4.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 19:12:50 +02:00
Mathias Kende
03775f0f80 docs(auth): update authelia configuration guide (#6465) 2026-04-16 10:24:58 +02:00
Erik Michelson
f0f0b3a3b4 docs(release): update checklist with minor fixes
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-04-16 10:21:45 +02:00
Erik Michelson
b09975a3ba chore(build): success message + executable bit for ot build script
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-04-15 21:35:05 +02:00
Erik Michelson
8b00b6419c chore(release): update AUTHORS and release notes
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-04-15 21:35:05 +02:00
Erik Michelson
20de73bb65 chore(release): bump version to 1.10.8
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-04-15 21:35:05 +02:00
renovate[bot]
bb32d61a7e chore(deps): lock file maintenance
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:32:54 +02:00
renovate[bot]
325c659e4b chore(deps): update actions/cache action to v5.0.5
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:23:40 +02:00
renovate[bot]
0b27277612 chore(deps): update dependency turndown to v7.2.4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:23:29 +02:00
renovate[bot]
5123f3b57d fix(deps): update dependency connect-session-sequelize to v8.0.6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:22:01 +02:00
renovate[bot]
ba91457a51 chore(deps): update dependency esbuild-loader to v4.4.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:20:42 +02:00
renovate[bot]
31933a9f7d fix(deps): update dependency file-type to v21.3.4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:20:09 +02:00
renovate[bot]
fbd2cdbc2f chore(deps): update dependency pymdown-extensions to v10.21.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:19:39 +02:00
renovate[bot]
0432642231 chore(deps): update dependency less to v4.6.4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:17:03 +02:00
renovate[bot]
451ae33f0d fix(deps): update dependency validator to v13.15.35
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:14:09 +02:00
renovate[bot]
8d10830dc4 fix(deps): update dependency sanitize-filename to v1.6.4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:12:44 +02:00
renovate[bot]
954d84129d fix(deps): update dependency lodash to v4.18.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:12:20 +02:00
renovate[bot]
779682d35f chore(deps): update yarn to v4.13.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:09:03 +02:00
renovate[bot]
254c1227d2 fix(deps): update dependency dompurify to v3.4.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 20:08:45 +02:00
renovate[bot]
e1c28c46e6 fix(deps): update dependency mysql2 to v3.22.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 19:56:47 +02:00
renovate[bot]
7ca140d9a7 fix(deps): update dependency express-rate-limit to v8.3.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 19:55:53 +02:00
renovate[bot]
78ff6fc49f chore(deps): update dependency mkdocs-redirects to v1.2.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 19:53:21 +02:00
Erik Michelson
ca31580db5 fix(ot): add minified updated OT library and build:ot script
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-03-24 21:12:41 +01:00
Erik Michelson
5a5e12a693 fix(ot): keep pendingOps in StaleWithBuffer during applyClient
Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-03-24 21:12:41 +01:00
Erik Michelson
c4841575c9 fix(ot): use default color constant for hex2rgb fallback
Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-03-24 21:12:41 +01:00
bxff
65bf11554a fix: buffer operations in Stale/StaleWithBuffer instead of throwing (#6342)
When 5+ users edit concurrently, revision gaps trigger the Stale state
which throws 'Ignored server-side change.' on incoming operations. This
causes document divergence, silent data corruption, and eventually
force-resets that discard local edits.

Changes:
- Stale/StaleWithBuffer.applyServer: buffer incoming operations instead
  of throwing, apply them sequentially after missing ops arrive
- Stale.applyClient: carry pendingOps to StaleWithBuffer on transition
- editor-socketio-server: retry-based force-reset (3 failures) instead
  of immediate nuclear reset on any OT error
- hex2rgb: defensive null checks for old non-hex color records

Signed-off-by: bxff <51504045+bxff@users.noreply.github.com>
2026-03-24 21:12:41 +01:00
renovate[bot]
f51f8dcc19 chore(deps): update linters
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 13:09:58 +01:00
renovate[bot]
ee1435748f chore(deps): update actions/upload-artifact action to v7
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 13:07:52 +01:00
renovate[bot]
cbed25c170 chore(deps): update dependency mkdocs-material to v9.7.6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 13:04:30 +01:00
renovate[bot]
90febd954e fix(deps): update dependency mysql2 to v3.20.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 13:03:35 +01:00
renovate[bot]
f7642f37f9 fix(deps): update dependency pg to v8.20.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 13:02:46 +01:00
renovate[bot]
32f119f7ac fix(deps): update dependency rimraf to v6.1.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 12:59:41 +01:00
renovate[bot]
f5fceed8e2 fix(deps): update dependency minio to v8.0.7
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 12:58:16 +01:00
renovate[bot]
c75b02bc66 fix(deps): update dependency file-type to v21.3.2 [security]
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 12:56:44 +01:00
renovate[bot]
8ffea9963a fix(deps): update dependency dompurify to v3.3.2 [security]
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 12:56:17 +01:00
renovate[bot]
11b299d186 chore(deps): update actions/setup-node action to v6.3.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 12:24:05 +01:00
renovate[bot]
2adc315f13 chore(deps): update dependency abcjs to v6.6.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 14:55:43 +01:00
renovate[bot]
6c91a8728e chore(deps): update dependency abcjs to v6.6.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 14:52:00 +01:00
Philip Molares
cdedc8df33 chore(dependency): fix pymdown-extensions version
Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-02-24 20:41:39 +01:00
Philip Molares
f0c0200eb9 docs(release-notes): add section about 1.10.7
Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-02-24 20:24:44 +01:00
Erik Michelson
5cefc3cfe1 fix(cli): do not silently drop errors in manage_users CLI
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-02-24 20:24:44 +01:00
Philip Molares
5d00de9892 chore(release): bump version to 1.10.7
Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-02-24 20:24:44 +01:00
Philip Molares
5603af245c docs: add changelog entry
Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-02-21 23:47:53 +01:00
Philip Molares
7e2a783161 fix(realtime): aboard finishConnection if user is missing
This can happen if the user disconnects during connection creation.

See #6101

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-02-21 23:47:53 +01:00
renovate[bot]
edf1f62b2e chore(deps): update dependency pymdown-extensions to v10.21.1 2026-02-21 20:48:08 +01:00
renovate[bot]
6a4ab2bc5b chore(deps): update actions/setup-node action to v6.2.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-21 20:44:01 +01:00
Erik Michelson
fd4d5bfbd1 fix(editor): always use random hex colors
chance.color() can output all valid CSS
values for colors, including rgba() syntax and predefined color
names. Due to the usage of hex2rgb and other methods, this can
break the editor. This commit ensures that all randomly generated
colors are specified in hex format.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-02-18 19:43:33 +01:00
xenein
7408217b6a docs(features): clarification on locations for light/dark mode (#6355)
Signed-off-by: xenein <76600392+xenein@users.noreply.github.com>
2026-02-15 21:07:54 +01:00
Erik Michelson
2e66e4d5ec fix(docs): update release checklist
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-02-04 21:34:39 +01:00
Erik Michelson
145e3ee318 chore(release): bump versions and release notes
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-02-04 20:27:41 +01:00
Erik Michelson
f910000d2b feat(security): sanitize SVG files on upload
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-02-04 20:27:41 +01:00
Erik Michelson
b930fe04ce fix(security): increase CSP policy by explicitly sandboxing uploads
Adding the sandbox attribute tells browsers to apply stricter
restrictions like preventing execution of plugins and scripts.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-02-04 20:27:41 +01:00
Erik Michelson
74daa0e7a1 fix(security): headers were not applied to uploads
Due to express static overriding the response, applying the extra headers
like CSP and Content-Disposition did not work for files served under the
/uploads route. By reordering, the code this works now.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-02-04 20:27:41 +01:00
Erik Michelson
6fc411d17f chore(deps): update lockfile
renovate was tired, so I did it manually

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-02-03 20:48:50 +01:00
Erik Michelson
0e9e2177cf chore(deps): update dependencies
renovate was tired, so I did it manually

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-02-03 20:44:27 +01:00
renovate[bot]
f529214660 chore(deps): update actions/cache action to v5.0.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 20:19:26 +01:00
renovate[bot]
ebf98130d7 chore(deps): update actions/checkout action to v6.0.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 20:18:58 +01:00
renovate[bot]
d2dd6762a7 chore(deps): update dependency jquery-ui to v1.14.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 20:16:19 +01:00
renovate[bot]
14edc1a7a5 fix(deps): update dependency connect-session-sequelize to v8.0.5
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 20:13:31 +01:00
renovate[bot]
58d436c924 fix(deps): update dependency mysql2 to v3.16.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 20:13:01 +01:00
renovate[bot]
a3b49a6e24 chore(deps): update dependency esbuild-loader to v4.4.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 19:56:20 +01:00
renovate[bot]
a96e9cbdf9 fix(deps): update dependency lodash to v4.17.23 [security]
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 19:55:09 +01:00
renovate[bot]
a2da69ede5 chore(deps): update dependency eslint-plugin-n to v17.23.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 19:53:31 +01:00
renovate[bot]
8725196df6 chore(deps): update dependency utf-8-validate to v6.0.6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 19:51:43 +01:00
renovate[bot]
7c9d1ec712 fix(deps): update dependency body-parser to v2.2.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 19:51:12 +01:00
Erik Michelson
092e4d428d docs: permissions page in the docs references
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2026-01-28 20:26:25 +01:00
Philip Molares
67b1917467 docs: fix number of permissions HD1 has
Signed-off-by: Philip Molares <philip.molares@udo.edu>
2026-01-28 20:08:25 +01:00
renovate[bot]
e7055b43a4 chore(deps): update dependency mkdocs-material to v9.7.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 19:18:55 +01:00
renovate[bot]
eba2b2d711 chore(deps): update actions/cache action to v5.0.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 19:18:10 +01:00
xenein
cb7588d6fa docs: Add speaker view and speaker notes to slide-options.md (#6322) 2026-01-15 13:12:10 +01:00
renovate[bot]
48a207571b chore(deps): update actions/upload-artifact action to v6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 20:31:28 +01:00
renovate[bot]
b76f397983 chore(deps): update actions/cache action to v5
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 20:27:48 +01:00
renovate[bot]
e77b9ced58 fix(deps): update dependency mysql2 to v3.16.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 20:17:17 +01:00
renovate[bot]
98e9e6cefc chore(deps): update dependency pymdown-extensions to v10.19.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 20:13:50 +01:00
renovate[bot]
35f0b44906 chore(deps): update dependency less to v4.5.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 20:08:47 +01:00
renovate[bot]
92efa38948 chore(deps): update linters to v9.39.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 20:00:12 +01:00
renovate[bot]
4df25c436b fix(deps): update dependency aws-sdk to v2.1693.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 22:02:02 +01:00
renovate[bot]
72e38c6bfd chore(deps): update dependency pymdown-extensions to v10.18
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 21:21:31 +01:00
renovate[bot]
b0911e4434 fix(deps): update dependency winston to v3.19.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 11:00:20 +01:00
Philip Molares
9cb00262f6 docs: update release-checklist
Add the missings tests that lead to the problem with
HedgeDoc 1.10.4

Signed-off-by: Philip Molares <philip.molares@udo.edu>
2025-12-07 16:37:16 +01:00
97 changed files with 5249 additions and 4345 deletions

View File

@@ -19,14 +19,14 @@ runs:
shell: bash shell: bash
- name: Cache yarn cache - name: Cache yarn cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: yarn-cache id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-master key: ${{ runner.os }}-node${{ inputs.NODEJS_VERSION }}-yarn-master
- name: Set up NodeJS - name: Set up NodeJS
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: ${{ inputs.NODEJS_VERSION }} node-version: ${{ inputs.NODEJS_VERSION }}

View File

@@ -15,11 +15,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node: ['18', '20', '22', '24'] node: ['20', '22', '24']
name: Node ${{ matrix.node }} name: Node ${{ matrix.node }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node - name: Setup Node
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
@@ -33,7 +33,7 @@ jobs:
run: yarn run mocha-suite run: yarn run mocha-suite
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v7.0.1
if: github.ref == 'refs/heads/master' && matrix.node-version == '22' if: github.ref == 'refs/heads/master' && matrix.node-version == '22'
with: with:
name: Prebuild with Node.js ${{ matrix.node-version }} name: Prebuild with Node.js ${{ matrix.node-version }}

View File

@@ -12,7 +12,7 @@ jobs:
name: Lint files name: Lint files
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup node - name: Setup node
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node

2
.gitignore vendored
View File

@@ -16,6 +16,8 @@ backups/
*.log *.log
*.sqlite *.sqlite
*~
# ignore config files # ignore config files
config.json config.json
.sequelizerc .sequelizerc

View File

@@ -33,3 +33,7 @@ Philip Molares <philip.molares@udo.edu> Philip Molares <git@molar.es>
Yannick Bungers <git@innay.de> Yannick Bungers <github@innay.de> Yannick Bungers <git@innay.de> Yannick Bungers <github@innay.de>
Renovate Bot <bot@renovateapp.com> renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Renovate Bot <bot@renovateapp.com> renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Tobias Dillig <tobias.dillig@frachtwerk.de> Tobi <tldev-de@users.noreply.github.com>
Andreas Boesen <boesen@belwue.de> Andreas Boesen <618847+Happy86@users.noreply.github.com>

File diff suppressed because one or more lines are too long

940
.yarn/releases/yarn-4.13.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

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

13
AUTHORS
View File

@@ -9,7 +9,9 @@ Alex Garcia <alexsebastian.garcia@gmail.com>
Alexander Hesse <alexander.hesse@sandstorm-media.de> Alexander Hesse <alexander.hesse@sandstorm-media.de>
Alexander Wellbrock <a.wellbrock@mailbox.org> Alexander Wellbrock <a.wellbrock@mailbox.org>
Amolith <amolith@nixnet.xyz> Amolith <amolith@nixnet.xyz>
Andreas Boesen <618847+Happy86@users.noreply.github.com> Andre Julius <noromoron@gmail.com>
Andreas Boesen <boesen@belwue.de>
Andy Piper <andypiper@users.noreply.github.com>
Antoine Aflalo <antoine@warrantymaster.com> Antoine Aflalo <antoine@warrantymaster.com>
aptalca <aptalca@users.noreply.github.com> aptalca <aptalca@users.noreply.github.com>
Augustin Trancart <augustin.trancart@oslandia.com> Augustin Trancart <augustin.trancart@oslandia.com>
@@ -18,6 +20,7 @@ Bartlomiej Szala <fenix440@gmail.com>
BoHong Li <a60814billy@gmail.com> BoHong Li <a60814billy@gmail.com>
Bryan Davis <bd808@wikimedia.org> Bryan Davis <bd808@wikimedia.org>
butlerx <butlerx@notthe.cloud> butlerx <butlerx@notthe.cloud>
bxff <51504045+bxff@users.noreply.github.com>
Carles Bruguera <carlesba@gmail.com> Carles Bruguera <carlesba@gmail.com>
chandi <git@chandi.it> chandi <git@chandi.it>
chbmb <chbmb@linuxserver.io> chbmb <chbmb@linuxserver.io>
@@ -116,6 +119,7 @@ Martijnpold <martijntje7@gmail.com>
Martin Turoci <martinturoci@gmail.com> Martin Turoci <martinturoci@gmail.com>
Marvin Gaube <dev@marvingaube.de> Marvin Gaube <dev@marvingaube.de>
Matej Cotman <matej.cotman@eficode.com> Matej Cotman <matej.cotman@eficode.com>
Mathias Kende <mathias@kende.fr>
Mathias Merscher <Mathias.Merscher@dg-i.net> Mathias Merscher <Mathias.Merscher@dg-i.net>
Matt Fisher <matt.fisher@nsidc.org> Matt Fisher <matt.fisher@nsidc.org>
Matteo Savatteri <matteosavatteri@lcm.mi.infn.it> Matteo Savatteri <matteosavatteri@lcm.mi.infn.it>
@@ -134,6 +138,7 @@ Nicolas Dietrich <nidi@mailbox.org>
Nils VAN ZUIJLEN <nils.van-zuijlen@mailo.com> Nils VAN ZUIJLEN <nils.van-zuijlen@mailo.com>
Norihito Nakae <norihito.n@gmail.com> Norihito Nakae <norihito.n@gmail.com>
NV <nvsofts@gmail.com> NV <nvsofts@gmail.com>
odpay <odpay@odpay.net>
Oliver Herrmann <oliver@monoxane.com> Oliver Herrmann <oliver@monoxane.com>
Ondřej Slabý <kron258@gmail.com> Ondřej Slabý <kron258@gmail.com>
oupala <oupala@users.noreply.github.com> oupala <oupala@users.noreply.github.com>
@@ -164,6 +169,7 @@ Salim B <salim@posteo.de>
Sandro Jäckel <sandro.jaeckel@gmail.com> Sandro Jäckel <sandro.jaeckel@gmail.com>
schneems <richard.schneeman@gmail.com> schneems <richard.schneeman@gmail.com>
Sergio Valverde <svg153@users.noreply.github.com> Sergio Valverde <svg153@users.noreply.github.com>
Shivam Shukla <lucifer84670@gmail.com>
Simeon Keske <git@n0emis.eu> Simeon Keske <git@n0emis.eu>
Simon C <simonc@linux.com> Simon C <simonc@linux.com>
Simon Fish <si@mon.fish> Simon Fish <si@mon.fish>
@@ -184,19 +190,23 @@ Thor77 <thor77@thor77.org>
Tilman Vatteroth <git@tilmanvatteroth.de> Tilman Vatteroth <git@tilmanvatteroth.de>
tkqubo <tk.qubo@gmail.com> tkqubo <tk.qubo@gmail.com>
tkykm <tkykm@users.noreply.github.com> tkykm <tkykm@users.noreply.github.com>
Tobias Dillig <tobias.dillig@frachtwerk.de>
Tobias Kremer <tobias.kremer@gmail.com> Tobias Kremer <tobias.kremer@gmail.com>
Tom Wyckhuys <tomwyckhuys@gmail.com> Tom Wyckhuys <tomwyckhuys@gmail.com>
Toma Tasovac <ttasovac@humanistika.org> Toma Tasovac <ttasovac@humanistika.org>
Tony Chau <90050474+fo5for@users.noreply.github.com>
toshi0123 <7948737+toshi0123@users.noreply.github.com> toshi0123 <7948737+toshi0123@users.noreply.github.com>
Turakar <turakar23@gmail.com> Turakar <turakar23@gmail.com>
VaiTon <eyadlorenzo@gmail.com> VaiTon <eyadlorenzo@gmail.com>
vazontang <31801725+vazontang@users.noreply.github.com> vazontang <31801725+vazontang@users.noreply.github.com>
Victor Berger <victor.berger@m4x.org> Victor Berger <victor.berger@m4x.org>
Vojtěch Káně <vojtech.kane@gmail.com> Vojtěch Káně <vojtech.kane@gmail.com>
Will Hawkins <whh8b@obs.cr>
WilliButz <wbutz@cyberfnord.de> WilliButz <wbutz@cyberfnord.de>
Wonder Chang <iwonder.tw@gmail.com> Wonder Chang <iwonder.tw@gmail.com>
Xaver Maierhofer <xaver.maierhofer@xwissen.info> Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Xavier Marques <xaviermarques4f@gmail.com> Xavier Marques <xaviermarques4f@gmail.com>
xenein <76600392+xenein@users.noreply.github.com>
Xiaodong Xu <xxdlhy@gmail.com> Xiaodong Xu <xxdlhy@gmail.com>
xnum <s000032001@gmail.com> xnum <s000032001@gmail.com>
Yannick Bungers <git@innay.de> Yannick Bungers <git@innay.de>
@@ -209,3 +219,4 @@ Zearin <Zearin@users.noreply.github.com>
Éric Gaspar <46165813+ericgaspar@users.noreply.github.com> Éric Gaspar <46165813+ericgaspar@users.noreply.github.com>
Ömer Erdinç Yağmurlu <omeryagmurlu@gmail.com> Ömer Erdinç Yağmurlu <omeryagmurlu@gmail.com>
蒼時弦也 <elct9620@frost.tw> 蒼時弦也 <elct9620@frost.tw>
지지 ᚠד (Jiji Freya Daniel) Maslowski <info@orangecms.org>

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
############################
# 1) Builder: bygg HedgeDoc
############################
FROM node:18-alpine AS builder
WORKDIR /build
# H??mta din branch som tarball (ingen git beh??vs)
ADD https://git.sa6anw.se/sa6anw/hedgedoc/archive/refs/heads/hedgeagent/1.10.5.tar.gz /tmp/hedgedoc.tar.gz
RUN tar -xzf /tmp/hedgedoc.tar.gz --strip-components=1 \
&& rm /tmp/hedgedoc.tar.gz
# Installera deps + bygg frontend/backend
RUN corepack enable \
&& yarn install --immutable \
&& yarn build
########################################
# 2) Runtime: officiell HedgeDoc-image
########################################
FROM quay.io/hedgedoc/hedgedoc:1.10.5
USER root
# Ers??tt upstream-koden
RUN rm -rf /hedgedoc
# Kopiera f??rdigbyggd app med r??tt UID/GID
COPY --from=builder --chown=10000:10000 /build /hedgedoc
USER 10000

View File

@@ -1,3 +1,21 @@
# HedgeDoc-HedgeAgent
This is a customized version that makes it possible to invite an AI agent to a note via the menu. "Invite Agent" sends the `note_id` to hedgeagent.
To start an instance for development, the following is required:
1. ```cp config.json.example config.json ```
2. Update ```"domain": "change this" ```
3. ```.yarn/releases/yarn-4.12.0.cjs install```
4. ```.yarn/releases/yarn-4.12.0.cjs run build```
5. ```.yarn/releases/yarn-4.12.0.cjs start```
To start for production:
1. `docker compose up -d`
Note: Use UID 10000 for data folders.
---
![HedgeDoc Logo](docs/content/images/hedgedoc_logo_black.svg) ![HedgeDoc Logo](docs/content/images/hedgedoc_logo_black.svg)
# HedgeDoc # HedgeDoc

32
app.js
View File

@@ -85,11 +85,14 @@ if (config.enableStatsApi) {
const io = new Server(server, { const io = new Server(server, {
pingInterval: config.heartbeatInterval, pingInterval: config.heartbeatInterval,
pingTimeout: config.heartbeatTimeout, pingTimeout: config.heartbeatTimeout,
cookie: false,
cors: { cors: {
origin: config.serverURL, origin: config.serverURL,
methods: ['GET', 'POST'], methods: ['GET', 'POST'],
credentials: true credentials: true
},
connectionStateRecovery: {
maxDisconnectionDuration: 2 * 60 * 1000,
skipMiddlewares: false
} }
}) })
@@ -156,6 +159,13 @@ app.use(cookieParser())
app.use(i18n.init) app.use(i18n.init)
// routes without sessions // routes without sessions
// security headers for uploads
app.use('/uploads', (req, res, next) => {
res.set('Content-Disposition', 'attachment')
res.set('Content-Security-Policy', "default-src 'none'; sandbox")
next()
})
// static files // static files
app.use('/', express.static(path.join(__dirname, '/public'), { app.use('/', express.static(path.join(__dirname, '/public'), {
maxAge: config.staticCacheTime, maxAge: config.staticCacheTime,
@@ -166,13 +176,6 @@ app.use('/docs', express.static(path.resolve(__dirname, config.docsPath), {
maxAge: config.staticCacheTime, maxAge: config.staticCacheTime,
redirect: false 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), { app.use('/uploads', express.static(path.resolve(__dirname, config.uploadsPath), {
maxAge: config.staticCacheTime, maxAge: config.staticCacheTime,
redirect: false redirect: false
@@ -253,6 +256,15 @@ app.locals.authProviders = {
allowEmailRegister: config.allowEmailRegister allowEmailRegister: config.allowEmailRegister
} }
// If none of the form-based methods (LDAP, Email, OpenID) are configured and
// exactly one redirect-style external provider is, skip the "Choose method"
// dialog and link "Sign In" straight to that provider's login page.
const externalProviderKeys = ['facebook', 'twitter', 'github', 'gitlab', 'mattermost', 'dropbox', 'google', 'saml', 'oauth2']
const enabledExternalProviders = externalProviderKeys.filter((key) => app.locals.authProviders[key])
app.locals.authProviders.autoLoginProvider = (!app.locals.authProviders.ldap && !app.locals.authProviders.email && !app.locals.authProviders.openID && enabledExternalProviders.length === 1)
? enabledExternalProviders[0]
: null
// Export/Import menu items // Export/Import menu items
app.locals.enableDropBoxSave = config.isDropboxEnable app.locals.enableDropBoxSave = config.isDropboxEnable
app.locals.enableGitHubGist = config.isGitHubEnable app.locals.enableGitHubGist = config.isGitHubEnable
@@ -262,8 +274,8 @@ app.use(require('./lib/web/baseRouter'))
app.use(require('./lib/web/statusRouter')) app.use(require('./lib/web/statusRouter'))
app.use(require('./lib/web/auth')) app.use(require('./lib/web/auth'))
app.use(require('./lib/web/historyRouter')) app.use(require('./lib/web/historyRouter'))
app.use(require('./lib/web/userRouter')) app.use(require('./lib/web/userRouter').default)
app.use(require('./lib/web/imageRouter')) app.use(require('./lib/web/imageRouter').default)
app.use(require('./lib/web/note/router')) app.use(require('./lib/web/note/router'))
// response not found if no any route matxches // response not found if no any route matxches

View File

@@ -7,6 +7,7 @@ logger.transports.forEach((transport) => transport.level = 'warning')
const models = require('../lib/models/') const models = require('../lib/models/')
const readline = require('readline-sync') const readline = require('readline-sync')
const minimist = require('minimist') const minimist = require('minimist')
const { ValidationError } = require('sequelize')
function showUsage (tips) { function showUsage (tips) {
console.log(`${tips} console.log(`${tips}
@@ -48,6 +49,7 @@ async function createUser (argv) {
} }
// Lets try to create, and check success // Lets try to create, and check success
try {
const ref = await models.User.create({ email: argv['add'], password: pass }) const ref = await models.User.create({ email: argv['add'], password: pass })
if (ref === undefined) { if (ref === undefined) {
console.log(`Could not create user with email ${argv['add']}`) console.log(`Could not create user with email ${argv['add']}`)
@@ -55,6 +57,14 @@ async function createUser (argv) {
} else { } else {
console.log(`Created user with email ${argv['add']}`) console.log(`Created user with email ${argv['add']}`)
} }
} catch (err) {
if (err instanceof ValidationError) {
console.log('Could not create user, since the email address is invalid.')
process.exit(1)
} else {
throw err
}
}
} }
// Using an async function to be able to use await inside // Using an async function to be able to use await inside
@@ -117,4 +127,7 @@ if (typeof argv[action] !== 'string') {
// Call respective processing functions // Call respective processing functions
options[action](argv).then(function () { options[action](argv).then(function () {
process.exit(0); process.exit(0);
}).catch(err => {
console.error('An error occurred:', err)
process.exit(1)
}); });

View File

@@ -18,20 +18,12 @@
"production": { "production": {
"domain": "change this", "domain": "change this",
"loglevel": "info", "loglevel": "info",
"hsts": {
"enable": true,
"maxAgeSeconds": 31536000,
"includeSubdomains": true,
"preload": true
},
"csp": { "csp": {
"enable": true, "enable": true,
"directives": { "directives": {
}, },
"upgradeInsecureRequests": "auto", "upgradeInsecureRequests": "auto",
"addDefaults": true, "addDefaults": true
"addDisqus": true,
"addGoogleAnalytics": true
}, },
"cookiePolicy": "lax", "cookiePolicy": "lax",
"db": { "db": {
@@ -42,103 +34,8 @@
"port": "5432", "port": "5432",
"dialect": "postgres" "dialect": "postgres"
}, },
"facebook": { "allowAnonymous": false,
"clientID": "change this", "allowAnonymousEdits": true,
"clientSecret": "change this" "enableStatsApi": false
},
"twitter": {
"consumerKey": "change this",
"consumerSecret": "change this"
},
"github": {
"clientID": "change this",
"clientSecret": "change this"
},
"gitlab": {
"baseURL": "change this",
"clientID": "change this",
"clientSecret": "change this",
"scope": "use 'read_user' scope for auth user only or remove this property if you need gitlab snippet import/export support (will result to be default scope 'api')",
"version": "use 'v4' if gitlab version > 11, 'v3' otherwise. Default to 'v4'"
},
"mattermost": {
"baseURL": "change this",
"clientID": "change this",
"clientSecret": "change this"
},
"dropbox": {
"clientID": "change this",
"clientSecret": "change this",
"appKey": "change this"
},
"google": {
"clientID": "change this",
"clientSecret": "change this",
"apiKey": "change this"
},
"ldap": {
"url": "ldap://change_this",
"bindDn": null,
"bindCredentials": null,
"searchBase": "change this",
"searchFilter": "change this",
"searchAttributes": ["change this"],
"usernameField": "change this e.g. cn",
"useridField": "change this e.g. uid",
"tlsOptions": {
"changeme": "See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback"
}
},
"saml": {
"idpSsoUrl": "change: authentication endpoint of IdP",
"idpCert": "change: certificate file path of IdP in PEM format",
"issuer": "change or delete: identity of the service provider (default: config.serverURL)",
"identifierFormat": "change or delete: name identifier format (default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')",
"disableRequestedAuthnContext": "change or delete: true to allow any authentication method, false restricts to password authentication method (default: false)",
"wantAssertionsSigned": "change or delete: true to enforce signed assertions, false allows unsigned assertions (default: true)",
"wantAuthnResponseSigned": "change or delete: true to enforce signed response, false allows unsigned response (default: true)",
"groupAttribute": "change or delete: attribute name for group list (ex: memberOf)",
"requiredGroups": [ "change or delete: group names that allowed" ],
"externalGroups": [ "change or delete: group names that not allowed" ],
"attribute": {
"id": "change or delete this: attribute map for `id` (default: NameID)",
"username": "change or delete this: attribute map for `username` (default: NameID)",
"email": "change or delete this: attribute map for `email` (default: NameID)"
}
},
"oauth2": {
"baseURL": "https://auth.example.com/",
"userProfileURL": "https://auth.example.com/oauth2/userinfo/",
"tokenURL": "https://auth.example.com/oauth2/token/",
"authorizationURL": "https://auth.example.com/oauth2/authorize/",
"clientID": "change-this-id",
"clientSecret": "change-this-secret",
"scope": "openid profile user",
"userProfileUsernameAttr": "preferred_username",
"userProfileEmailAttr": "email",
"userProfileDisplayNameAttr": "name",
"pkce": true
},
"imgur": {
"clientID": "change this"
},
"minio": {
"accessKey": "change this",
"secretKey": "change this",
"endPoint": "change this",
"secure": true,
"port": 9000
},
"s3": {
"accessKeyId": "change this",
"secretAccessKey": "change this",
"region": "change this"
},
"s3bucket": "change this",
"azure":
{
"connectionString": "change this",
"container": "change this"
}
} }
} }

50
docker-compose.yaml Normal file
View File

@@ -0,0 +1,50 @@
services:
database:
image: postgres:13.4-alpine
environment:
- POSTGRES_USER=hedgedoc
- POSTGRES_PASSWORD=password
- POSTGRES_DB=hedgedoc
volumes:
- ./db:/var/lib/postgresql/data
restart: always
networks:
- internal
app:
build: .
image: hedgedoc-hedgeagent:1.10.5
container_name: hedgedoc
environment:
CMD_DB_URL: postgres://hedgedoc:password@database:5432/hedgedoc
CMD_PORT: "3000"
CMD_HOST: "0.0.0.0"
CMD_DOMAIN: hedgedoc.sa6anw.se
CMD_URL_ADDPORT: "false"
CMD_PROTOCOL_USESSL: "true"
CMD_EMAIL: "true"
CMD_ALLOW_EMAIL_REGISTER: "true"
volumes:
- ./uploads:/hedgedoc/public/uploads
restart: always
depends_on:
- database
# Since I use Caddy I don't need this
# ports:
# - "3000:3000"
## If you use Caddy like I do keep below
networks:
- internal
- proxy
labels:
caddy: hedgedoc.sa6anw.se
caddy.reverse_proxy: "{{upstreams 3000}}"
networks:
internal:
internal: true
proxy:
external: true
## If you don't, you can deleta all of this

View File

@@ -6,14 +6,14 @@ Environment variables take precedence over configurations from the config files.
our own options, but we also list node-specific options you can configure this way. our own options, but we also list node-specific options you can configure this way.
- Environment variables are processed in [ - Environment variables are processed in [
`lib/config/environment.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/environment.js) - so this is `lib/config/environment.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/environment.js) which makes it
the first place to look if anything is missing not obvious from this document. The default values are defined in [ the first place to look if anything is missing or not obvious from this document. The default values are defined in [
`lib/config/default.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/default.js), in case you wonder `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. if you even need to override it.
- The config file is processed in [ - 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 `lib/config/index.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/index.js) which makes it
place to look if anything is missing not obvious from this document. The default values are defined in [ the first place to look if anything is missing or not obvious from this document. The default values are defined in [
`lib/config/default.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/default.js), in case you wonder `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 [ 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 `config.json.example`](https://github.com/hedgedoc/hedgedoc/tree/master/config.json.example) and copy it
@@ -32,7 +32,7 @@ to work.*
## HedgeDoc basics ## HedgeDoc basics
| config file | environment | **default** and example value | description | | config file | environment | **default** and example value | description |
|----------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------------|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| | `CMD_CONFIG_FILE` | **no default**, `/path/to/config.json` | optional override for the path to HedgeDoc's config file | | | `CMD_CONFIG_FILE` | **no default**, `/path/to/config.json` | optional override for the path to HedgeDoc's config file |
| `db` | | **`undefined`**, `{ "dialect": "sqlite", "storage": "./db.hedgedoc.sqlite" }` | set the db configs, [see more here](https://sequelize.org/v5/manual/dialects.html) | | `db` | | **`undefined`**, `{ "dialect": "sqlite", "storage": "./db.hedgedoc.sqlite" }` | set the db configs, [see more here](https://sequelize.org/v5/manual/dialects.html) |
| `db.username` | `CMD_DB_USERNAME` | **`undefined`**, `hedgedoc-db-user` | Username used to authenticate to the database (host). | | `db.username` | `CMD_DB_USERNAME` | **`undefined`**, `hedgedoc-db-user` | Username used to authenticate to the database (host). |
@@ -44,7 +44,9 @@ to work.*
| `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. | | `dbURL` | `CMD_DB_URL` | **`undefined`**, `postgres://username:password@localhost:5432/hedgedoc`, `mysql://username:password@localhost:3306/hedgedoc`, or `sqlite:///path/to/hedgedoc.db` | Set the db in URL style. If set, then the relevant `db` config entries will be overridden. |
| `loglevel` | `CMD_LOGLEVEL` | **`info`**, `debug` ... | Defines what kind of logs are provided to stdout. Available options: `debug`, `verbose`, `info`, `warn`, `error` | | `loglevel` | `CMD_LOGLEVEL` | **`info`**, `debug` ... | Defines what kind of logs are provided to stdout. Available options: `debug`, `verbose`, `info`, `warn`, `error` |
| `forbiddenNoteIDs` | `CMD_FORBIDDEN_NOTE_IDS` | **`['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views']`**, `['robots.txt']` or `'robots.txt'` | disallow creation of notes, even if `allowFreeUrl` or `CMD_ALLOW_FREEURL` is `true` | | `forbiddenNoteIDs` | `CMD_FORBIDDEN_NOTE_IDS` | **`['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views']`**, `['robots.txt']` or `'robots.txt'` | disallow creation of notes, even if `allowFreeUrl` or `CMD_ALLOW_FREEURL` is `true` |
| `imageUploadType` | `CMD_IMAGE_UPLOAD_TYPE` | **`filesystem`**, `imgur`, `s3`, `minio`, `azure`, `lutim` | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/s3-image-upload.md) or [Minio](guides/minio-image-upload.md), also there's a whole section on their respective env vars below. | | `externalLinkWarning` | `CMD_EXTERNAL_LINK_WARNING` | **`true`**, `false` | When set to `false`, disables the external link warning page entirely. All external links open directly without being routed through the `/_link` redirect. |
| `externalLinkWhitelist` | `CMD_EXTERNAL_LINK_WHITELIST` | **`[]`**, `example.com,trusted.org,*.example.org` | Comma-separated list of hosts that bypass the external link warning page. An entry like `example.com` matches that host only; prefix with `*.` (e.g. `*.example.com`) to also match all of its subdomains. |
| `imageUploadType` | `CMD_IMAGE_UPLOAD_TYPE` | **`filesystem`**, `imgur`, `s3`, `minio`, `azure`, `lutim` | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/media/s3-image-upload.md) or [Minio](guides/media/minio-image-upload.md), also there's a whole section on their respective env vars below. |
| `sourceURL` | `CMD_SOURCE_URL` | **no default**, `https://github.com/hedgedoc/hedgedoc/tree/<current commit>` | Provides the link to the source code of HedgeDoc on the entry page (Please, make sure you change this when you run a modified version) | | `sourceURL` | `CMD_SOURCE_URL` | **no default**, `https://github.com/hedgedoc/hedgedoc/tree/<current commit>` | Provides the link to the source code of HedgeDoc on the entry page (Please, make sure you change this when you run a modified version) |
| `tooBusyLag` | `CMD_TOOBUSY_LAG` | **`70`** | CPU time for one event loop tick until node throttles connections. (milliseconds) | | `tooBusyLag` | `CMD_TOOBUSY_LAG` | **`70`** | CPU time for one event loop tick until node throttles connections. (milliseconds) |
| `staticCacheTime` | | **`1 * 24 * 60 * 60 * 1000`** | static file cache time | | `staticCacheTime` | | **`1 * 24 * 60 * 60 * 1000`** | static file cache time |
@@ -80,16 +82,15 @@ these are rarely used for various reasons.
| `urlPath` | `CMD_URL_PATH` | **`null`**, `hedgedoc` | If HedgeDoc is run from a subdirectory like `www.example.com/<urlpath>` | | `urlPath` | `CMD_URL_PATH` | **`null`**, `hedgedoc` | If HedgeDoc is run from a subdirectory like `www.example.com/<urlpath>` |
| `host` | `CMD_HOST` | **`0.0.0.0`**, `localhost` | interface/ip to listen on | | `host` | `CMD_HOST` | **`0.0.0.0`**, `localhost` | interface/ip to listen on |
| `port` | `CMD_PORT` | **`3000`**, `80` | port to listen on | | `port` | `CMD_PORT` | **`3000`**, `80` | port to listen on |
| `path` | `CMD_PATH` | **no default**, `/var/run/hedgedoc.sock` | path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored) | | `path` | `CMD_PATH` | **no default**, `/var/run/hedgedoc.sock` | path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored for listening; however, `port` or `CMD_PORT` is still used for URL generation when `urlAddPort` is enabled) |
| `protocolUseSSL` | `CMD_PROTOCOL_USESSL` | **`false`** or `true` | set to use SSL protocol for resources path (only applied when domain is set) | | `protocolUseSSL` | `CMD_PROTOCOL_USESSL` | **`false`** or `true` | set to use SSL protocol for resources path (only applied when domain is set) |
| `useSSL` | | **`false`** or `true` | set to use SSL server (if `true`, will auto turn on `protocolUseSSL`) | | `useSSL` | | **`false`** or `true` | set to use SSL server (if `true`, will auto turn on `protocolUseSSL`) |
| `urlAddPort` | `CMD_URL_ADDPORT` | **`false`** or `true` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) | | `urlAddPort` | `CMD_URL_ADDPORT` | **`false`** or `true` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) |
| `allowOrigin` | `CMD_ALLOW_ORIGIN` | **`['localhost']`**, `['hedgedoc.org']`, `['localhost', 'hedgedoc.org']` | domain name whitelist (use comma to separate) |
## Web security aspects ## Web security aspects
| config file | environment | **default** and example value | description | | config file | environment | **default** and example value | description |
|-------------------------------|--------------------------------|-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------------------|-----------------------------------|-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `hsts` | | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) | | `hsts` | | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) |
| | `CMD_HSTS_ENABLE` | **`true`** or `false` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) | | | `CMD_HSTS_ENABLE` | **`true`** or `false` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) |
| | `CMD_HSTS_INCLUDE_SUBDOMAINS` | **`true`** or `false` | set to include subdomains in HSTS (default is `false`) | | | `CMD_HSTS_INCLUDE_SUBDOMAINS` | **`true`** or `false` | set to include subdomains in HSTS (default is `false`) |
@@ -107,6 +108,8 @@ these are rarely used for various reasons.
| `csp.allowPDFEmbed` | `CMD_CSP_ALLOW_PDF_EMBED` | **`true`** or `false` | Disable to disallow embedding PDFs. We recommend disabling this option, as it increases the attack surface of XSS attacks. | | `csp.allowPDFEmbed` | `CMD_CSP_ALLOW_PDF_EMBED` | **`true`** or `false` | Disable to disallow embedding PDFs. We recommend disabling this option, as it increases the attack surface of XSS attacks. |
| `cookiePolicy` | `CMD_COOKIE_POLICY` | **`lax`**, `strict` or `none` | Set a SameSite policy whether cookies are send from cross-origin. Be careful: setting a SameSite value of none without https breaks the editor. | | `cookiePolicy` | `CMD_COOKIE_POLICY` | **`lax`**, `strict` or `none` | Set a SameSite policy whether cookies are send from cross-origin. Be careful: setting a SameSite value of none without https breaks the editor. |
| `rateLimitNewNotes` | `CMD_RATE_LIMIT_NEW_NOTES` | **`20`**, `0` or any positive number | Sets the maximum amount of new note creations per 5-minute window per user. Can be disabled by setting to `0`. | | `rateLimitNewNotes` | `CMD_RATE_LIMIT_NEW_NOTES` | **`20`**, `0` or any positive number | Sets the maximum amount of new note creations per 5-minute window per user. Can be disabled by setting to `0`. |
| `rateLimitUser` | `CMD_RATE_LIMIT_USER` | **`10`**, `0` or any positive number | Sets the maximum amount of user auth interactions (login, signup) per 5-minute window per user. Can be disabled by setting to `0`. |
| `rateLimitUsingCloudflare` | `CMD_RATE_LIMIT_USING_CLOUDFLARE` | **`false`** or `true` | Activate specialy cloudflare ip handling. If you use cloudflare for your instance you want this to be true. |
## Privacy and External Requests ## Privacy and External Requests
@@ -123,7 +126,7 @@ these are rarely used for various reasons.
| `allowFreeURL` | `CMD_ALLOW_FREEURL` | **`false`** or `true` | Set to allow new note creation by accessing a nonexistent note URL. This is the behavior familiar from [Etherpad](https://github.com/ether/etherpad-lite). | | `allowFreeURL` | `CMD_ALLOW_FREEURL` | **`false`** or `true` | Set to allow new note creation by accessing a nonexistent note URL. This is the behavior familiar from [Etherpad](https://github.com/ether/etherpad-lite). |
| `requireFreeURLAuthentication` | `CMD_REQUIRE_FREEURL_AUTHENTICATION` | **`false`** or `true` | Set to require authentication for FreeURL mode style note creation. | | `requireFreeURLAuthentication` | `CMD_REQUIRE_FREEURL_AUTHENTICATION` | **`false`** or `true` | Set to require authentication for FreeURL mode style note creation. |
| `disableNoteCreation` | `CMD_DISABLE_NOTE_CREATION` | **`false`** or `true` | Set to `true` to disallow any person to create notes. | | `disableNoteCreation` | `CMD_DISABLE_NOTE_CREATION` | **`false`** or `true` | Set to `true` to disallow any person to create notes. |
| `defaultPermission` | `CMD_DEFAULT_PERMISSION` | **`editable`**, `freely`, `limited`, `locked`, `protected` or `private` | Set notes default permission (only applied on signed-in users). | | `defaultPermission` | `CMD_DEFAULT_PERMISSION` | **`editable`**, `freely`, `limited`, `locked`, `protected` or `private` | Set notes default permission (only applied on signed-in users). See the [permissions documentation](./references/permissions.md) |
| `enableUploads` | `CMD_ENABLE_UPLOADS` | **`all`** , `registered`, `none` | Set to specify who can upload images (`all` includes guest users, `registered` restricts to registered users, `none` disables uploads completely). If not set, but both `allowAnonymous` and `allowAnonymousEdits` are `false`, this will be set to `registered`. | | `enableUploads` | `CMD_ENABLE_UPLOADS` | **`all`** , `registered`, `none` | Set to specify who can upload images (`all` includes guest users, `registered` restricts to registered users, `none` disables uploads completely). If not set, but both `allowAnonymous` and `allowAnonymousEdits` are `false`, this will be set to `registered`. |
| `sessionName` | | **`connect.sid`** | Cookie session name. | | `sessionName` | | **`connect.sid`** | Cookie session name. |
| `sessionLife` | `CMD_SESSION_LIFE` | **`14 * 24 * 60 * 60 * 1000`**, `1209600000` (14 days) | Cookie session life time in milliseconds. | | `sessionLife` | `CMD_SESSION_LIFE` | **`14 * 24 * 60 * 60 * 1000`**, `1209600000` (14 days) | Cookie session life time in milliseconds. |
@@ -273,7 +276,7 @@ you don't have to use either of these.
| config file | environment | **default** and example value | description | | config file | environment | **default** and example value | description |
|-----------------|----------------------------|-------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| |-----------------|----------------------------|-------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| `s3` | | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION" }` | When `imageuploadtype` be set to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](guides/s3-image-upload.md) | | `s3` | | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION" }` | When `imageuploadtype` be set to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](guides/media/s3-image-upload.md) |
| | `CMD_S3_ACCESS_KEY_ID` | **no default** | AWS access key id | | | `CMD_S3_ACCESS_KEY_ID` | **no default** | AWS access key id |
| | `CMD_S3_SECRET_ACCESS_KEY` | **no default** | AWS secret key | | | `CMD_S3_SECRET_ACCESS_KEY` | **no default** | AWS secret key |
| | `CMD_S3_REGION` | **no default**, `ap-northeast-1` | AWS S3 region | | | `CMD_S3_REGION` | **no default**, `ap-northeast-1` | AWS S3 region |
@@ -299,7 +302,7 @@ you don't have to use either of these.
| config file | environment | **default** and example value | description | | config file | environment | **default** and example value | description |
|-------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| |-------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| `minio` | | `{ "accessKey": "YOUR_MINIO_ACCESS_KEY", "secretKey": "YOUR_MINIO_SECRET_KEY", "endpoint": "YOUR_MINIO_HOST", port: 9000, secure: true }` | When `imageUploadType` is set to `minio`, you need to set this key. Also check out our [Minio Image Upload Guide](guides/minio-image-upload.md) | | `minio` | | `{ "accessKey": "YOUR_MINIO_ACCESS_KEY", "secretKey": "YOUR_MINIO_SECRET_KEY", "endpoint": "YOUR_MINIO_HOST", port: 9000, secure: true }` | When `imageUploadType` is set to `minio`, you need to set this key. Also check out our [Minio Image Upload Guide](guides/media/minio-image-upload.md) |
| | `CMD_MINIO_ACCESS_KEY` | **no default** | Minio access key | | | `CMD_MINIO_ACCESS_KEY` | **no default** | Minio access key |
| | `CMD_MINIO_SECRET_KEY` | **no default** | Minio secret key | | | `CMD_MINIO_SECRET_KEY` | **no default** | Minio secret key |
| | `CMD_MINIO_ENDPOINT` | **no default**, `minio.example.org` | Address of your Minio endpoint/instance | | | `CMD_MINIO_ENDPOINT` | **no default**, `minio.example.org` | Address of your Minio endpoint/instance |

View File

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

View File

@@ -11,3 +11,10 @@ And even more in this 2010 article series:
- <https://drive.googleblog.com/2010/09/whats-different-about-new-google-docs_21.html> - <https://drive.googleblog.com/2010/09/whats-different-about-new-google-docs_21.html>
- <https://drive.googleblog.com/2010/09/whats-different-about-new-google-docs_22.html> - <https://drive.googleblog.com/2010/09/whats-different-about-new-google-docs_22.html>
- <https://drive.googleblog.com/2010/09/whats-different-about-new-google-docs.html> - <https://drive.googleblog.com/2010/09/whats-different-about-new-google-docs.html>
## OT library
HedgeDoc uses a vendored version of OT.js. The source code lives under `public/vendor/ot/`.
The HedgeDoc frontend uses a minified build `public/vendor/ot/ot.min.js`.
This build is generated by `yarn run build:ot`. This needs to be done after changes to the OT code.

View File

@@ -1,10 +1,14 @@
# Release Checklist: # Release Checklist:
To copy this checklist please go to [this link](https://github.com/hedgedoc/hedgedoc/raw/refs/heads/master/docs/content/dev/release_checklist.md).
## Preparations: ## Preparations:
- [ ] Create release PR(s) - [ ] 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) - [ ] 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/container>
- [ ] Update version in `.github/workflows/release.yml`
- [ ] Update version in `docker-compose.yml`
- [ ] Release PR in <https://github.com/hedgedoc/social-media> - [ ] Release PR in <https://github.com/hedgedoc/social-media>
- [ ] Security fixes (make sure all available undisclosed security fixes are merged) - [ ] Security fixes (make sure all available undisclosed security fixes are merged)
- [ ] Bump the version - [ ] Bump the version
@@ -48,6 +52,7 @@
mkdir /tmp/hedgedoc && cd /tmp/hedgedoc mkdir /tmp/hedgedoc && cd /tmp/hedgedoc
git clone -b master https://github.com/hedgedoc/hedgedoc.git . git clone -b master https://github.com/hedgedoc/hedgedoc.git .
yarn install yarn install
yarn build:ot
yarn build yarn build
cd .. 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 tar cvzf hedgedoc-x.y.z.tar.gz --sort=name --exclude hedgedoc/node_modules --exclude hedgedoc/.git --exclude hedgedoc/.github --exclude hedgedoc/.yarn/cache hedgedoc
@@ -55,11 +60,36 @@ tar cvzf hedgedoc-x.y.z.tar.gz --sort=name --exclude hedgedoc/node_modules --exc
Use this tar ball to test the following things: Use this tar ball to test the following things:
### bin/scripts
- [ ] `bin/setup`
- [ ] `bin/cleanup`
- [ ] `bin/manage_users`
- [ ] `bin/manage_users --add test@example.com`
- [ ] `bin/manage_users --reset test@example.com`
- [ ] `bin/manage_users --del test@example.com`
- [ ] `bin/migrate_from_fs_to_minio`
1. Clear the uploads directory.
2. Start HedgeDoc 1 with filesystem uploads and a Postgres DB locally.
3. Create a new note and upload an image. The image should be now in the uploads directory.
4. Stop HedgeDoc. Update the config to configure a Minio instance (minio and s3bucket keys in config.json). Ensure the config on the Minio instance is existing (valid access key and secret, bucket existing and empty).
5. Run `bin/migrate_fs_to_minio`.
6. Start the HedgeDoc again. The note with the previously uploaded image should now contain the new image URL. The image should load.
7. Check that the bucket in Minio contains the uploaded image.
### Config
- [ ] Setting `allowAnonymous: false` disables guest note creation
- [ ] Setting `enableStatsApi: false` disables /metrics and /status endpoint
### Account system ### Account system
- [ ] User registration works - [ ] User registration works
- [ ] User login works - [ ] User login works
- [ ] User self-deletion works - [ ] User self-deletion works (notes are deleted as well)
- [ ] User data export works
- [ ] Rate-limiting for basic user/password (`curl -X POST --data email=test@example.com%40password=123456 http://localhost:3000/login`)
### Notes ### Notes
@@ -74,6 +104,7 @@ Use this tar ball to test the following things:
- [ ] Working with 2 (or more) devices on a page works and results in the same document - [ ] Working with 2 (or more) devices on a page works and results in the same document
- [ ] Uploads work for images - [ ] Uploads work for images
- [ ] Uploads fail for other data (e.g. binaries) - [ ] Uploads fail for other data (e.g. binaries)
- [ ] Opening uploads in new tab downloads them instead of displaying
### Database ### Database
@@ -86,7 +117,7 @@ Use this tar ball to test the following things:
- [ ] Postgres works - [ ] Postgres works
Run `docker run -d --name=hd1-pg -p 5432:5432 -e POSTGRES_USER=hd1db -e POSTGRES_PASSWORD=hd1db -e POSTGRES_DB=hd1db postgres:latest` Run `docker run -d --name=hd1-pg -p 5432:5432 -e POSTGRES_USER=hd1db -e POSTGRES_PASSWORD=hd1db -e POSTGRES_DB=hd1db --rm postgres:latest`
and put this into your config: and put this into your config:
``` ```
"db": { "db": {
@@ -165,19 +196,21 @@ opengraph:
--- ---
``` ```
### GDPR features ### Slides
- [ ] Delete account works <http://localhost:3000/slide-example>
- [ ] When account is deleted, verify notes are gone as well
- [ ] Data export works - [ ] Make sure the slide mode works as intended
- [ ] Speaker view is toggled by `S`
### Auth ### Auth
- [ ] SAML - [ ] SAML
- config - Download certificate from Authentik Admin -> Certificates -> auth.hedgedoc.cloud to `/tmp/auth.hedgedoc.cloud.pem`
- add to config.json:
``` ```
"saml": { "saml": {
"idpSsoUrl": "https://auth.hedgedoc.cloud/application/saml/test-hd1/sso/binding/redirect/", "idpSsoUrl": "https://auth.hedgedoc.cloud/application/saml/test-hd1-saml/sso/binding/redirect/",
"idpCert": "/tmp/auth.hedgedoc.cloud.pem", "idpCert": "/tmp/auth.hedgedoc.cloud.pem",
"identifierFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "identifierFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
"attribute": { "attribute": {
@@ -204,9 +237,10 @@ opengraph:
} }
``` ```
- [ ] OAuth2 - [ ] OAuth2
- add to config.json and replace `REDACTED` with the proper client ID and secret
``` ```
"oauth2": { "oauth2": {
"baseURL": "https://auth.hedgedoc.cloud/application/o/test-hedgedoc/", "baseURL": "https://auth.hedgedoc.cloud/application/o/test-hd1-oauth2/",
"userProfileURL": "https://auth.hedgedoc.cloud/application/o/userinfo/", "userProfileURL": "https://auth.hedgedoc.cloud/application/o/userinfo/",
"tokenURL": "https://auth.hedgedoc.cloud/application/o/token/", "tokenURL": "https://auth.hedgedoc.cloud/application/o/token/",
"authorizationURL": "https://auth.hedgedoc.cloud/application/o/authorize/", "authorizationURL": "https://auth.hedgedoc.cloud/application/o/authorize/",
@@ -220,7 +254,63 @@ opengraph:
} }
``` ```
- [ ] GitHub - [ ] GitHub
- [ ] Rate-limiting for basic user/password (try to login with e.g. test@example.com and invalid password about 10 to 15 times in a row -> you should receive a message "Too many requests" at some point) ```
"github": {
"clientID": "CHANGE_THIS",
"clientSecret": "CHANGE_THIS"
}
```
- [ ] GitLab
```
"gitlab": {
"baseURL": "https://gitlab.fachschaften.org",
"clientID": "CHANGE_THIS",
"clientSecret": "CHANGE_THIS",
"scope": "read_user"
}
```
### Docker images
1. You should push the release branch to Github
2. You need to clone the [container repo](https://github.com/hedgedoc/container) and use the commands in the root there.
3. Edit `debian/Dockerfile` and `alpine/Dockerfile` to use the release branch instead of the `master` branch for building
- [ ] debian
```
docker buildx build -f debian/Dockerfile -t hedgedoc-local:1.x.y-debian .
docker run --rm -p 3000:3000 -e CMD_DOMAIN="localhost" -e CMD_URL_ADDPORT=true -e "CMD_DB_URL=sqlite://:memory:" hedgedoc-local:1.x.y-debian
```
The server is then accessable via <http://localhost:3000>. The log should inlcude a line like
```
127.0.0.1 - - [06/Dec/2025:16:17:51 +0000] "GET /_health HTTP/1.1" 200 14 "-" "hedgedoc-container-healthcheck/1.2"
```
You should also check `docker ps` for
```
hedgedoc-local:1.x.y-debian "/usr/local/bin/dock…" 29 seconds ago Up 28 seconds (healthy)
```
- [ ] alpine
```
docker buildx build -f alpine/Dockerfile -t hedgedoc-local:1.x.y-alpine .
docker run --rm -p 3000:3000 -e CMD_DOMAIN="localhost" -e CMD_URL_ADDPORT=true -e "CMD_DB_URL=sqlite://:memory:" hedgedoc-local:1.x.y-alpine
```
The server is then accessable via <http://localhost:3000>. The log should inlcude a line like
```
127.0.0.1 - - [06/Dec/2025:16:17:51 +0000] "GET /_health HTTP/1.1" 200 14 "-" "hedgedoc-container-healthcheck/1.2"
```
You should also check `docker ps` for
```
hedgedoc-local:1.x.y-alpine "/usr/local/bin/dock…" 29 seconds ago Up 28 seconds (healthy)
```
## Release: ## Release:
@@ -228,12 +318,14 @@ opengraph:
- [ ] Merge Release PR in main repo - [ ] Merge Release PR in main repo
- [ ] Tag commit with `git tag 1.x.y` and push it - [ ] Tag commit with `git tag 1.x.y` and push it
- [ ] Create release in GitHub and upload tar ball to GitHub - [ ] Create release in GitHub and upload tar ball to GitHub
- [ ] Update placeholders in Security Advisories (if they exist)
- [ ] Publish Security Advisories (if they exist) - [ ] Publish Security Advisories (if they exist)
- [ ] Merge Release PR in <https://github.com/hedgedoc/container> - [ ] 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> - Wait for the images to be available at <https://quay.io/repository/hedgedoc/hedgedoc?tab=tags>
- [ ] Update website by running the ["deploy" workflow](https://github.com/hedgedoc/hedgedoc.github.io/actions?query=workflow%3A%22Deploy+to+github+actions+branch%22) in hedgedoc/hedgedoc.github.io - [ ] Update website by running the ["deploy" workflow](https://github.com/hedgedoc/hedgedoc.github.io/actions/workflows/deploy.yml) in hedgedoc/hedgedoc.github.io
- [ ] Update docs.hedgedoc.org by running the ["build" workflow](https://github.com/hedgedoc/docs.hedgedoc.org/actions/workflows/build.yml) - [ ] 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> - [ ] Merge Release PR in <https://github.com/hedgedoc/social-media>
- (optional) All people doing the release boost the post - (optional) All people doing the release boost the post
- [ ] Share the good news in the Matrix-Chatroom - [ ] Share the good news in the Matrix-Chatroom
- [ ] Request CVEs (if applicable)
- [ ] Change this release checklist if necessary - [ ] Change this release checklist if necessary

View File

@@ -47,17 +47,17 @@ If you still want to allow embedding via iframe, ensure that:
- `cookiePolicy` / `CMD_COOKIE_POLICY` is set to `none` (Otherwise you will get a `AUTH failed: No cookie transmitted` error.) - `cookiePolicy` / `CMD_COOKIE_POLICY` is set to `none` (Otherwise you will get a `AUTH failed: No cookie transmitted` error.)
- `csp.allowFraming` / `CMD_CSP_ALLOW_FRAMING` is set to `true` - `csp.allowFraming` / `CMD_CSP_ALLOW_FRAMING` is set to `true`
See also the [configuration docs](/configuration/#web-security-aspects) for more information about these options. See also the [configuration docs](configuration.md#web-security-aspects) for more information about these options.
## I can't upload images or the upload gets stuck ## I can't upload images or the upload gets stuck
This problem is typically accompanied by the error `Invalid URL: /uploads/` in the log and is often caused by a missing This problem is typically accompanied by the error `Invalid URL: /uploads/` in the log and is often caused by a missing
`domain` / `CMD_DOMAIN` config option or an incorrect reverse proxy config. `domain` / `CMD_DOMAIN` config option or an incorrect reverse proxy config.
Have a look at our [reverse proxy documentation](https://docs.hedgedoc.org/guides/reverse-proxy/) Have a look at our [reverse proxy documentation](guides/reverse-proxy.md)
and make sure that `protocolUseSSL` / `CMD_PROTOCOL_USESSL` is set to `true` if you serve HedgeDoc via HTTPS. and make sure that `protocolUseSSL` / `CMD_PROTOCOL_USESSL` is set to `true` if you serve HedgeDoc via HTTPS.
## HedgeDoc fails executing migrations and does not start ## HedgeDoc fails executing migrations and does not start
Unfortunately, older versions of HedgeDoc had some bugs regarding migrations and didn't always record that a migration was executed. Unfortunately, older versions of HedgeDoc had some bugs regarding migrations and didn't always record that a migration was executed.
Have a look at the *[Troubleshooting Migrations](/guides/migration-troubleshooting/)* guide for more information. Have a look at the *[Troubleshooting Migrations](guides/migration-troubleshooting.md)* guide for more information.
## Why does my interface look weird? ## Why does my interface look weird?
@@ -76,4 +76,4 @@ You should check the following settings (or their corresponding environment vari
- `protocolUseSSL` (env: `CMD_PROTOCOL_USESSL`) - `protocolUseSSL` (env: `CMD_PROTOCOL_USESSL`)
- `urlAddPort` (env: `CMD_URL_ADDPORT`) - `urlAddPort` (env: `CMD_URL_ADDPORT`)
Check [the reverse proxy guide](../guides/reverse-proxy/#configure-asset-link-generation) for an explanation. Check [the reverse proxy guide](guides/reverse-proxy.md#configure-asset-link-generation) for an explanation.

View File

@@ -22,12 +22,11 @@ identity_providers:
refresh_token_lifespan: 90m refresh_token_lifespan: 90m
enable_client_debug_messages: false enable_client_debug_messages: false
clients: clients:
- id: HedgeDoc # this should be changed to something more secure - client_id: <random client id>
description: HedgeDoc SSO client_name: HedgeDoc
secret: <client secret here> secret: <client secret here>
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
audience: []
scopes: scopes:
- openid - openid
- email - email
@@ -43,7 +42,7 @@ identity_providers:
- form_post - form_post
- query - query
- fragment - fragment
userinfo_signing_algorithm: none token_endpoint_auth_method: 'client_secret_post'
``` ```
4. Restart Authelia to apply to new configuration and check for any errors in the log 4. Restart Authelia to apply to new configuration and check for any errors in the log
@@ -53,7 +52,7 @@ identity_providers:
- CMD_URL_ADDPORT=false - CMD_URL_ADDPORT=false
- CMD_PROTOCOL_USESSL=true - CMD_PROTOCOL_USESSL=true
- CMD_OAUTH2_PROVIDERNAME=Authelia - CMD_OAUTH2_PROVIDERNAME=Authelia
- CMD_OAUTH2_CLIENT_ID=HedgeDoc - CMD_OAUTH2_CLIENT_ID=<client id here>
- CMD_OAUTH2_CLIENT_SECRET=<client secret here> - CMD_OAUTH2_CLIENT_SECRET=<client secret here>
- CMD_OAUTH2_SCOPE=openid email profile - CMD_OAUTH2_SCOPE=openid email profile
- CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=sub - CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=sub
@@ -61,7 +60,7 @@ identity_providers:
- CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email - CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email
- CMD_OAUTH2_USER_PROFILE_URL=https://<your-authelia-url>/api/oidc/userinfo - CMD_OAUTH2_USER_PROFILE_URL=https://<your-authelia-url>/api/oidc/userinfo
- CMD_OAUTH2_TOKEN_URL=https://<your-authelia-url>/api/oidc/token - CMD_OAUTH2_TOKEN_URL=https://<your-authelia-url>/api/oidc/token
- CMD_OAUTH2_AUTHORIZATION_URL=https://<your-authelia-url>/api/oidc/authorize - CMD_OAUTH2_AUTHORIZATION_URL=https://<your-authelia-url>/api/oidc/authorization
``` ```
6. Run `docker-compose up -d` on HedgeDoc to apply your settings. 6. Run `docker-compose up -d` on HedgeDoc to apply your settings.

View File

@@ -21,22 +21,22 @@
docker logs test-minio docker logs test-minio
``` ```
![docker logs](../images/minio-image-upload/docker-logs.png) ![docker logs](../../images/minio-image-upload/docker-logs.png)
3. Open <http://localhost:9000> and login with the shown credentials. 3. Open <http://localhost:9000> and login with the shown credentials.
![minio default view](../images/minio-image-upload/default-view.png) ![minio default view](../../images/minio-image-upload/default-view.png)
4. Create a bucket for HedgeDoc 4. Create a bucket for HedgeDoc
![minio create bucket](../images/minio-image-upload/create-bucket.png) ![minio create bucket](../../images/minio-image-upload/create-bucket.png)
5. Add a policy for the prefix `uploads` and make it read-only. 5. Add a policy for the prefix `uploads` and make it read-only.
![minio edit policy](../images/minio-image-upload/open-edit-policy.png) ![minio edit policy](../../images/minio-image-upload/open-edit-policy.png)
*Open policy editor* *Open policy editor*
![minio policy adding](../images/minio-image-upload/create-policy.png) ![minio policy adding](../../images/minio-image-upload/create-policy.png)
*Add policy for uploads* *Add policy for uploads*
6. Set credentials and configs for Minio in HedgeDoc's `config.json` 6. Set credentials and configs for Minio in HedgeDoc's `config.json`

View File

@@ -63,7 +63,7 @@ In order to view the images uploaded to HedgeDoc the storage bucket needs to be
## Configure HedgeDoc ## Configure HedgeDoc
Set the following environment variables (or the [corresponding config.json values](/configuration/#amazon-s3)): Set the following environment variables (or the [corresponding config.json values](../../configuration.md#amazon-s3)):
```env ```env
CMD_IMAGE_UPLOAD_TYPE="s3" CMD_IMAGE_UPLOAD_TYPE="s3"

View File

@@ -3,13 +3,13 @@
**Note:** *This guide was written before the renaming. Just replace `HackMD` with `HedgeDoc` in your mind 😃 thanks!* **Note:** *This guide was written before the renaming. Just replace `HackMD` with `HedgeDoc` in your mind 😃 thanks!*
1. Go to [AWS S3 console](https://console.aws.amazon.com/s3/home) and create a new bucket. 1. Go to [AWS S3 console](https://console.aws.amazon.com/s3/home) and create a new bucket.
![create-bucket](../images/s3-image-upload/create-bucket.png) ![create-bucket](../../images/s3-image-upload/create-bucket.png)
2. Click on bucket, select **Properties** on the side panel, and find **Permission** section. Click **Edit bucket policy**. 2. Click on bucket, select **Properties** on the side panel, and find **Permission** section. Click **Edit bucket policy**.
![bucket-property](../images/s3-image-upload/bucket-property.png) ![bucket-property](../../images/s3-image-upload/bucket-property.png)
3. Enter the following policy, replace `bucket_name` with your bucket name: 3. Enter the following policy, replace `bucket_name` with your bucket name:
![bucket-policy-editor](../images/s3-image-upload/bucket-policy-editor.png) ![bucket-policy-editor](../../images/s3-image-upload/bucket-policy-editor.png)
```json ```json
{ {
@@ -28,13 +28,13 @@
4. Go to IAM console and create a new IAM user. Remember your user credentials(`key`/`access token`) 4. Go to IAM console and create a new IAM user. Remember your user credentials(`key`/`access token`)
5. Enter user page, select **Permission** tab, look at **Inline Policies** section, and click **Create User Policy** 5. Enter user page, select **Permission** tab, look at **Inline Policies** section, and click **Create User Policy**
![iam-user](../images/s3-image-upload/iam-user.png) ![iam-user](../../images/s3-image-upload/iam-user.png)
6. Select **Custom Policy** 6. Select **Custom Policy**
![custom-policy](../images/s3-image-upload/custom-policy.png) ![custom-policy](../../images/s3-image-upload/custom-policy.png)
7. Enter the following policy, replace `bucket_name` with your bucket name: 7. Enter the following policy, replace `bucket_name` with your bucket name:
![review-policy](../images/s3-image-upload/review-policy.png) ![review-policy](../../images/s3-image-upload/review-policy.png)
```json ```json
{ {
@@ -70,7 +70,7 @@
} }
``` ```
9. In additional to edit `config.json` directly, you could also try [environment variables](../configuration.md). 9. In additional to edit `config.json` directly, you could also try [environment variables](../../configuration.md).
## Related Tools ## Related Tools

View File

@@ -0,0 +1,49 @@
# How to make your HedgeDoc instance production ready
If you run your own HedgeDoc instance at one point you should probably consider a few things and how you want to handle them.
In this guide we try to show you a few of those considerations and want to discuss what's best to do.
## Node Env
You want to start HedgeDoc with `NODE_ENV=production`. This tells HedgeDoc to run in production, log a bit less and use the production configuration in your config.json.
## HTTPS
You want to configure a reverse proxy that handles your HedgeDoc TLS termination. For this you should consult the [reverse proxy guide](reverse-proxy.md).
Look also at the hsts options in our [configuration](../configuration.md#web-security-aspects) to specify what works for your setup.
## Uploads
Depending on your instance and your expected amount of usage you may want to configure a different upload backend or at the very least configure external monitoring for the filesystem folder where HedgeDoc should put uploads to account for any sudden increases in usage.
## Database
We strongly recommend you use a different database than SQLite for your production instance. If you're a single user instance this is probably fine, but a database server is typically more robust than a single sqlite.db file.
## Login
You should configure a production ready user backend. This can either be via LDAP, SAML or OAuth2 or you use one of HedgeDoc preset configurations for e.g. GitHub. Our username / password system works, but lacks many functionalities you probably want like two-factor-auth or self-service password resets.
## Guest Users
When HegdeDoc was first written the internet was a different place. Back then allow anonymous usage of a service like HedgeDoc seemed like a cool idea.
Unfortunatly we don't live in that time anymore and anonymous usage is probably not the best idea anymore.
To prevent anonyomous usage you should set `allowAnonymous` or `CMD_ALLOW_ANONYMOUS` to `false`. After that anonymous users are not able to create any notes.
You could set `allowAnonymousEdits` or `CMD_ALLOW_ANONYMOUS_EDITS` to `true` to allow anonymous users to edit pages where the note owner explicity allowed this.
## Default Note Permission
Depending on your needs to may want to change the default permissions of notes to something more strict. Per default everybody expect guests can write to the notes, but you may want to have a different default. See [this table](../references/permissions.md) to understand the different options.
Regardless of what you chose here, the users will be able to change this behavior.
## Metrics
Please be aware that HedgeDoc exposes a few metric endpoints `/status`, `/_health` and `/metrics`. You either want to disable the more verbose of these endpoints (`/status` and `/metrics`) via `enableStatsApi` or `CMD_ENABLE_STATS_API` or configure your reverse proxy to only route these endpoints to a private network that your metrics grabber has access to.
## Backup
We recommend keeping backups of your instance in case something fails allong the lines. These backups should include:
- Your database
- Your uploads
- Your configuration

View File

@@ -19,7 +19,7 @@ This documentation will cover HTTPS setup, with comments for HTTP setup.
| `domain` | `CMD_DOMAIN` | The full domain where your instance will be available | `hedgedoc.example.com` | | `domain` | `CMD_DOMAIN` | The full domain where your instance will be available | `hedgedoc.example.com` |
| `host` | `CMD_HOST` | An ip or domain name that is only available to HedgeDoc and your reverse proxy | `localhost` | | `host` | `CMD_HOST` | An ip or domain name that is only available to HedgeDoc and your reverse proxy | `localhost` |
| `port` | `CMD_PORT` | An available port number on that IP | `3000` | | `port` | `CMD_PORT` | An available port number on that IP | `3000` |
| `path` | `CMD_PATH` | path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored) | `/var/run/hedgedoc.sock` | | `path` | `CMD_PATH` | path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored for listening; however, `port` or `CMD_PORT` is still used for URL generation when `urlAddPort` is enabled) | `/var/run/hedgedoc.sock` |
| `protocolUseSSL` | `CMD_PROTOCOL_USESSL` | `true` if you want to serve your instance over SSL (HTTPS), `false` if you want to use plain HTTP | `true` | | `protocolUseSSL` | `CMD_PROTOCOL_USESSL` | `true` if you want to serve your instance over SSL (HTTPS), `false` if you want to use plain HTTP | `true` |
| `useSSL` | | `false`, the communications between HedgeDoc and the proxy are unencrypted | `false` | | `useSSL` | | `false`, the communications between HedgeDoc and the proxy are unencrypted | `false` |
| `urlAddPort` | `CMD_URL_ADDPORT` | `false`, HedgeDoc should not append its port to the URLs it links | `false` | | `urlAddPort` | `CMD_URL_ADDPORT` | `false`, HedgeDoc should not append its port to the URLs it links | `false` |

View File

@@ -0,0 +1,3 @@
<svg width="1650" height="1780" xmlns="http://www.w3.org/2000/svg">
<path d="M880 128c-62 0-112 50-112 112v656h-32V368c0-62-50-112-112-112s-112 50-112 112v784L358 947c-24-32-62-51-102-51-71 0-128 58-128 128 0 28 9 55 26 77l384 512c24 32 62 51 102 51h688c46 0 86-33 95-78l76-405c3-19 5-39 5-59V624c0-62-50-112-112-112s-112 50-112 112v272h-32V368c0-62-50-112-112-112s-112 50-112 112v528h-32V240c0-62-50-112-112-112zm0-128c90 0 173 51 214 132 14-3 28-4 42-4 132 0 240 108 240 240v17c139-8 256 99 256 239v498c0 28-3 56-8 83l-76 404c-19 106-112 183-220 183H640c-80 0-157-39-205-102L51 1178c-33-44-51-99-51-154 0-141 114-256 256-256 42 0 93 10 128 34V368c0-132 108-240 240-240 14 0 28 1 42 4C707 51 790 0 880 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 720 B

View File

@@ -0,0 +1,3 @@
<svg width="2050" height="1680" xmlns="http://www.w3.org/2000/svg">
<path d="M896 1212c0-132-32-284-164-284-40 40-95 64-156 64s-116-24-156-64c-132 0-164 152-164 284 0 73 48 132 107 132h426c59 0 107-59 107-132zM768 768c0-106-86-192-192-192s-192 86-192 192 86 192 192 192 192-86 192-192zm1024 480v-64c0-18-14-32-32-32h-704c-18 0-32 14-32 32v64c0 18 14 32 32 32h704c18 0 32-14 32-32zm-384-256v-64c0-18-14-32-32-32h-320c-18 0-32 14-32 32v64c0 18 14 32 32 32h320c18 0 32-14 32-32zm384 0v-64c0-18-14-32-32-32h-192c-18 0-32 14-32 32v64c0 18 14 32 32 32h192c18 0 32-14 32-32zm0-256v-64c0-18-14-32-32-32h-704c-18 0-32 14-32 32v64c0 18 14 32 32 32h704c18 0 32-14 32-32zM128 384h1792v-96c0-18-14-32-32-32H160c-18 0-32 14-32 32v96zm1920-96v1216c0 88-72 160-160 160H160c-88 0-160-72-160-160V288c0-88 72-160 160-160h1728c88 0 160 72 160 160z"/>
</svg>

After

Width:  |  Height:  |  Size: 842 B

View File

@@ -0,0 +1,3 @@
<svg width="1900" height="1700" xmlns="http://www.w3.org/2000/svg">
<path d="M1280 704c0-35-29-64-64-64-354 0-582 148-813 403-12 13-19 27-19 45 0 35 29 64 64 64 18 0 32-7 45-19 49-44 93-92 141-137 181-163 336-228 582-228 35 0 64-29 64-64zm512-198c0 64-7 129-20 193-64 311-264 513-542 651-135 68-286 108-438 108-96 0-195-16-286-47-48-16-144-79-184-79-50 0-110 204-197 204-63 0-82-31-109-77-9-17-16-23-16-44 0-104 198-185 198-243 0-9-26-62-30-82-6-34-9-69-9-104 0-318 253-545 537-639 205-68 641 11 780-121 55-51 82-98 166-98 113 0 150 293 150 378z"/>
</svg>

After

Width:  |  Height:  |  Size: 560 B

View File

@@ -0,0 +1,3 @@
<svg width="1550" height="1550" xmlns="http://www.w3.org/2000/svg">
<path d="M320 768h512V576c0-141-115-256-256-256S320 435 320 576v192zm832 96v576c0 53-43 96-96 96H96c-53 0-96-43-96-96V864c0-53 43-96 96-96h32V576c0-246 202-448 448-448s448 202 448 448v192h32c53 0 96 43 96 96z"/>
</svg>

After

Width:  |  Height:  |  Size: 291 B

View File

@@ -0,0 +1,3 @@
<svg width="1550" height="2100" xmlns="http://www.w3.org/2000/svg">
<path d="M363 2000l91-91-235-235-91 91v107h128v128h107zm523-928c0-13-9-22-22-22-6 0-12 2-17 7l-542 542c-5 5-7 11-7 17 0 13 9 22 22 22 6 0 12-2 17-7l542-542c5-5 7-11 7-17zm-54-192l416 416-832 832H0v-416zm683 96c0 34-14 67-37 90l-166 166-416-416 166-165c23-24 56-38 90-38s67 14 91 38l235 234c23 24 37 57 37 91z"/>
</svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1,3 @@
<svg width="1750" height="1750" xmlns="http://www.w3.org/2000/svg">
<path d="M896 828v580c0 139-117 256-256 256s-256-117-256-256c0-35 29-64 64-64s64 29 64 64c0 67 61 128 128 128s128-61 128-128V828c21-7 42-11 64-11s43 4 64 11zm768 27c0 17-15 32-32 32-9 0-16-4-23-10-58-54-112-92-195-92-95 0-177 59-231 134-12 17-21 35-32 52-7 11-15 17-28 17-14 0-22-6-29-17-11-17-20-35-32-52-54-75-135-134-230-134s-176 59-230 134c-12 17-21 35-32 52-7 11-15 17-29 17-13 0-21-6-28-17-11-17-20-35-32-52-54-75-136-134-231-134-83 0-137 38-195 92-7 6-14 10-23 10-17 0-32-15-32-32 0-3 0-5 1-7 92-378 457-592 831-592 372 0 741 214 831 592 1 2 1 4 1 7zM896 128v98c-21-1-43-2-64-2s-43 1-64 2v-98c0-35 29-64 64-64s64 29 64 64z"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@@ -16,6 +16,6 @@ If you have any questions that aren't answered here, feel free to ask us on [Mat
[hedgedoc-demo]: https://demo.hedgedoc.org [hedgedoc-demo]: https://demo.hedgedoc.org
[hedgedoc-history]: https://hedgedoc.org/history [hedgedoc-history]: https://hedgedoc.org/history
[hedgedoc-history-details]: https://hedgedoc.org/history/details [hedgedoc-history-details]: https://hedgedoc.org/history/details
[hedgedoc-faq]: /faq [hedgedoc-faq]: faq.md
[matrix.org-url]: https://chat.hedgedoc.org [matrix.org-url]: https://chat.hedgedoc.org
[hedgedoc-community]: https://community.hedgedoc.org [hedgedoc-community]: https://community.hedgedoc.org

View File

@@ -0,0 +1,23 @@
# Permissions
HedgeDoc 1.x uses a basic permission system that differentiates between the owner of a note (the account who initially
created it), logged-in users and guests.
It is possible to change the access permission of a note through the little button on the top right of the view pane
in the editor. However, only the note owner can change the permission of a note.
The permissions are named **Freely**, **Editable**, **Limited**, **Locked**, **Protected** and **Private**.
You can configure the default permission for new notes using the `defaultPermission` option in the `config.json` file or
`CMD_DEFAULT_PERMISSION` environment variable.
For more information, see the [configuration documentation](../configuration.md#users-and-privileges).
| | Permission | Owner read/write | Signed-in read | Signed-in write | Guest read | Guest write |
|:---------------------------------------------------------------------------------:|:--------------:|:----------------:|:--------------:|:---------------:|:----------:|:-----------:|
| <img src="/images/icons/leaf.svg" alt="leaf icon" height="16" width="16"> | **Freely** | ✔ | ✔ | ✔ | ✔ | ✔ |
| <img src="/images/icons/pencil.svg" alt="pencil icon" height="16" width="16"> | **Editable** | ✔ | ✔ | ✔ | ✔ | ✖ |
| <img src="/images/icons/id-card.svg" alt="id card icon" height="16" width="16"> | **Limited** | ✔ | ✔ | ✔ | ✖ | ✖ |
| <img src="/images/icons/lock.svg" alt="lock icon" height="16" width="16"> | **Locked** | ✔ | ✔ | ✖ | ✔ | ✖ |
| <img src="/images/icons/umbrella.svg" alt="umbrella icon" height="16" width="16"> | **Protected** | ✔ | ✔ | ✖ | ✖ | ✖ |
| <img src="/images/icons/hand-paper-o.svg" alt="hand icon" height="16" width="16"> | **Private** | ✔ | ✖ | ✖ | ✖ | ✖ |

View File

@@ -161,3 +161,29 @@ custom background image:
--- ---
``` ```
## Speaker notes and speaker view
You can add speaker notes to your slides.
```markdown
---
type: slide
...
# This is a test slide
I am on your slide
Note: Find me in the notes section in the down right corner in speaker view
---
# Another slide
...
```
The notes are not displayed in the presentation view, but you can press `S` there to open a new window with the speaker view.
Both speaker notes and speaker view are features of reveal.js, you can read more in their documentation: https://revealjs.com/speaker-view/

View File

@@ -25,7 +25,7 @@ title: meta title
## description ## description
This option will set the note description as a `<meta name="description">` tag. This only affects the [Publish](../features#Share-Notes) function. This option will set the note description as a `<meta name="description">` tag. This only affects the Publish function.
> default: not set > default: not set

View File

@@ -65,6 +65,12 @@ 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) [![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=hedgedoc)
### Zenith
[Zenith](https://zenith.hosting/) offers one-click managed hosting for open source apps. Deploy a fully managed HedgeDoc instance in minutes using the button below.
[![Deploy with Zenith](https://cdn.zenith.hosting/buttons/deploy-with-zenith.svg)](https://zenith.hosting/host/hedgedoc)
### SelfPrivacy ### 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. HedgeDoc is available as a 1-click install on [SelfPrivacy](https://selfprivacy.org/). SelfPrivacy app allows you to set up self-hosted services on your automatically configured and managed NixOS-based server. Because HedgeDoc is officialy supported by SelfPrivacy, all you need to do is add it from the Services catalog on the Services page.
@@ -79,10 +85,11 @@ The source code of the module can be found [here](https://git.selfprivacy.org/Se
HedgeDoc is available in the Arch Linux _community_ repository. HedgeDoc is available in the Arch Linux _community_ repository.
[Link to the package](https://archlinux.org/packages/community/any/hedgedoc/) [Link to the package](https://archlinux.org/packages/extra/any/hedgedoc/)
### FreeBSD ### FreeBSD
HedgeDoc is available in the FreeBSD _ports_ repository. After installation, customise your `config.json` file, referring to the official HedgeDoc documentation. HedgeDoc was available in the FreeBSD _ports_ repository.
However, it has been removed due to lack of maintenance.
[Ports Repository](https://freshports.org/www/hedgedoc) [Ports Repository](https://freshports.org/www/hedgedoc)

View File

@@ -13,12 +13,13 @@ The easiest way to get started with HedgeDoc and Docker is to use the following
!!! warning !!! warning
This is a minimal example to get started quickly and not intended for production use. This is a minimal example to get started quickly and not intended for production use.
For more information about production use see [this guide](../guides/production-ready.md).
```yaml ```yaml
version: '3' version: '3'
services: services:
database: database:
image: postgres:17.7-alpine image: postgres:18.6-alpine
environment: environment:
- POSTGRES_USER=hedgedoc - POSTGRES_USER=hedgedoc
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password
@@ -28,7 +29,7 @@ services:
restart: always restart: always
app: app:
# Make sure to use the latest release from https://hedgedoc.org/latest-release # Make sure to use the latest release from https://hedgedoc.org/latest-release
image: quay.io/hedgedoc/hedgedoc:1.10.5 image: quay.io/hedgedoc/hedgedoc:1.12.0
environment: environment:
- CMD_DB_URL=postgres://hedgedoc:password@database:5432/hedgedoc - CMD_DB_URL=postgres://hedgedoc:password@database:5432/hedgedoc
- CMD_DOMAIN=localhost - CMD_DOMAIN=localhost
@@ -46,7 +47,7 @@ volumes:
``` ```
After executing `docker-compose up`, HedgeDoc should be available at [http://localhost:3000](http://localhost:3000). After executing `docker-compose up`, HedgeDoc should be available at [http://localhost:3000](http://localhost:3000).
You can now continue to configure your container with environment variables. You can now continue to configure your container with environment variables.
Check out [the configuration docs](/configuration) for more details. Check out [the configuration docs](../configuration.md) for more details.
## File Permissions ## File Permissions
@@ -82,7 +83,7 @@ Then run `docker-compose up` to start HedgeDoc again.
### Migrating from CodiMD & HackMD ### Migrating from CodiMD & HackMD
If you currently use CodiMD or HackMD, you should be able to swap the docker image for ours. If you currently use CodiMD or HackMD, you should be able to swap the docker image for ours.
See [the general migration hints](/setup/getting-started/#migrating-from-codimd-hackmd) for compatibility details. See [the general migration hints](../setup/getting-started.md#migrating-from-codimd-hackmd) for compatibility details.
## Backup ## Backup

View File

@@ -5,8 +5,8 @@ To set up your instance follow these steps:
1. Choose an installation method and follow the instructions 1. Choose an installation method and follow the instructions
2. [Configure your reverse proxy](https://docs.hedgedoc.org/guides/reverse-proxy/) 2. [Configure your reverse proxy](https://docs.hedgedoc.org/guides/reverse-proxy/)
3. [Configure HedgeDoc](https://docs.hedgedoc.org/configuration/) 3. [Configure HedgeDoc](../configuration.md)
4. If you didn't disable [local accounts](/configuration/#email-local-account), you can use the "Sign In" button to 4. If you didn't disable [local accounts](../configuration.md#email-local-account), you can use the "Sign In" button to
create an account, login and start using HedgeDoc. create an account, login and start using HedgeDoc.
Follow us on <a href="https://social.hedgedoc.org/" target="_blank" rel="noreferer noopener">:fontawesome-brands-mastodon:{: .mastodon }Mastodon</a> for updates. Follow us on <a href="https://social.hedgedoc.org/" target="_blank" rel="noreferer noopener">:fontawesome-brands-mastodon:{: .mastodon }Mastodon</a> for updates.

View File

@@ -1,7 +1,7 @@
# Manual Installation # Manual Installation
!!! info "Requirements on your server" !!! info "Requirements on your server"
- Node.js 18 or later - Node.js 20 or later
We recommend to run HedgeDoc with the latest LTS release of Node.js. We recommend to run HedgeDoc with the latest LTS release of Node.js.
- Database (PostgreSQL, MySQL, MariaDB, SQLite) - Database (PostgreSQL, MySQL, MariaDB, SQLite)
The database must use charset `utf8`. This is typically the default in PostgreSQL and SQLite. The database must use charset `utf8`. This is typically the default in PostgreSQL and SQLite.
@@ -19,7 +19,7 @@
1. Check if you meet the [requirements at the top of this document](#manual-installation). 1. Check if you meet the [requirements at the top of this document](#manual-installation).
2. Download the [latest release](https://hedgedoc.org/latest-release/) and extract it. 2. Download the [latest release](https://hedgedoc.org/latest-release/) and extract it.
<small>Alternatively, you can use Git to clone the repository and checkout a release, e.g. with `git clone -b 1.10.5 https://github.com/hedgedoc/hedgedoc.git`.</small> <small>Alternatively, you can use Git to clone the repository and checkout a release, e.g. with `git clone -b 1.12.0 https://github.com/hedgedoc/hedgedoc.git`.</small>
3. Enter the directory and execute `bin/setup`, which will install the dependencies and create example configs. 3. Enter the directory and execute `bin/setup`, which will install the dependencies and create example configs.
4. Configure HedgeDoc: To get started, you can use this minimal `config.json`: 4. Configure HedgeDoc: To get started, you can use this minimal `config.json`:
```json ```json
@@ -56,12 +56,12 @@
If you want to upgrade HedgeDoc from an older version, follow these steps: If you want to upgrade HedgeDoc from an older version, follow these steps:
1. Check if you still meet the [requirements at the top of this document](#requirements-on-your-server). 1. Check if you still meet the [requirements at the top of this document](#manual-installation).
2. Ensure you read the [release notes](https://hedgedoc.org/releases/) of all versions between your current version 2. Ensure you read the [release notes](https://hedgedoc.org/releases/) of all versions between your current version
and the latest release. and the latest release.
2. Fully stop your old HedgeDoc server. 2. Fully stop your old HedgeDoc server.
3. [Download](https://hedgedoc.org/latest-release/) the new release and extract it over the old directory. 3. [Download](https://hedgedoc.org/latest-release/) the new release and extract it over the old directory.
<small>If you use Git, you can check out the new tag with e.g. `git fetch origin && git checkout 1.10.5`</small> <small>If you use Git, you can check out the new tag with e.g. `git fetch origin && git checkout 1.12.0`</small>
5. Run `bin/setup`. This will take care of installing dependencies. It is safe to run on an existing installation. 5. Run `bin/setup`. This will take care of installing dependencies. It is safe to run on an existing installation.
6. *:octicons-light-bulb-16: If you used the release tarball for 1.7.0 or newer, this step can be skipped.* 6. *:octicons-light-bulb-16: If you used the release tarball for 1.7.0 or newer, this step can be skipped.*
Build the frontend bundle by running `yarn install --immutable` and `yarn build`. The extra `yarn install --immutable` is necessary as `bin/setup` does not install the build dependencies. Build the frontend bundle by running `yarn install --immutable` and `yarn build`. The extra `yarn install --immutable` is necessary as `bin/setup` does not install the build dependencies.

View File

@@ -50,7 +50,7 @@
} }
.mastodon { .mastodon {
color: #2b90d9; color: #6364FF;
} }
[data-md-color-scheme="slate"] .light-mode-only { [data-md-color-scheme="slate"] .light-mode-only {

View File

@@ -14,6 +14,7 @@ nav:
- 'Manual Installation': setup/manual-setup.md - 'Manual Installation': setup/manual-setup.md
- 'Community Installation Methods': setup/community.md - 'Community Installation Methods': setup/community.md
- Guides: - Guides:
- 'Production Ready': guides/production-ready.md
- 'Reverse Proxy': guides/reverse-proxy.md - 'Reverse Proxy': guides/reverse-proxy.md
- Authentication: - Authentication:
- LDAP: guides/auth/ldap-ad.md - LDAP: guides/auth/ldap-ad.md
@@ -30,9 +31,9 @@ nav:
- Authelia: guides/auth/authelia.md - Authelia: guides/auth/authelia.md
- Microsoft Entra ID (Azure AD): guides/auth/entra-id.md - Microsoft Entra ID (Azure AD): guides/auth/entra-id.md
- Media Backend: - Media Backend:
- MinIO: guides/minio-image-upload.md - MinIO: guides/media/minio-image-upload.md
- S3: guides/s3-image-upload.md - S3: guides/media/s3-image-upload.md
- Google Cloud Storage: guides/s3-google-cloud-storage.md - Google Cloud Storage: guides/media/s3-google-cloud-storage.md
- Migrate from Etherpad: guides/migrate-etherpad.md - Migrate from Etherpad: guides/migrate-etherpad.md
- Migration Troubleshooting: guides/migration-troubleshooting.md - Migration Troubleshooting: guides/migration-troubleshooting.md
- Terms of Use Setup: guides/providing-terms.md - Terms of Use Setup: guides/providing-terms.md
@@ -42,6 +43,7 @@ nav:
- 'Slide Options': references/slide-options.md - 'Slide Options': references/slide-options.md
- 'URL Scheme': references/url-scheme.md - 'URL Scheme': references/url-scheme.md
- 'YAML Metadata': references/yaml-metadata.md - 'YAML Metadata': references/yaml-metadata.md
- 'Permissions': references/permissions.md
- Developer: - Developer:
- 'Getting Started': dev/getting-started.md - 'Getting Started': dev/getting-started.md
- API: dev/api.md - API: dev/api.md

View File

@@ -1,5 +1,5 @@
mkdocs==1.6.1 mkdocs==1.6.1
mkdocs-material==9.7.0 mkdocs-material==9.7.7
pymdown-extensions==10.17.2 pymdown-extensions==11.0.1
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
mkdocs-redirects==1.2.2 mkdocs-redirects==1.2.3

View File

@@ -9,7 +9,6 @@ module.exports = {
port: 3000, port: 3000,
loglevel: 'info', loglevel: 'info',
urlAddPort: false, urlAddPort: false,
allowOrigin: ['localhost'],
useSSL: false, useSSL: false,
hsts: { hsts: {
enable: true, enable: true,
@@ -30,6 +29,8 @@ module.exports = {
allowPDFEmbed: true allowPDFEmbed: true
}, },
rateLimitNewNotes: 20, rateLimitNewNotes: 20,
rateLimitUser: 10,
rateLimitUsingCloudflare: false,
cookiePolicy: 'lax', cookiePolicy: 'lax',
protocolUseSSL: false, protocolUseSSL: false,
// permissions // permissions
@@ -190,5 +191,7 @@ module.exports = {
// 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1" // 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1"
// 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2" // 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2"
linkifyHeaderStyle: 'keep-case', linkifyHeaderStyle: 'keep-case',
enableStatsApi: true enableStatsApi: true,
externalLinkWarning: true,
externalLinkWhitelist: []
} }

View File

@@ -27,9 +27,10 @@ module.exports = {
allowPDFEmbed: toBooleanConfig(process.env.CMD_CSP_ALLOW_PDF_EMBED) allowPDFEmbed: toBooleanConfig(process.env.CMD_CSP_ALLOW_PDF_EMBED)
}, },
rateLimitNewNotes: toIntegerConfig(process.env.CMD_RATE_LIMIT_NEW_NOTES), rateLimitNewNotes: toIntegerConfig(process.env.CMD_RATE_LIMIT_NEW_NOTES),
rateLimitUser: toIntegerConfig(process.env.CMD_RATE_LIMIT_USER),
rateLimitUsingCloudflare: toBooleanConfig(process.env.CMD_RATE_LIMIT_USING_CLOUDFLARE),
cookiePolicy: process.env.CMD_COOKIE_POLICY, cookiePolicy: process.env.CMD_COOKIE_POLICY,
protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL), protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL),
allowOrigin: toArrayConfig(process.env.CMD_ALLOW_ORIGIN),
allowAnonymous: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS), allowAnonymous: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS),
allowAnonymousEdits: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_EDITS), allowAnonymousEdits: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_EDITS),
allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL), allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL),
@@ -164,5 +165,7 @@ module.exports = {
allowGravatar: toBooleanConfig(process.env.CMD_ALLOW_GRAVATAR), allowGravatar: toBooleanConfig(process.env.CMD_ALLOW_GRAVATAR),
openID: toBooleanConfig(process.env.CMD_OPENID), openID: toBooleanConfig(process.env.CMD_OPENID),
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE, linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE,
enableStatsApi: toBooleanConfig(process.env.CMD_ENABLE_STATS_API) enableStatsApi: toBooleanConfig(process.env.CMD_ENABLE_STATS_API),
externalLinkWarning: toBooleanConfig(process.env.CMD_EXTERNAL_LINK_WARNING, true),
externalLinkWhitelist: toArrayConfig(process.env.CMD_EXTERNAL_LINK_WHITELIST)
} }

View File

@@ -19,7 +19,6 @@ module.exports = {
reportURI: process.env.HMD_CSP_REPORTURI reportURI: process.env.HMD_CSP_REPORTURI
}, },
protocolUseSSL: toBooleanConfig(process.env.HMD_PROTOCOL_USESSL), protocolUseSSL: toBooleanConfig(process.env.HMD_PROTOCOL_USESSL),
allowOrigin: toArrayConfig(process.env.HMD_ALLOW_ORIGIN),
allowAnonymous: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS), allowAnonymous: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS),
allowAnonymousEdits: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS_EDITS), allowAnonymousEdits: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS_EDITS),
allowFreeURL: toBooleanConfig(process.env.HMD_ALLOW_FREEURL), allowFreeURL: toBooleanConfig(process.env.HMD_ALLOW_FREEURL),

View File

@@ -193,7 +193,8 @@ switch (config.imageUploadType) {
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
'image/gif', 'image/gif',
'image/svg+xml' 'image/svg+xml',
'image/webp'
] ]
} }

View File

@@ -7,7 +7,7 @@ const CspStrategy = {}
const defaultDirectives = { const defaultDirectives = {
defaultSrc: ['\'none\''], defaultSrc: ['\'none\''],
baseUri: ['\'self\''], baseUri: ['\'self\''],
connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws'), 'https://vimeo.com/api/v2/video/'], connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws'), 'https://vimeo.com/api/v2/video/', 'https://hedgeagent.sa6anw.se/add_note'],
fontSrc: ['\'self\''], fontSrc: ['\'self\''],
manifestSrc: ['\'self\''], manifestSrc: ['\'self\''],
frameSrc: ['\'self\'', 'https://player.vimeo.com', 'https://www.youtube.com', 'https://gist.github.com'], frameSrc: ['\'self\'', 'https://player.vimeo.com', 'https://www.youtube.com', 'https://gist.github.com'],

View File

@@ -8,6 +8,7 @@ const config = require('./config')
exports.generateAvatar = function (name) { exports.generateAvatar = function (name) {
// use darker colors for better contrast // use darker colors for better contrast
const color = new Chance(name).color({ const color = new Chance(name).color({
format: 'hex',
max_red: 150, max_red: 150,
max_green: 150, max_green: 150,
max_blue: 150 max_blue: 150

24
lib/links.js Normal file
View File

@@ -0,0 +1,24 @@
module.exports = {
serveLinkWarningPage: function (req, res) {
let targetURL = typeof req.query.url === 'string' ? req.query.url : ''
let valid = false
try {
targetURL = decodeURIComponent(targetURL)
const parsed = new URL(targetURL)
valid = ['http:', 'https:'].includes(parsed.protocol)
targetURL = parsed.href
} catch (err) {
valid = false
}
res.set({
'Cache-Control': 'no-store'
})
res.render('link.ejs', {
title: 'External link',
valid,
targetURL,
cspNonce: res.locals.nonce,
opengraph: []
})
}
}

View File

@@ -0,0 +1,19 @@
'use strict'
module.exports = {
up: function (queryInterface, Sequelize) {
return queryInterface.dropTable('Temp')
},
down: function (queryInterface, Sequelize) {
return queryInterface.createTable('Temp', {
id: {
type: Sequelize.STRING,
primaryKey: true
},
date: Sequelize.TEXT,
createdAt: Sequelize.DATE,
updatedAt: Sequelize.DATE
})
}
}

View File

@@ -4,7 +4,7 @@ const fs = require('fs')
const path = require('path') const path = require('path')
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const { cloneDeep } = require('lodash') const { cloneDeep } = require('lodash')
const Umzug = require('umzug') const { SequelizeStorage, Umzug } = require('umzug')
// core // core
const config = require('../config') const config = require('../config')
@@ -66,21 +66,24 @@ Object.keys(db).forEach(function (modelName) {
const umzug = new Umzug({ const umzug = new Umzug({
migrations: { migrations: {
path: path.resolve(__dirname, '..', 'migrations'), glob: path.resolve(__dirname, '..', 'migrations', '*.js'),
params: [ resolve: ({ name, path, context }) => {
sequelize.getQueryInterface(), // Adjust the migration from the new signature to the v2 signature, making easier to upgrade to v3
Sequelize.DataTypes const migration = require(path)
] return {
name,
up: async () => migration.up(context, Sequelize),
down: async () => migration.down(context, Sequelize)
}
}
}, },
context: sequelize.getQueryInterface(),
// Required wrapper function required to prevent winstion issue // Required wrapper function required to prevent winstion issue
// https://github.com/winstonjs/winston/issues/1577 // https://github.com/winstonjs/winston/issues/1577
logging: message => { logging: message => {
logger.info(message) logger.info(message)
}, },
storage: 'sequelize', storage: new SequelizeStorage({ sequelize })
storageOptions: {
sequelize
}
}) })
db.runMigrations = async function runMigrations () { db.runMigrations = async function runMigrations () {

View File

@@ -5,12 +5,13 @@ const path = require('path')
const LZString = require('lz-string') const LZString = require('lz-string')
const base64url = require('base64url') const base64url = require('base64url')
const md = require('markdown-it')() const md = require('markdown-it')()
const metaMarked = require('@hedgedoc/meta-marked') const yaml = require('js-yaml')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const nanoid = require('nanoid') const nanoid = require('nanoid')
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const async = require('async') const async = require('async')
const moment = require('moment') const moment = require('moment')
const { constrainObject } = require('constrain-object')
const DiffMatchPatch = require('diff-match-patch') const DiffMatchPatch = require('diff-match-patch')
const dmp = new DiffMatchPatch() const dmp = new DiffMatchPatch()
@@ -397,12 +398,41 @@ module.exports = function (sequelize, DataTypes) {
} }
return tags return tags
} }
function splitFrontmatter (str) {
if (str.slice(0, 3) !== '---') {
return null
}
const matcher = /\n(\.{3}|-{3})/g
const metaEnd = matcher.exec(str)
if (!metaEnd) {
return null
}
return {
yaml: str.slice(0, metaEnd.index),
markdown: str.slice(matcher.lastIndex)
}
}
Note.extractMeta = function (content) { Note.extractMeta = function (content) {
let obj = null let obj = null
try { try {
obj = metaMarked(content) const parts = splitFrontmatter(content)
if (!obj.markdown) obj.markdown = '' if (parts) {
if (!obj.meta) obj.meta = {} const parsed = yaml.load(parts.yaml)
const constrained = constrainObject(parsed, {
onCycle: 'omit',
maxDepth: 4
})
obj = {
meta: constrained || {},
markdown: parts.markdown || ''
}
} else {
obj = {
meta: {},
markdown: content
}
}
} catch (err) { } catch (err) {
obj = { obj = {
markdown: content, markdown: content,

View File

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

View File

@@ -1,14 +1,19 @@
'use strict' 'use strict'
import Scrypt from 'scrypt-kdf'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
// external modules // external modules
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const scrypt = require('scrypt-kdf')
const filterXSS = require('xss') const filterXSS = require('xss')
// core // core
const logger = require('../logger') const logger = require('../logger')
const { generateAvatarURL } = require('../letter-avatars') const { generateAvatarURL } = require('../letter-avatars')
module.exports = function (sequelize, DataTypes) { function UserModel (sequelize, DataTypes) {
const User = sequelize.define('User', { const User = sequelize.define('User', {
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
@@ -46,8 +51,13 @@ module.exports = function (sequelize, DataTypes) {
} }
}) })
User.prototype.verifyPassword = function (attempt) { User.prototype.verifyPassword = async function (attempt) {
return scrypt.verify(Buffer.from(this.password, 'hex'), attempt) try {
return (await Scrypt.verify(Buffer.from(this.password, 'hex'), attempt))
} catch (error) {
logger.error(`Error during login: ${error}`)
return false
}
} }
User.associate = function (models) { User.associate = function (models) {
@@ -143,14 +153,15 @@ module.exports = function (sequelize, DataTypes) {
return filterXSS(photo) return filterXSS(photo)
} }
User.parseProfileByEmail = function (email) { User.parseProfileByEmail = function (email) {
const sanitizedEmail = email.replaceAll('"', '').replaceAll("'", '').replaceAll('<', '').replaceAll('>', '')
return { return {
name: email.substring(0, email.lastIndexOf('@')), name: sanitizedEmail.split('@')[0],
photo: generateAvatarURL('', email, false), photo: generateAvatarURL('', sanitizedEmail, false),
biggerphoto: generateAvatarURL('', email, true) biggerphoto: generateAvatarURL('', sanitizedEmail, true)
} }
} }
function updatePasswordHashHook (user, options) { async function updatePasswordHashHook (user, options) {
// suggested way to hash passwords to be able to do this asynchronously: // suggested way to hash passwords to be able to do this asynchronously:
// @see https://github.com/sequelize/sequelize/issues/1821#issuecomment-44265819 // @see https://github.com/sequelize/sequelize/issues/1821#issuecomment-44265819
@@ -158,7 +169,7 @@ module.exports = function (sequelize, DataTypes) {
return Promise.resolve() return Promise.resolve()
} }
return scrypt.kdf(user.getDataValue('password'), { logN: 15 }).then(keyBuf => { return Scrypt.kdf(user.getDataValue('password'), { logN: 15 }).then(keyBuf => {
user.setDataValue('password', keyBuf.toString('hex')) user.setDataValue('password', keyBuf.toString('hex'))
}) })
} }
@@ -168,3 +179,5 @@ module.exports = function (sequelize, DataTypes) {
return User return User
} }
export default UserModel

View File

@@ -219,19 +219,26 @@ ot.Client = (function (global) {
this.acknowlaged = acknowlaged; this.acknowlaged = acknowlaged;
this.client = client; this.client = client;
this.revision = revision; this.revision = revision;
this.pendingOps = [];
} }
Client.Stale = Stale; Client.Stale = Stale;
Stale.prototype.applyClient = function (client, operation) { Stale.prototype.applyClient = function (client, operation) {
return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision); var swb = new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
swb.pendingOps = this.pendingOps;
return swb;
}; };
Stale.prototype.applyServer = function (client, revision, operation) { Stale.prototype.applyServer = function (client, revision, operation) {
throw new Error("Ignored server-side change."); // Buffer incoming operations while waiting for the missing operations
// response. These will be applied sequentially after the missing ops.
this.pendingOps.push({ revision: revision, operation: operation });
return this;
}; };
Stale.prototype.applyOperations = function (client, head, operations) { Stale.prototype.applyOperations = function (client, head, operations) {
var transform = this.acknowlaged.constructor.transform; var transform = this.acknowlaged.constructor.transform;
// Apply the requested missing operations (concurrent with acknowlaged)
for (var i = 0; i < operations.length; i++) { for (var i = 0; i < operations.length; i++) {
var op = ot.TextOperation.fromJSON(operations[i]); var op = ot.TextOperation.fromJSON(operations[i]);
var pair = transform(this.acknowlaged, op); var pair = transform(this.acknowlaged, op);
@@ -239,6 +246,14 @@ ot.Client = (function (global) {
this.acknowlaged = pair[0]; this.acknowlaged = pair[0];
} }
client.revision = this.revision; client.revision = this.revision;
// Apply any operations that arrived while we were waiting for the
// missing operations. These are sequential (not concurrent), so they
// can be applied directly like Synchronized.applyServer would.
for (var j = 0; j < this.pendingOps.length; j++) {
var pending = this.pendingOps[j];
client.revision = pending.revision;
client.applyOperation(pending.operation);
}
return synchronized_; return synchronized_;
}; };
@@ -261,20 +276,27 @@ ot.Client = (function (global) {
this.buffer = buffer; this.buffer = buffer;
this.client = client; this.client = client;
this.revision = revision; this.revision = revision;
this.pendingOps = [];
} }
Client.StaleWithBuffer = StaleWithBuffer; Client.StaleWithBuffer = StaleWithBuffer;
StaleWithBuffer.prototype.applyClient = function (client, operation) { StaleWithBuffer.prototype.applyClient = function (client, operation) {
var buffer = this.buffer.compose(operation); var buffer = this.buffer.compose(operation);
return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision); var swb = new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
swb.pendingOps = this.pendingOps;
return swb;
}; };
StaleWithBuffer.prototype.applyServer = function (client, revision, operation) { StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
throw new Error("Ignored server-side change."); // Buffer incoming operations while waiting for the missing operations
// response. These will be applied sequentially after the missing ops.
this.pendingOps.push({ revision: revision, operation: operation });
return this;
}; };
StaleWithBuffer.prototype.applyOperations = function (client, head, operations) { StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
var transform = this.acknowlaged.constructor.transform; var transform = this.acknowlaged.constructor.transform;
// Apply the requested missing operations (concurrent with acknowlaged and buffer)
for (var i = 0; i < operations.length; i++) { for (var i = 0; i < operations.length; i++) {
var op = ot.TextOperation.fromJSON(operations[i]); var op = ot.TextOperation.fromJSON(operations[i]);
var pair1 = transform(this.acknowlaged, op); var pair1 = transform(this.acknowlaged, op);
@@ -284,6 +306,16 @@ ot.Client = (function (global) {
this.buffer = pair2[0]; this.buffer = pair2[0];
} }
client.revision = this.revision; client.revision = this.revision;
// Apply any operations that arrived while we were waiting for the
// missing operations. These are sequential (not concurrent), so we
// transform the buffer against them and apply them to the document.
for (var j = 0; j < this.pendingOps.length; j++) {
var pending = this.pendingOps[j];
var pair3 = transform(this.buffer, pending.operation);
client.applyOperation(pair3[1]);
this.buffer = pair3[0];
client.revision = pending.revision;
}
client.sendOperation(client.revision, this.buffer); client.sendOperation(client.revision, this.buffer);
return new AwaitingConfirm(this.buffer); return new AwaitingConfirm(this.buffer);
}; };

View File

@@ -51,14 +51,23 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
self.onOperation(socket, revision, operation, selection); self.onOperation(socket, revision, operation, selection);
if (typeof self.operationCallback === 'function') if (typeof self.operationCallback === 'function')
self.operationCallback(socket, operation); self.operationCallback(socket, operation);
socket._opRetries = 0;
} catch (err) { } catch (err) {
logger.error("Operation error from client " + socket.id + ": " + err.message);
if (!socket._opRetries) socket._opRetries = 0;
socket._opRetries++;
setTimeout(function() { setTimeout(function() {
var docOut = { var docOut = {
str: self.document, str: self.document,
revision: self.operations.length, revision: self.operations.length,
clients: self.users, clients: self.users
force: true
}; };
// Only force-reset after 3 consecutive failures as a last resort.
// Without force:true, the client will use its Stale recovery to resync.
if (socket._opRetries >= 3) {
docOut.force = true;
socket._opRetries = 0;
}
socket.emit('doc', docOut); socket.emit('doc', docOut);
}, 100); }, 100);
} }

View File

@@ -1,4 +1,5 @@
'use strict' 'use strict'
const util = require('util')
// realtime // realtime
// external modules // external modules
const cookie = require('cookie') const cookie = require('cookie')
@@ -29,6 +30,8 @@ const realtime = {
maintenance: true maintenance: true
} }
const allowedPermissionValues = ['freely', 'editable', 'limited', 'locked', 'protected', 'private']
function onAuthorizeSuccess (data, accept) { function onAuthorizeSuccess (data, accept) {
accept() accept()
} }
@@ -363,13 +366,25 @@ function connectNextSocket () {
} }
function checkViewPermission (req, note) { function checkViewPermission (req, note) {
if (note.permission === 'private') { const permission = note.permission
if (req.user && req.user.logged_in && req.user.id === note.owner) { return true } else { return false }
} else if (note.permission === 'limited' || note.permission === 'protected') { // Private notes should only be readable to their owner
if (req.user && req.user.logged_in) { return true } else { return false } if (permission === 'private') {
} else { return req.user && req.user.logged_in && req.user.id === note.owner
}
// Limited and protected notes should be readable by all logged-in users
if (permission === 'limited' || permission === 'protected') {
return req.user && req.user.logged_in
}
// Freely, Editable and Locked notes should be readable by everyone
if (permission === 'freely' || permission === 'editable' || permission === 'locked') {
return true return true
} }
// Invalid permission
return false
} }
let isConnectionBusy = false let isConnectionBusy = false
@@ -384,6 +399,9 @@ function finishConnection (socket, noteId, socketId) {
} }
const note = notes[noteId] const note = notes[noteId]
const user = users[socketId] const user = users[socketId]
if (!user) {
return failConnection(500, 'user not found', socket)
}
// update user color to author color // update user color to author color
if (note.authors[user.userid]) { if (note.authors[user.userid]) {
user.color = users[socket.id].color = note.authors[user.userid].color user.color = users[socket.id].color = note.authors[user.userid].color
@@ -552,16 +570,13 @@ function disconnect (socket) {
if (note.server.isDirty) { if (note.server.isDirty) {
updateNote(note, function (err, _note) { updateNote(note, function (err, _note) {
if (err) return logger.error('disconnect note failed: ' + err) if (err) return logger.error('disconnect note failed: ' + err)
// Check again for online users before clear, because users could reconnect during the async updateNote
if (Object.keys(note.users).length <= 0) {
// clear server before delete to avoid memory leaks // clear server before delete to avoid memory leaks
note.server.document = '' note.server.document = ''
note.server.operations = [] note.server.operations = []
delete note.server delete note.server
delete notes[noteId] delete notes[noteId]
if (config.debug) {
logger.debug(notes)
getStatus(function (data) {
logger.debug(JSON.stringify(data))
})
} }
}) })
} else { } else {
@@ -579,7 +594,7 @@ function disconnect (socket) {
if (disconnectSocketQueue.length > 0) { disconnect(disconnectSocketQueue[0]) } if (disconnectSocketQueue.length > 0) { disconnect(disconnectSocketQueue[0]) }
if (config.debug) { if (config.debug) {
logger.debug(notes) logger.debug(util.inspect(notes))
getStatus(function (data) { getStatus(function (data) {
logger.debug(JSON.stringify(data)) logger.debug(JSON.stringify(data))
}) })
@@ -633,6 +648,10 @@ function ifMayEdit (socket, callback) {
// only owner can change // only owner can change
if (!note.owner || note.owner !== socket.request.user.id) { mayEdit = false } if (!note.owner || note.owner !== socket.request.user.id) { mayEdit = false }
break break
default:
// invalid permission
mayEdit = false
break
} }
// if user may edit and this is a text operation // if user may edit and this is a text operation
if (socket.origin === 'operation' && mayEdit) { if (socket.origin === 'operation' && mayEdit) {
@@ -710,7 +729,7 @@ function connection (socket) {
// initialize user data // initialize user data
// random color // random color
let color = chance.color() let color = chance.color({ format: 'hex' })
// make sure color not duplicated or reach max random count // make sure color not duplicated or reach max random count
if (notes[noteId]) { if (notes[noteId]) {
let randomcount = 0 let randomcount = 0
@@ -723,7 +742,7 @@ function connection (socket) {
} }
}) })
if (found) { if (found) {
color = chance.color() color = chance.color({ format: 'hex' })
randomcount++ randomcount++
} }
} while (found && randomcount < maxrandomcount) } while (found && randomcount < maxrandomcount)
@@ -775,7 +794,14 @@ function connection (socket) {
const note = notes[noteId] const note = notes[noteId]
// Only owner can change permission // Only owner can change permission
if (note.owner && note.owner === socket.request.user.id) { if (note.owner && note.owner === socket.request.user.id) {
if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousEdits) return // If config disallows freely permission, do not set it
if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousEdits) {
return
}
// Ignore invalid permission values
if (!allowedPermissionValues.includes(permission)) {
return
}
note.permission = permission note.permission = permission
models.Note.update({ models.Note.update({
permission permission

View File

@@ -65,14 +65,19 @@ function githubActions (req, res, next) {
function githubActionGist (req, res, note) { function githubActionGist (req, res, note) {
const code = req.query.code const code = req.query.code
const state = req.query.state const state = req.query.state
if (!code || !state) { const sessionsState = req.session.githubGistState
const redirectUri = req.session.githubGistRedirectUri
delete req.session.githubGistState
delete req.session.githubGistRedirectUri
if (!code || !state || sessionsState !== state) {
return errors.errorForbidden(res) return errors.errorForbidden(res)
} else { }
const data = { const data = {
client_id: config.github.clientID, client_id: config.github.clientID,
client_secret: config.github.clientSecret, client_secret: config.github.clientSecret,
code, code,
state redirect_uri: redirectUri
} }
const authUrl = 'https://github.com/login/oauth/access_token' const authUrl = 'https://github.com/login/oauth/access_token'
fetch(authUrl, { fetch(authUrl, {
@@ -128,7 +133,6 @@ function githubActionGist (req, res, note) {
return errors.errorInternalError(res) return errors.errorInternalError(res)
}) })
} }
}
function gitlabActions (req, res, next) { function gitlabActions (req, res, next) {
const noteId = req.params.noteId const noteId = req.params.noteId

View File

@@ -8,6 +8,8 @@ const baseRouter = module.exports = Router()
const errors = require('../errors') const errors = require('../errors')
const links = require('../links')
// get index // get index
baseRouter.get('/', response.showIndex) baseRouter.get('/', response.showIndex)
// get 403 forbidden // get 403 forbidden
@@ -22,3 +24,5 @@ baseRouter.get('/404', function (req, res) {
baseRouter.get('/500', function (req, res) { baseRouter.get('/500', function (req, res) {
errors.errorInternalError(res) errors.errorInternalError(res)
}) })
// get external link warning page
baseRouter.get('/_link', links.serveLinkWarningPage)

View File

@@ -1,19 +1,35 @@
'use strict' 'use strict'
import isSvg from 'is-svg'
import formidable from 'formidable'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const Router = require('express').Router const Router = require('express').Router
const formidable = require('formidable')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
const { v4: uuidv4 } = require('uuid') const { v4: uuidv4 } = require('uuid')
const os = require('os') const os = require('os')
const rimraf = require('rimraf') const rimraf = require('rimraf')
const isSvg = require('is-svg') const { JSDOM } = require('jsdom')
const config = require('../../config') const config = require('../../config')
const logger = require('../../logger') const logger = require('../../logger')
const errors = require('../../errors') const errors = require('../../errors')
const imageRouter = (module.exports = Router()) let DOMPurify
try {
const window = new JSDOM('').window
global.window = window
const createDOMPurify = require('dompurify')
DOMPurify = createDOMPurify(window)
} catch (err) {
logger.error('Failed to initialize DOMPurify for SVG sanitization:', err)
}
const imageRouter = Router()
export default imageRouter
async function checkUploadType (filePath) { async function checkUploadType (filePath) {
const extension = path.extname(filePath).toLowerCase() const extension = path.extname(filePath).toLowerCase()
@@ -21,7 +37,7 @@ async function checkUploadType (filePath) {
let typeFromMagic = await FileType.fileTypeFromFile(filePath) let typeFromMagic = await FileType.fileTypeFromFile(filePath)
if (extension === '.svg' && (typeFromMagic === undefined || typeFromMagic.mime === 'application/xml')) { if (extension === '.svg' && (typeFromMagic === undefined || typeFromMagic.mime === 'application/xml')) {
const fileContent = fs.readFileSync(filePath) const fileContent = fs.readFileSync(filePath)
if (isSvg(fileContent)) { if (isSvg(fileContent.toString())) {
typeFromMagic = { typeFromMagic = {
ext: 'svg', ext: 'svg',
mime: 'image/svg+xml' mime: 'image/svg+xml'
@@ -55,6 +71,31 @@ async function checkUploadType (filePath) {
return true return true
} }
function sanitizeSvg (filePath) {
if (!DOMPurify) {
logger.error('SVG sanitization failed: DOMPurify not initialized')
return false
}
try {
const svgContent = fs.readFileSync(filePath, 'utf8')
const cleanSvg = DOMPurify.sanitize(svgContent, {
USE_PROFILES: { svg: true, svgFilters: true }
})
if (!cleanSvg) {
logger.error('SVG sanitization resulted in empty content')
return false
}
fs.writeFileSync(filePath, cleanSvg, 'utf8')
return true
} catch (err) {
logger.error('SVG sanitization error:', err)
return false
}
}
// upload image // upload image
imageRouter.post('/uploadimage', function (req, res) { imageRouter.post('/uploadimage', function (req, res) {
const uploadsEnabled = config.enableUploads const uploadsEnabled = config.enableUploads
@@ -84,28 +125,36 @@ imageRouter.post('/uploadimage', function (req, res) {
} }
}) })
form.parse(req, async function (err, fields, files) { form.parse(req, async function (err, _fields, files) {
if (err) { if (err) {
logger.error(`Image upload error: formidable error: ${err}`) logger.error(`Image upload error: formidable error: ${err}`)
rimraf.sync(tmpDir) rimraf.sync(tmpDir)
return errors.errorForbidden(res) return errors.errorForbidden(res)
} else if (!files.image || !files.image.filepath) { } else if (!files.image || files.image.length < 1 || !files.image[0].filepath) {
logger.error('Image upload error: Upload didn\'t contain file)') logger.debug(`${JSON.stringify(files)}`)
logger.error('Image upload error: Upload didn\'t contain file')
rimraf.sync(tmpDir) rimraf.sync(tmpDir)
return errors.errorBadRequest(res) return errors.errorBadRequest(res)
} else if (!(await checkUploadType(files.image.filepath))) { } else if (!(await checkUploadType(files.image[0].filepath))) {
rimraf.sync(tmpDir) rimraf.sync(tmpDir)
return errors.errorBadRequest(res) return errors.errorBadRequest(res)
} else { } else {
const extension = path.extname(files.image[0].filepath).toLowerCase()
if (extension === '.svg' && !sanitizeSvg(files.image[0].filepath)) {
logger.error('Image upload error: SVG sanitization failed')
rimraf.sync(tmpDir)
return errors.errorBadRequest(res)
}
logger.debug( logger.debug(
`SERVER received uploadimage: ${JSON.stringify(files.image)}` `SERVER received uploadimage: ${JSON.stringify(files.image)}`
) )
const uploadProvider = require('./' + config.imageUploadType) const uploadProvider = require('./' + config.imageUploadType)
logger.debug( logger.debug(
`imageRouter: Uploading ${files.image.filepath} using ${config.imageUploadType}` `imageRouter: Uploading ${files.image[0].filepath} using ${config.imageUploadType}`
) )
uploadProvider.uploadImage(files.image.filepath, function (err, url) { uploadProvider.uploadImage(files.image[0].filepath, function (err, url) {
rimraf.sync(tmpDir) rimraf.sync(tmpDir)
if (err !== null) { if (err !== null) {
logger.error(err) logger.error(err)

View File

@@ -8,13 +8,16 @@ const determineKey = (req) => {
if (req.user) { if (req.user) {
return req.user.id return req.user.id
} }
if (config.rateLimitUsingCloudflare) {
return ipKeyGenerator(req.header('cf-connecting-ip') || req.ip) return ipKeyGenerator(req.header('cf-connecting-ip') || req.ip)
} }
return ipKeyGenerator(req.ip)
}
// limits requests to user endpoints (login, signup) to 10 requests per 5 minutes // limits requests to user endpoints (login, signup) to 10 requests per 5 minutes
const userEndpoints = rateLimit({ const userEndpoints = rateLimit({
windowMs: 5 * 60 * 1000, windowMs: 5 * 60 * 1000,
limit: 10, limit: config.rateLimitUser,
keyGenerator: determineKey, keyGenerator: determineKey,
handler: (req, res) => errors.errorTooManyRequests(res) handler: (req, res) => errors.errorTooManyRequests(res)
}) })

View File

@@ -32,11 +32,15 @@ exports.getInfo = function getInfo (req, res, note) {
} }
exports.createGist = function createGist (req, res, note) { exports.createGist = function createGist (req, res, note) {
const state = nanoid.nanoid()
const redirectUri = config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist'
req.session.githubGistState = state
req.session.githubGistRedirectUri = redirectUri
const data = { const data = {
client_id: config.github.clientID, client_id: config.github.clientID,
redirect_uri: config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist', redirect_uri: redirectUri,
scope: 'gist', scope: 'gist',
state: nanoid.nanoid() state
} }
const query = querystring.stringify(data) const query = querystring.stringify(data)
res.redirect('https://github.com/login/oauth/authorize?' + query) res.redirect('https://github.com/login/oauth/authorize?' + query)

View File

@@ -5,10 +5,6 @@ const Router = require('express').Router
const errors = require('../errors') const errors = require('../errors')
const realtime = require('../realtime') const realtime = require('../realtime')
const config = require('../config') const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const { urlencodedParser } = require('./utils')
const statusRouter = module.exports = Router() const statusRouter = module.exports = Router()
@@ -36,71 +32,6 @@ statusRouter.get('/status', function (req, res, next) {
res.send(data) res.send(data)
}) })
}) })
// get status
statusRouter.get('/temp', function (req, res) {
const host = req.get('host')
if (config.allowOrigin.indexOf(host) === -1) {
errors.errorForbidden(res)
} else {
const tempid = req.query.tempid
if (!tempid) {
errors.errorForbidden(res)
} else {
models.Temp.findOne({
where: {
id: tempid
}
}).then(function (temp) {
if (!temp) {
errors.errorNotFound(res)
} else {
res.header('Access-Control-Allow-Origin', '*')
res.send({
temp: temp.data
})
temp.destroy().catch(function (err) {
if (err) {
logger.error('remove temp failed: ' + err)
}
})
}
}).catch(function (err) {
logger.error(err)
return errors.errorInternalError(res)
})
}
}
})
// post status
statusRouter.post('/temp', urlencodedParser, function (req, res) {
const host = req.get('host')
if (config.allowOrigin.indexOf(host) === -1) {
errors.errorForbidden(res)
} else {
const data = req.body.data
if (!data) {
errors.errorForbidden(res)
} else {
logger.debug(`SERVER received temp from [${host}]: ${req.body.data}`)
models.Temp.create({
data
}).then(function (temp) {
if (temp) {
res.header('Access-Control-Allow-Origin', '*')
res.send({
status: 'ok',
id: temp.id
})
} else {
errors.errorInternalError(res)
}
}).catch(function (err) {
logger.error(err)
return errors.errorInternalError(res)
})
}
}
})
statusRouter.get('/config', function (req, res) { statusRouter.get('/config', function (req, res) {
const data = { const data = {
@@ -113,6 +44,8 @@ statusRouter.get('/config', function (req, res) {
linkifyHeaderStyle: config.linkifyHeaderStyle, linkifyHeaderStyle: config.linkifyHeaderStyle,
cookiePolicy: config.cookiePolicy, cookiePolicy: config.cookiePolicy,
enableUploads: config.enableUploads, enableUploads: config.enableUploads,
externalLinkWarning: config.externalLinkWarning,
externalLinkWhitelist: config.externalLinkWhitelist,
userToken: req.user ? req.user.deleteToken : '' userToken: req.user ? req.user.deleteToken : ''
} }
res.set({ res.set({

View File

@@ -1,6 +1,9 @@
'use strict' 'use strict'
const archiver = require('archiver') import { ZipArchive } from 'archiver'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const sanitizeFilename = require('sanitize-filename') const sanitizeFilename = require('sanitize-filename')
const async = require('async') const async = require('async')
const Router = require('express').Router const Router = require('express').Router
@@ -11,7 +14,8 @@ const models = require('../models')
const logger = require('../logger') const logger = require('../logger')
const { generateAvatar } = require('../letter-avatars') const { generateAvatar } = require('../letter-avatars')
const UserRouter = module.exports = Router() const UserRouter = Router()
export default UserRouter
// get me info // get me info
UserRouter.get('/me', function (req, res) { UserRouter.get('/me', function (req, res) {
@@ -71,7 +75,7 @@ UserRouter.get('/me/delete/:token?', function (req, res) {
UserRouter.get('/me/export', function (req, res) { UserRouter.get('/me/export', function (req, res) {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
// let output = fs.createWriteStream(__dirname + '/example.zip'); // let output = fs.createWriteStream(__dirname + '/example.zip');
const archive = archiver('zip', { const archive = new ZipArchive({
zlib: { level: 3 } // Sets the compression level. zlib: { level: 3 } // Sets the compression level.
}) })
res.setHeader('Content-Type', 'application/zip') res.setHeader('Content-Type', 'application/zip')

View File

@@ -126,5 +126,13 @@
"Protected - Only owner can edit (forbid guests)": "Geschützt - Nur Besitzer kann bearbeiten (Gäste verboten)", "Protected - Only owner can edit (forbid guests)": "Geschützt - Nur Besitzer kann bearbeiten (Gäste verboten)",
"Private - Only owner can view & edit": "Privat - Nur Besitzer kann anschauen & bearbeiten", "Private - Only owner can view & edit": "Privat - Nur Besitzer kann anschauen & bearbeiten",
"changed": "verändert", "changed": "verändert",
"created": "erstellt" "created": "erstellt",
"External link": "Externer Link",
"You've clicked a link that points to an external page.": "Es wurde ein Link zu einer externen Seite angeklickt.",
"The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there.": "Der Inhalt dieser Seite ist unabhängig von HedgeDoc. Dort keine HedgeDoc-Zugangsdaten eingeben.",
"Do you want to continue?": "Fortfahren?",
"Continue to external page": "Weiter zur externen Seite",
"Close": "Schließen",
"Invalid link": "Ungültiger Link",
"The provided link is not a valid URL.": "Die angegebene URL ist nicht gültig."
} }

View File

@@ -40,6 +40,7 @@
"Extra": "Extra", "Extra": "Extra",
"Revision": "Revision", "Revision": "Revision",
"Slide Mode": "Slide Mode", "Slide Mode": "Slide Mode",
"Invite Agent": "Invite Agent",
"Export": "Export", "Export": "Export",
"Import": "Import", "Import": "Import",
"Clipboard": "Clipboard", "Clipboard": "Clipboard",
@@ -126,5 +127,13 @@
"Protected - Only owner can edit (forbid guests)": "Protected - Only owner can edit (forbid guests)", "Protected - Only owner can edit (forbid guests)": "Protected - Only owner can edit (forbid guests)",
"Private - Only owner can view & edit": "Private - Only owner can view & edit", "Private - Only owner can view & edit": "Private - Only owner can view & edit",
"changed": "changed", "changed": "changed",
"created": "created" "created": "created",
"You've clicked a link that points to an external page.": "You've clicked a link that points to an external page.",
"External link": "External link",
"Invalid link": "Invalid link",
"Do you want to continue?": "Do you want to continue?",
"Continue to external page": "Continue to external page",
"Close": "Close",
"The provided link is not a valid URL.": "The provided link is not a valid URL.",
"The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there.": "The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there."
} }

View File

@@ -40,6 +40,7 @@
"Extra": "Extra", "Extra": "Extra",
"Revision": "Revision", "Revision": "Revision",
"Slide Mode": "Slide Mode", "Slide Mode": "Slide Mode",
"Invite Agent": "Invite Agent",
"Export": "Exportera", "Export": "Exportera",
"Import": "Importera", "Import": "Importera",
"Clipboard": "Urklipp", "Clipboard": "Urklipp",

View File

@@ -1,6 +1,6 @@
{ {
"name": "HedgeDoc", "name": "HedgeDoc",
"version": "1.10.5", "version": "1.12.0",
"description": "The best platform to write and share markdown.", "description": "The best platform to write and share markdown.",
"main": "app.js", "main": "app.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
@@ -14,65 +14,68 @@
"dev": "webpack --config webpack.dev.js --progress --watch", "dev": "webpack --config webpack.dev.js --progress --watch",
"heroku-prebuild": "bin/heroku", "heroku-prebuild": "bin/heroku",
"build": "webpack --config webpack.prod.js --progress", "build": "webpack --config webpack.prod.js --progress",
"build:ot": "bash -c \"cd public/vendor/ot && ./compress.sh\"",
"start": "node app.js" "start": "node app.js"
}, },
"dependencies": { "dependencies": {
"@hedgedoc/meta-marked": "14.1.0",
"@node-saml/passport-saml": "5.1.0", "@node-saml/passport-saml": "5.1.0",
"@passport-next/passport-openid": "1.0.0", "@passport-next/passport-openid": "1.0.0",
"Idle.Js": "git+https://github.com/shawnmclean/Idle.js#commit=2b57cc6e49d177b7ddce0cca00ef5cbe07453541", "Idle.Js": "git+https://github.com/shawnmclean/Idle.js#commit=2b57cc6e49d177b7ddce0cca00ef5cbe07453541",
"archiver": "7.0.1", "archiver": "8.0.0",
"async": "3.2.6", "async": "3.2.6",
"aws-sdk": "2.1692.0", "aws-sdk": "2.1693.0",
"azure-storage": "2.10.7", "azure-storage": "2.10.7",
"base64url": "3.0.1", "base64url": "3.0.1",
"body-parser": "2.2.1", "body-parser": "2.3.0",
"chance": "1.1.13", "chance": "1.1.13",
"cheerio": "0.22.0", "cheerio": "1.2.0",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
"compression": "1.8.1", "compression": "1.8.1",
"connect-flash": "0.1.1", "connect-flash": "0.1.1",
"connect-session-sequelize": "8.0.4", "connect-session-sequelize": "8.0.6",
"constrain-object": "1.1.0",
"cookie": "1.1.1", "cookie": "1.1.1",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"diff-match-patch": "git+https://github.com/hackmdio/diff-match-patch.git#commit=59a9395ad9fe143e601e7ae5765ed943bdd2b11e", "diff-match-patch": "git+https://github.com/hackmdio/diff-match-patch.git#commit=59a9395ad9fe143e601e7ae5765ed943bdd2b11e",
"dompurify": "3.4.14",
"ejs": "3.1.10", "ejs": "3.1.10",
"express": "4.22.1", "express": "4.22.2",
"express-rate-limit": "8.2.1", "express-rate-limit": "8.6.2",
"express-session": "1.18.2", "express-session": "1.19.0",
"file-type": "21.1.1", "file-type": "21.3.4",
"formidable": "2.1.5", "formidable": "3.5.4",
"graceful-fs": "4.2.11", "graceful-fs": "4.2.11",
"helmet": "8.1.0", "helmet": "8.3.0",
"i18n": "0.15.3", "i18n": "0.15.3",
"is-svg": "4.4.0", "is-svg": "6.1.0",
"jsdom-nogyp": "0.8.3", "js-yaml": "4.3.1",
"lodash": "4.17.21", "jsdom": "24.1.3",
"lodash": "4.18.1",
"lutim": "1.0.3", "lutim": "1.0.3",
"lz-string": "git+https://github.com/hackmdio/lz-string.git#commit=6edfccb79cd8c210f03fd3bf18e41ca144fbeefb", "lz-string": "git+https://github.com/hackmdio/lz-string.git#commit=6edfccb79cd8c210f03fd3bf18e41ca144fbeefb",
"mariadb": "3.4.5", "mariadb": "3.5.3",
"markdown-it": "13.0.2", "markdown-it": "14.3.0",
"markdown-it-abbr": "1.0.4", "markdown-it-abbr": "2.0.0",
"markdown-it-container": "3.0.0", "markdown-it-container": "4.0.0",
"markdown-it-deflist": "2.1.0", "markdown-it-deflist": "4.0.0",
"markdown-it-emoji": "2.0.2", "markdown-it-emoji": "3.1.0",
"markdown-it-footnote": "3.0.3", "markdown-it-footnote": "4.0.0",
"markdown-it-imsize": "2.0.1", "markdown-it-imsize": "2.0.1",
"markdown-it-ins": "3.0.1", "markdown-it-ins": "4.0.0",
"markdown-it-mark": "3.0.1", "markdown-it-mark": "4.0.0",
"markdown-it-mathjax": "2.0.0", "markdown-it-mathjax": "2.0.0",
"markdown-it-regexp": "0.4.0", "markdown-it-regexp": "0.4.0",
"markdown-it-sub": "1.0.0", "markdown-it-sub": "2.0.0",
"markdown-it-sup": "1.0.0", "markdown-it-sup": "2.0.0",
"mattermost": "3.4.0", "mattermost": "3.4.0",
"method-override": "3.0.0", "method-override": "3.0.0",
"minimist": "1.2.8", "minimist": "1.2.8",
"minio": "8.0.6", "minio": "8.0.7",
"moment": "2.30.1", "moment": "2.30.1",
"morgan": "1.10.1", "morgan": "1.11.0",
"mysql2": "3.15.3", "mysql2": "3.23.4",
"nanoid": "3.3.11", "nanoid": "3.3.18",
"passport": "patch:passport@npm%3A0.7.0#~/.yarn/patches/passport-npm-0.7.0-df02531736.patch", "passport": "patch:passport@npm%3A0.7.0#~/.yarn/patches/passport-npm-0.7.0-df02531736.patch",
"passport-dropbox-oauth2": "1.1.0", "passport-dropbox-oauth2": "1.1.0",
"passport-facebook": "3.0.0", "passport-facebook": "3.0.0",
@@ -85,23 +88,23 @@
"passport-twitter": "1.0.4", "passport-twitter": "1.0.4",
"passport.socketio": "3.7.0", "passport.socketio": "3.7.0",
"pdfobject": "2.3.1", "pdfobject": "2.3.1",
"pg": "8.16.3", "pg": "8.23.0",
"pg-hstore": "2.3.4", "pg-hstore": "2.3.4",
"prom-client": "15.1.3", "prom-client": "15.1.3",
"prometheus-api-metrics": "4.0.0", "prometheus-api-metrics": "4.0.0",
"readline-sync": "1.4.10", "readline-sync": "1.4.10",
"rimraf": "6.1.2", "rimraf": "6.1.3",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.4",
"scrypt-kdf": "2.0.1", "scrypt-kdf": "3.0.0",
"sequelize": "5.22.5", "sequelize": "5.22.5",
"socket.io": "4.8.1", "socket.io": "4.8.3",
"sqlite3": "5.1.7", "sqlite3": "5.1.7",
"store": "2.0.12", "store": "2.0.12",
"toobusy-js": "0.5.1", "toobusy-js": "0.5.1",
"umzug": "2.3.0", "umzug": "3.8.3",
"uuid": "11.1.0", "uuid": "14.0.2",
"validator": "13.15.23", "validator": "13.15.35",
"winston": "3.18.3", "winston": "3.19.0",
"xss": "1.0.15" "xss": "1.0.15"
}, },
"engines": { "engines": {
@@ -134,10 +137,10 @@
"url": "https://github.com/hedgedoc/hedgedoc.git" "url": "https://github.com/hedgedoc/hedgedoc.git"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "3.3.3", "@eslint/eslintrc": "3.3.6",
"@eslint/js": "9.39.1", "@eslint/js": "9.39.5",
"@hedgedoc/codemirror-5": "5.65.12", "@hedgedoc/codemirror-5": "5.65.12",
"abcjs": "6.5.2", "abcjs": "6.7.0",
"babel-cli": "6.26.0", "babel-cli": "6.26.0",
"babel-core": "6.26.3", "babel-core": "6.26.3",
"babel-loader": "7.1.5", "babel-loader": "7.1.5",
@@ -150,12 +153,12 @@
"copy-webpack-plugin": "6.4.1", "copy-webpack-plugin": "6.4.1",
"css-loader": "5.2.7", "css-loader": "5.2.7",
"emojify.js": "1.1.0", "emojify.js": "1.1.0",
"esbuild-loader": "4.4.0", "esbuild-loader": "4.5.0",
"eslint": "9.39.1", "eslint": "9.39.5",
"eslint-config-standard": "17.1.0", "eslint-config-standard": "17.1.0",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.23.1", "eslint-plugin-n": "17.24.0",
"eslint-plugin-promise": "7.2.1", "eslint-plugin-promise": "7.3.0",
"eslint-plugin-standard": "5.0.0", "eslint-plugin-standard": "5.0.0",
"exports-loader": "1.1.1", "exports-loader": "1.1.1",
"expose-loader": "1.0.3", "expose-loader": "1.0.3",
@@ -163,50 +166,49 @@
"file-saver": "2.0.5", "file-saver": "2.0.5",
"flowchart.js": "1.18.0", "flowchart.js": "1.18.0",
"fork-awesome": "1.2.0", "fork-awesome": "1.2.0",
"globals": "16.5.0", "globals": "17.11.0",
"highlight.js": "10.7.3", "highlight.js": "11.12.0",
"html-webpack-plugin": "4.5.2", "html-webpack-plugin": "4.5.2",
"imports-loader": "1.2.0", "imports-loader": "1.2.0",
"ionicons": "2.0.1", "ionicons": "2.0.1",
"jquery": "3.7.1", "jquery": "3.7.1",
"jquery-mousewheel": "3.2.2", "jquery-mousewheel": "3.2.2",
"jquery-ui": "1.14.1", "jquery-ui": "1.14.2",
"js-cookie": "3.0.5", "js-cookie": "3.0.8",
"js-sequence-diagrams": "git+https://github.com/hedgedoc/js-sequence-diagrams.git", "js-sequence-diagrams": "git+https://github.com/hedgedoc/js-sequence-diagrams.git",
"js-yaml": "3.14.2",
"jsonlint": "1.6.3", "jsonlint": "1.6.3",
"keymaster": "1.6.2", "keymaster": "1.6.2",
"less": "4.4.2", "less": "4.9.0",
"less-loader": "7.3.0", "less-loader": "7.3.0",
"list.js": "2.3.1", "list.js": "2.3.1",
"mathjax": "2.7.9", "mathjax": "2.7.9",
"mermaid": "9.1.7", "mermaid": "9.1.7",
"mini-css-extract-plugin": "1.6.2", "mini-css-extract-plugin": "1.6.2",
"mocha": "11.7.5", "mocha": "11.8.0",
"mock-require": "3.0.3", "mock-require": "3.0.3",
"optimize-css-assets-webpack-plugin": "6.0.1", "optimize-css-assets-webpack-plugin": "6.0.1",
"prismjs": "1.30.0", "prismjs": "1.30.0",
"raphael": "2.3.0", "raphael": "2.3.0",
"remark-cli": "12.0.1", "remark-cli": "12.0.1",
"remark-preset-lint-markdown-style-guide": "5.1.3", "remark-preset-lint-markdown-style-guide": "6.0.1",
"reveal.js": "3.9.2", "reveal.js": "3.9.2",
"select2": "3.5.2-browserify", "select2": "3.5.2-browserify",
"socket.io-client": "4.8.1", "socket.io-client": "4.8.3",
"spin.js": "4.1.2", "spin.js": "4.1.2",
"string-loader": "0.0.1", "string-loader": "0.0.1",
"turndown": "7.2.2", "turndown": "7.2.4",
"uglify-js": "3.19.3",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"velocity-animate": "1.5.2", "velocity-animate": "1.5.2",
"visibilityjs": "2.0.2", "visibilityjs": "2.0.2",
"viz.js": "1.8.2", "viz.js": "1.8.2",
"webpack": "4.47.0", "webpack": "4.47.0",
"webpack-cli": "4.10.0", "webpack-cli": "4.10.0",
"webpack-merge": "5.10.0", "webpack-merge": "5.10.0"
"wurl": "2.5.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"bufferutil": "4.0.9", "bufferutil": "4.1.0",
"utf-8-validate": "6.0.5" "utf-8-validate": "6.0.6"
}, },
"packageManager": "yarn@4.12.0" "packageManager": "yarn@4.13.0"
} }

View File

@@ -0,0 +1,3 @@
pre code.hljs {
padding: 0;
}

View File

@@ -44,10 +44,6 @@
overflow: hidden; overflow: hidden;
} }
.markdown-body.slides section[data-markdown] > ul {
display: inline-block;
}
.markdown-body.slides > section > section + section::after { .markdown-body.slides > section > section + section::after {
content: ''; content: '';
position: absolute; position: absolute;
@@ -61,7 +57,8 @@
display: none; display: none;
} }
.markdown-body.slides ul, .markdown-body.slides ol { .markdown-body.slides section[data-markdown] > div:first-child > ul,
.markdown-body.slides section[data-markdown] > div:first-child > ol {
display: inline-block; display: inline-block;
text-align: left; text-align: left;
margin: 0 0 0 1em; margin: 0 0 0 1em;

View File

@@ -30,9 +30,9 @@ Or meet us on [**Matrix**](https://chat.hedgedoc.org) for dev-talk and interacti
### Night Mode ### Night Mode
When you are tired of a white screen and like a night mode, click on the little moon <i class="fa fa-moon-o"></i> and turn on the night view of HedgeDoc. When you are tired of a white screen and like a night mode, click on the little moon <i class="fa fa-moon-o"></i> in the header bar and turn on the night view of HedgeDoc.
The editor view, which is in night mode by default, can also be toggled between night and day view using the the little sun<i class="fa fa-sun-o fa-fw"></i>. The editor view, which is in night mode by default, can also be toggled between night and day view using the the little sun<i class="fa fa-sun-o fa-fw"></i>. It is below the editor in the bottom bar.
### Image Upload ### Image Upload
@@ -58,7 +58,7 @@ Similarly to the *save* feature, you can also import a Markdown file from **Drop
### Permissions ### Permissions
It is possible to change the access permission of a note through the little button on the top right of the view. It is possible to change the access permission of a note through the little button on the top right of the view.
There are four possible options: There are six possible options:
| | Owner read/write | Signed-in read | Signed-in write | Guest read | Guest write | | | Owner read/write | Signed-in read | Signed-in write | Guest read | Guest write |
|:------------------------------------------------------------------------------------ |:----------------:|:--------------:|:---------------:|:----------:|:-----------:| |:------------------------------------------------------------------------------------ |:----------------:|:--------------:|:---------------:|:----------:|:-----------:|

View File

@@ -1,5 +1,117 @@
# Release Notes # Release Notes
## <i class="fa fa-tag"></i> 1.12.0 <i class="fa fa-calendar-o"></i> 2026-08-21
### Important changes
⚠️ **HedgeDoc 1.12.0 and onwards will require Node 20.17 or later in order to run.**
Since Node 18 security support was stopped over one year ago, we don't maintain backwards-compatability for that anymore.
Some syntax highlighting languages have been removed and/or changed. You can find more information in
[the highlight.js docs](https://highlightjs.readthedocs.io/en/latest/upgrade-11.html#built-in-set-of-common-languages).
### Enhancements
- Automatically skip the login modal if no form-based login and exactly one external login provider is configured
### Bugfixes
- All links will be opened in new tabs instead of the same tab as the note. This was changed when the external link warning was implemented and now the old behavior is restored
- Fixed multiple bugs in the realtime connection handling that could lead to data-loss of users with a flaky connection
### Contributors
- [Tony Chau](https://github.com/fo5for)
- [Lars Kiesow](https://github.com/lkiesow)
## <i class="fa fa-tag"></i> 1.11.1 <i class="fa fa-calendar-o"></i> 2026-07-24
### Security fixes
- [GHSA-93w7-49m2-cqwg](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-93w7-49m2-cqwg) reports possible corruption of permission values due to missing validation. This should not impact permission checks, except when the note owner intentionally set their note permission to something invalid.
### Enhancements
- Added external link warning setting (`externalLinkWarning` in config.json or `CMD_EXTERNAL_LINK_WARNING`) to disable the external link warning page entirely
- Added external link whitelist setting (`externalLinkWhitelist` in config.json or `CMD_EXTERNAL_LINK_WHITELIST`) to skip warning page for certain domains
- Added support for webp file uploads (for all backends except `imgur`, since that does not support these)
### Bugfixes
- Fixed external link warning for subpath instances
- Restore native browser zoom-in keyboard shortcuts in the editor
- Nested list items render properly again in the preview pane for slides
### Refactoring / Clean-up
- Removed the old Temp database object and related API endpoints. This was used by a very old way of migrating the history and is no longer needed.
- Removed unused `allowOrigin` (environment variable `CMD_ALLOW_ORIGIN`) config option.
- Removed unnecessary/duplicate entries in the XSS filtering whitelist
### Contributors
- [Shivam Shukla](https://github.com/Shivam-Shukla0)
- [Tobi](https://github.com/tldev-de)
## <i class="fa fa-tag"></i> 1.11.0 <i class="fa fa-calendar-o"></i> 2026-06-18
### Security fixes
This release contains four security fixes:
- [CVE-2026-58487](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-6c2w-8w96-3pcv) reports a possible HTML injection via the localpart of an email address.
- [CVE-2026-58486](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-qj78-mjch-wwrv) reports a possible Denial-of-Service attack using the YAML frontmatter parsing.
- [CVE-2026-58489](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-8v9p-5j95-826j) reports a possible CSRF attack vector in the GitHub Gist export.
- [CVE-2026-58488](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-2f9f-w8xq-276v) reports a rate-limiting bypass by abusing the CF-Connecting-IP header.
Thanks to [Chandler Johnson](https://github.com/chndlrx), [taylorodell](https://github.com/taylorodell) and [alanturing881](https://github.com/alanturing881) for reporting!
### Important notices
- When using Cloudflare in front of HedgeDoc, you should set `rateLimitUsingCloudflare` in the config.json or `CMD_RATE_LIMIT_USING_CLOUDFLARE` as environment variable to `true`.
### Enhancements
- Added a warning page when clicking external links
- Improve the config.json.example file, which is used by `bin/setup`
- Allow configuration of login / signup rate-limits
- Allow configuration of Cloudflare usage in regards of rate-limits
- Several improvements in the documentation at <https://docs.hedgedoc.org>
## <i class="fa fa-tag"></i> 1.10.8 <i class="fa fa-calendar-o"></i> 2026-04-15
### Bugfixes
- Fix data loss when 5+ users edit a document concurrently, caused by the OT client discarding operations during revision gap recovery ([#6342](https://github.com/hedgedoc/hedgedoc/issues/6342))
- Add defensive null checks to `hex2rgb` to prevent crashes from non-hex color values
### Contributors
- [Dex Devlon](https://github.com/bxff)
## <i class="fa fa-tag"></i> 1.10.7 <i class="fa fa-calendar-o"></i> 2026-02-24
### Bugfixes
- Random colors for user's cursors and selections are now always in hex format to avoid conversion errors
- Correctly close realtime connections if they disconnect during connection creation
- manage_users CLI does not silently drop errors
## <i class="fa fa-tag"></i> 1.10.6 <i class="fa fa-calendar-o"></i> 2026-02-04
### Security fixes
This release contains two medium severity security fixes:
- [GHSA-x74j-jmf9-534w](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-x74j-jmf9-534w) reports a bug where security headers for upload files were not set correctly.
- [GHSA-672m-p72w-gw28](https://github.com/hedgedoc/hedgedoc/security/advisories/GHSA-672m-p72w-gw28) reports potential security issues with limited script execution in uploaded SVG files.
Thanks to [@HUSEYNKHANLI](https://github.com/HUSEYNKHANLI) and [@drkim-dev](https://github.com/drkim-dev) for reporting!
### Maintenance
- Dependency updates
- Enhancements in the documentation at [docs.hedgedoc.org](https://docs.hedgedoc.org)
### Contributors
- [xenein](https://github.com/xenein) (#6322)
## <i class="fa fa-tag"></i> 1.10.5 <i class="fa fa-calendar-o"></i> 2025-12-06 ## <i class="fa fa-tag"></i> 1.10.5 <i class="fa fa-calendar-o"></i> 2025-12-06
This release is just a fix for the docker container. It does not contain any This release is just a fix for the docker container. It does not contain any

View File

@@ -61,7 +61,6 @@ const options = {
} }
const historyList = new List('history', options) const historyList = new List('history', options)
window.migrateHistoryFromTempCallback = pageInit
setloginStateChangeEvent(pageInit) setloginStateChangeEvent(pageInit)
pageInit() pageInit()

View File

@@ -11,9 +11,17 @@ import { escapeHtml, unescapeHtml } from './utils'
import markdownit from 'markdown-it' import markdownit from 'markdown-it'
import markdownitContainer from 'markdown-it-container' import markdownitContainer from 'markdown-it-container'
import MarkdownItRegexpPlugin from 'markdown-it-regexp'
/* Defined regex markdown it plugins */ import MarkdownItAbbrPlugin from 'markdown-it-abbr'
import Plugin from 'markdown-it-regexp' import MarkdownItFootnotePlugin from 'markdown-it-footnote'
import MarkdownItDeflistPlugin from 'markdown-it-deflist'
import MarkdownItMarkPlugin from 'markdown-it-mark'
import MarkdownItInsPlugin from 'markdown-it-ins'
import MarkdownItSubPlugin from 'markdown-it-sub'
import MarkdownItSupPlugin from 'markdown-it-sup'
import MarkdownItMathjaxPlugin from 'markdown-it-mathjax'
import { full as MarkdownItEmojiPlugin } from 'markdown-it-emoji'
import MarkdownItImsizePlugin from 'markdown-it-imsize'
require('prismjs/themes/prism.css') require('prismjs/themes/prism.css')
require('prismjs/components/prism-wiki') require('prismjs/components/prism-wiki')
@@ -517,7 +525,9 @@ export function finishView (view) {
if (!languages.includes(reallang)) { if (!languages.includes(reallang)) {
result = hljs.highlightAuto(code) result = hljs.highlightAuto(code)
} else { } else {
result = hljs.highlight(reallang, code) result = hljs.highlight(code, {
language: reallang
})
} }
if (codeDiv.length > 0) codeDiv.html(result.value) if (codeDiv.length > 0) codeDiv.html(result.value)
else langDiv.html(result.value) else langDiv.html(result.value)
@@ -557,10 +567,6 @@ export function postProcess (code) {
html = html.replace(/@import url\(([^)]*)\);?/gi, '') html = html.replace(/@import url\(([^)]*)\);?/gi, '')
$(value).html(html) $(value).html(html)
}) })
// link should open in new window or tab
// also add noopener to prevent clickjacking
// See details: https://mathiasbynens.github.io/rel-noopener/
result.find('a:not([href^="#"]):not([target])').attr('target', '_blank').attr('rel', 'noopener')
// If it's hashtag link then make it base uri independent // If it's hashtag link then make it base uri independent
result.find('a[href^="#"]').each((index, linkTag) => { result.find('a[href^="#"]').each((index, linkTag) => {
@@ -568,6 +574,7 @@ export function postProcess (code) {
currentLocation.hash = linkTag.hash currentLocation.hash = linkTag.hash
linkTag.href = currentLocation.toString() linkTag.href = currentLocation.toString()
}) })
rewriteExternalLinks(result)
// update continue line numbers // update continue line numbers
const linenumberdivs = result.find('.gutter.linenumber').toArray() const linenumberdivs = result.find('.gutter.linenumber').toArray()
@@ -593,6 +600,54 @@ export function postProcess (code) {
} }
window.postProcess = postProcess window.postProcess = postProcess
// rewrite external links to go through the /_link warning page
export function rewriteExternalLinks (view) {
if (window.externalLinkWarning === false) {
view.find('a:not([href^="#"]):not([target])').attr('target', '_blank').attr('rel', 'noopener')
return
}
const whitelist = window.externalLinkWhitelist || []
view.find('a[href]').each((index, linkTag) => {
const anchor = $(linkTag)
const href = anchor.attr('href')
if (!href || href.startsWith('#')) {
return
}
let parsed
try {
parsed = new URL(href)
} catch (err) {
return
}
// only rewrite links that have an absolute http(s) URL on a different origin
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin === window.location.origin) {
return
}
// skip rewriting for whitelisted domains while differentiating between wildcard entries and plain host entries
const hostname = parsed.hostname.toLowerCase()
if (whitelist.some(domain => {
const lowercaseDomain = domain.toLowerCase()
if (lowercaseDomain.startsWith('*.')) {
return hostname.endsWith('.' + lowercaseDomain.slice(2))
}
return hostname === lowercaseDomain
})) {
if (!anchor.attr('target')) {
anchor.attr('target', '_blank').attr('rel', 'noopener')
}
return
}
const urlPath = window.urlpath ? `/${window.urlpath}` : ''
const warningURL = `${window.location.origin}${urlPath}/_link?url=${encodeURIComponent(parsed.href)}`
anchor.attr('href', warningURL)
anchor.on('click', event => {
event.preventDefault()
window.open(warningURL, '_blank')
})
})
}
window.rewriteExternalLinks = rewriteExternalLinks
const domevents = Object.getOwnPropertyNames(document).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(document)))).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(window))).filter(function (i) { const domevents = Object.getOwnPropertyNames(document).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(document)))).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(window))).filter(function (i) {
return !i.indexOf('on') && (document[i] === null || typeof document[i] === 'function') return !i.indexOf('on') && (document[i] === null || typeof document[i] === 'function')
}).filter(function (elem, pos, self) { }).filter(function (elem, pos, self) {
@@ -1014,14 +1069,14 @@ export const md = markdownit('default', {
}) })
window.md = md window.md = md
md.use(require('markdown-it-abbr')) md.use(MarkdownItAbbrPlugin)
md.use(require('markdown-it-footnote')) md.use(MarkdownItFootnotePlugin)
md.use(require('markdown-it-deflist')) md.use(MarkdownItDeflistPlugin)
md.use(require('markdown-it-mark')) md.use(MarkdownItMarkPlugin)
md.use(require('markdown-it-ins')) md.use(MarkdownItInsPlugin)
md.use(require('markdown-it-sub')) md.use(MarkdownItSubPlugin)
md.use(require('markdown-it-sup')) md.use(MarkdownItSupPlugin)
md.use(require('markdown-it-mathjax')({ md.use(MarkdownItMathjaxPlugin({
beforeMath: '<span class="mathjax raw">', beforeMath: '<span class="mathjax raw">',
afterMath: '</span>', afterMath: '</span>',
beforeInlineMath: '<span class="mathjax raw">\\(', beforeInlineMath: '<span class="mathjax raw">\\(',
@@ -1029,9 +1084,9 @@ md.use(require('markdown-it-mathjax')({
beforeDisplayMath: '<span class="mathjax raw">\\[', beforeDisplayMath: '<span class="mathjax raw">\\[',
afterDisplayMath: '\\]</span>' afterDisplayMath: '\\]</span>'
})) }))
md.use(require('markdown-it-imsize')) md.use(MarkdownItImsizePlugin)
md.use(require('markdown-it-emoji'), { md.use(MarkdownItEmojiPlugin, {
shortcuts: {} shortcuts: {}
}) })
@@ -1102,7 +1157,7 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
} }
// youtube // youtube
const youtubePlugin = new Plugin( const youtubePlugin = new MarkdownItRegexpPlugin(
// regexp to match // regexp to match
/{%youtube\s*([\w-]{11})\s*%}/, /{%youtube\s*([\w-]{11})\s*%}/,
@@ -1120,7 +1175,7 @@ const youtubePlugin = new Plugin(
} }
) )
// vimeo // vimeo
const vimeoPlugin = new Plugin( const vimeoPlugin = new MarkdownItRegexpPlugin(
// regexp to match // regexp to match
/{%vimeo\s*(\d{6,11})\s*%}/, /{%vimeo\s*(\d{6,11})\s*%}/,
@@ -1135,7 +1190,7 @@ const vimeoPlugin = new Plugin(
} }
) )
// gist // gist
const gistPlugin = new Plugin( const gistPlugin = new MarkdownItRegexpPlugin(
// regexp to match // regexp to match
/{%gist\s*(\w+\/\w+)\s*%}/, /{%gist\s*(\w+\/\w+)\s*%}/,
@@ -1145,14 +1200,14 @@ const gistPlugin = new Plugin(
} }
) )
// TOC // TOC
const tocPlugin = new Plugin( const tocPlugin = new MarkdownItRegexpPlugin(
// regexp to match // regexp to match
/^\[TOC\]$/i, /^\[TOC\]$/i,
(match, utils) => '<div class="toc"></div>' (match, utils) => '<div class="toc"></div>'
) )
// slideshare // slideshare
const slidesharePlugin = new Plugin( const slidesharePlugin = new MarkdownItRegexpPlugin(
// regexp to match // regexp to match
/{%slideshare\s*(\w+\/[\w-]+)\s*%}/, /{%slideshare\s*(\w+\/[\w-]+)\s*%}/,
@@ -1164,7 +1219,7 @@ const slidesharePlugin = new Plugin(
} }
) )
// speakerdeck // speakerdeck
const speakerdeckPlugin = new Plugin( const speakerdeckPlugin = new MarkdownItRegexpPlugin(
// regexp to match // regexp to match
/{%speakerdeck\s*(\w+\/[\w-]+)\s*%}/, /{%speakerdeck\s*(\w+\/[\w-]+)\s*%}/,
@@ -1176,7 +1231,7 @@ const speakerdeckPlugin = new Plugin(
} }
) )
// pdf // pdf
const pdfPlugin = new Plugin( const pdfPlugin = new MarkdownItRegexpPlugin(
// regexp to match // regexp to match
/{%pdf\s*([\d\D]*?)\s*%}/, /{%pdf\s*([\d\D]*?)\s*%}/,
@@ -1189,7 +1244,7 @@ const pdfPlugin = new Plugin(
} }
) )
const emojijsPlugin = new Plugin( const emojijsPlugin = new MarkdownItRegexpPlugin(
// regexp to match emoji shortcodes :something: // regexp to match emoji shortcodes :something:
// We generate an universal regex that guaranteed only contains the // We generate an universal regex that guaranteed only contains the
// emojies we have available. This should prevent all false-positives // emojies we have available. This should prevent all false-positives
@@ -1226,7 +1281,11 @@ function meta (state, start, end, silent) {
if (line >= end) return false if (line >= end) return false
try { try {
md.meta = window.jsyaml.safeLoad(data.join('\n')) || {} const parsed = window.jsyaml.load(data.join('\n')) || {}
md.meta = window.ConstrainObject.constrainObject(parsed, {
onCycle: 'omit',
maxDepth: 4
})
delete md.metaError delete md.metaError
} catch (err) { } catch (err) {
md.metaError = err md.metaError = err

View File

@@ -3,7 +3,6 @@
import store from 'store' import store from 'store'
import LZString from 'lz-string' import LZString from 'lz-string'
import url from 'wurl'
import { import {
checkNoteIdValid, checkNoteIdValid,
@@ -19,40 +18,6 @@ import {
urlpath urlpath
} from './lib/config' } from './lib/config'
window.migrateHistoryFromTempCallback = null
migrateHistoryFromTemp()
function migrateHistoryFromTemp () {
if (url('#tempid')) {
$.get(`${serverurl}/temp`, {
tempid: url('#tempid')
})
.done(data => {
if (data && data.temp) {
getStorageHistory(olddata => {
if (!olddata || olddata.length === 0) {
saveHistoryToStorage(JSON.parse(data.temp))
}
})
}
})
.always(() => {
let hash = location.hash.split('#')[1]
hash = hash.split('&')
for (let i = 0; i < hash.length; i++) {
if (hash[i].indexOf('tempid') === 0) {
hash.splice(i, 1)
i--
}
}
hash = hash.join('&')
location.hash = hash
if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() }
})
}
}
export function saveHistory (notehistory) { export function saveHistory (notehistory) {
checkIfAuth( checkIfAuth(
() => { () => {

View File

@@ -10,7 +10,8 @@ require('prismjs/components/prism-typescript')
require('prismjs/components/prism-jsx') require('prismjs/components/prism-jsx')
require('prismjs/components/prism-makefile') require('prismjs/components/prism-makefile')
require('prismjs/components/prism-gherkin') require('prismjs/components/prism-gherkin')
require('highlight.js/styles/github-gist.css') require('highlight.js/styles/github.css')
require('../css/highlightjs.css')
require('emojify.js/dist/css/basic/emojify.min.css') require('emojify.js/dist/css/basic/emojify.min.css')
require('../css/github-extract.css') require('../css/github-extract.css')
require('../css/markdown.css') require('../css/markdown.css')

View File

@@ -18,7 +18,6 @@ import hex2rgb from '../vendor/ot/hex2rgb'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import chance from 'chance' import chance from 'chance'
import store from 'store' import store from 'store'
import url from 'wurl'
import { Spinner } from 'spin.js' import { Spinner } from 'spin.js'
import _ from 'lodash' import _ from 'lodash'
@@ -55,6 +54,7 @@ import {
renderTOC, renderTOC,
renderTags, renderTags,
renderTitle, renderTitle,
rewriteExternalLinks,
scrollToHash, scrollToHash,
smoothHashScroll, smoothHashScroll,
updateLastChange, updateLastChange,
@@ -92,7 +92,8 @@ require('../css/extra.css')
require('../css/slide-preview.css') require('../css/slide-preview.css')
require('../css/site.css') require('../css/site.css')
require('highlight.js/styles/github-gist.css') require('highlight.js/styles/github.css')
require('../css/highlightjs.css')
require('./fix-aria-hidden-for-modals') require('./fix-aria-hidden-for-modals')
let defaultTextHeight = 20 let defaultTextHeight = 20
@@ -427,7 +428,7 @@ const supportExtraTags = [
text: '[random color tag]', text: '[random color tag]',
search: '[]', search: '[]',
command: function () { command: function () {
const color = chance().color() const color = chance().color({ format: 'hex' })
return '[color=' + color + ']' return '[color=' + color + ']'
} }
} }
@@ -1206,6 +1207,23 @@ ui.toolbar.publish.attr('href', noteurl + '/publish')
// extra // extra
// slide // slide
ui.toolbar.extra.slide.attr('href', noteurl + '/slide') ui.toolbar.extra.slide.attr('href', noteurl + '/slide')
// invite agent
ui.toolbar.extra.inviteAgent.click(function (e) {
e.preventDefault()
$.ajax({
url: 'https://hedgeagent.sa6anw.se/add_note',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ note_id: noteid }),
error: function (xhr, status, err) {
console.error('Invite agent failed:', status, err)
alert('Failed to invite agent.')
}
})
})
// download // download
// markdown // markdown
ui.toolbar.download.markdown.click(function (e) { ui.toolbar.download.markdown.click(function (e) {
@@ -1797,7 +1815,7 @@ $('#gistImportModalConfirm').click(function () {
false false
) )
} else { } else {
const hostname = url('hostname', gisturl) const hostname = new URL(gisturl).hostname
if (hostname !== 'gist.github.com') { if (hostname !== 'gist.github.com') {
showMessageModal( showMessageModal(
'<i class="fa fa-github"></i> Import from Gist', '<i class="fa fa-github"></i> Import from Gist',
@@ -1808,7 +1826,8 @@ $('#gistImportModalConfirm').click(function () {
) )
} else { } else {
ui.spinner.show() ui.spinner.show()
$.get('https://api.github.com/gists/' + url('-1', gisturl)) const gistId = new URL(gisturl).pathname.split('/').pop()
$.get('https://api.github.com/gists/' + gistId)
.done(function (data) { .done(function (data) {
if (data.files) { if (data.files) {
let contents = '' let contents = ''
@@ -2232,7 +2251,6 @@ socket.on('delete', function () {
}) })
} }
}) })
let retryTimer = null
socket.on('maintenance', function () { socket.on('maintenance', function () {
cmClient.revision = -1 cmClient.revision = -1
}) })
@@ -2245,21 +2263,17 @@ socket.on('disconnect', function (data) {
if (!editor.getOption('readOnly')) { if (!editor.getOption('readOnly')) {
editor.setOption('readOnly', true) editor.setOption('readOnly', true)
} }
if (!retryTimer) {
retryTimer = setInterval(function () {
if (!needRefresh) socket.connect()
}, 1000)
}
}) })
socket.on('reconnect', function (data) { socket.on('connect', function (data) {
if (socket.recovered) {
// sync back any change in offline // sync back any change in offline
console.debug('Reconnected client')
emitUserStatus(true) emitUserStatus(true)
cursorActivity(editor) cursorActivity(editor)
socket.emit('online users') socket.emit('online users')
}) return
socket.on('connect', function (data) { }
clearInterval(retryTimer) console.debug('Client connected (no reconnect)')
retryTimer = null
personalInfo.id = socket.id personalInfo.id = socket.id
showStatus(statusType.connected) showStatus(statusType.connected)
socket.emit('version') socket.emit('version')
@@ -3498,6 +3512,7 @@ function updateViewInner () {
} }
removeDOMEvents(ui.area.markdown) removeDOMEvents(ui.area.markdown)
finishView(ui.area.markdown) finishView(ui.area.markdown)
rewriteExternalLinks(ui.area.markdown)
autoLinkify(ui.area.markdown) autoLinkify(ui.area.markdown)
deduplicatedHeaderId(ui.area.markdown) deduplicatedHeaderId(ui.area.markdown)
renderTOC(ui.area.markdown) renderTOC(ui.area.markdown)

View File

@@ -8,6 +8,9 @@ window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %>
window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>' window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'
window.externalLinkWarning = <%- JSON.stringify(externalLinkWarning) %>
window.externalLinkWhitelist = <%- JSON.stringify(externalLinkWhitelist) %>
window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>' window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>'
window.cookiePolicy = '<%- cookiePolicy %>' window.cookiePolicy = '<%- cookiePolicy %>'

View File

@@ -197,15 +197,6 @@ export default class Editor {
'Shift-Ctrl-6': cm => { 'Shift-Ctrl-6': cm => {
utils.wrapTextWith(this.editor, cm, '^') utils.wrapTextWith(this.editor, cm, '^')
}, },
'Ctrl-+': cm => {
utils.wrapTextWith(this.editor, cm, '+')
},
'Shift-Ctrl-=': cm => {
utils.wrapTextWith(this.editor, cm, '+')
},
'Ctrl-=': cm => {
utils.wrapTextWith(this.editor, cm, '=')
},
'Shift-Ctrl-Backspace': cm => { 'Shift-Ctrl-Backspace': cm => {
utils.wrapTextWith(this.editor, cm, 'Backspace') utils.wrapTextWith(this.editor, cm, 'Backspace')
} }

View File

@@ -12,7 +12,8 @@ export const getUIElements = () => ({
publish: $('.ui-publish'), publish: $('.ui-publish'),
extra: { extra: {
revision: $('.ui-extra-revision'), revision: $('.ui-extra-revision'),
slide: $('.ui-extra-slide') slide: $('.ui-extra-slide'),
inviteAgent: $('.ui-invite-agent')
}, },
download: { download: {
markdown: $('.ui-download-markdown'), markdown: $('.ui-download-markdown'),

View File

@@ -21,7 +21,8 @@ require('../css/extra.css')
require('../css/slide-preview.css') require('../css/slide-preview.css')
require('../css/site.css') require('../css/site.css')
require('highlight.js/styles/github-gist.css') require('highlight.js/styles/github.css')
require('../css/highlightjs.css')
const markdown = $('#doc.markdown-body') const markdown = $('#doc.markdown-body')
const text = markdown.text() const text = markdown.text()

View File

@@ -18,22 +18,14 @@ whiteList.ol = ['start']
whiteList.li = ['value'] whiteList.li = ['value']
// allow style tag // allow style tag
whiteList.style = [] whiteList.style = []
// allow kbd tag
whiteList.kbd = []
// allow ifram tag with some safe attributes // allow ifram tag with some safe attributes
whiteList.iframe = ['allowfullscreen', 'name', 'referrerpolicy', 'src', 'width', 'height'] whiteList.iframe = ['allowfullscreen', 'name', 'referrerpolicy', 'src', 'width', 'height']
// allow summary tag
whiteList.summary = []
// allow ruby tag // allow ruby tag
whiteList.ruby = [] whiteList.ruby = []
// allow rp tag for ruby // allow rp tag for ruby
whiteList.rp = [] whiteList.rp = []
// allow rt tag for ruby // allow rt tag for ruby
whiteList.rt = [] whiteList.rt = []
// allow figure tag
whiteList.figure = []
// allow figcaption tag
whiteList.figcaption = []
const filterXSSOptions = { const filterXSSOptions = {
allowCommentTag: true, allowCommentTag: true,

View File

@@ -219,19 +219,26 @@ ot.Client = (function (global) {
this.acknowlaged = acknowlaged; this.acknowlaged = acknowlaged;
this.client = client; this.client = client;
this.revision = revision; this.revision = revision;
this.pendingOps = [];
} }
Client.Stale = Stale; Client.Stale = Stale;
Stale.prototype.applyClient = function (client, operation) { Stale.prototype.applyClient = function (client, operation) {
return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision); var swb = new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
swb.pendingOps = this.pendingOps;
return swb;
}; };
Stale.prototype.applyServer = function (client, revision, operation) { Stale.prototype.applyServer = function (client, revision, operation) {
throw new Error("Ignored server-side change."); // Buffer incoming operations while waiting for the missing operations
// response. These will be applied sequentially after the missing ops.
this.pendingOps.push({ revision: revision, operation: operation });
return this;
}; };
Stale.prototype.applyOperations = function (client, head, operations) { Stale.prototype.applyOperations = function (client, head, operations) {
var transform = this.acknowlaged.constructor.transform; var transform = this.acknowlaged.constructor.transform;
// Apply the requested missing operations (concurrent with acknowlaged)
for (var i = 0; i < operations.length; i++) { for (var i = 0; i < operations.length; i++) {
var op = ot.TextOperation.fromJSON(operations[i]); var op = ot.TextOperation.fromJSON(operations[i]);
var pair = transform(this.acknowlaged, op); var pair = transform(this.acknowlaged, op);
@@ -239,6 +246,14 @@ ot.Client = (function (global) {
this.acknowlaged = pair[0]; this.acknowlaged = pair[0];
} }
client.revision = this.revision; client.revision = this.revision;
// Apply any operations that arrived while we were waiting for the
// missing operations. These are sequential (not concurrent), so they
// can be applied directly like Synchronized.applyServer would.
for (var j = 0; j < this.pendingOps.length; j++) {
var pending = this.pendingOps[j];
client.revision = pending.revision;
client.applyOperation(pending.operation);
}
return synchronized_; return synchronized_;
}; };
@@ -261,20 +276,27 @@ ot.Client = (function (global) {
this.buffer = buffer; this.buffer = buffer;
this.client = client; this.client = client;
this.revision = revision; this.revision = revision;
this.pendingOps = [];
} }
Client.StaleWithBuffer = StaleWithBuffer; Client.StaleWithBuffer = StaleWithBuffer;
StaleWithBuffer.prototype.applyClient = function (client, operation) { StaleWithBuffer.prototype.applyClient = function (client, operation) {
var buffer = this.buffer.compose(operation); var buffer = this.buffer.compose(operation);
return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision); var swb = new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
swb.pendingOps = this.pendingOps;
return swb;
}; };
StaleWithBuffer.prototype.applyServer = function (client, revision, operation) { StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
throw new Error("Ignored server-side change."); // Buffer incoming operations while waiting for the missing operations
// response. These will be applied sequentially after the missing ops.
this.pendingOps.push({ revision: revision, operation: operation });
return this;
}; };
StaleWithBuffer.prototype.applyOperations = function (client, head, operations) { StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
var transform = this.acknowlaged.constructor.transform; var transform = this.acknowlaged.constructor.transform;
// Apply the requested missing operations (concurrent with acknowlaged and buffer)
for (var i = 0; i < operations.length; i++) { for (var i = 0; i < operations.length; i++) {
var op = ot.TextOperation.fromJSON(operations[i]); var op = ot.TextOperation.fromJSON(operations[i]);
var pair1 = transform(this.acknowlaged, op); var pair1 = transform(this.acknowlaged, op);
@@ -284,6 +306,16 @@ ot.Client = (function (global) {
this.buffer = pair2[0]; this.buffer = pair2[0];
} }
client.revision = this.revision; client.revision = this.revision;
// Apply any operations that arrived while we were waiting for the
// missing operations. These are sequential (not concurrent), so we
// transform the buffer against them and apply them to the document.
for (var j = 0; j < this.pendingOps.length; j++) {
var pending = this.pendingOps[j];
var pair3 = transform(this.buffer, pending.operation);
client.applyOperation(pair3[1]);
this.buffer = pair3[0];
client.revision = pending.revision;
}
client.sendOperation(client.revision, this.buffer); client.sendOperation(client.revision, this.buffer);
return new AwaitingConfirm(this.buffer); return new AwaitingConfirm(this.buffer);
}; };

3
public/vendor/ot/compress.sh vendored Normal file → Executable file
View File

@@ -1,4 +1,4 @@
uglifyjs --compress --mangle --output ot.min.js \ yarn uglifyjs --compress --mangle --output ot.min.js \
./text-operation.js \ ./text-operation.js \
./selection.js \ ./selection.js \
./wrapped-operation.js \ ./wrapped-operation.js \
@@ -8,3 +8,4 @@ uglifyjs --compress --mangle --output ot.min.js \
./socketio-adapter.js \ ./socketio-adapter.js \
./ajax-adapter.js \ ./ajax-adapter.js \
./editor-client.js ./editor-client.js
echo "Built new ot.min.js"

View File

@@ -1,12 +1,19 @@
const defaultColor = { red: 0, green: 0, blue: 0 }
function hex2rgb (hex) { function hex2rgb (hex) {
if (!hex || typeof hex !== 'string') return defaultColor
if (hex[0] == '#') hex = hex.substr(1) if (hex[0] == '#') hex = hex.substr(1)
if (hex.length == 3) { if (hex.length == 3) {
var temp = hex var temp = hex
hex = '' hex = ''
temp = /^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(temp).slice(1) var match = /^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(temp)
if (!match) return defaultColor
temp = match.slice(1)
for (var i = 0; i < 3; i++) hex += temp[i] + temp[i] for (var i = 0; i < 3; i++) hex += temp[i] + temp[i]
} }
var triplets = /^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex).slice(1) var result = /^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex)
if (!result) return defaultColor
var triplets = result.slice(1)
return { return {
red: parseInt(triplets[0], 16), red: parseInt(triplets[0], 16),
green: parseInt(triplets[1], 16), green: parseInt(triplets[1], 16),

File diff suppressed because one or more lines are too long

View File

@@ -6,9 +6,9 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<%- include('../includes/favicon') %> <%- include('../includes/favicon') %>
<% for (var og in opengraph) { %> <% for (var og in opengraph) { %>
<% if (opengraph.hasOwnProperty(og) && String(opengraph[og]).trim() !== '') { %> <% if (Object.keys(opengraph).includes(og) && typeof opengraph[og] === 'string' && opengraph[og].trim() !== '') { %>
<meta property="og:<%= og %>" content="<%= String(opengraph[og]) %>"> <meta property="og:<%= og %>" content="<%= String(opengraph[og]) %>">
<% }} if (!opengraph.hasOwnProperty('image')) { %> <% }} if (!Object.keys(opengraph).includes('image')) { %>
<meta property="og:image" content="<%- serverURL %>/icons/android-chrome-512x512.png"> <meta property="og:image" content="<%- serverURL %>/icons/android-chrome-512x512.png">
<meta property="og:image:alt" content="HedgeDoc logo"> <meta property="og:image:alt" content="HedgeDoc logo">
<meta property="og:image:type" content="image/png"> <meta property="og:image:type" content="image/png">

View File

@@ -34,6 +34,8 @@
</li> </li>
<li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank" rel="noopener"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a> <li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank" rel="noopener"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a>
</li> </li>
<li role="presentation"><a role="menuitem" class="ui-invite-agent" tabindex="-1" href="#"><i class="fa fa-robot fa-fw"></i> <%= __('Invite Agent') %></a>
</li>
<% if(enableGitHubGist || enableDropBoxSave || enableGitlabSnippets) { %> <% if(enableGitHubGist || enableDropBoxSave || enableGitlabSnippets) { %>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header"><%= __('Export') %></li> <li class="dropdown-header"><%= __('Export') %></li>
@@ -136,6 +138,8 @@
</li> </li>
<li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank" rel="noopener"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a> <li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank" rel="noopener"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a>
</li> </li>
<li role="presentation"><a role="menuitem" class="ui-invite-agent" tabindex="-1" href="#"><i class="fa fa-robot fa-fw"></i> <%= __('Invite Agent') %></a>
</li>
<% if(enableGitHubGist || enableDropBoxSave || enableGitlabSnippets) { %> <% if(enableGitHubGist || enableDropBoxSave || enableGitlabSnippets) { %>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header"><%= __('Export') %></li> <li class="dropdown-header"><%= __('Export') %></li>

View File

@@ -1,4 +1,5 @@
<script src="config"></script> <script src="config"></script>
<script src="build/constrain-object.min.js"></script>
<% for (var js in htmlWebpackPlugin.files.js) { %> <% for (var js in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[js] %>" defer></script> <script src="<%= htmlWebpackPlugin.files.js[js] %>" defer></script>
<% } %> <% } %>

View File

@@ -15,7 +15,9 @@
<% if (allowAnonymous && !disableNoteCreation) { %> <% if (allowAnonymous && !disableNoteCreation) { %>
<a type="button" href="<%- serverURL %>/new" class="btn btn-sm btn-primary"><i class="fa fa-plus"></i> <%= __('New guest note') %></a> <a type="button" href="<%- serverURL %>/new" class="btn btn-sm btn-primary"><i class="fa fa-plus"></i> <%= __('New guest note') %></a>
<% } %> <% } %>
<% if (authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.saml || authProviders.oauth2 || authProviders.email) { %> <% if (authProviders.autoLoginProvider) { %>
<a type="button" href="<%- serverURL %>/auth/<%- authProviders.autoLoginProvider %>" class="btn btn-sm btn-success ui-signin"><%= __('Sign In') %></a>
<% } else if (authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.saml || authProviders.oauth2 || authProviders.email) { %>
<button class="btn btn-sm btn-success ui-signin" data-toggle="modal" data-target=".signin-modal"><%= __('Sign In') %></button> <button class="btn btn-sm btn-success ui-signin" data-toggle="modal" data-target=".signin-modal"><%= __('Sign In') %></button>
<% } %> <% } %>
</div> </div>
@@ -52,7 +54,13 @@
<% if (errorMessage && errorMessage.length > 0) { %> <% if (errorMessage && errorMessage.length > 0) { %>
<div class="alert alert-danger" style="max-width: 400px; margin: 0 auto;"><%= errorMessage %></div> <div class="alert alert-danger" style="max-width: 400px; margin: 0 auto;"><%= errorMessage %></div>
<% } %> <% } %>
<% if (authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.saml || authProviders.oauth2 || authProviders.email) { %> <% if (authProviders.autoLoginProvider) { %>
<span class="ui-signin">
<br>
<a type="button" href="<%- serverURL %>/auth/<%- authProviders.autoLoginProvider %>" class="btn btn-lg btn-success ui-signin" style="min-width: 200px;"><%= __('Sign In') %></a>
</span>
<span class="ui-or"><%= __('or') %></span>
<% } else if (authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.saml || authProviders.oauth2 || authProviders.email) { %>
<span class="ui-signin"> <span class="ui-signin">
<br> <br>
<a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="min-width: 200px;"><%= __('Sign In') %></a> <a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="min-width: 200px;"><%= __('Sign In') %></a>

44
public/views/link.ejs Normal file
View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include('hedgedoc/head') %>
<link rel="stylesheet" href="<%- serverURL %>/css/center.css">
</head>
<body>
<%- include('hedgedoc/header') %>
<div class="container-fluid text-center">
<div class="vertical-center-row">
<h1>
<% if (valid) { %>
<%= __('External link') %>
<% } else { %>
<%= __('Invalid link') %>
<% } %>
</h1>
<p>
<%= __('You\'ve clicked a link that points to an external page.') %>
<br>
<%= __('The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there.') %>
</p>
<% if (valid) { %>
<p><code class="text-break"><%- targetURL %></code></p>
<p><%= __('Do you want to continue?') %></p>
<p>
<a class="btn btn-primary" href="<%- targetURL %>"><%= __('Continue to external page') %></a>
<a class="btn btn-default" id="close-button"><%= __('Close') %></a>
</p>
<% } else { %>
<p class="text-danger"><strong><%= __('The provided link is not a valid URL.') %></strong></p>
<p>
<a class="btn btn-default" id="close-button"><%= __('Close') %></a>
</p>
<% } %>
</div>
</div>
</body>
<script nonce="<%= cspNonce %>">
document.getElementById("close-button").addEventListener('click', _ => window.close())
</script>
</html>

View File

@@ -15,9 +15,9 @@
<meta name="description" content="<%= description %>"> <meta name="description" content="<%= description %>">
<% } %> <% } %>
<% for (var og in opengraph) { %> <% for (var og in opengraph) { %>
<% if (opengraph.hasOwnProperty(og) && opengraph[og].trim() !== '') { %> <% if (Object.keys(opengraph).includes(og) && typeof opengraph[og] === 'string' && opengraph[og].trim() !== '') { %>
<meta property="og:<%= og %>" content="<%= opengraph[og] %>"> <meta property="og:<%= og %>" content="<%= opengraph[og] %>">
<% }} if (!opengraph.hasOwnProperty('image')) { %> <% }} if (!Object.keys(opengraph).includes('image')) { %>
<meta property="og:image" content="<%- serverURL %>/icons/android-chrome-512x512.png"> <meta property="og:image" content="<%- serverURL %>/icons/android-chrome-512x512.png">
<meta property="og:image:alt" content="HedgeDoc logo"> <meta property="og:image:alt" content="HedgeDoc logo">
<meta property="og:image:type" content="image/png"> <meta property="og:image:type" content="image/png">
@@ -34,7 +34,7 @@
<small> <small>
<span> <span>
<% if(lastchangeuserprofile) { %> <% if(lastchangeuserprofile) { %>
<span class="ui-lastchangeuser">&thinsp;<i class="ui-user-icon small" style="background-image: url(<%- lastchangeuserprofile.photo %>);" data-toggle="tooltip" data-placement="right" title="<%- lastchangeuserprofile.name %>"></i></span> <span class="ui-lastchangeuser">&thinsp;<i class="ui-user-icon small" style="background-image: url(<%- lastchangeuserprofile.photo %>);" data-toggle="tooltip" data-placement="right" title="<%= lastchangeuserprofile.name %>"></i></span>
<% } else { %> <% } else { %>
<span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o fa-fw" style="width: 18px;"></i></span> <span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o fa-fw" style="width: 18px;"></i></span>
<% } %> <% } %>
@@ -45,7 +45,7 @@
<br> <br>
<% if(ownerprofile && owner !== lastchangeuser) { %> <% if(ownerprofile && owner !== lastchangeuser) { %>
<span class="ui-owner"> <span class="ui-owner">
&thinsp;<i class="ui-user-icon small" style="background-image: url(<%- ownerprofile.photo %>);" data-toggle="tooltip" data-placement="right" title="<%- ownerprofile.name %>"></i> &thinsp;<i class="ui-user-icon small" style="background-image: url(<%- ownerprofile.photo %>);" data-toggle="tooltip" data-placement="right" title="<%= ownerprofile.name %>"></i>
&nbsp;<span class="text-uppercase">owned this note</span> &nbsp;<span class="text-uppercase">owned this note</span>
</span> </span>
<% } %> <% } %>

138
test/note.js Normal file
View File

@@ -0,0 +1,138 @@
'use strict'
const assert = require('assert')
const models = require('../lib/models')
const Note = models.Note
describe('Note.extractMeta', function () {
it('parses standard frontmatter with title and description', function () {
const content = '---\ntitle: My Note\ndescription: A test note\n---\n# Hello World'
const result = Note.extractMeta(content)
assert.strictEqual(result.meta.title, 'My Note')
assert.strictEqual(result.meta.description, 'A test note')
assert.strictEqual(result.markdown, '\n# Hello World')
})
it('parses frontmatter with tags as array', function () {
const content = '---\ntitle: Tagged\ntags:\n - foo\n - bar\n - baz\n---\nBody'
const result = Note.extractMeta(content)
assert.strictEqual(result.meta.title, 'Tagged')
assert.deepStrictEqual(result.meta.tags, ['foo', 'bar', 'baz'])
})
it('parses frontmatter with tags as string', function () {
const content = '---\ntags: foo, bar, baz\n---\nBody'
const result = Note.extractMeta(content)
assert.strictEqual(result.meta.tags, 'foo, bar, baz')
})
it('parses frontmatter with opengraph object', function () {
const content = '---\nopengraph:\n title: OG Title\n description: OG Description\n image: https://example.com/img.png\n---\nBody'
const result = Note.extractMeta(content)
assert.strictEqual(result.meta.opengraph.title, 'OG Title')
assert.strictEqual(result.meta.opengraph.description, 'OG Description')
assert.strictEqual(result.meta.opengraph.image, 'https://example.com/img.png')
})
it('parses frontmatter with slideOptions', function () {
const content = '---\nslideOptions:\n theme: moon\n transition: slide\n---\n# Slide 1'
const result = Note.extractMeta(content)
assert.strictEqual(result.meta.slideOptions.theme, 'moon')
assert.strictEqual(result.meta.slideOptions.transition, 'slide')
})
it('parses frontmatter with boolean and numeric values', function () {
const content = '---\nbreaks: false\nGA: UA-12345\nlang: en\n---\nBody'
const result = Note.extractMeta(content)
assert.strictEqual(result.meta.breaks, false)
assert.strictEqual(result.meta.GA, 'UA-12345')
assert.strictEqual(result.meta.lang, 'en')
})
it('returns empty meta and full content when no frontmatter present', function () {
const content = '# Just a heading\n\nSome text here.'
const result = Note.extractMeta(content)
assert.deepStrictEqual(result.meta, {})
assert.strictEqual(result.markdown, content)
})
it('returns empty meta for empty frontmatter', function () {
const content = '---\n---\nBody text'
const result = Note.extractMeta(content)
assert.deepStrictEqual(result.meta, {})
assert.strictEqual(result.markdown, '\nBody text')
})
it('handles frontmatter closed with ...', function () {
const content = '---\ntitle: Dot Delimiter\n...\nBody'
const result = Note.extractMeta(content)
assert.strictEqual(result.meta.title, 'Dot Delimiter')
assert.strictEqual(result.markdown, '\nBody')
})
it('returns empty meta for malformed YAML', function () {
const content = '---\ntitle: [invalid yaml\n---\nBody'
const result = Note.extractMeta(content)
assert.deepStrictEqual(result.meta, {})
assert.strictEqual(result.markdown, content)
})
it('handles empty content', function () {
const result = Note.extractMeta('')
assert.deepStrictEqual(result.meta, {})
assert.strictEqual(result.markdown, '')
})
it('handles content that starts with --- but has no closing delimiter', function () {
const content = '---\ntitle: No closing\nNo delimiter here'
const result = Note.extractMeta(content)
assert.deepStrictEqual(result.meta, {})
assert.strictEqual(result.markdown, content)
})
it('neutralizes YAML alias bomb (10 levels of 10-way branching)', function () {
const content = [
'---',
'a0: &a0 1',
'a1: &a1 [*a0,*a0,*a0,*a0,*a0,*a0,*a0,*a0,*a0,*a0]',
'a2: &a2 [*a1,*a1,*a1,*a1,*a1,*a1,*a1,*a1,*a1,*a1]',
'a3: &a3 [*a2,*a2,*a2,*a2,*a2,*a2,*a2,*a2,*a2,*a2]',
'a4: &a4 [*a3,*a3,*a3,*a3,*a3,*a3,*a3,*a3,*a3,*a3]',
'a5: &a5 [*a4,*a4,*a4,*a4,*a4,*a4,*a4,*a4,*a4,*a4]',
'a6: &a6 [*a5,*a5,*a5,*a5,*a5,*a5,*a5,*a5,*a5,*a5]',
'a7: &a7 [*a6,*a6,*a6,*a6,*a6,*a6,*a6,*a6,*a6,*a6]',
'a8: &a8 [*a7,*a7,*a7,*a7,*a7,*a7,*a7,*a7,*a7,*a7]',
'a9: &a9 [*a8,*a8,*a8,*a8,*a8,*a8,*a8,*a8,*a8,*a8]',
'a10: &a10 [*a9,*a9,*a9,*a9,*a9,*a9,*a9,*a9,*a9,*a9]',
'opengraph: *a10',
'---',
'x'
].join('\n')
const start = Date.now()
const result = Note.extractMeta(content)
const elapsed = Date.now() - start
assert.ok(elapsed < 1000, `extractMeta took ${elapsed}ms, should complete in under 1000ms`)
assert.ok(result.meta !== null, 'meta should not be null')
assert.ok(typeof result.meta === 'object', 'meta should be an object')
assert.strictEqual(result.markdown, '\nx')
const json = JSON.stringify(result.meta)
assert.ok(json.length < 100000, `stringified meta should be small, got ${json.length} bytes`)
})
it('truncates excessively long string values', function () {
const longString = 'a'.repeat(20000)
const content = `---\ntitle: ${longString}\n---\nBody`
const result = Note.extractMeta(content)
assert.ok(result.meta.title.length <= 10000, `title should be truncated, got ${result.meta.title.length}`)
})
it('preserves normal nested objects within depth limit', function () {
const content = '---\nlevel1:\n level2:\n level3: deep value\n---\nBody'
const result = Note.extractMeta(content)
assert.strictEqual(result.meta.level1.level2.level3, 'deep value')
})
})

View File

@@ -137,6 +137,11 @@ module.exports = {
context: path.join(__dirname, 'node_modules/reveal.js'), context: path.join(__dirname, 'node_modules/reveal.js'),
from: 'plugin', from: 'plugin',
to: 'reveal.js/plugin' to: 'reveal.js/plugin'
},
{
context: path.join(__dirname, 'node_modules/constrain-object/dist'),
from: 'index.iife.js',
to: 'constrain-object.min.js'
} }
] ]
}), }),
@@ -301,11 +306,16 @@ module.exports = {
'socket.io-client': 'io', 'socket.io-client': 'io',
'jquery': '$', 'jquery': '$',
'moment': 'moment', 'moment': 'moment',
'select2': 'select2' 'select2': 'select2',
'constrain-object': 'ConstrainObject'
}, },
module: { module: {
rules: [ rules: [
{
test: /\.mjs$/,
type: 'javascript/auto'
},
{ {
test: require.resolve(path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.js')), test: require.resolve(path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.js')),
use: { use: {

View File

@@ -18,7 +18,7 @@ module.exports = [
new EsbuildPlugin({ new EsbuildPlugin({
target: 'es2015', target: 'es2015',
format: 'cjs', format: 'cjs',
exclude: ['MathJax/extensions/a11y/mathmaps', 'reveal.js/plugin/markdown/marked.js'] exclude: ['MathJax/extensions/a11y/mathmaps', 'reveal.js/plugin/markdown/marked.js', 'constrain-object.min.js']
}) })
], ],
splitChunks: { splitChunks: {

5593
yarn.lock

File diff suppressed because it is too large Load Diff