History log of /dokuwiki/inc/Parsing/ (Results 1 – 25 of 98)
Revision Date Author Comments
(<<< Hide modified files)
(Show modified files >>>)
b22130d208-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 ...

3a56ec9b08-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 ...

1c00c02109-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 ...


/dokuwiki/_test/tests/Feed/FeedParserTest.php
/dokuwiki/_test/tests/Parsing/Lexer/LexerTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/FormattingTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmDeletedTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisStrongTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisStrongUnderscoreTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisUnderscoreTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmStrongUnderscoreTest.php
/dokuwiki/_test/tests/Remote/ApiCoreAclCheckTest.php
/dokuwiki/_test/tests/Search/Index/LockTest.php
/dokuwiki/_test/tests/Search/IndexerTest.php
/dokuwiki/_test/tests/inc/httputils_xaccel.test.php
/dokuwiki/_test/tests/inc/infoutils_getversiondata.test.php
/dokuwiki/composer.json
/dokuwiki/composer.lock
/dokuwiki/inc/Feed/FeedParserFile.php
Lexer/CloserPattern.php
Lexer/Lexer.php
Lexer/ParallelRegex.php
Lexer/StateStack.php
ParserMode/AbstractFormatting.php
ParserMode/AbstractMode.php
ParserMode/Deleted.php
ParserMode/Emphasis.php
ParserMode/Footnote.php
ParserMode/GfmBacktickDouble.php
ParserMode/GfmBacktickSingle.php
ParserMode/GfmDeleted.php
ParserMode/GfmEmphasis.php
ParserMode/GfmEmphasisStrong.php
ParserMode/GfmEmphasisStrongUnderscore.php
ParserMode/GfmEmphasisUnderscore.php
ParserMode/GfmLink.php
ParserMode/GfmStrongUnderscore.php
ParserMode/Monospace.php
ParserMode/Strong.php
ParserMode/Subscript.php
ParserMode/Superscript.php
ParserMode/Underline.php
/dokuwiki/inc/Remote/ApiCore.php
/dokuwiki/inc/Search/Collection/DirectCollection.php
/dokuwiki/inc/Search/Index/FileIndex.php
/dokuwiki/inc/Search/Index/Lock.php
/dokuwiki/inc/Search/Indexer.php
/dokuwiki/inc/Ui/Admin.php
/dokuwiki/inc/httputils.php
/dokuwiki/inc/infoutils.php
/dokuwiki/lib/plugins/info/syntax.php
/dokuwiki/vendor/composer/installed.json
/dokuwiki/vendor/composer/installed.php
/dokuwiki/vendor/splitbrain/slika/src/ImageInfo.php
63b2278608-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 ...

3a5ba39508-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 ...

2d05a06d08-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 ...

7595d8da06-Jul-2026 splitbrain <86426+splitbrain@users.noreply.github.com>

�� Rector and PHPCS fixes

9a38b8db05-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 ...

945681fc05-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 ...

21d6f17605-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 ...

e248eb6f05-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 ...


