| e014d4c8 | 20-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
docs: updated the security policy
More details for out-of-scope issue including previous rejected examples. |
| fe58309e | 16-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
extension: don't crash on malformed conflict/dependency ids
The conflicts and depends fields in the repository metadata are free form text entered by extension authors and may contain values that ar
extension: don't crash on malformed conflict/dependency ids
The conflicts and depends fields in the repository metadata are free form text entered by extension authors and may contain values that are not valid extension ids, such as "sprintdoc template". Since 9af82229f routed every Extension construction through the strict setBase() validation, such a value made Extension::createFromId() throw an uncaught RuntimeException while building the notices, taking down the whole extension manager.
Skip entries that cannot be parsed into an extension id when checking for missing dependencies and conflicts.
As an immeadiate fix this will also be fixed in the repository API to avoid sending invalid extension IDs to the extension manager.
Fixes #4691
show more ...
|
| 6d10fb86 | 14-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
style(changelog): fix overlong line |
| 057ab5a0 | 14-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
ci: update workflow actions to their latest releases
Bumps actions/checkout to v7, actions/cache to v6, actions/github-script to v9, peter-evans/create-pull-request to v8 and softprops/action-gh-rel
ci: update workflow actions to their latest releases
Bumps actions/checkout to v7, actions/cache to v6, actions/github-script to v9, peter-evans/create-pull-request to v8 and softprops/action-gh-release to v3. The cache bump is required since the v3 cache service backend was retired. All existing inputs and outputs remain compatible.
show more ...
|
| a37b7ba5 | 14-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(parser): guard row_counter in tablerow_open()
The row counter used for row CSS classes was only initialized in table_open(). Renderers that open a row without a preceding table_open() call (such
fix(parser): guard row_counter in tablerow_open()
The row counter used for row CSS classes was only initialized in table_open(). Renderers that open a row without a preceding table_open() call (such as the data plugin's empty-result list) triggered an 'Undefined array key row_counter' warning on PHP 8. Initialize it defensively, mirroring the existing cell_counter handling.
show more ...
|
| b22130d2 | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(parser): stop group-quantifier body patterns from exhausting memory
Several patterns repeat a group — (?:…)* / (?:…)+ — over a body that can span a large region. On the non-JIT PCRE engine each
fix(parser): stop group-quantifier body patterns from exhausting memory
Several patterns repeat a group — (?:…)* / (?:…)+ — over a body that can span a large region. On the non-JIT PCRE engine each iteration of a group repetition keeps its own backtracking frame, so a big body retains one frame per byte: a linear, unbounded memory spike that can fatal a request (and, with JIT on, trips pcre.jit_stacklimit so the lexer dumps the rest of the document as literal text). A repetition over a single item (.* , [^x]+) is optimized by PCRE and unaffected.
Make each body possessive: the repeated group cannot match the delimiter that follows it, so it always stops at the first closer and never needs to backtrack.
- media {{…}}, windowssharelink, gfm_quote, gfm_link: possessive body. - email validation (used by emaillink and mail_isvalid): possessive local-part and domain groups; a long dotted `a.a.a…` local part or domain was a ReDoS vector. Match results are unchanged.
show more ...
|
| 3a56ec9b | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(parser): stop unclosed GFM fenced code and emphasis from exhausting memory
Both modes repeat a group over their body, and on the non-JIT PCRE engine each iteration of a group repetition keeps it
fix(parser): stop unclosed GFM fenced code and emphasis from exhausting memory
Both modes repeat a group over their body, and on the non-JIT PCRE engine each iteration of a group repetition keeps its own backtracking frame. On unclosed input the body runs to end of input (or a paragraph break), the required closer fails, and the engine unwinds frame by frame: a linear, unbounded memory spike that can fatal a request once the body grows past the memory limit (and, with JIT on, trips pcre.jit_stacklimit so the lexer dumps the rest of the document as literal text).
gfm_code/gfm_file: the fence body (?:(?!CLOSE).)* already stops at the first valid closer, so no backtracking into it is ever legitimate. Make it possessive so the no-closer case fails immediately.
gfm_emphasis: its closer lookahead ended with a character the body had to backtrack to reach (the [^\s*] before the closing *). Verify that flanking-closer rule with a lookbehind instead, so the [^*] body — which cannot match the * it stops at — can be possessive too.
Matching is unchanged for well-formed input. The fenced-code patterns were introduced in b1c59bed2.
show more ...
|
| 1c00c021 | 09-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(parser): validate inline formatting closers with a single memoized scan
The inline formatting modes only open a span when a valid closer exists ahead. That check was a lookahead built on CONTENT
fix(parser): validate inline formatting closers with a single memoized scan
The inline formatting modes only open a span when a valid closer exists ahead. That check was a lookahead built on CONTENT_UNTIL_PARA, tested character by character up to the next paragraph break and re-evaluated from scratch for every opener candidate — openers times paragraph length. With pcre.jit=0 a crafted 32KB page took 16s and an ordinary 34KB page with long paragraphs 37s; with the JIT on (the PHP default) the per-character lookahead exhausted the JIT stack, the match silently failed, and the formatting — or everything after it — rendered as plain text.
The check also decided the wrong thing. It scanned raw text, so a closer lookalike inside content the lexer consumes atomically — a nowiki or %% span, a backtick code span, a link, a URL — counted as a real closer even though the mode's exit pattern can never fire there. And it ignored the enclosing span: an inner delimiter whose only closer lay past the closer of the mode it sits in was entered anyway, so a stray delimiter paired with one in a following sibling span and dragged the boundary along — the `*` in ''glob/*.conf'' joined the `*` of the next ''...'' span and corrupted the paragraph; the same held for //, ** and __ inside monospace, and an emphasis opened inside ((...)) ran past the footnote's )) and the enclosing bold's **.
Each formatting mode now declares its closer through Lexer::addCloserPattern(), mirroring addExitPattern(), and the lexer answers "does a valid closer exist ahead" with one anchored possessive scan per range instead of a lookahead per opener:
- The scan runs left to right from the opener, hopping over opaque spans derived from already-registered patterns — a plain or special match is consumed in one step, an entry into a verbatim mode (nowiki, the backtick code spans) extends to that mode's first exit — so a closer lookalike inside consumed content is never mistaken for a closer. Each hop finds the earliest of boundary, closer, or opaque span in a single leftmost search, keeping the check linear. - An opener is rejected when the nearest enclosing mode that has a closer of its own would close before the opener's own closer, so a delimiter that can never close within its span stays literal. That ancestor is found by walking the mode stack past modes that declare no closer (plugins, footnotes); the nearest guarded ancestor suffices, as it was itself validated against its own when it opened. - Both verdicts are memoized and reset per parse() run: a proven closer validates every earlier candidate, and a proven closer-free range rejects every later candidate before the next boundary. With the lexer consuming each opened span, the whole parse is linear in document size.
Closer patterns match the closing delimiter itself with flanking context in lookarounds — the convention exit patterns already follow — so closer positions compare exactly across modes and a closer directly after an inner opener is seen. AbstractFormatting derives the closer from the exit pattern and registers it with the paragraph break as the boundary, preserving the rule that formatting never spans paragraphs; a mode with other needs can pass a different boundary or none.
Footnote declares its )) as a closer rather than guarding its (( entry with a (?=.*)) lookahead, so the footnote becomes a boundary the scan sees and formatting inside it no longer pairs across the )); its closer takes no paragraph boundary, as footnotes are block-level. GfmEmphasis gains a closer pattern so single * emphasis is validated the same way, while its entry lookahead still enforces CommonMark nearest-delimiter pairing. GfmEmphasis and GfmStrong span bodies cannot contain their delimiter, so their in-pattern lookaheads stay linear on their own; the GFM backtick span bodies get deterministic alternatives with possessive quantifiers, removing their per-character backtracking.
CONTENT_UNTIL_PARA is removed: any entry pattern built with it recreates the quadratic scan. ParallelRegex gains escapePattern() so embedded closer fragments follow the lexer's bare-parenthesis convention, reports PREG_JIT_STACKLIMIT_ERROR so a future JIT exhaustion surfaces instead of silently truncating, and no longer rewrites its registered patterns in place while compiling the compound regex.
The adversarial 32KB page drops to 0.1s, the 37-second benign page to 0.1s, and a 128KB variant stays under 0.6s.
show more ...
|
| 1ed5d398 | 08-Jul-2026 |
splitbrain <86426+splitbrain@users.noreply.github.com> |
Rector and PHPCS fixes |
| 846f53c1 | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(search): lock the metadata registry read-modify-write
updateMetadataRegistry() read metadata.idx, merged in new keys and wrote the file back without holding a lock across the sequence. Two index
fix(search): lock the metadata registry read-modify-write
updateMetadataRegistry() read metadata.idx, merged in new keys and wrote the file back without holding a lock across the sequence. Two indexer processes each registering a different new key could both read the same registry and the later writer would clobber the other's key, dropping it. A dropped key is not cleared from its collection on deletePage(), leaving orphaned index entries.
Guard the whole read-merge-write with a dedicated metadata lock so concurrent registrations can no longer lose a key.
show more ...
|
| d9043e78 | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(search): make DirectCollection::getEntitiesWithData() linear
The method resolved each entity name with a separate retrieveRow() call, each of which rescans the entity index from the start, makin
fix(search): make DirectCollection::getEntitiesWithData() linear
The method resolved each entity name with a separate retrieveRow() call, each of which rescans the entity index from the start, making the whole operation quadratic in the number of entities. This runs on MetadataSearch::getPages('title') and hurts large wikis. Collect the entity IDs in one pass and resolve their names with a single batched retrieveRows() read instead.
show more ...
|
| 45fa8bb2 | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(search): stop FileIndex::retrieveRows() scanning past the last row
The early-exit check compared the array_shift() result against false, but array_shift() returns null on an empty array, so the
fix(search): stop FileIndex::retrieveRows() scanning past the last row
The early-exit check compared the array_shift() result against false, but array_shift() returns null on an empty array, so the break never fired and every call read the index file to EOF after collecting the last requested row. Compare against null and skip the file entirely when nothing is requested.
show more ...
|
| 61dea710 | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(search): wait for a contended index lock and apply dperm
The Lock rewrite (c66b5ec65) made Lock::acquire() fail immediately when the lock directory already existed, where the old indexer lock re
fix(search): wait for a contended index lock and apply dperm
The Lock rewrite (c66b5ec65) made Lock::acquire() fail immediately when the lock directory already existed, where the old indexer lock retried until the holder released it. Concurrent saves or indexer runs then aborted indexing instead of serializing (recoverable only via the .indexed tag on the next edit), and the lock directory was created without the configured directory permissions, unlike every other directory DokuWiki creates.
Mirror the io_lock() convention: wait for a contended lock, bounded by a tunable wait timeout, before throwing; keep clearing locks older than five minutes as stale; and chmod the new lock directory to $conf['dperm']. The give-up path still throws IndexLockException, so the self-healing retry via the .indexed tag is unchanged.
show more ...
|
| 7ec464d9 | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(remote): make aclCheck self-check case-insensitive-backend aware
The check gating aclCheck() for other users compared the requested user against REMOTE_USER verbatim. On a case-insensitive auth
fix(remote): make aclCheck self-check case-insensitive-backend aware
The check gating aclCheck() for other users compared the requested user against REMOTE_USER verbatim. On a case-insensitive auth backend a user naming themselves in a different case than their login was treated as a different user and wrongly denied checking their own ACL. Normalize both names the way auth_isMember() does before comparing.
The self-check was introduced in 884caed92.
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 ...
|
| 9255329a | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(feed): surface real fetch error instead of "unsupported status code"
DokuHTTPClient reports transport failures with negative pseudo statuses while keeping the descriptive message in its error fi
fix(feed): surface real fetch error instead of "unsupported status code"
DokuHTTPClient reports transport failures with negative pseudo statuses while keeping the descriptive message in its error field. SimplePie only turns a file error into a reported error when the status code is zero, so connection, SSL and timeout failures surfaced as "unsupported status code -100" under allowdebug, hiding the real cause. Clamp the pseudo statuses to zero so the descriptive error reaches the feed output.
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 ...
|
| 65ba535c | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(media): suppress exif_read_data() warnings on malformed JPEGs
Reading the EXIF orientation of a JPEG called exif_read_data() without error suppression, so a file with a broken EXIF block emitted
fix(media): suppress exif_read_data() warnings on malformed JPEGs
Reading the EXIF orientation of a JPEG called exif_read_data() without error suppression, so a file with a broken EXIF block emitted a warning that the error handler logged on every media manager, detail page and media diff render.
Bump slika to 1.2.1, which suppresses the warning at the source.
show more ...
|
| 63b22786 | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
docs(parser): fix GfmListblock sub-parser docblock
The class docblock claimed the per-item loop runs inside a ModeRegistry::withSubParser() callback, but the code acquires a pooled sub-parser direct
docs(parser): fix GfmListblock sub-parser docblock
The class docblock claimed the per-item loop runs inside a ModeRegistry::withSubParser() callback, but the code acquires a pooled sub-parser directly and releases it after the loop. Describe the actual mechanism.
show more ...
|
| 6f10c544 | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
chore(changelog): rename $clogRev to $recordedRev
It's a bit easier to decipher |
| 3a5ba395 | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(parser): treat quotes next to bold/underline markup as smart quotes
With typography=2 a single-quoted phrase wrapped in strong or underline markup (**'x'** / __'x'__) rendered apostrophes instea
fix(parser): treat quotes next to bold/underline markup as smart quotes
With typography=2 a single-quoted phrase wrapped in strong or underline markup (**'x'** / __'x'__) rendered apostrophes instead of opening and closing quotes: the lexer matches against the full subject at a byte offset, so the singlequoteopening lookbehind sees the consumed * or _ delimiter and fails, letting apostrophe win. Add * and _ to the quote boundary set, as / (emphasis) already was, so the surrounding markup is recognised as a word boundary.
show more ...
|
| 2d05a06d | 08-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(info): read parser modes from the registry, not the deprecated global
The info plugin's ~~INFO:syntaxtypes~~ read the $PARSER_MODES global directly. Since the mode taxonomy moved into ModeRegist
fix(info): read parser modes from the registry, not the deprecated global
The info plugin's ~~INFO:syntaxtypes~~ read the $PARSER_MODES global directly. Since the mode taxonomy moved into ModeRegistry (c8dd1b9d2), that global is only populated as a side effect of a parse, so on a warm instruction cache with no parse in the request it is unset and the syntax-types table renders empty. ~~INFO:syntaxmodes~~ used the deprecated p_get_parsermodes(), logging a deprecation on every render.
Both now build a ModeRegistry and read the categories and modes from it directly, independent of parse or cache state.
show more ...
|
| 18cdf102 | 07-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(editor): give toolbar and picker buttons an explicit type
Toolbar and picker buttons are created as <button> elements with no type attribute, so they default to type="submit". When a plugin rend
fix(editor): give toolbar and picker buttons an explicit type
Toolbar and picker buttons are created as <button> elements with no type attribute, so they default to type="submit". When a plugin renders the toolbar into a container inside its own form, clicking any button (e.g. a smiley picker) submits that form. Set type="button" so the buttons never trigger a submit.
show more ...
|
| 6a8e48ed | 07-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
fix(extension): skip invalid directories when listing extensions
A leftover directory whose name is not a valid extension base (e.g. "myplugin (copy)") made Extension::setBase() throw, which propaga
fix(extension): skip invalid directories when listing extensions
A leftover directory whose name is not a valid extension base (e.g. "myplugin (copy)") made Extension::setBase() throw, which propagated out of the unguarded getPlugins()/getTemplates() calls and fataled the whole Extension Manager page and the extension list CLI.
Catch the exception per-directory in readExtensionsFromDirectory() so a single stray directory is skipped and logged instead of breaking every caller. The throw is kept for the install path.
show more ...
|
| 85a452e2 | 07-Jul-2026 |
Andreas Gohr <gohr@cosmocode.de> |
feat(admin): show template and syntax on admin info
two more values we always ask for when debugging |