Skip to content

Thelia 3 support, menus addressed by code (2.0.0) - #21

Open
robinallezard wants to merge 19 commits into
mainfrom
feat/thelia-3-compatibility
Open

Thelia 3 support, menus addressed by code (2.0.0)#21
robinallezard wants to merge 19 commits into
mainfrom
feat/thelia-3-compatibility

Conversation

@robinallezard

Copy link
Copy Markdown

feat!: Thelia 3 support, menus addressed by code (2.0.0)

What this does

CustomFrontMenu runs on Thelia 3. module.xml now requires <thelia>3.0.0</thelia> and the module version goes to 2.0.0. The Smarty plugin and its templates are gone, the composition screen is rebuilt on the default-twig back-office, and the menu tree is exposed through API Platform instead of the OpenApi module, which Thelia 3 no longer ships.

19 commits, 46 files, +2463 / -3187. The line count is negative because the old screen carried 1249 lines of hand-written JavaScript that the theme's own tree component replaces.

Breaking changes

A theme built on 1.x will not work as is.

1.x 2.0
{CustomFrontMenuPlugin menu_id=5} custom_front_menu('header')
Returns HTML, styled through .cfm-menu classes Returns the tree as data, no markup, no CSS
GET /open_api/custom-front-menu/{id} GET /api/front/custom-front-menus/{code}
A menu is addressed by its numeric id A menu is addressed by a code the shop owner chooses

The id came from an autoincrement shared with the entries, so it differed from one installation to the next and a theme written against it broke on the next shop. A code survives a reinstall, and the composition screen shows the exact call to copy.

The function answers nodes rather than markup because a navigation is where a theme's layout, breakpoints and interaction live. Integrators write the elements they want:

{% for item in custom_front_menu('header') %}
    <a href="{{ item.href }}">{{ item.title }}</a>
    {% for child in item.children %}
        <a href="{{ child.href }}">{{ child.title }}</a>
    {% endfor %}
{% endfor %}

Each node is {id, title, href, children}, nested to any depth. href is always http(s), site-relative, or empty. An unknown code answers an empty list instead of raising, so a menu deleted in the back-office cannot take the storefront down.

Migration from 1.2.0

Automatic, no manual step. update() adds the code column and its unique index, gives every existing menu a code derived from its title, and clears the links that the 1.x screen let through.

Every step asks the database what it looks like instead of trusting the recorded version, and can be run again. ModuleManagement writes the new version before it calls update(), and MySQL commits that write implicitly on the first DDL, so a failure half-way through would otherwise leave a shop recorded as 2.0.0 with nothing left to finish the job. The same work runs from postActivation(), which makes deactivating and reactivating the module a real way out.

The DDL goes through information_schema rather than ADD COLUMN IF NOT EXISTS, which is a MariaDB extension that a shop on MySQL 8 would reject.

Security

Two holes in 1.2.0 disappear with the files this branch deletes. Validator called addslashes() before a Propel save(), which double-escaped apostrophes in the database and explained the quotePattern gymnastics in the old main.js. CustomFrontMenuSaveService filtered URLs with FILTER_SANITIZE_URL, which only strips illegal characters and leaves javascript: intact on a link rendered on every page.

Service/MenuLink replaces both. It accepts http, https and site-relative paths, and rejects protocol-relative addresses: //evil.com resolves to https://evil.com in a browser, and /\evil.com is normalised to the same thing by Chrome and Firefox, so both leave the shop while looking like an internal path.

The filter runs on write and on read. Validating on write alone would protect the rows this version writes and nothing else, and a shop coming from 1.2.0 can carry a poisoned link into a menu served by a public API.

Back-office

The screen is rebuilt on bo-category-tree, the tree component the default-twig theme already ships, so drag and drop, the confirmation modal and the HTMX target field cost no module JavaScript. Entries are reparented either by dragging or from a select on the entry page, which is how you move one back up a level without aiming at a drop zone.

Each action saves on its own. The 1.x screen held the whole tree in the browser and saved it by deleting and recreating every row, so a closed tab lost the work and every save renumbered the ids.

A menu code is validated rather than slugified: typing Header! is refused with a message instead of silently becoming header. An empty code field at creation derives one from the menu name.

Verified

  • Activation, 11 routes, services autowired, 5 Twig templates valid, PHPStan level 5 at zero on the module.
  • Back-office: menu list, composition screen, entry creation and editing, target selection, drag and drop, reordering, deletion of an entry and of a menu, in French, English, Spanish and Italian.
  • Refusals: 403 without a CSRF token, 403 unauthenticated, 403 for an administrator without the MODULE right on CustomFrontMenu.
  • API: the tree by code, 404 on an unknown code, on an entry id and on the nested-set root, and an entry whose target is unpublished dropped from the payload.
  • Front: rendered in a live theme, and an unknown code renders nothing.
  • Migration replayed on an isolated database from a reconstructed 1.2.0 state, including a second run on an already-migrated schema and a recovery through reactivation.

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