/dokuwiki/_test/composer.lock
/dokuwiki/_test/tests/Feed/FeedParserTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/PreformattedTest.php
/dokuwiki/_test/tests/Remote/ApiCoreAclCheckTest.php
/dokuwiki/_test/tests/Search/Collection/CollectionSearchTest.php
/dokuwiki/_test/tests/Search/Collection/LookupCollectionTest.php
/dokuwiki/_test/tests/Search/Index/LockTest.php
/dokuwiki/_test/tests/Search/Index/MemoryIndexTest.php
/dokuwiki/_test/tests/Search/Index/TupleOpsTest.php
/dokuwiki/_test/tests/Search/Query/QueryEvaluatorTest.php
/dokuwiki/_test/tests/Search/Query/QueryParserTest.php
/dokuwiki/_test/tests/inc/IpTest.php
/dokuwiki/composer.lock
/dokuwiki/data/deleted.files
/dokuwiki/data/pages/wiki/syntax.txt
/dokuwiki/inc/Action/Register.php
/dokuwiki/inc/Cache/CacheParser.php
/dokuwiki/inc/Cache/CacheRenderer.php
/dokuwiki/inc/Extension/AuthPlugin.php
/dokuwiki/inc/Feed/FeedParserFile.php
/dokuwiki/inc/Ip.php
/dokuwiki/inc/JpegMeta.php
Lexer/Lexer.php
ParserMode/GfmQuote.php
/dokuwiki/inc/Remote/ApiCore.php
/dokuwiki/inc/Search/Collection/AbstractCollection.php
/dokuwiki/inc/Search/Index/AbstractIndex.php
/dokuwiki/inc/Search/Index/Lock.php
/dokuwiki/inc/Search/Index/TupleOps.php
/dokuwiki/inc/Search/Query/QueryEvaluator.php
/dokuwiki/inc/Search/Tokenizer.php
/dokuwiki/inc/lang/cs/lang.php
/dokuwiki/inc/media.php
/dokuwiki/inc/parser/metadata.php
/dokuwiki/inc/parser/xhtml.php
/dokuwiki/lib/exe/css.php
/dokuwiki/lib/exe/js.php
/dokuwiki/lib/plugins/authad/auth.php
/dokuwiki/lib/plugins/authldap/auth.php
/dokuwiki/lib/plugins/authpdo/auth.php
/dokuwiki/lib/plugins/authplain/_test/userdata.test.php
/dokuwiki/lib/plugins/authplain/auth.php
/dokuwiki/lib/plugins/config/core/Setting/Setting.php
/dokuwiki/lib/plugins/config/core/Setting/SettingArray.php
/dokuwiki/lib/plugins/config/core/Setting/SettingEmail.php
/dokuwiki/lib/plugins/config/core/Setting/SettingImConvert.php
/dokuwiki/lib/plugins/config/core/Setting/SettingMulticheckbox.php
/dokuwiki/lib/plugins/config/core/Setting/SettingMultichoice.php
/dokuwiki/lib/plugins/config/core/Setting/SettingOnoff.php
/dokuwiki/lib/plugins/config/core/Setting/SettingSavedir.php
/dokuwiki/lib/plugins/config/core/Setting/SettingString.php
/dokuwiki/lib/plugins/config/lang/cs/lang.php
/dokuwiki/lib/plugins/config/lang/fr/lang.php
/dokuwiki/lib/plugins/config/lang/pl/lang.php
/dokuwiki/lib/plugins/extension/Extension.php
/dokuwiki/lib/plugins/extension/_test/ExtensionTest.php
/dokuwiki/lib/plugins/extension/_test/testdata/evilbase/plugin.info.txt
/dokuwiki/lib/plugins/usermanager/lang/cs/lang.php
/dokuwiki/lib/scripts/media.js
/dokuwiki/vendor/composer/installed.json
/dokuwiki/vendor/composer/installed.php
/dokuwiki/vendor/phpseclib/phpseclib/README.md
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/DES.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/PKCS1.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/PrivateKey.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PKCS1.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/PublicKey.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/File/ASN1.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/File/X509.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/GMP.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Net/SFTP.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php
/dokuwiki/vendor/splitbrain/php-archive/src/Zip.php
a7e1021625-Jun-2026 splitbrain <86426+splitbrain@users.noreply.github.com>

�� Rector and PHPCS fixes

4b31eadf04-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 ...

47a02a1004-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 ...


/dokuwiki/_test/bootstrap.php
/dokuwiki/_test/tests/Parsing/HandlerTest.php
/dokuwiki/_test/tests/Parsing/Lexer/RecordingHandler.php
/dokuwiki/_test/tests/Parsing/Markdown/GfmSpecTest.php
/dokuwiki/_test/tests/Parsing/ModeRegistryTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/ExternallinkTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmBacktickDoubleTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmBacktickSingleTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmCodeTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmDeletedTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisStrongTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisStrongUnderscoreTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisUnderscoreTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmEscapeTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmFileTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmHeaderTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmHrTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmLinebreakTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmLinkTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmListblockTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmMediaTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmQuoteTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/GfmStrongUnderscoreTest.php
/dokuwiki/_test/tests/Parsing/ParserMode/ParserTestBase.php
/dokuwiki/_test/tests/Parsing/ParserMode/PreformattedTest.php
/dokuwiki/inc/Extension/SyntaxPlugin.php
Handler.php
ModeRegistry.php
Parser.php
ParserMode/AbstractFormatting.php
ParserMode/AbstractMode.php
ParserMode/Base.php
ParserMode/Eol.php
ParserMode/Externallink.php
ParserMode/Footnote.php
ParserMode/GfmBacktickSingle.php
ParserMode/GfmCode.php
ParserMode/GfmEscape.php
ParserMode/GfmHr.php
ParserMode/GfmHtmlEntity.php
ParserMode/GfmListblock.php
ParserMode/GfmQuote.php
ParserMode/GfmTable.php
ParserMode/Listblock.php
ParserMode/Preformatted.php
ParserMode/Table.php
/dokuwiki/inc/deprecated.php
/dokuwiki/inc/parserutils.php
4f32c45b26-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 ...

