Skip to content

Security hardening: fix code execution, XSS, CSRF, auth and upload issues - #11

Merged
codeling merged 38 commits into
mainfrom
claude/confident-keller-7ul9ne
Oct 2, 2026
Merged

codeling merged 38 commits into
mainfrom
claude/confident-keller-7ul9ne

Conversation

@codeling

@codeling codeling commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes the issues found in a security review of w2wiki, in small commits (one per contiguous change), and adds an automated test suite with CI. The most serious issue allowed remote code execution without authentication (with the default REQUIRE_PASSWORD=false) by renaming an uploaded image to .php.

Critical / high

  • Code execution via rename of uploads: renaming an image now requires an allowed upload extension, only the base name is used, existing files are not overwritten, hidden files (e.g. .htaccess) can't be touched.
  • .htaccess files for pages/ (no direct access to page sources / .git), pages/images/ (no script execution, nosniff, sandboxing CSP for SVG) and vendor/, no directory listings; nginx equivalents documented in INSTALL.md.
  • Reflected and stored XSS: new h() helper used for all output of request values, page/file names and messages; {{image}} shorthand and heading anchors are escaped; link/image URLs are limited to safe schemes (no javascript:/data:).
  • CSRF: per-session token on all forms; save/upload/rename/delete (page and image) require POST + token; logout carries the token.
  • SVG: uploads disabled by default. Optional support via enshrined/svg-sanitize (composer require, SVG_UPLOADS_ENABLED); the library is GPL-2.0-or-later, so it is deliberately not bundled (only a test dependency). SVGs are never passed to ImageMagick.

Medium / low

  • Regex injection/ReDoS: preg_quote for names, escaped replacement strings, plain substring search; non-greedy link rewriting (deleting/renaming a page no longer damages other links on the same line).
  • Login: password_hash support (legacy SHA-1 still accepted), constant-time comparison (no more "magic hash" 0e… matches), session ID regenerated on login, HttpOnly/SameSite/(Secure) cookie, failed logins logged and delayed, login refused while the default password secret is configured. A plain W2_PASSWORD also works again (with an empty W2_PASSWORD_HASH the constant was defined twice and login could never succeed).
  • api.php now requires login/IP check (shared auth.php), no longer allows path traversal, and ignores hidden files.
  • IP allowlist: exact, CIDR (IPv4/IPv6) or explicit prefix instead of raw string prefix.
  • Uploads: server-side overwrite check, maxsize clamped to 20-8192.
  • No server paths or shell commands in user-visible messages (logged instead); PAGES_PATH shell-escaped for git.
  • Page names: refused inside images/, with hidden or empty segments; renames never overwrite pages.
  • Page names were URL-decoded twice (A+B opened A B, x%41y opened xAy): fixed, old + links get a 301.
  • Missing braces around the "used on page" scan on the uploads page; "${var}" string interpolation deprecated since PHP 8.2.

Refactoring

Side-effect-free helper functions moved unchanged from index.php/auth.php into functions.php/auth_functions.php (so they can be unit tested); the class autoloader resolves files relative to its own folder; isCorrectPassword() takes the configured hash/password as optional parameters.

Tests and CI

composer install && composer test (see tests/README.md); all suites run on GitHub Actions:

Suite Tests What
unit 233 escaping, URLs, file/page names, URL filtering, Markdown → HTML with script injection texts, IP allowlist, password checking, CSRF tokens, docs in sync, a tokenizer-based source guard (dangerous functions, unquoted regex values, request data read outside entry points)
integration 171 the wiki over HTTP on PHP's built-in server: XSS (HTML checked for scripts, handlers, dangerous URLs), CSRF, page names, regex safety, uploads/renames, login/session/cookie, IP allowlist, api.php, main views; fails on logged PHP warnings/deprecations
svg 53 22 SVG fixtures with scripts, handlers, external references, entities, non-SVG content; fail-closed without sanitizer/flag
server 39 × 2 real Apache (php:apache, .htaccess files) and nginx + php-fpm (rules from INSTALL.md) in Docker: no access to page sources/.git/vendor, scripts in uploads never executed, nosniff + sandboxing CSP for SVG, no directory listings, end-to-end upload
browser 63 Playwright/Chromium: the same script injections must not run (stored, reflected, file names, editor, SVG opened directly); also against Apache/nginx incl. legacy SVGs with scripts

