db6cc87c90
After a v11->v14 upgrade, non-admin editors often can't edit content (no edit icons) and/or the Media tab is missing, even though the group has "edit content" and tables_modify includes tt_content. The upgrade wizards do not convert the backend group access-lists. Document the fix for all three facets on be_groups: 1. explicit_allowdeny: v<=11 ":ALLOW"-suffix format vs v12+ exact inList match (no suffix) — REPLACE strips it; blocks ALL editing. 2. CType allow-list missing renamed v14 names (wst3bootstrap_* -> t3bs_*, ws_slider, heroitem, news_*, address_*). 3. non_exclude_fields missing v14 fields -> hidden Media tab (e.g. hero tx_heroitem_bg*). Includes the read-only recordEditAccessInternals/check/doesUserHaveAccess CLI diagnosis and the db_mountpoints red-herring note. Bump to v1.1; extend description + when-to-use for the symptom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
179 lines
13 KiB
Markdown
179 lines
13 KiB
Markdown
---
|
||
name: typo3-v11-to-v14-ddev-upgrade
|
||
description: "Bring an existing TYPO3 v11 site up and booting on TYPO3 v14 LTS (v14.3) locally under DDEV, starting from a v11 SQL dump + the old sitepackage. Use when upgrading/migrating a TYPO3 v11 (or v12/13) project to v14 for local dev, when a v11 database dump must be made to boot on v14, when a t3bootstrap-based v11 site needs its composer/config/schema/upgrade-wizards/site-sets brought to v14, or when after such an upgrade a non-admin editor can't edit content (no edit icons / missing Media tab). Covers: composer.json rework to ^14 + private git repos & composer registry auth, config/system/settings.php, the v14 .htaccess, database:updateschema, backend admin, upgrade wizards (incl. confirmable + one-way ones), language packs, wiring t3bootstrap TypoScript via site sets, fixing non-admin editor permissions (be_groups explicit_allowdeny :ALLOW-format conversion, v14 CType allow-list, non_exclude_fields), and the recurring v11->v14 traps. NOT for fresh installs and NOT for server deployment (see the konsoleH staging-deploy skill for that)."
|
||
metadata:
|
||
author: Wappler
|
||
version: "1.1"
|
||
---
|
||
|
||
# TYPO3 v11 → v14 upgrade (local, DDEV)
|
||
|
||
Get an existing **TYPO3 v11 site** installable and **booting on v14 LTS (14.3.x)** under **DDEV**,
|
||
from a **v11 SQL dump** + the old sitepackage. This is the "make it run" phase — a later phase
|
||
matches the frontend design (see the *t3bootstrap live-design parity* skill) and another deploys to
|
||
staging (see the *konsoleH staging-deploy* skill).
|
||
|
||
Work **one step at a time and verify each** before moving on. **`ddev snapshot` before anything
|
||
destructive** (especially upgrade wizards — several are one-way). Never let a live secret appear in a
|
||
command. Many errors here look like config bugs but are data/schema/host issues — always confirm the
|
||
*actual* error before "fixing".
|
||
|
||
## Placeholders to resolve first
|
||
- `<PROJECT_DIR>` — the DDEV project root (docroot `public/`).
|
||
- `<SITE_ID>` — the site identifier folder under `config/sites/<SITE_ID>/`.
|
||
- `<SITE_BASE_HOST>` — the site's base host from `config/sites/<SITE_ID>/config.yaml` (e.g. the
|
||
`*.ddev.site` host). The FE only answers on this host, NOT `localhost`.
|
||
- `<REGISTRY_URL>` — the private Composer registry for the theme/vendor packages, if any (e.g. a
|
||
Gitea `.../api/packages/<vendor>/composer` URL). `<GIT_REPOS>` — any `gitea@`/GitHub SSH git repos
|
||
in `composer.json` `repositories`.
|
||
- `<TEMPLATE_PACKAGE>` — the sitepackage's composer `name` (⚠ often ≠ repo name — read the repo's
|
||
own `composer.json`). `<TEMPLATE_EXTKEY>` — its extension key (keep it stable across the rename).
|
||
|
||
---
|
||
|
||
## Step 0 — DDEV up + import the dump (read-only baseline)
|
||
`ddev start`; import the v11 dump into the `db` database (`ddev import-db --file=dump-*.sql` or
|
||
`ddev mysql < dump.sql`). Confirm PHP/DB versions match the target (v14.3 wants PHP 8.2–8.5). Snapshot:
|
||
`ddev snapshot --name pre-upgrade`.
|
||
|
||
## Step 1 — Rework `composer.json` for v14
|
||
- Bump every `typo3/cms-*` to `^14`. Bump third-party exts (news, address, t3bootstrap/*, …) to their
|
||
^14 releases. Drop anything with no v14 release (e.g. **flux/vhs are gone in v14 t3bootstrap** —
|
||
`t3bootstrap/flux-*` do not exist; do not re-add them).
|
||
- **Custom-repo package name ≠ repo name.** A git repo `…/<name>.git` may publish itself as a
|
||
*different* composer package name — read the repo's `composer.json` `name` and require THAT.
|
||
- **Contradictory constraints fail:** `"^14 dev-release/v14"` (tag AND branch) → composer "cannot
|
||
possibly match". Use plain `"^14"` for a tagged package, or `"dev-<branch>"` for an untagged one.
|
||
- **Repository order controls the source.** If the same package name is served by both a git repo and
|
||
the registry, composer takes the **first** repo listed as canonical → list the project's git repo
|
||
**before** the composer registry in `repositories` to make it win.
|
||
|
||
## Step 2 — Composer auth
|
||
- SSH git repos: `ddev auth ssh` (loads your keys into the DDEV agent; covers `gitea@…`/GitHub).
|
||
- Private registry: `ddev composer config http-basic.<registry-host> <user> <token>` (writes the
|
||
project `auth.json`). Get the token from the user — never invent/guess it.
|
||
|
||
## Step 3 — Install
|
||
`ddev composer update -W` (resolve + install with dependencies). Fix constraint conflicts per Step 1.
|
||
|
||
## Step 4 — Create `config/system/settings.php`
|
||
v14 does **not** auto-write `settings.php`; `ConfigurationManager` reads it if present, else falls
|
||
back to the old `typo3conf/LocalConfiguration.php` in place — so a v11→v14 jump with **production DB
|
||
creds** in the old file fails to boot first. Create `settings.php` from the old
|
||
`LocalConfiguration.php`, but with **DDEV DB creds** (host `db`, name `db`, user `db`, pass `db`, port
|
||
3306, `utf8mb4`). Preserve `SYS/encryptionKey`, install-tool password, sitename, systemMaintainers,
|
||
mail sender, and per-extension `EXTENSIONS` config. Drop obsolete v11 feature flags.
|
||
|
||
## Step 5 — Replace `public/.htaccess`
|
||
Swap in the v14 template:
|
||
`cp vendor/typo3/cms-install/Resources/Private/FolderStructureTemplateFiles/root-htaccess public/.htaccess`.
|
||
Symptom of the stale v11 `.htaccess`: Apache **500 "Request exceeded the limit of 10 internal
|
||
redirects"** (looks like a config error; it's the htaccess).
|
||
|
||
## Step 6 — Update the DB schema
|
||
`ddev exec vendor/bin/typo3 database:updateschema "*.add,*.change"` — adds the ~20 tables v12–v14 need
|
||
(e.g. `sys_csp_resolution`). Non-destructive (add/change only). Symptom if skipped:
|
||
`Table 'db.sys_csp_resolution' doesn't exist` on any backend request.
|
||
|
||
## Step 7 — Backend admin
|
||
`ddev exec vendor/bin/typo3 backend:createadmin <user> <pass>` (or `backend:resetpassword` if an admin
|
||
already exists in the dump). Do NOT create admin users the user didn't ask for on shared/live systems;
|
||
local DDEV is fine.
|
||
|
||
## Step 8 — Upgrade wizards (⚠ snapshot first)
|
||
`ddev snapshot --name pre-upgradewizards`. Then `vendor/bin/typo3 upgrade:list` and run wizards:
|
||
- **`upgrade:run` gates on a "Database Up-to-Date" prerequisite** → clear data blockers, re-run
|
||
`database:updateschema`, then `upgrade:run`. Blockers commonly seen:
|
||
- `tx_news_domain_model_news.related_links` NULL/non-int → `UPDATE … SET related_links=0 WHERE
|
||
related_links IS NULL OR NOT related_links REGEXP '^[0-9]+$'`.
|
||
- `sys_category_record_mm` duplicate PK tuples (table has no `uid`) → add a temp AUTO_INCREMENT PK,
|
||
`DELETE` via self-join keeping the lowest, drop the temp column.
|
||
- **Confirmable wizards** (e.g. `convertHeadersize`, `rewriteIconPaths`) error *"You have to
|
||
acknowledge this wizard"* under `--no-interaction`. There is **no `--confirm` flag** (`--confirm all`
|
||
throws). Run interactively with piped input: `printf 'yes\n' | ddev exec vendor/bin/typo3
|
||
upgrade:run <id>` (omit `--no-interaction`).
|
||
- **One-way wizards** (e.g. container migration, address→contacts) — the snapshot is your only undo.
|
||
- **news 14.0.x vs `FlexFormTools::cleanFlexFormXML()`**: older news calls it with 3 args, v14.3 wants
|
||
4 → blocks `txNewsPluginUpdater`. Fix: update news to a patched 14.x, or one-time-patch the vendor
|
||
`PluginUpdater` (the wizard marks itself done; the patch is then harmless if reverted).
|
||
|
||
## Step 8b — Language packs
|
||
`ddev exec vendor/bin/typo3 language:update`. Composer mode ships **no** language packs → labels fall
|
||
back to English (e.g. news "more-link"). Run this whenever the site language isn't English.
|
||
|
||
## Step 8c — Non-admin editor permissions (⚠ the migration does NOT fix these)
|
||
Very confusing symptom: a **non-admin editor** (e.g. a "Redakteur" group) opens the Page module, **sees
|
||
the content but there are no edit icons**, and/or the **Media tab is missing** from the content form —
|
||
while the Access (Berechtigungen) module shows the group *has* "edit content" and `tables_modify` includes
|
||
`tt_content`. Cause: the v11→v14 upgrade wizards do **not** convert the backend group access-lists. Fix
|
||
three separate `be_groups` fields, per editor group (find it: `SELECT uid,title FROM be_groups WHERE
|
||
deleted=0`). Editors must **log out/in** afterwards.
|
||
|
||
1. **`explicit_allowdeny` format — THE big one (blocks ALL content editing).** v≤11 stored allowed values
|
||
as `tt_content:CType:text:ALLOW`; **v12+ dropped the suffix** and `BackendUserAuthentication::
|
||
checkAuthMode()` does an exact `GeneralUtility::inList()` against `tt_content:CType:text` (no suffix).
|
||
The migrated `:ALLOW` never matches → every CType (even plain Text/Header) is denied → no edit icons on
|
||
anything. Strip the suffix (all groups):
|
||
`UPDATE be_groups SET explicit_allowdeny = REPLACE(explicit_allowdeny, ':ALLOW', '') WHERE deleted=0`
|
||
(`:DENY` entries may stay — `…:value:DENY` never equals `…:value`, so the value correctly stays
|
||
non-allowed.) Verify: `FIND_IN_SET('tt_content:CType:text', explicit_allowdeny) > 0`.
|
||
2. **Missing v14 CType names in the allow-list.** The container migration renamed CTypes `wst3bootstrap_*`
|
||
→ `t3bs_*`; the allow-list only has the old names. Append the new names actually used — compare
|
||
`SELECT DISTINCT CType FROM tt_content` vs the list and add `tt_content:CType:<name>` (NO `:ALLOW`
|
||
suffix) for each missing one; typically `t3bs_fluidrow/_column/_tabs/_tab_item/_accordion/
|
||
_accordion_item/_megamenu`, `ws_slider`, `heroitem`, `news_*`, `address_*`.
|
||
3. **`non_exclude_fields` missing v14 fields → hidden tabs (e.g. Media).** TCA `exclude` fields are hidden
|
||
from non-admins unless granted here (format `table:field`, unchanged in v12). The v11 list lacks fields
|
||
ADDED in v14, so any tab whose fields are ALL exclude+denied won't render — e.g. the hero **Media** tab
|
||
contains only `tx_heroitem_bg`/`_bg_tablet`/`_bg_smartphone` (all denied → no tab). Grant the missing
|
||
`tt_content` exclude fields (append `tt_content:<field>` for each). Common culprits: `imagecols_grid,
|
||
effects, aos_*, bg_container, tx_wsslider_{renderer,layout,source}, tx_heroitem_*, tx_teaser2_layout`.
|
||
NOTE `image`/`assets`/`media` are NOT exclude fields (always shown) — the missing tab is the *settings*
|
||
fields, not the FAL field. Other record types editors touch (news, address) can be stale the same way.
|
||
|
||
**Diagnose precisely — read-only CLI bootstrap** (also the way to *confirm* a fix without a BE login):
|
||
```php
|
||
SystemEnvironmentBuilder::run(0, SystemEnvironmentBuilder::REQUESTTYPE_CLI | SystemEnvironmentBuilder::REQUESTTYPE_BE);
|
||
Bootstrap::init($classLoader);
|
||
$be = GeneralUtility::makeInstance(BackendUserAuthentication::class);
|
||
$be->user = BackendUtility::getRecord('be_users', <uid>); $be->fetchGroupData(); $GLOBALS['BE_USER'] = $be;
|
||
$be->recordEditAccessInternals('tt_content', $row, false, false, true); // false → $be->errorMsg names the failing gate
|
||
$be->check('non_exclude_fields', 'tt_content:<field>'); // tab/field visibility
|
||
$be->doesUserHaveAccess($pageRow, 16); // page "edit content" perm
|
||
```
|
||
`recordEditAccessInternals` failing with `authMode "explicitAllow" failed for field "CType"` == facet 1/2.
|
||
(Writing `be_groups` *from* such a script may be blocked by agent tooling — apply fixes as plain SQL
|
||
`UPDATE`/`REPLACE`/`CONCAT`.) Red herring: an empty `db_mountpoints` on the user is fine if the group (or a
|
||
subgroup) supplies a mount — check the user's *effective* `getWebmounts()` before chasing it.
|
||
|
||
## Step 9 — Wire TypoScript via site sets
|
||
v14 t3bootstrap delivers TypoScript (incl. the `PAGE` object) through **site sets**, not `sys_template`.
|
||
The site config must declare them under `dependencies:` in `config/sites/<SITE_ID>/config.yaml`, or the
|
||
FE dies with **"No page configured for type=0"**. Add the set identifiers (the `name:` in each ext's
|
||
`Configuration/Sets/*/config.yaml` — independent of package name/extkey), e.g. the theme set +
|
||
container-bs5-templates + news/form/address/blog/slider as used. See the design skill for details.
|
||
|
||
## Step 10 — Flush + verify
|
||
`ddev exec vendor/bin/typo3 cache:flush`. Verify over HTTP with the **correct host**: `/typo3/` → 200,
|
||
FE with `curl -H "Host: <SITE_BASE_HOST>" …` (or the real DDEV URL) → 200. **"No site configuration
|
||
found" is usually a false alarm from curling `localhost`** instead of `<SITE_BASE_HOST>`.
|
||
|
||
---
|
||
|
||
## Other recurring traps
|
||
- **Leftover custom content element with no v14 provider → FE fatal** empty-`templateName`
|
||
`InvalidTemplateResourceException`. Don't assume it's flux — check the old sitepackage; a plain
|
||
custom FSC CType must be re-registered for v14 (see the design skill's iconmenu example).
|
||
- **Relocating a git-repo package's vendor path:** change the `name` in *that repo's own*
|
||
`composer.json` (composer installs by declared name, not repo URL). Keep the `extension-key`
|
||
unchanged so `EXT:<key>/…` refs + set names keep working. Untagged repo → require `dev-<branch>`.
|
||
- **v14 FlexForm relational fields:** `FlexFormFieldValues::get()` returns a `LazyRecordCollection` /
|
||
`RecordInterface` for relation fields (NOT a CSV of uids), which breaks `intExplode()`/`(int)` casts
|
||
in migrated/old ext code → `TypeError`. Normalize to `(string)`/`(string)$record->getUid()` before
|
||
parsing. Reproduce/verify from CLI with a small bootstrap script (SystemEnvironmentBuilder::run +
|
||
Bootstrap::init + the record API).
|
||
- **TYPO3 Console** (`helhum/typo3-console` ^8.3) works with v14 — `vendor/bin/typo3` is the console
|
||
binary.
|
||
|
||
Each step verified + a snapshot before wizards = a reversible, debuggable upgrade.
|