65dd204226-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 ...

7686f20312-May-2026 Anna Dabrowska <dabrowska@cosmocode.de>

Fix syntax plugin rendering

Reverse the order in which core modes and plugin modes are handled by the \dokuwiki\Parsing\Handler.
Otherwise only the handle() method of the plugin is called, which is

Fix syntax plugin rendering

Reverse the order in which core modes and plugin modes are handled by the \dokuwiki\Parsing\Handler.
Otherwise only the handle() method of the plugin is called, which is fine for core modes. Syntax plugins need to go through plugin() to actually add their own calls.

show more ...

e7dae73b12-May-2026 Andreas Gohr <andi@splitbrain.org>

fix: apply rector and code sniffer fixes

d331a83912-May-2026 Andreas Gohr <andi@splitbrain.org>

GFM modes: follow CATEGORY_SUBSTITION → CATEGORY_SUBSTITUTION rename

Constant was renamed on master (the typo'd 'substition' value is kept,
but the constant name spells it correctly). Update GfmTabl

GFM modes: follow CATEGORY_SUBSTITION → CATEGORY_SUBSTITUTION rename

Constant was renamed on master (the typo'd 'substition' value is kept,
but the constant name spells it correctly). Update GfmTable's use of
the constant, plus stale docblock/comment references in GfmEscape,
GfmHtmlEntity, GfmLinebreak, and GfmLinebreakTest.

show more ...

15429f0212-May-2026 Andreas Gohr <andi@splitbrain.org>

Externallink: GFM autolink extension - parens and entity-ref tail

In Markdown-preferred mode, allow `(` and `)` inside URL char classes
and consume an optional trailing entity reference via the shar

Externallink: GFM autolink extension - parens and entity-ref tail

In Markdown-preferred mode, allow `(` and `)` inside URL char classes
and consume an optional trailing entity reference via the shared
HtmlEntity::PATTERN. The Markdown-only post-processing peels off
mismatched closing parens and decodes the trailing entity reference,
emitting the peeled chars as cdata after the link. Refactors handle()
to dispatch to handleAngleAutolink() and handleBareUrl(), with the new
trim logic in peelGfmTail() and the protocol-prefix step in
addProtocolPrefix(). DW-only mode behavior is unchanged.

Brings GFM spec examples #624, #625, #626 to passing.

show more ...

73dc0a8906-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 ...

8788dbbd06-May-2026 splitbrain <86426+splitbrain@users.noreply.github.com>

�� Rector and PHPCS fixes