The tests were checked against the vulnerable code on main (they fail there, and the browser tests see the scripts run), and with protections deliberately removed (sanitizer, .htaccess files, regex quoting, ...). Manual test plan: TEST_PLAN.md (shared with this PR, not committed).

Not covered by automated tests: git integration, ImageMagick paths (resize/convert/rotate), HTTPS Secure cookie flag, non-en/de locales, other web servers.

Upgrade notes (behaviour changes)

  • Logging in is refused while W2_PASSWORD is still secret; set a password or a W2_PASSWORD_HASH (password_hash output recommended).
  • SVG uploads are off by default (see INSTALL.md to enable).
  • Search is now a plain, case-insensitive substring search (no regular expressions).
  • Forms opened before the upgrade get a 403 on submit and need a reload; uploads larger than post_max_size also give the 403 message.
  • Apache needs AllowOverride All (or AuthConfig, FileInfo, Options) for the new .htaccess files.
  • $allowedIPs entries written as bare prefixes (e.g. 192.168.1) no longer match; use 192.168.1., a CIDR range or the full address.
  • Page URLs encode spaces as %20 instead of +.

Follow-ups

🤖 Generated with Claude Code

https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS

Renaming an uploaded file did not check the new name's extension, so an
image with embedded PHP code (which passes the MIME check) could be
renamed to .php and then executed via the statically served images
folder. Restrict new names to the valid upload extensions, strip any
path components, and refuse to overwrite existing files on rename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Add .htaccess files so that page sources and a potential .git folder in
PAGES_PATH can't be fetched directly (bypassing the password and IP
checks), so that nothing in the uploads folder can be executed, and so
that uploaded SVG files are served with a sandboxing CSP. Protect hidden
files in the uploads folder (e.g. its .htaccess) from being renamed,
deleted or overwritten through the wiki, and document equivalent nginx
rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
SVG files can contain scripts that run in the wiki's origin when opened
directly, so remove them from the default list of allowed upload types.
If they are enabled anyway, don't pass them to ImageMagick for resizing,
as its SVG handling can resolve external references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Page names, search queries, file names, form values and messages were
printed into the HTML unescaped, allowing reflected and stored XSS (e.g.
via ?action=search&q=<script>..., page names in the URL, upload file
names, or "</textarea>" in page content opened in the editor).

Add an h() helper wrapping htmlspecialchars and use it wherever such
values end up in the page, also for untranslated labels returned by
__(). The edit form now always posts to SELF, since the page name is
part of the form data anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
The Markdown parser escapes '<' and '&' but not quotes, so the {{image}}
shorthand and the anchors generated for headings allowed breaking out of
HTML attributes (e.g. {{x" onerror="alert(1)}}). Escape both properly.

Also filter link and image URLs through the parser's url_filter_func so
that only safe schemes (http, https, mailto, ftp, ftps, tel) or relative
URLs are emitted; others such as javascript: or data: are replaced by #.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
There was no protection against cross-site request forgery; since saving
read its parameters from $_REQUEST, even a plain GET request (e.g. an
<img> tag on another site) could overwrite pages.

Add a per-session token to all forms that modify data, and require both
a POST request and a valid token for saving, uploading, renaming and
deleting pages and files. The logout link also carries the token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
The search query as well as page and image names were interpolated into
regular expressions unquoted, allowing regex injection: expensive
patterns (ReDoS), PHP warnings, and link rewriting across all pages
going wrong on rename/delete. Page and file names in replacement strings
could also inject backreferences.

