| 0579c2f8 | 19-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
security(media): authorize the resolved target of media write operations
The media manager computed a permission level for the request's namespace and passed it to the media write helpers, which the
security(media): authorize the resolved target of media write operations
The media manager computed a permission level for the request's namespace and passed it to the media write helpers, which then acted on a target that need not live in that namespace. Reachability differs per helper:
- media_restore() took the id to restore from the separate "image" parameter, unrelated to the namespace the permission was computed for. A user with upload permission in any one namespace could restore revisions of media in namespaces they cannot write.
- media_upload() and media_upload_xhr() derive the target from the upload namespace plus a user-controlled filename that may carry its own namespace segments, so the target can only resolve into a sub-namespace of the upload namespace. Where that child has a stricter ACL, the parent's permission was applied instead.
- media_metasave() was not vulnerable: its target and its permission both derive from the same request id, so the namespaces always matched. It is changed only for consistency.
Each helper now recomputes the permission from its resolved target id, as media_delete() already does. The redundant $auth parameter is removed from media_restore() and media_metasave(), which have no other callers, and ignored but kept for compatibility in media_upload() and media_upload_xhr().
Severity: medium - an authenticated user with upload rights in one namespace can restore media revisions in other namespaces, or write into a stricter sub-namespace of one they can upload to; an authorization bypass affecting media integrity, not code execution or disclosure.
fixes #4702
show more ...
|
| 3ed34089 | 19-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
security(auth): use strict comparison in membership and ACL checks
Loose comparison treated numerically-equal strings as the same value, so a superuser, manager, or ACL entry named with a numeric st
security(auth): use strict comparison in membership and ACL checks
Loose comparison treated numerically-equal strings as the same value, so a superuser, manager, or ACL entry named with a numeric string (e.g. 1e3 or @1e3) matched any user or group whose name was numerically equal (1000, 01000, 1000.0). Such names survive auth_nameencode() untouched, allowing silent privilege escalation. Match names strictly in auth_isMember() and auth_aclcheck_cb().
Severity: low - only exploitable when an administrator names a superuser, manager or ACL group with a numeric string such as 1e3, an unusual configuration, so realistic deployments are rarely affected.
fixes #4701
show more ...
|
| e5358e0d | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(infoutils): use git --pretty=reference to avoid Windows escaping
On Windows escapeshellarg() replaces percent signs with spaces, so the --pretty=format:%h %cd argument reached git as literal tex
fix(infoutils): use git --pretty=reference to avoid Windows escaping
On Windows escapeshellarg() replaces percent signs with spaces, so the --pretty=format:%h %cd argument reached git as literal text. Git then echoed the format string verbatim, a truthy value that made the version display report an empty hash and the date "h" while skipping the working .git-directory fallback.
The named --pretty=reference format carries no percent signs on the command line, so the shell path works on Windows too. Its "hash (subject, date)" output is validated before being trusted.
Introduced in b9e35b2f0.
show more ...
|
| 12ead38a | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(httputils): canonicalize paths when building X-Accel-Redirect URLs
http_xaccel_url() compared the fullpath()-collapsed target file against a non-canonicalized DOKU_INC and the raw savedir config
fix(httputils): canonicalize paths when building X-Accel-Redirect URLs
http_xaccel_url() compared the fullpath()-collapsed target file against a non-canonicalized DOKU_INC and the raw savedir config value. The lib/exe entry points define DOKU_INC as __DIR__.'/../../' and savedir defaults to the relative './data', so the in-tree prefix check never matched: nginx installs using X-Accel-Redirect served in-tree files behind the /_x_accel_redirect/ escape hatch and returned 404 without extra location config.
Canonicalize DOKU_INC and every configured root with fullpath() before the prefix comparisons so they match the already-canonicalized file path.
The comparison lost its fullpath() wrapping in b8c2692f7, which replaced the earlier substr($file, strlen(fullpath(DOKU_INC)) + 1) with a raw DOKU_INC prefix check.
show more ...
|
| c3a14f67 | 06-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(media): don't upscale small images in the detail previews
The detail page, media manager and media diff requested a bounding-box resize at the full box size, so an image smaller than the box was
fix(media): don't upscale small images in the detail previews
The detail page, media manager and media diff requested a bounding-box resize at the full box size, so an image smaller than the box was enlarged and fetch.php generated and cached the upscaled copy.
slika 1.2 adds an $upscale option to resize/crop. getDisplayDimensions() now takes a $fit flag mirroring fetch.php one to one and predicts the matching no-upscale dimensions, media_resize_image()/media_mod_image() forward $upscale, and fetch.php disables upscaling for fit=1 requests. In-page image scaling is unchanged.
show more ...
|
| 6372bc80 | 06-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(changelog): detect an externally restored deleted page as a re-creation
A deleted page restored outside DokuWiki with a preserved mtime predating the deletion (cp -p from an old backup, rsync --
fix(changelog): detect an externally restored deleted page as a re-creation
A deleted page restored outside DokuWiki with a preserved mtime predating the deletion (cp -p from an old backup, rsync --times) has content matching the delete revision's attic copy. That copy holds the pre-delete content, so the unchanged-content shortcut in getCurrentRevisionInfo() wrongly treated the restore as an unchanged file, touched the mtime back to the delete revision and kept the DELETE as the current revision. The page then stayed "deleted" on every subsequent read.
A last recorded revision of type DELETE means the page did not exist then, so an existing file is an external re-creation regardless of its mtime. getCurrentRevisionInfo() now classifies the external change and delegates to synthesizeExternalDeletion/synthesizeExternalCreate/synthesizeExternalEdit; only the edit path keeps the unchanged-content shortcut. A re-creation records the full file as its size change and, when the restored mtime predates the delete, is dated just after it with an unknown date.
The getCurrentRevisionInfo() method got rather unwieldy, so it was refactored to use three helper methods, for synthesizing the changelog data.
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 ...
|
| 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 ...
|
| 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 ...
|
| 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 ...
|
| 73dc0a89 | 06-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(mail): keep '&' intact in mailto links with multiple query params
Move the email-handling helpers (obfuscate, mail_isvalid, mail_quotedprintable_encode, mail_setup) out of the procedural inc/mai
fix(mail): keep '&' intact in mailto links with multiple query params
Move the email-handling helpers (obfuscate, mail_isvalid, mail_quotedprintable_encode, mail_setup) out of the procedural inc/mail.php into a namespaced dokuwiki\MailUtils class plus a new Mailer::configInit(), and add a separate MailUtils::obfuscateUrl() for the mailto-href context.
The xhtml renderer and PluginTrait now build the link label and the href separately: the address half is run through the mailguard obfuscation, the query string is preserved verbatim with only HTML escaping applied. This fixes #1690 — in 'visible' mode the previous code rawurlencoded the entire address+query, turning '?' into '%3F' and breaking multi-parameter mailto links; in all modes the query string is no longer mangled by the [at]/[dot] substitution.
Core call sites (Mailer, auth, LegacyApiCore, common, the xhtml renderer, the parser, the bundled config/styling/usermanager plugins) are migrated to MailUtils directly. The old top-level functions and PREG_PATTERN_VALID_EMAIL constant remain as deprecated shims with rector mappings.
Tests for obfuscate / mail_isvalid / mail_quotedprintable_encode are consolidated into a single _test/tests/MailUtilsTest.php and extended with regression coverage for the multi-parameter, double-escape and URL-shape cases.
Closes #1690 Replaces #1964
show more ...
|
| 01e8d739 | 25-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
refactor(changelog): persist external-edit detection on first read
This addresses the flaky test that makes tests randomly fail (mostly on windows runners).
The flake in common_saveWikiText_test::t
refactor(changelog): persist external-edit detection on first read
This addresses the flaky test that makes tests randomly fail (mostly on windows runners).
The flake in common_saveWikiText_test::test_savesequence5 came from this line in ChangeLog::getCurrentRevisionInfo():
'date' => max($lastRev + 1, time() - 1)
The synthesized "external delete" entry was kept in memory only and only persisted later, when saveWikiText next called detectExternalEdit. That meant the formula was evaluated twice on different ChangeLog instances — once during the test's inspection, and again during the following saveWikiText — and the two evaluations could pick different seconds depending on how long the surrounding I/O took. The test cached the first result in $expectExternal and asserted it against the on-disk entry written during the second call. On the slower Windows runner the second call sometimes crossed a second boundary, producing the off-by-one date mismatch.
The questions I had was, why are we persisting external file deletions (or edits) only when a page is saved when we are obviously already detecting it earlier during the changelog read already?
Instead of recording the external delete at the time a new page is written, it makes sense to record it as soon as we detect it (when the changelog is requested by a user or a bot). This will make the recoded timestamp closer to the actual deletion.
This patch refactors the changelog accordingly, but still tries to be minimal invasive (I think the changelog handling would need much more refactoring, but that's beyond the scope of this change).
To enable proper locking (when logging an external edit and copying the attic file), locking had to be moved to the Changelog class, duplicating some code of io_saveFile.
PageFile::detectExternalEdit() and the deprecated procedural wrapper detectExternalEdit() in inc/common.php are removed. A codesearch.dokuwiki.org check confirmed no plugin calls the method directly; the only external caller of the procedural function is the farmsync plugin, which needs a parallel update.
show more ...
|
| 90c2f6e3 | 18-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
Clean up stale realip references after client_ip_header rename
Update docblocks in Ip.php and common.php, fix old tests to use the new config key, remove outdated translations, fix method casing in
Clean up stale realip references after client_ip_header rename
Update docblocks in Ip.php and common.php, fix old tests to use the new config key, remove outdated translations, fix method casing in test, and add example to English config description.
show more ...
|
| 504c13e8 | 18-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
migrate parser tests to modern namespaced classes
Move old-style parser tests from _test/tests/inc/parser/ to namespaced test classes under _test/tests/Parsing/ParserMode/ and _test/tests/Parsing/Le
migrate parser tests to modern namespaced classes
Move old-style parser tests from _test/tests/inc/parser/ to namespaced test classes under _test/tests/Parsing/ParserMode/ and _test/tests/Parsing/Lexer/.
- Add ParserTestBase with assertCalls() helper for comparing handler call sequences with byte index stripping - Split lexer.test.php into ParallelRegexTest, StateStackTest, and LexerTest with a RecordingHandler that extends Handler - Merge handler_parse_highlight_options tests into CodeTest - Add new tests for previously untested modes: Nocache, Notoc, Rss, and all individual formatting modes (Strong, Emphasis, etc.) - Modernize test code: [] syntax, lowercase null, correct assertEquals argument order, replace deprecated withConsecutive and string callables - Renderer tests remain in old location (renderers not yet migrated)
show more ...
|
| 71096e46 | 18-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
move handler methods into ParserMode classes and rename Handler
Each ParserMode class now implements handle() from ModeInterface, containing the token handling logic that previously lived as individ
move handler methods into ParserMode classes and rename Handler
Each ParserMode class now implements handle() from ModeInterface, containing the token handling logic that previously lived as individual methods on Doku_Handler.
The Handler class (formerly Doku_Handler) is the single dispatch point: Lexer passes tokens to Handler::handleToken() which routes to mode objects, plugins, or returns false. The Lexer only tokenizes and resolves mapHandler aliases.
Key changes: - Add handle() to ModeInterface, implemented by all mode classes - Move Doku_Handler to dokuwiki\Parsing\Handler namespace - File extends Code (shared parsing via $type property) - Quotes uses mapHandler() + Handler::getModeName() for sub-modes - Media::parseMedia() replaces Doku_Handler_Parse_Media() - Code::parseHighlightOptions() replaces parse_highlight_options() - Per-parse state (footnote, doublequote) stays on Handler - Deprecated wrappers kept for base/header/internallink/media - Class alias and rector rules added for backward compatibility
show more ...
|
| 1f443476 | 16-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
split Formatting into individual classes per formatting type
Introduce AbstractFormatting as a base class and seven concrete classes (Strong, Emphasis, Underline, Monospace, Subscript, Superscript,
split Formatting into individual classes per formatting type
Introduce AbstractFormatting as a base class and seven concrete classes (Strong, Emphasis, Underline, Monospace, Subscript, Superscript, Deleted) that each define their own patterns and sort order. Delete the old Formatting class and update tests to use the new classes directly. ModeRegistry now treats formatting modes as regular built-in modes.
show more ...
|
| c8dd1b9d | 16-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
introduce ModeRegistry to encapsulate parser mode categories
Replace the global $PARSER_MODES definition in inc/parser/parser.php with a ModeRegistry singleton that initializes and manages the mode
introduce ModeRegistry to encapsulate parser mode categories
Replace the global $PARSER_MODES definition in inc/parser/parser.php with a ModeRegistry singleton that initializes and manages the mode categories. The global array is still populated for backward compatibility with plugins that access it directly.
Mode constructors now use ModeRegistry::getModesForCategories() instead of accessing the global directly. p_get_parsermodes() and p_sort_modes() are moved to inc/deprecated.php as thin wrappers.
show more ...
|
| 8ab4ec30 | 16-Apr-2026 |
Andreas Gohr <gohr@cosmocode.de> |
remove dead ParallelRegex::apply() method
Remove apply() which was never called from production code. Rewrite the inherited SimpleTest tests to use split() instead, and add a test for pre/post-match
remove dead ParallelRegex::apply() method
Remove apply() which was never called from production code. Rewrite the inherited SimpleTest tests to use split() instead, and add a test for pre/post-match splitting.
show more ...
|
| fe6048cc | 14-Apr-2026 |
Alexander Lehmann <alexlehm@gmail.com> |
remove realip option, add default in conf/dokuwiki.php |
| bfc167db | 11-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
Limit namespace depth in io_createNamespace() #4613
Throw a RuntimeException when the given ID contains 128 or more colon-separated segments, preventing creation of excessively deep directory hierar
Limit namespace depth in io_createNamespace() #4613
Throw a RuntimeException when the given ID contains 128 or more colon-separated segments, preventing creation of excessively deep directory hierarchies.
show more ...
|
| 5e9d26e3 | 07-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
SearchIndex: move search() function tests back to tests/inc/search/
The search.test.php file tests the search() function from inc/search.php, not the Search namespace classes. It was incorrectly mov
SearchIndex: move search() function tests back to tests/inc/search/
The search.test.php file tests the search() function from inc/search.php, not the Search namespace classes. It was incorrectly moved into tests/Search/ during the test suite reorganization. Move it and its data files (ns1/, ns2/) back to their original location, keeping only searchtest.txt in tests/Search/data/ where it belongs.
show more ...
|
| e1272c08 | 07-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
SearchIndex: add backward compatibility wrappers
Add deprecated wrappers for idx_* and ft_* functions that were removed when inc/indexer.php and inc/fulltext.php were replaced by the new Search clas
SearchIndex: add backward compatibility wrappers
Add deprecated wrappers for idx_* and ft_* functions that were removed when inc/indexer.php and inc/fulltext.php were replaced by the new Search classes. These wrappers delegate to the new architecture and ensure existing plugins continue to work.
Deprecated standalone functions: idx_get_indexer, idx_getIndex, idx_lookup, idx_listIndexLengths, idx_indexLengths, ft_pageSearch, ft_backlinks, ft_mediause, ft_pageLookup, ft_snippet, ft_pagesorter, ft_snippet_re_preprocess, ft_queryParser.
Deprecated methods on Indexer: lookupKey, getPages, addMetaKeys, renameMetaValue, getPID, lookup.
Also migrates remaining core callers (Ajax, FeedCreator, ApiCore) to use the new classes directly and fixes a UTF-8 case folding bug in MetadataSearch title lookups.
show more ...
|
| 74a9499c | 07-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
SearchIndex: remove legacy intermediate classes from PR #2943
Remove FulltextIndex, MetadataIndex, and the old AbstractIndex which were introduced as a stepping stone in #2943. All callers now use t
SearchIndex: remove legacy intermediate classes from PR #2943
Remove FulltextIndex, MetadataIndex, and the old AbstractIndex which were introduced as a stepping stone in #2943. All callers now use the Collection/Index architecture directly.
Also fix a bug in detail.php where mediause() was called with ignore_perms=true, leaking references from hidden/protected pages to unprivileged users. This bug existed on master as well.
Old test files replaced by their modernized equivalents in tests/Search/.
show more ...
|
| ede46466 | 06-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
SearchIndex: reorganize and expand test suite
Move all Search tests from _test/tests/inc/Search/ to _test/tests/Search/ to match the dokuwiki\test autoloader convention. Fix namespaces from tests\*
SearchIndex: reorganize and expand test suite
Move all Search tests from _test/tests/inc/Search/ to _test/tests/Search/ to match the dokuwiki\test autoloader convention. Fix namespaces from tests\* to dokuwiki\test\* so all tests work in isolation.
Extract inline test helpers into separate autoloadable mock files: TestDirectCollection → MockDirectCollection, TestLookupCollection → MockLookupCollection, TestFrequencyCollection → MockFrequencyCollection.
Rename AbstractIndexTest → AbstractIndexTestCase to fix PHPUnit warning about abstract classes with Test suffix.
Replace dead xxxRealWord() with proper testWildcardSearch() verifying exact token matches and frequencies for all three wildcard types. Add testTokenizedPageSearch() using a dedicated test data file. Add testNoMatchReturnsEmptyFrequencies() which exposed a bug in Term where uninitialized $tokens/$frequencies caused crashes on zero-match terms.
Replace fulltext_query.test.php with modern QueryParserTest in the Search\Query namespace.
Add new test files: - LockTest: acquire/release, reference counting, stale lock override, foreign lock rejection, releaseAll, independent locks - NamespacePredicateTest: filter/exclude, sub-namespaces, partial prefix safety, empty sets, score preservation - PageSetTest: intersect, unite, subtract, isEmpty - QueryEvaluatorTest: word lookups, AND/OR/NOT, namespace filtering, combined queries, partial namespace prefix safety
Fix Term.php: initialize $tokens and $frequencies to [] instead of null.
show more ...
|