/dokuwiki/_test/core/DokuWikiTest.php
/dokuwiki/_test/data/media/wiki/exif-orient-6.jpg
/dokuwiki/_test/tests/Feed/FeedCreatorOptionsTest.php
/dokuwiki/_test/tests/File/MediaFileTest.php
/dokuwiki/_test/tests/Remote/ApiCoreTest.php
/dokuwiki/_test/tests/Search/BacklinksTest.php
/dokuwiki/_test/tests/Search/Collection/CollectionSearchTest.php
/dokuwiki/_test/tests/Search/Collection/DirectCollectionTest.php
/dokuwiki/_test/tests/Search/Collection/FrequencyCollectionTest.php
/dokuwiki/_test/tests/Search/Collection/LookupCollectionTest.php
/dokuwiki/_test/tests/Search/Collection/MockDirectCollection.php
/dokuwiki/_test/tests/Search/Collection/MockFrequencyCollection.php
/dokuwiki/_test/tests/Search/Collection/MockLookupCollection.php
/dokuwiki/_test/tests/Search/Collection/TermTest.php
/dokuwiki/_test/tests/Search/Index/AbstractIndexTestCase.php
/dokuwiki/_test/tests/Search/Index/FileIndexTest.php
/dokuwiki/_test/tests/Search/Index/LockTest.php
/dokuwiki/_test/tests/Search/Index/MemoryIndexTest.php
/dokuwiki/_test/tests/Search/Index/TupleOpsTest.php
/dokuwiki/_test/tests/Search/IndexerTest.php
/dokuwiki/_test/tests/Search/IntegrityTest.php
/dokuwiki/_test/tests/Search/MediauseTest.php
/dokuwiki/_test/tests/Search/MetadataSearchTest.php
/dokuwiki/_test/tests/Search/Query/NamespacePredicateTest.php
/dokuwiki/_test/tests/Search/Query/PageSetTest.php
/dokuwiki/_test/tests/Search/Query/QueryEvaluatorTest.php
/dokuwiki/_test/tests/Search/Query/QueryParserTest.php
/dokuwiki/_test/tests/Search/data/searchtest.txt
/dokuwiki/_test/tests/Ui/Media/DisplayTest.php
/dokuwiki/_test/tests/inc/IpTest.php
/dokuwiki/_test/tests/inc/changelog_getrelativerevision.test.php
/dokuwiki/_test/tests/inc/common_clientip.test.php
/dokuwiki/_test/tests/inc/common_saveWikiText.test.php
/dokuwiki/_test/tests/lib/exe/fetch_imagetoken.test.php
/dokuwiki/bin/indexer.php
/dokuwiki/composer.json
/dokuwiki/composer.lock
/dokuwiki/conf/dokuwiki.php
/dokuwiki/feed.php
/dokuwiki/inc/Action/Preview.php
/dokuwiki/inc/Action/Search.php
/dokuwiki/inc/Ajax.php
/dokuwiki/inc/ChangeLog/ChangeLog.php
/dokuwiki/inc/ChangeLog/MediaChangeLog.php
/dokuwiki/inc/ChangeLog/PageChangeLog.php
/dokuwiki/inc/Feed/FeedCreator.php
/dokuwiki/inc/Feed/FeedCreatorOptions.php
/dokuwiki/inc/File/MediaFile.php
/dokuwiki/inc/File/PageFile.php
/dokuwiki/inc/Ip.php
Handler.php
/dokuwiki/inc/Remote/ApiCore.php
/dokuwiki/inc/Search/Collection/AbstractCollection.php
/dokuwiki/inc/Search/Collection/CollectionSearch.php
/dokuwiki/inc/Search/Collection/DirectCollection.php
/dokuwiki/inc/Search/Collection/FrequencyCollection.php
/dokuwiki/inc/Search/Collection/LookupCollection.php
/dokuwiki/inc/Search/Collection/PageFulltextCollection.php
/dokuwiki/inc/Search/Collection/PageMetaCollection.php
/dokuwiki/inc/Search/Collection/PageTitleCollection.php
/dokuwiki/inc/Search/Collection/Term.php
/dokuwiki/inc/Search/Exception/IndexAccessException.php
/dokuwiki/inc/Search/Exception/IndexIntegrityException.php
/dokuwiki/inc/Search/Exception/IndexLockException.php
/dokuwiki/inc/Search/Exception/IndexUsageException.php
/dokuwiki/inc/Search/Exception/IndexWriteException.php
/dokuwiki/inc/Search/Exception/SearchException.php
/dokuwiki/inc/Search/FulltextSearch.php
/dokuwiki/inc/Search/Index/AbstractIndex.php
/dokuwiki/inc/Search/Index/FileIndex.php
/dokuwiki/inc/Search/Index/Lock.php
/dokuwiki/inc/Search/Index/MemoryIndex.php
/dokuwiki/inc/Search/Index/TupleOps.php
/dokuwiki/inc/Search/Indexer.php
/dokuwiki/inc/Search/MetadataSearch.php
/dokuwiki/inc/Search/Query/NamespacePredicate.php
/dokuwiki/inc/Search/Query/NegatedEntry.php
/dokuwiki/inc/Search/Query/PageSet.php
/dokuwiki/inc/Search/Query/QueryEvaluator.php
/dokuwiki/inc/Search/Query/QueryParser.php
/dokuwiki/inc/Search/Query/StackEntry.php
/dokuwiki/inc/Search/Tokenizer.php
/dokuwiki/inc/Search/concept.txt
/dokuwiki/inc/Sitemap/Mapper.php
/dokuwiki/inc/Subscriptions/BulkSubscriptionSender.php
/dokuwiki/inc/TaskRunner.php
/dokuwiki/inc/Ui/Backlinks.php
/dokuwiki/inc/Ui/Media/Display.php
/dokuwiki/inc/Ui/MediaDiff.php
/dokuwiki/inc/Ui/Search.php
/dokuwiki/inc/Ui/SearchState.php
/dokuwiki/inc/common.php
/dokuwiki/inc/deprecated.php
/dokuwiki/inc/html.php
/dokuwiki/inc/infoutils.php
/dokuwiki/inc/lang/fr/lang.php
/dokuwiki/inc/lang/pl/lang.php
/dokuwiki/inc/lang/tr/lang.php
/dokuwiki/inc/lang/tr/onceexisted.txt
/dokuwiki/inc/load.php
/dokuwiki/inc/media.php
/dokuwiki/inc/search.php
/dokuwiki/inc/template.php
/dokuwiki/lib/exe/fetch.php
/dokuwiki/lib/plugins/authldap/lang/tr/settings.php
/dokuwiki/lib/plugins/authpdo/lang/tr/lang.php
/dokuwiki/lib/plugins/authplain/lang/tr/lang.php
/dokuwiki/lib/plugins/config/lang/cs/lang.php
/dokuwiki/lib/plugins/config/lang/de/lang.php
/dokuwiki/lib/plugins/config/lang/en/lang.php
/dokuwiki/lib/plugins/config/lang/es/lang.php
/dokuwiki/lib/plugins/config/lang/fr/lang.php
/dokuwiki/lib/plugins/config/lang/hu/lang.php
/dokuwiki/lib/plugins/config/lang/it/lang.php
/dokuwiki/lib/plugins/config/lang/pl/lang.php
/dokuwiki/lib/plugins/config/lang/pt/lang.php
/dokuwiki/lib/plugins/config/lang/tr/lang.php
/dokuwiki/lib/plugins/config/settings/config.metadata.php
/dokuwiki/lib/plugins/extension/lang/tr/lang.php
/dokuwiki/lib/plugins/info/syntax.php
/dokuwiki/lib/scripts/page.js
/dokuwiki/lib/scripts/toolbar.js
/dokuwiki/lib/tpl/dokuwiki/detail.php
/dokuwiki/vendor/composer/installed.json
/dokuwiki/vendor/composer/installed.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/OpenSSH.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php
/dokuwiki/vendor/phpseclib/phpseclib/phpseclib/File/ASN1.php
/dokuwiki/vendor/splitbrain/slika/README.md
/dokuwiki/vendor/splitbrain/slika/src/GdAdapter.php
/dokuwiki/vendor/splitbrain/slika/src/ImageInfo.php
56c730b506-May-2026 Andreas Gohr <andi@splitbrain.org>