Use preg_quote for names in patterns, escape replacement strings, match
link targets non-greedily so deleting a page doesn't remove other links
on the same line, and do a plain case-insensitive substring search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
- Support password_hash() hashes in W2_PASSWORD_HASH (the legacy SHA-1
  hash is still accepted) and compare in constant time; the previous
  loose == comparison of SHA-1 hex strings was open to "magic hash"
  collisions such as 0e... values
- Refuse logging in while the well-known default password is configured
- Regenerate the session ID on login to prevent session fixation, and
  enable session.use_strict_mode
- Set HttpOnly and SameSite=Lax (and Secure on HTTPS) on the session
  cookie
- Log failed login attempts and slow them down to hinder brute-forcing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
api.php neither checked the password nor the IP allowlist, and passed
the requested file name unfiltered to file_exists, which allowed anyone
to probe for the existence of arbitrary files on the server (e.g. via
filename=../../../etc/passwd).

Move session setup, IP restriction and password checking into auth.php,
shared by index.php and api.php, and let api.php reject requests that
are not logged in. Only check plain file names within the uploads
folder, and URL-encode the file name in the upload form's request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
The IP allowlist compared string prefixes, so e.g. an entry "10.0.0.1"
also allowed 10.0.0.10-19 and 10.0.0.100-199, and "192.168.1" allowed
192.168.10.x etc. Entries now match exactly, as CIDR ranges (IPv4 and
IPv6), or as explicit prefixes if they end in "." or ":".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Uploads silently overwrote existing files; the overwrite confirmation
only happened client-side. Now the upload form only sets an overwrite
flag after the user confirmed, and the server refuses to replace an
existing file (also the target of a format conversion) without it.

Also clamp the requested maximum image size to the range offered by the
form (20-8192 pixels) instead of trusting the submitted value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Error messages shown to users contained absolute filesystem paths as
well as the failed git command and its output. Show generic messages
instead and write the details to the web server's error log. Also
shell-escape PAGES_PATH in the git commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Page names may contain '/' to create pages in subfolders, which also
allowed storing pages in the uploads folder (served statically, i.e.
readable without login) or in hidden folders such as .git. Refuse such
names, and empty path segments, when creating or renaming pages. Also
don't let renaming a page silently overwrite another existing page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Missing braces meant only the getAllPageNames() call was conditional,
while the loop over all pages always ran (on an undefined variable if
the option was disabled).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Page names from PATH_INFO and request variables are already decoded by
the web server/PHP, but were passed through urldecode() once more. This
compensated for pageURL() encoding spaces as '+' (which is a literal
plus in a URL path), but mangled names containing '+' or '%' (e.g.
"A+B" became "A B", "x%41y" became "xAy"), and made the page actually
operated on differ from the one the user saw.

Encode page URLs with rawurlencode, drop the extra decoding (also for
the image and previous page parameters), and redirect old links with
'+' for spaces to the correct page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Make the class autoloader ignore classes without a file, so that
class_exists() can be used to probe for optional libraries instead of
failing fatally, and load Composer's autoloader if present.

Add svgUploadsAvailable() and validUploadTypes()/validUploadExts()
helpers, which add SVG to the allowed uploads only if SVG_UPLOADS_ENABLED
is set (new config option, off by default) and the enshrined/svg-sanitize
library is installed. SVG uploads are still refused for now, since
sanitizing them on upload is added separately.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Allow uploading SVG files again if SVG_UPLOADS_ENABLED is set and the
enshrined/svg-sanitize library is installed. Uploaded SVGs are passed
through the sanitizer, which removes scripts, event handlers,
javascript:/data: URLs, foreignObject content, external references and
stylesheet imports; files it doesn't accept as valid SVG, or that are
larger than 1 MB, are rejected. SVG content in files with other
extensions is rejected as well. Without the library SVG uploads stay
refused (fail closed).

