| 79305873 | 26-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix RSS feed aggregation broken by SimplePie 1.9 upgrade
SimplePie 1.9 added a FileClient that rejects any response whose error is non-null while the status code is zero. FeedParserFile never set a
fix RSS feed aggregation broken by SimplePie 1.9 upgrade
SimplePie 1.9 added a FileClient that rejects any response whose error is non-null while the status code is zero. FeedParserFile never set a status code and copied DokuHTTPClient's error verbatim, which is an empty string (not null) on success - so every successful fetch was misclassified as a failed request and feeds rendered the 'rssfailed' message.
Populate the response through SimplePie's Response interface methods backed by our own properties (status code, body, normalized headers, requested URL) and report a missing error as null, instead of writing File's deprecated backwards-compatibility properties.
show more ...
|
| 7a48b45e | 25-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
Updated dependencies |
| 884caed9 | 25-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
Remote: restrict core.aclCheck for other users to superusers
aclCheck() let any API-enabled user pass an arbitrary username and learn that user's effective permission level on any page/namespace, en
Remote: restrict core.aclCheck for other users to superusers
aclCheck() let any API-enabled user pass an arbitrary username and learn that user's effective permission level on any page/namespace, enabling ACL-posture enumeration of other accounts. Checking another user's permissions is now limited to superusers; users may still check their own. The deprecated legacyAclCheck() delegates here and is covered too.
Not really a big security concern, but there is no reason to enable it.
Note: arbitrary groups can still be checked by anyone.
show more ...
|
| 40981bcc | 25-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
Ip: validate CIDR mask to prevent fatal error and over-broad proxy trust
A non-numeric or empty mask in trustedproxies (e.g. 10.0.0.0/abc or 10.0.0.0/) threw an uncaught TypeError on the IPv4 path,
Ip: validate CIDR mask to prevent fatal error and over-broad proxy trust
A non-numeric or empty mask in trustedproxies (e.g. 10.0.0.0/abc or 10.0.0.0/) threw an uncaught TypeError on the IPv4 path, and a negative mask (10.0.0.0/-1) passed the bounds check and produced a bitmask that matched every IPv4, silently trusting all proxies.
Validate the mask as a non-negative integer in ipInRange() and broaden the ipMatches() catch to Throwable so an invalid range degrades to 'no match' instead of a 500.
show more ...
|
| c651c34b | 17-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
BacklinksTest: give testLinksInDeletedPages its own page
testLinksInDeletedPages reused test:internallinks, the same page testInternallink already saves and indexes. Since the data dir is shared acr
BacklinksTest: give testLinksInDeletedPages its own page
testLinksInDeletedPages reused test:internallinks, the same page testInternallink already saves and indexes. Since the data dir is shared across the class, when the re-save and the earlier index land in the same second, needsIndexing() now (correctly) reports the page as up to date and addPage() skips reindexing, leaving stale link data. backlinks('test:internallink') then returned an empty array.
Use a dedicated page (test:deletedlinks) with its own link targets so the test no longer collides with testInternallink's index state.
show more ...
|
| 2cda0166 | 17-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
Indexer: signal nothing-to-do via boolean return instead of void
The TaskRunner runs indexing, sitemap, digest and changelog-trim tasks in sequence and relies on each task returning false when it di
Indexer: signal nothing-to-do via boolean return instead of void
The TaskRunner runs indexing, sitemap, digest and changelog-trim tasks in sequence and relies on each task returning false when it did no work so the next one is tried. The indexer rewrite changed addPage(), deletePage() and renamePage() to return void and only abort via exceptions, breaking that contract: indexing always looked like work was done and the following tasks never ran.
Restore the boolean return on these three methods (true when work was done, false when there was nothing to do) while still using exceptions to signal errors, and propagate it through TaskRunner::runIndexer(). runIndexer() also no longer forces reindexing on every call.
The legacy compatibility layer is adjusted to match: LegacyIndexer and idx_addPage() forward the boolean, mapping SearchExceptions back to the historic error-message/false returns. LegacyIndexer::renamePage() restores the 'page is not in index' message that the move plugin expects.
Closes #4661
show more ...
|
| 79dae64d | 17-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
Indexer: treat same-second save and index as up to date
needsIndexing() compared the .indexed tag mtime against the page mtime with <=, so a page that was saved and indexed within the same second wa
Indexer: treat same-second save and index as up to date
needsIndexing() compared the .indexed tag mtime against the page mtime with <=, so a page that was saved and indexed within the same second was always reported as still needing indexing. Require the page to be strictly newer than the index tag instead, so an equal mtime correctly counts as up to date.
show more ...
|
| 2ff7e61c | 10-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(indexer): explicitly handle renames
In an attempt to simplify the index handling, the newly refactored indexer implemented a rename as delete+add sequence.
This had unintended consequences for
fix(indexer): explicitly handle renames
In an attempt to simplify the index handling, the newly refactored indexer implemented a rename as delete+add sequence.
This had unintended consequences for the move plugin which may move several pages at once, requiring a working index even while some pages have already been moved while others still remain at their old location.
Related to #4646
show more ...
|
| eab6268c | 07-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(changelog): keep out-of-order external edits out of recent changes (#4634)
A detected external edit is recorded both in the page's own changelog and in the global recent-changes feed. When its d
fix(changelog): keep out-of-order external edits out of recent changes (#4634)
A detected external edit is recorded both in the page's own changelog and in the global recent-changes feed. When its date is older than the most recent change already in the global changelog — an old file surfacing after the feed has moved on — appending it placed it at the top of recent changes with an old date, above genuinely newer entries.
writeLogEntry() now delegates the global-feed append to writeGlobalLogEntry(), which skips an external edit whose date predates the global changelog's last-modified time. The entry still lands in the page's own changelog; it is only kept out of the cross-page feed. Normal edits are always appended.
show more ...
|
| 0a245329 | 07-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(changelog): don't record an external edit when the content is unchanged (#4634)
A current revision's file mtime can change without its content changing — a backup restore, a git checkout, an in-
fix(changelog): don't record an external edit when the content is unchanged (#4634)
A current revision's file mtime can change without its content changing — a backup restore, a git checkout, an in-place rewrite. Such a bump was detected as an external edit and logged with a 0-byte size change, cluttering history.
getCurrentRevisionInfo() now compares the current content against the last recorded revision before treating an mtime change as an external edit. When the content is identical no external edit happened: the file mtime is reset to the recorded revision date and the last revision is returned. The comparison is an abstract currentContentMatchesRevision() — pages compare the decompressed text, media compare file size then md5_file().
show more ...
|
| 2bde879a | 07-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
Fix diff view comparing a deleted page with itself (#4635)
When opening the diff of a deleted page without explicit rev parameters (?do=diff), Diff::handle() resolved the older side via getRevisions
Fix diff view comparing a deleted page with itself (#4635)
When opening the diff of a deleted page without explicit rev parameters (?do=diff), Diff::handle() resolved the older side via getRevisions(0, 1). That helper only skips the current revision when the item file still exists, so for a deleted page it returned the deletion entry itself and the view ended up comparing the current revision with itself ("no way to compare when less than two revisions", empty diff).
Use getRelativeRevision($rev2, -1) instead, which returns the revision immediately before the current one regardless of whether the page file is still present. This covers both pages deleted through DokuWiki and externally deleted pages (whose synthesized deletion entry is now persisted to the changelog).
Add PageDiffTest and PageChangeLogTest covering the resolved revision pair and the underlying changelog walk-back for both deletion kinds.
show more ...
|
| b12755b0 | 07-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
Make common_pageinfo tests independent of execution order
The pageinfo() tests relied on running in a fixed order: test_editor_and_externaledits mutated wiki:syntax (changelog entry + file mtime), a
Make common_pageinfo tests independent of execution order
The pageinfo() tests relied on running in a fixed order: test_editor_and_externaledits mutated wiki:syntax (changelog entry + file mtime), and the other tests relied on that page being pristine. Move the mutating test to its own page (wiki:dokuwiki) so the shared per-class data dir stays clean, prime the last_change metadata within each test instead of across tests, and fix the affected expectations. The suite now passes in isolation and in random/reverse order.
While here, modernize the file: - capture the pageinfo() result once instead of calling it twice - add public/protected visibility markers to all methods - convert array() to the short [] syntax - replace the deprecated addLogEntry() with the PageFile API it wraps
show more ...
|
| 37bd308a | 06-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
Add unit tests for the Draft class
The Draft class had no dedicated tests. Cover the save/retrieve round trip for whole-page and section edits, the cases where no draft is written (drafts disabled,
Add unit tests for the Draft class
The Draft class had no dedicated tests. Cover the save/retrieve round trip for whole-page and section edits, the cases where no draft is written (drafts disabled, nothing edited), draft deletion, reading a missing draft, and the constructor's purging of drafts that predate the current page revision.
show more ...
|
| 5d8c9d42 | 06-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
(security) Require a security token for the lock AJAX call
The lock AJAX call refreshes the edit lock and saves a draft, both of which change server state. It was gated only by the write ACL and, un
(security) Require a security token for the lock AJAX call
The lock AJAX call refreshes the edit lock and saves a draft, both of which change server state. It was gated only by the write ACL and, unlike the sibling draft-delete call, did not verify a security token (low severity).
A cross-site forged POST against a logged-in user could, within that user's own write permissions, take or hold an edit lock and store attacker-controlled draft content under their name.
The call now verifies the security token before taking the lock or saving the draft. Logged out users are unaffected, as no token is issued or checked for them. The edit lock timer now always sends the token with its refresh request, including when draft saving is disabled.
show more ...
|
| e8c9256a | 06-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
(security) Clean the media upload namespace in AJAX upload
The namespace passed to the AJAX backend was not cleaned correctly, resulting in two separate issues.
1. Theoretical Reflected XSS The
(security) Clean the media upload namespace in AJAX upload
The namespace passed to the AJAX backend was not cleaned correctly, resulting in two separate issues.
1. Theoretical Reflected XSS The raw namespace was reflected into the JSON response and injected into the mediamanager DOM. However since the media manager only passes cleaned namespaces to AJAX and the ajax backen only returns JSON, this issue was not exploitable. 2. Cross-namespace ACL bypass (medium severity) The uncleaned namespace was directly used to check ACLs. In a wiki where a user has upload permission in a namespace above a namespace where they don't have permissions (eg. upload allowed in :user:*, but upload denied in :user:secret:*) they could pass an upper case namespace (eg :user:SECRET) - no ACL does exist for this upper case namespace and the acl of the namespace above applies (:user). When the file is written a cleanID is applied to the full filename, turning the uppercase namespace into lowercase. This can allow users to write into a namespace they normally should not be allowed to write to, but it does require upload permissions in a higher namespace.
show more ...
|
| 39904235 | 04-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
Emit a guard newline in TextareaElement to preserve leading newlines
A browser's HTML parser ignores a single newline immediately after a <textarea> start tag, so a value whose first character is a
Emit a guard newline in TextareaElement to preserve leading newlines
A browser's HTML parser ignores a single newline immediately after a <textarea> start tag, so a value whose first character is a newline lost it on round-trip (e.g. saving a page or section whose source starts with a blank line silently dropped that line).
Emit a guard newline right after the start tag so the one the browser drops is absorbed and the value stays intact.
show more ...
|
| 4b31eadf | 04-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix (parsing): avoid newline loss on GFM section editing
The GFM header parsing returned a byte position pointing at the newline before the actual header resulting in the observed newline eatings as
fix (parsing): avoid newline loss on GFM section editing
The GFM header parsing returned a byte position pointing at the newline before the actual header resulting in the observed newline eatings as reported in https://github.com/dokuwiki/dokuwiki/pull/4636#issuecomment-4491970909
Additionally this fixes an oddity of DW header parsing which accidentally allowed text on the line before the opening = chars. Whitespace is still allowed.
show more ...
|
| 2e43b799 | 04-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
Render locale and plugin-bundled text as DokuWiki syntax
Static "intro" text shipped with DokuWiki, templates and plugins — inc/lang/*/*.txt, template locale files, each plugin's lang/<lc>/*.txt — i
Render locale and plugin-bundled text as DokuWiki syntax
Static "intro" text shipped with DokuWiki, templates and plugins — inc/lang/*/*.txt, template locale files, each plugin's lang/<lc>/*.txt — is authored in DokuWiki syntax. It is a core/plugin asset, not user content. When an admin configures the wiki for Markdown the DW link and monospace modes are not loaded, so these files render as literal text: [[wiki:syntax]] and ''Save'' pairs survive into the HTML.
Pin those entry points to the 'dw' flavour via the override added in the previous commit:
- p_locale_xhtml() and tpl_locale_xhtml() pass 'dw'. - PluginTrait::locale_xhtml() passes 'dw'. - PluginTrait::render_text() / PluginInterface::render_text() gain a $syntax parameter defaulting to 'dw'. The default is 'dw', not null, because the method predates GFM and its callers pass DW-syntax strings; plugins rendering user content opt back into the configured syntax with render_text($text, 'xhtml', null).
Locale output is now deterministic across a syntax switch, so its caches get over-invalidated but never under-invalidated — acceptable, as the locale render path is rare and cheap.
show more ...
|
| 16999ed1 | 04-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
parserutils: allow callers to override the parse syntax, key cache on it
With the registry now carrying the flavour as a parameter, expose that to callers: p_get_instructions(), p_cached_instruction
parserutils: allow callers to override the parse syntax, key cache on it
With the registry now carrying the flavour as a parameter, expose that to callers: p_get_instructions(), p_cached_instructions() and p_cached_output() gain an optional $syntax argument. null (the default) means "use the configured $conf['syntax']", preserving behaviour for every existing call site; an explicit flavour parses under it regardless of the wiki's configured preference.
This is what lets bundled assets render deterministically — e.g. a plugin forcing 'dw' on a document whose configured syntax is 'md'.
Because that case renders the same file under two flavours within one request, key both the in-request memo (the $run map) and the on-disk cache on $syntax so the two do not collide. The syntax enters the cache key only when passed explicitly (non-null); when null the key is unchanged, so existing caches are untouched. Plumbed through new optional $syntax args on CacheParser / CacheInstructions, appended to the key string.
show more ...
|
| 47a02a10 | 04-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
Parsing: make parse syntax a per-parse value, drop ModeInterface
The active parse's syntax flavour is a per-parse question, not process- global state: within a single request a plugin can render bun
Parsing: make parse syntax a per-parse value, drop ModeInterface
The active parse's syntax flavour is a per-parse question, not process- global state: within a single request a plugin can render bundled DokuWiki-syntax text inside an otherwise-Markdown page. Yet ModeRegistry was a singleton that read $conf['syntax'] and the $PARSER_MODES global, and every mode reached it through ModeRegistry::getInstance() — so the flavour lived in shared mutable state that two parses in one request would fight over.
Make the registry a short-lived value instead:
- ModeRegistry is constructed once per parse with an explicit $syntax and injected into Parser, Handler and every mode. getSyntax() / isDwPreferred() / isMdPreferred() consult $this->syntax; the DOKU_UNITTEST-gated mode-list cache hack is gone (each registry is fresh, nothing to invalidate). - p_get_instructions() is now the single place in the pipeline where $conf['syntax'] is read; from there the flavour travels as a parameter. No code under inc/Parsing/ reads $conf['syntax'] directly anymore — the five syntax-reading modes (Preformatted, GfmHr, GfmEscape, Externallink, GfmQuote) route through $this->registry.
Keep the two concepts apart, as documented in the ModeRegistry and AbstractMode docblocks: the user's configured *preference* stays in $conf['syntax'] for UI code (toolbar, settings), while the active parse's syntax is a parameter carried by the registry.
$PARSER_MODES is demoted to a deprecated, read-only mirror, published during loadPluginModes() — third-party syntax plugins (columnlist, alphalist2, phpwikify, skipentity) and the bundled info plugin read the global directly, often from their constructors, so the taxonomy must stay visible there. No core code reads the mirror.
Fold ModeInterface into AbstractMode while here: getSort()/handle() are abstract, the connect callbacks carry defaults, and the public $Lexer "FIXME should be done by setter" becomes setLexer()/getLexer() injected by Parser::addMode() alongside the registry. Nested-content resolution moves to the allowedCategories()/filterAllowedModes() hooks, resolved once when the registry is attached.
Tests build their own parser/registry through ParserTestBase::setSyntax() instead of mutating $conf and calling the removed ModeRegistry::reset().
show more ...
|
| b8c2692f | 29-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(httputils): build a proper internal URL for nginx X-Accel-Redirect
Unlike Apache's mod_xsendfile and lighttpd's X-LIGHTTPD-send-file, which both accept an absolute file system path and stream th
fix(httputils): build a proper internal URL for nginx X-Accel-Redirect
Unlike Apache's mod_xsendfile and lighttpd's X-LIGHTTPD-send-file, which both accept an absolute file system path and stream the file directly, nginx treats X-Accel-Redirect as a URL: it performs an internal subrequest against the value and resolves it through `internal` location blocks in its own configuration. The redirect target therefore must be a request path nginx can map back to a file, not the file system path itself.
The previous implementation blindly stripped DOKU_INC's length from the file path, which produced a broken URL whenever a data directory had been relocated outside the DokuWiki tree.
Route the file through three cases instead: files inside DOKU_INC keep their path relative to the wiki root; files in a relocated mediadir, mediaolddir, cachedir or savedir are mapped back to their default URL below data/; everything else falls through an opt-in /_x_accel_redirect/ prefix.
Path segments are URL-encoded so filenames with spaces or other special characters resolve (fixes another potential bug in the previous implementation).
Nginx configuration:
For the default layout no extra configuration is needed - files keep their path relative to the wiki root and the regular wiki location already serves them. For relocated data directories admins must add an `internal` location below the wiki's URL space pointing at the actual file system path, e.g.:
location /data/media/ { internal; alias /srv/dokuwiki-media/; }
For the /_x_accel_redirect/ fallback (used when a plugin serves files from outside the wiki and its data directories), admins opt in by adding:
location /_x_accel_redirect/ { internal; alias /; }
When the wiki is installed under a sub path, prefix both locations accordingly (e.g. /wiki/data/media/ and /wiki/_x_accel_redirect/).
fixes: #2895
show more ...
|
| 7e687fd8 | 29-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(auth): scope media ACL checks to the namespace
Media files have no per-file ACLs; permissions must be evaluated against the namespace they live in. Several call sites passed the raw media ID to
fix(auth): scope media ACL checks to the namespace
Media files have no per-file ACLs; permissions must be evaluated against the namespace they live in. Several call sites passed the raw media ID to auth_quickaclcheck(), so a page-intended exact-ID rule (e.g. on wiki:secret.png) could silently apply to a media file sharing that ID.
Introduce mediaAclPath() that builds the correct namespace wildcard path (handling root-namespace media) and route all media-related ACL checks through it. Also normalize the lone `:X` sentinel variant in fetch.functions.php to the standard `:*` form.
fixes: #4647
show more ...
|
| 4f32c45b | 26-May-2026 |
Andreas Gohr <gohr@cosmocode.de> |
GfmLink: allow soft line break inside link text
The label character class explicitly forbade `\n`, so a CommonMark soft line break inside link text (e.g. `[link with<EOL>more](url)`) fell through to
GfmLink: allow soft line break inside link text
The label character class explicitly forbade `\n`, so a CommonMark soft line break inside link text (e.g. `[link with<EOL>more](url)`) fell through to literal text instead of producing a link. Loosen the class to accept a bare `\n` as long as it is not followed by a blank line — soft breaks are spec-allowed inside link text, blank lines are not, and refusing them also keeps `\n#`-anchored block modes (header, hr, ...) from being swallowed by a runaway link match.
The `\n` survives into the label string and renders as a literal line ending in HTML, which browsers display as a single space. This soft break behavior has been checked against https://spec.commonmark.org/dingus/
Note that this behavior differs from github where the line break is rendered as a hard break <br>.
show more ...
|
| 65dd2042 | 26-May-2026 |
Andreas Gohr <gohr@cosmocode.de> |
GfmEscape: defer \\<EOL> to DW Linebreak in mixed-syntax modes
Both GfmEscape (sort 5) and DW Linebreak (sort 140) can claim the two backslashes of `\\` followed by space/tab/newline. The lexer's ti
GfmEscape: defer \\<EOL> to DW Linebreak in mixed-syntax modes
Both GfmEscape (sort 5) and DW Linebreak (sort 140) can claim the two backslashes of `\\` followed by space/tab/newline. The lexer's tie-breaker picked GfmEscape, so DW's forced linebreak silently lost its delimiter under dw+md and md+dw. Add a negative lookahead that declines `\\[ \t\n]` whenever DW syntax is loaded — pure md keeps GFM-spec behavior. Mid-line `\\` (UNC paths etc.) still escapes.
show more ...
|
| 36ba8ead | 12-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
test: add tests to catch #4637
Issue #4637 was fixed in 7686f2030b19f948d2e50df1613ef6592fa44b46 but I didn't like that no test had caught the issue.
This adds a specific test for the issue as a Ha
test: add tests to catch #4637
Issue #4637 was fixed in 7686f2030b19f948d2e50df1613ef6592fa44b46 but I didn't like that no test had caught the issue.
This adds a specific test for the issue as a HandlerTest. Additionally a simple smoke test is added that renders the wiki:syntax page and inspects the created DOM. It's not comprehensive but might help flagging similar issue in the future.
show more ...
|