keep historic typo in value but not in constant

We need to keep the historic typo in the value ("substition"), but there
is no reason to keep it in the constant.

0f69437605-May-2026 Andreas Gohr <gohr@cosmocode.de>

GfmLink: accept escaped brackets inside link labels

The label slot used `[^\[\]\n]+`, which rejected `\[` / `\]` and
left labels with escaped brackets unmatched. Promote it to
`(?:\\.|[^\[\]\n])+` —

GfmLink: accept escaped brackets inside link labels

The label slot used `[^\[\]\n]+`, which rejected `\[` / `\]` and
left labels with escaped brackets unmatched. Promote it to
`(?:\\.|[^\[\]\n])+` — the same backslash-escape trick the URL
slot already uses — so spec example 523 (`[link \[bar](/uri)`)
matches and unescapes cleanly. The image-as-label sub-pattern
gets the same upgrade.

handle() needs no change: the new class still rejects bare `]`,
so the first literal `](` in the match is still the separator;
Escape::unescapeBackslashes() was already collapsing `\[` to `[`
before the label reached the link handler.

Adds two GfmLinkTest cases for the `\[` / `\]` forms.

show more ...

113171bb05-May-2026 Andreas Gohr <gohr@cosmocode.de>

Preformatted: strip leading/trailing blank-line padding from body

The lexer's `\n ` continuation pattern eats the indent off
blank-but-indented source lines, leaving a `\n` in the rewriter
buffer

Preformatted: strip leading/trailing blank-line padding from body

The lexer's `\n ` continuation pattern eats the indent off
blank-but-indented source lines, leaving a `\n` in the rewriter
buffer for each one. Trim those padding newlines before emitting
so the preformatted body starts and ends on a non-blank line, as
GFM example #87 requires. Whitespace-only blocks are still
skipped entirely.

Adds two PreformattedTest cases pinning the new behavior.

show more ...

1234