The library is GPL-2.0-or-later, so it isn't bundled with the MIT
licensed W2 but installed with Composer (composer.json suggests it).
Add a vendor/.htaccess denying web access to installed libraries, and
document setup in INSTALL.md and README.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
The rename/delete forms used "${action}d", which PHP 8.2+ reports as a
deprecation on every request showing these forms (noticed by the new
integration tests checking the error log).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Tests run a private copy of the wiki with an individual config.php on
PHP's built-in web server and talk to it over HTTP with a small
cookie-aware client. Each test starts from the initial pages, without
uploads and with a new session, and fails if the app logged PHP
warnings, notices or deprecations. W2_APP_ROOT allows running the suite
against another checkout (e.g. to see that tests fail without the fixes).

Run with "composer install && composer test".

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Covers reflected XSS (search, page names, parameters, forms, editor),
stored XSS through Markdown (table of malicious inputs checked by
parsing the resulting HTML for scripts, event handlers and
javascript:/data: URLs), escaped file and page names, that safe links
still work, and that all state-changing actions require POST and the
session's CSRF token.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Cover URL round trips of page names with special characters (no double
decoding, redirect for old "+" links), refused page names, renaming and
deleting pages with link updates (special characters are literal, other
links stay untouched, other pages are never overwritten), literal
handling of search queries and image names, and quick return for
catastrophic regular expressions. Also let csrfToken() return an empty
token for versions without CSRF protection, to check the tests fail
without the fixes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Cover valid uploads and listing, file name handling (spaces, paths),
refused script/hidden/disguised files including SVG by default, the
overwrite confirmation, renaming and deleting images (extension check,
no overwriting, no paths, hidden files protected, page references
updated), the usage column, sorting, and the disabled-uploads and
no-usage-column configurations.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
checkupload answered "true" for "." (the uploads folder itself) and for
hidden files such as .htaccess. Found by the new API tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
…views

Cover logging in with password_hash, SHA-1 and plain passwords, refusal
of the default password and of "magic hash" collisions, hidden wiki
content, slowed-down failed logins, session regeneration and cookie
flags on login, logout, the api.php login requirement and traversal
checks, the IP allowlist (allowed and refused addresses), and a
regression suite for all main views and workflows. W2_IGNORE_PHP_LOG
allows running the suite against old versions that log PHP errors.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Add a workflow that checks PHP syntax and validates composer.json, and
runs the integration test suite, on PHP 8.1 to 8.4 for pushes to main
and pull requests. Document how to run the tests, and remove the stale
Travis CI Apache configuration.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Add enshrined/svg-sanitize as a development dependency (it stays
optional and unbundled for users) and let the test harness install it
into the tested app like Composer would. The new "svg" suite uploads 22
fixtures with scripts, event handlers, javascript:/data: URLs, external
references, entities, processing instructions, foreignObject and
non-SVG content, and checks that each is refused or stored without
anything dangerous; that harmless files are kept; that SVG content in
other file names, huge or malformed files are refused; and that uploads
stay refused if the library is missing or the feature is not enabled.
Also runs on CI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
….php

index.php did everything on include (sessions, output), so its helper
functions could not be tested on their own. Move the helpers without
side effects, unchanged, into functions.php (escaping, file and page
names, URLs, Markdown to HTML, upload checks) and auth_functions.php
(CSRF tokens, IP allowlist, password check, login state), which do
nothing when loaded.

