| 315c18b1 | 07-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(subscription): use a literal & separator in plain-text list mail diff links
The diff link in 'list' subscription mails was built once with wl()'s default & separator and reused in both the H
fix(subscription): use a literal & separator in plain-text list mail diff links
The diff link in 'list' subscription mails was built once with wl()'s default & separator and reused in both the HTML and the plain-text list. In a plain-text mail client the entity is not decoded, so clicking the link sends do=diff&rev=... and the revision is parsed as a bogus amp;rev parameter, losing the intended revision. Build a separate plain-text link with a literal & separator, matching the adjacent subscribe link.
show more ...
|
| 42685d0d | 07-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(feed): flatten repeated HTTP response headers when fetching feeds
DokuHTTPClient stores a response header that occurs more than once (e.g. multiple Set-Cookie headers) as a nested array. FeedPar
fix(feed): flatten repeated HTTP response headers when fetching feeds
DokuHTTPClient stores a response header that occurs more than once (e.g. multiple Set-Cookie headers) as a nested array. FeedParserFile cast each header value to a string, turning those into the literal "Array" and emitting an "Array to string conversion" warning on every such fetch. Normalize each value on its own so repeated headers keep all their values in SimplePie's list representation.
show more ...
|
| 5cf0f76c | 07-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
ci: enable the gd extension in the test workflows
The Windows runner had no image backend, so the media resize helpers threw and returned the original file. This went unnoticed until c3a14f677 added
ci: enable the gd extension in the test workflows
The Windows runner had no image backend, so the media resize helpers threw and returned the original file. This went unnoticed until c3a14f677 added the first test asserting real resized dimensions. Enable gd on both the Windows and Linux workflows so the media subsystem is actually exercised instead of relying on gd being present by chance.
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 ...
|
| 7595d8da | 06-Jul-2026 |
splitbrain <86426+splitbrain@users.noreply.github.com> |
Rector and PHPCS fixes |
| 5a285deb | 06-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(changelog): stop media mtime bumps from recording bogus external edits
Commit 01e8d739c ("persist external-edit detection on first read") began writing detected external edits to the changelog a
fix(changelog): stop media mtime bumps from recording bogus external edits
Commit 01e8d739c ("persist external-edit detection on first read") began writing detected external edits to the changelog and snapshotting the new content to the attic via saveExternalAttic(). That is correct for pages, which archive every revision, but wrong for media: a media file's current revision is never archived (media only copies content to the attic on replace or delete, to save space).
So getCurrentRevisionInfo()'s external-change detection had no attic copy of the current revision to work with: the unchanged-content guard always failed and the old-size lookup returned 0. A mere mtime bump (touch, rsync --times, unzip) of an unchanged media file was therefore recorded as an external edit sized as the whole file, and since detection is now persisted, that bogus entry (plus a redundant attic copy) became permanent.
The "before" size of an external change is now taken through a lastRevisionSize() seam: the base reads it from the last revision's attic copy (unchanged for pages), and MediaChangeLog reconstructs it from the previous revision's archived size plus the size change logged for the last revision. currentContentMatchesRevision() compares the current file size against that reconstructed size, so an unchanged size is treated as a mere touch (mtime reset, no entry) and a changed size is a real external edit with the correct size change.
Media no longer snapshots an external edit to the attic at all, consistent with not archiving the current revision: it will be archived if and when the file is later replaced. The mtime repair for an unreliable external date moved to a base repairExternalMtime() shared by pages and media.
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 ...
|
| 9a38b8db | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(parser): keep a mode's directly-assigned allowedModes when it also declares categories
setModeRegistry() replaced $this->allowedModes with the category-derived list whenever a mode declared cate
fix(parser): keep a mode's directly-assigned allowedModes when it also declares categories
setModeRegistry() replaced $this->allowedModes with the category-derived list whenever a mode declared categories, discarding any mode names the subclass had assigned to the property directly. The old SyntaxPlugin::accepts() merged the category modes into the existing list instead, so a mode using the sibling-component pattern - a non-empty getAllowedTypes() plus $this->allowedModes[] = 'plugin_foo_bar' in its constructor - kept both and its sibling syntax nested as intended.
Merge the category-derived modes into the directly-assigned list rather than replacing it, then deduplicate. The no-categories path is unchanged: the directly-assigned list is used as-is.
show more ...
|
| 945681fc | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(parser): keep an indented list from being swallowed by a table
The parser rework dropped the list-awareness lookahead from preformatted's entry patterns ('\n (?![\*\-])' became '\n '). In base
fix(parser): keep an indented list from being swallowed by a table
The parser rework dropped the list-awareness lookahead from preformatted's entry patterns ('\n (?![\*\-])' became '\n '). In base mode this is masked by sort order - listblock sorts before preformatted and wins the tie - but table mode connects preformatted as a protected sub-mode without connecting any list mode. Inside a table an indented list line therefore entered preformatted instead of ending the table, and the table rewriter discarded the resulting call: the list vanished from the output and the table's section-edit range grew to cover it, so saving via the inline table editor overwrote the list text.
Restore the lookahead. For DokuWiki syntax it is the original '(?![\*\-])' (Listblock treats a bare * or - as a marker). For Markdown-preferred syntax the guard follows GfmListblock, rejecting -, *, + and ordered markers only when followed by whitespace or end of line, so mixed dw+md / md+dw wikis are fixed too while an indented code block that merely starts with such a character (e.g. a *** line) still parses as code.
show more ...
|
| ebaa8481 | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(search): restore histogram() as a Collection method with a BC shim
The indexer rework dropped Doku_Indexer::histogram(), which plugins reach through idx_get_indexer()->histogram() to build tag c
fix(search): restore histogram() as a Collection method with a BC shim
The indexer rework dropped Doku_Indexer::histogram(), which plugins reach through idx_get_indexer()->histogram() to build tag clouds (e.g. tagfilter via the 'subject' metadata index). Calling it fatalled through LegacyIndexer::__call as an undefined method.
Add histogram() to AbstractCollection: it sums each token's frequency across all entities and returns them ordered by frequency, filtered by min/max/minlen, handling both length-split (fulltext) and single-group (metadata) collections. DirectCollection overrides it for the 1:1 title case (count shared values). LegacyIndexer::histogram() is a deprecated shim dispatching by $key to the matching collection.
show more ...
|
| 21d6f176 | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(parser): restore BC for manual Handler/Parser construction
The parser rework made ModeRegistry a required constructor argument on Handler and Parser and dropped the Doku_Parser class alias, brea
fix(parser): restore BC for manual Handler/Parser construction
The parser rework made ModeRegistry a required constructor argument on Handler and Parser and dropped the Doku_Parser class alias, breaking the long-documented manual sub-parsing pattern used by many plugins (new Doku_Handler(); new Parser($handler); ...): both threw ArgumentCountError and new Doku_Parser() fatalled as an unknown class.
Default the ModeRegistry argument on both constructors: Handler builds a registry for the configured syntax, Parser takes it from the handler. Re-add the Doku_Parser alias. Constructing without a registry emits a deprecation pointing at p_get_instructions().
show more ...
|
| e248eb6f | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(parser): don't drop document content after a blank indented line
Preformatted's exit pattern (?=\n[^ \t\n]) is a lookahead-only exit: it consumes no byte so the boundary \n stays in the stream f
fix(parser): don't drop document content after a blank indented line
Preformatted's exit pattern (?=\n[^ \t\n]) is a lookahead-only exit: it consumes no byte so the boundary \n stays in the stream for a following block mode (an <hr> or header after an indented code block) to anchor on.
When that exit fired with nothing else consumed - a "blank" line that is empty after its indent, followed by a column-0 line - the match was zero-width at the current offset and the lexer's no-advance guard aborted the whole parse, silently discarding the rest of the document. Reachable on ordinary trailing whitespace ("abc\n \nmore") and inside footnotes.
Exempt zero-width MODE_EXIT matches from the guard: popping the mode stack is real progress and leaves the boundary byte for the parent mode to consume on the next iteration. The stack strictly shrinks on each such exit, so this cannot loop; the infinite-loop protection is unchanged for every other zero-width match. Preformatted's pattern is left as-is, so <hr>/header after an indented code block still works, including after a blank indented line.
Add regressions for the blank-line, blank-line-after-code, and boundary-preservation (hr after a blank indented line) cases.
show more ...
|
| 9e4de2f7 | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(search): don't let collections release a caller-owned index lock
Indexer::addPage() locks the page index once and hands the same instance to several collections, each of which lock() and unlock(
fix(search): don't let collections release a caller-owned index lock
Indexer::addPage() locks the page index once and hands the same instance to several collections, each of which lock() and unlock() it around their work.
AbstractCollection::lock() locked and tracked every index for later release. When a shared AbstractIndex was already locked by the caller, locking it again was a no-op but the collection still tracked it, so the collection's unlock() released the caller's lock and flipped the shared index read-only. Each later collection then re-acquired and re-released it, leaving windows in which another process could grab the page lock mid-operation; contention could abort addPage() with the title index updated but the fulltext index stale.
Treat a shared index that is already writable as owned by its creator: leave its lock untouched and only manage the locks the collection acquires itself.
Add a regression test asserting a caller-held index lock survives repeated collection lock()/unlock() cycles.
show more ...
|
| 4e61aa71 | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(search): create index lock directories inside data/locks
Index locks were built by concatenating the configured lock directory and the index name without a path separator.
That produced paths l
fix(search): create index lock directories inside data/locks
Index locks were built by concatenating the configured lock directory and the index name without a path separator.
That produced paths like data/lockspage.index instead of data/locks/page.index, so index lock directories were created in the data root rather than inside the configured lock directory.
Fix this by joining the lock directory and index name with an explicit slash, and update the lock test to assert the correct location and guard against the old broken path.
show more ...
|
| bc12b8fe | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(search): read split index suffixes correctly
AbstractIndex::max() used /(\d)+\.idx$/ to determine the highest split index suffix. For filenames like w10.idx or w12.idx that regex captured only t
fix(search): read split index suffixes correctly
AbstractIndex::max() used /(\d)+\.idx$/ to determine the highest split index suffix. For filenames like w10.idx or w12.idx that regex captured only the last digit, so the maximum token-length group was truncated to 9. Wildcard searches therefore skipped all fulltext index shards for words with 10 or more characters.
Tighten the match to the basename and require the current index name followed immediately by digits: ^<idx>(\d+)\.idx$. This captures the full numeric suffix and also avoids counting unrelated index families that share the same prefix, such as treating wiki2.idx as a numbered shard of the w index.
Add regressions for both behaviors: multi-digit suffixes are parsed correctly, and same-prefix indexes are ignored when determining the maximum shard number.
show more ...
|
| b1f7ba64 | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(search): fatals on invalid short wildcard terms
Searches such as "wiki a*" could crash in QueryEvaluator::opAnd() with a TypeError. The query parser still emitted the wildcard term, but the full
fix(search): fatals on invalid short wildcard terms
Searches such as "wiki a*" could crash in QueryEvaluator::opAnd() with a TypeError. The query parser still emitted the wildcard term, but the fulltext search later dropped it because its non-wildcard base was below the minimum search term length.
That left the evaluator with an AND operator whose right-hand operand was missing, causing a stack underflow during RPN evaluation.
Fix this in two places: - filter invalid short wildcard terms already in Tokenizer::getWords() when wildcard parsing is enabled, so they never enter the parsed query - harden QueryEvaluator against missing operands for AND/OR/NOT to avoid fatals if tokens disappear during later processing
Add regression tests covering the parser output for "wiki a*" and the evaluator behavior when an operand is missing.
show more ...
|
| e282ae55 | 05-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix(search): search index corruption in updateTuple
Require an end-of-tuple boundary when removing an existing tuple in TupleOps::updateTuple(), so updating row ID 17 no longer corrupts tuples for I
fix(search): search index corruption in updateTuple
Require an end-of-tuple boundary when removing an existing tuple in TupleOps::updateTuple(), so updating row ID 17 no longer corrupts tuples for IDs like 170 or 171.
Add regression tests covering both replacement and deletion when one numeric row ID is a decimal prefix of another.
show more ...
|
| 45717242 | 03-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
Skip JPEG markers too short to hold the probed signature
_parseMarkerXmp() and the other signature-probing loops read a fixed-length signature from every marker of a given type to identify it. Since
Skip JPEG markers too short to hold the probed signature
_parseMarkerXmp() and the other signature-probing loops read a fixed-length signature from every marker of a given type to identify it. Since _getFixedString() throws on a short read, a valid but short APP1 segment (e.g. a 22-byte non-XMP APP1) made the XMP probe throw while reading 29 bytes. That was logged as a damaged image and aborted the marker loop, discarding any real XMP/EXIF segment that followed.
The issue was introduced in c9c56f8a0.
This is another symptom fix until the library is replaced as discussed in #1970
Skip markers too short to contain the signature so probing continues instead of failing. Fixes #4526.
show more ...
|
| baea39e3 | 03-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
Handle array EXIF date values
This is a fix for a warning thrown by the EXIF library. It fixes a specific symptom without addressing the actual issue of the library being outdated and in need of rep
Handle array EXIF date values
This is a fix for a warning thrown by the EXIF library. It fixes a specific symptom without addressing the actual issue of the library being outdated and in need of replacement (as discussed in #1970)
fixes #4628
Co-authored by Zheyon <110686204+Zhey-on@users.noreply.github.com>
show more ...
|
| 10287e99 | 25-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
Cache: key on resolved base URL instead of the raw Host header
The instruction (.i), render (.xhtml) and metadata cache keys embedded the client-supplied HTTP_HOST and SERVER_PORT, as did the JS and
Cache: key on resolved base URL instead of the raw Host header
The instruction (.i), render (.xhtml) and metadata cache keys embedded the client-supplied HTTP_HOST and SERVER_PORT, as did the JS and CSS dispatchers. A crawler varying the Host header - or a wiki reachable under several names - thus multiplied every cached artifact per host value, growing the cache directory without bound.
Instructions and metadata never embed resolved URLs, so they drop the host component entirely. Rendered output and the JS/CSS bundles key on DOKU_BASE (plus the SSL flag for JS) instead: fixed when baseurl is configured, and only varying for genuine multi-domain canonical setups. Feeds keep host/port keying since their links are always absolute.
This should address the obvious cases mentioned in #4574
show more ...
|
| 3e37eccf | 03-Jul-2026 |
Andreas Gohr <andi@splitbrain.org> |
Harden media manager select callback (defense-in-depth)
The media manager popup derives the select callback name and the target edit-field id from URL parameters (onselect, edid) and calls opener[cb
Harden media manager select callback (defense-in-depth)
The media manager popup derives the select callback name and the target edit-field id from URL parameters (onselect, edid) and calls opener[cb](edid, ...). Tighten this handling:
- sanitize edid to word characters and hyphens - strip all non-word characters from the callback name (global replace, not just the first run) - refuse to call anything that is not a page-script function, rejecting browser/JS built-ins such as eval, Function, setTimeout and open
This is a precaution, not a fix for an exploitable vulnerability. In core the popup is only ever opened by the trusted, same-origin editor, which builds the URL itself and never sets onselect; a directly loaded popup has no opener and a cross-origin opener is blocked by the same-origin policy, so an attacker cannot reach the sink. The change removes the reliance on that ambient browser protection and guards plugins that open the popup with their own callback.
show more ...
|
| ed28f990 | 01-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
use actionOK() to gate the register action
The Register action duplicated the openregister and addUser capability checks that actionOK('register') already performs. Delegate to it so the action gate
use actionOK() to gate the register action
The Register action duplicated the openregister and addUser capability checks that actionOK('register') already performs. Delegate to it so the action gate and register() itself share a single source of truth.
See https://github.com/dokuwiki/dokuwiki/issues/4682#issuecomment-4854987500
show more ...
|
| aa331cf2 | 29-Jun-2026 |
Martin Růžička <martinr@post.cz> |
Translation update (cs) |
| d18485a9 | 29-Jun-2026 |
Andreas Gohr <gohr@cosmocode.de> |
some clarification on the syntax page
With merging the changes for the markdown support some smaller changes in how the DokuWiki syntax is parsed were introduced. This adds the most important rules
some clarification on the syntax page
With merging the changes for the markdown support some smaller changes in how the DokuWiki syntax is parsed were introduced. This adds the most important rules to the syntax page.
show more ...
|
| d13b5779 | 28-Jun-2026 |
Andreas Gohr <andi@splitbrain.org> |
fix duplicated TOC caused by corrupted persistent metadata (#3179)
The table of contents is appended to the metadata on every render. When a tableofcontents entry leaks into the persistent metadata
fix duplicated TOC caused by corrupted persistent metadata (#3179)
The table of contents is appended to the metadata on every render. When a tableofcontents entry leaks into the persistent metadata (e.g. via buggy plugin versions or callers of p_set_metadata that write back a full description array), document_start() copied it into the current metadata before the headings re-appended it, permanently duplicating the TOC.
The duplication was only visible when the XHTML output was served from cache, since a fresh render populates the global $TOC from a single pass. This explains why purging the cache or editing the page fixed it temporarily while a reload brought it back.
Strip the tableofcontents from the persistent metadata in document_start() so already-corrupted .meta files heal themselves on the next render.
show more ...
|