Skip to content

Menu builder and page builder (reworked, with XSS and permission fixes) - #21

Merged
godzam merged 3 commits into
mainfrom
claude/menus-pages
Nov 1, 2026
Merged

godzam merged 3 commits into
mainfrom
claude/menus-pages

Conversation

@godzam

@godzam godzam commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Summary

Starts from the menu/page builder code on the freebuff/… branch (the tables, models, controllers and editor UI are kept) and fixes what was wrong in it. Independent of the account-features PR (#20).

What the builders do

  • Menu builder (admin/menus): sections and links for the admin sidebar, nested up to three levels, drag or arrow buttons, optional permission / super-admin-only per item, merged after the items in menu.php.
  • Page builder (admin/pages): public pages from blocks (hero, text, features, CTA, image, custom HTML) at /page/{address}; drafts are previews for editors.

Fixed over the original

  • Link/image/menu addresses were printed as typed: javascript: became a clickable link, and the sidebar did not escape the URL (an attribute-breaking " was possible). Now App\Page\Links allows only http(s)/mailto/tel and this site's pages; the sidebar escapes and handles external links (rel="noopener noreferrer").
  • Text blocks used strip_tags with an allow-list, which keeps every attribute (<a onmouseover=…>, <a href="javascript:…">). Replaced by a DOM allow-list (App\Page\Html): no attributes except a safe href; scripts/styles/svg/math/forms removed; 22 hostile payloads in the tests.
  • Relative addresses resolved against /page/… (a register button went to /page/register); they now go through site_url.
  • Raw HTML blocks were open to every page editor. Now they need pages.html, which is not in the admin role (fresh installs included: not_in_wildcard), and an editor without it can keep existing HTML blocks but not add/change one.
  • Menu parent_id was not validated (missing parent, a section, a loop, unlimited depth); now it is (MenuTree::canNest, found and fixed a bug for new items), and the form shows the errors.
  • The page list loaded every page with its full JSON; now a server-side DataTable (admin/pages/data).
  • The colour field submitted white by default (white text on a white CTA); it starts empty = theme colour.
  • Existing admin roles get the menus/pages permissions through a migration.
  • Two older bugs found on the way: data-order of our DataTables helper clashed with DataTables' own attribute (any table using the order option crashed), and data-confirm on a non-submit button threw in requestSubmit.

Test plan

  • 180 unit tests (new PageSafetyTest, extended PageSectionsTest/MenuTreeTest), PHPStan and CS-Fixer clean (local runner is a stub; CI runs PHPUnit)
  • Browser run on MariaDB, 56 checks, 0 failed: hostile page content on the public page (no alert fires), draft 404/preview, pages.html rules, 403s, DataTable list, menu validation (bad schemes, parents, depth, section with children), escaped sidebar, permission-gated items, reorder + delete, phone width
  • MySQL 8.4 and real PHPUnit: CI only

Generated by Claude Code

Admin -> Menu builder builds the admin sidebar (sections and links up to three
levels, drag or arrow buttons, per-item permission); Admin -> Pages builds public
pages from blocks served at /page/{address}.

Fixes over the code this starts from: link and image addresses are validated
(no javascript:/data:/protocol-relative), the sidebar escapes them and supports
external links; the text block is rebuilt from a DOM allow-list instead of
strip_tags (which kept every attribute); relative addresses resolve against the
site, not /page/; raw HTML needs its own pages.html permission that is not in the
admin role or the '*' default; menu parents are validated (exists, is a link, no
loops, depth); the list is a server-side DataTable; permissions for existing admin
roles are granted by a migration; colour fields no longer default to white
(white text on white CTA). Also fixes data-initial-order clashing with
DataTables' own data-order and data-confirm on non-submit buttons.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYbe92Bns1mKdy23b7txT8
- A new "dropdown" kind: no address of its own, it opens the links put inside it
  (and is hidden while empty); links and dropdowns can be parents, sections cannot.
  The form shows only the fields that matter for the chosen kind (the old inline
  script is replaced by assets/js/menu-form.js, which the CSP-nonce page no longer needs).
- Icon picker (assets/js/icon-picker.js): a searchable grid of all 2,050 Bootstrap Icons
  (list in vendor/bootstrap-icons/icons.json, drawn in chunks of 150), live preview, "no icon",
  full screen on phones. Used by the menu form and the module generator's icon field.
- Fix: links to pages of this site in the sidebar opened in a new tab, because the
  resolved (absolute) address was mistaken for an external one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYbe92Bns1mKdy23b7txT8
…r the account ones

The menus and pages migrations (20261006/07) were older than the account-security
migration (20261008) that is now on main. CodeIgniter only runs migrations newer than
the current version, so a database that had already migrated to 20261008 would have
skipped them and never got the menus and pages tables. They are renumbered to
20261009/10 (the permissions grant to 20261011) so every database creates them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYbe92Bns1mKdy23b7txT8
@godzam
godzam marked this pull request as ready for review November 1, 2026 05:39
@godzam
godzam merged commit 2521b48 into main Nov 1, 2026
10 checks passed
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