Two small changes come with this: the class autoloader resolves files
relative to its own folder instead of the current directory, and
isCorrectPassword() takes the configured hash and password as optional
parameters (defaulting to the constants from config.php).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Test escaping, URL and file name handling, page and upload name
validation, URL scheme filtering, the Markdown conversion (including
the table of script injection attempts also used by the integration
tests), the IP allowlist matching (IPv4, IPv6, CIDR ranges, prefixes,
malformed entries), password checking (password_hash, SHA-1, plain,
default and "magic hash" passwords) and CSRF tokens. Share HTML
assertions between unit and integration tests, and run the unit tests
on CI for PHP 8.1 to 8.4.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Apache installations like Debian's enable directory listings by default,
which would list all uploaded files (and the folders of the wiki) when
their folder is requested. Found by the new web server tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Run the wiki behind php:apache (with the .htaccess files) and nginx with
php-fpm (with the rules from INSTALL.md) in Docker containers, and check
over HTTP that page sources, .git, vendor and hidden files are not
served, that scripts in the uploads folder are never executed (also with
double extensions and through the pages folder), that uploads have
nosniff and SVGs a sandboxing CSP, that folders are not listed, that
source files are not disclosed, and that uploading works end to end.
Run by tests/Server/run.sh, and on CI for both servers. A unit test
keeps the tested nginx rules identical to the ones in INSTALL.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Try to run scripts in a real browser: the script injection texts of the
PHP tests as stored page content, reflected through URLs and forms,
uploaded file names, the editor, and all SVG upload fixtures opened
directly. Any dialog or event handler that runs fails a test; a canary
test makes sure the helpers notice scripts. With W2_BROWSER=1,
tests/Server/run.sh also runs them against Apache or nginx, including a
check that scripts in old SVG files in the uploads folder are blocked by
the server headers (with a control file that shows scripts do run
elsewhere). tests/bin/serve-app.php starts the app for the tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Add a job running the browser tests against the wiki on PHP's built-in
server (including SVG uploads), and let the web server jobs also run
the browser tests against Apache and nginx. Upload the Playwright report
if a test fails.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Use PHP's tokenizer to check that the wiki's files don't call functions
which run code or commands (exec() only in checkedExecute()), don't
include files by variable names, only put values into regular
expressions and replacement strings when they are quoted, and only read
request data in the entry points. This would have found several of the
problems fixed earlier.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
Takes over "Error check / fix prevpage storage" from main, which only
accepts existing pages as the page to return to after uploading or
changing images. Conflicts in index.php, where the functions it touches
had been moved to functions.php, and where the same lines were already
escaped here, are resolved as follows:

- main's isValidPageName() (does the page exist) is renamed
  isExistingPage() and moved to functions.php, as isValidPageName()
  already checks the syntax of new page names here
- requireValidPreviousPage() stays in index.php, but doesn't urldecode()
  the value again: request values are already decoded, which would
  turn a page like "A+B" into "A B" (fixed earlier on this branch)
- the upload link in the toolbar falls back to the default page if the
  current page doesn't exist (yet), otherwise the upload page of a page
  being created would fail with "Invalid page name"
- the forms keep escaping with h() and the CSRF token / overwrite fields

Tests are adapted (invalid pages are now refused instead of being shown
escaped) and extended for the new behavior.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
wiki.js, which asks for confirmation before leaving the editor with
unsaved changes, was only loaded when editing existing pages. Typing a
new page and clicking e.g. the upload icon silently lost the text.

Load it for new pages too, and track the title field as well. Also make
the script URL absolute: it was relative, so on addresses like
/index.php/Page it resolved to /index.php/wiki.js, which is not the
script. wiki.js no longer fails if the formatting help (drawer) or the
save button is missing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
The Markdown formatting help (drawer) was only available when editing
existing pages, although new pages use the same editor and need it just
as much. wiki.js, which opens it, is now loaded for new pages as well.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
codeling and others added 2 commits October 2, 2026 23:13
… wiki.js

The condition "edit or new" was written out in three places in
index.php: for loading wiki.js, for showing the editor form and for
showing the formatting help. They had already got out of step once (the
help was missing on new pages). Use isEditorAction() for all of them,
and test that the editor, the save button, wiki.js and the formatting
help always appear together on every kind of page.

As wiki.js is only loaded where the editor is, it can rely on the text
area, the save button and the formatting help, so remove the guard
around the save button. The title field only exists for new pages and
stays optional.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153ydi3ZeUKJmfyPpTKoQfS
@codeling
codeling merged commit 866ccc0 into main Oct 2, 2026
15 checks passed
@codeling
codeling deleted the claude/confident-keller-7ul9ne branch October 2, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants