Compare commits
147
Commits
de8e3f55df
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17b08df283 | ||
|
|
20bcbeb731 | ||
|
|
47c8e2902f | ||
|
|
6a00926b89 | ||
|
|
4f47382679 | ||
|
|
8031247acf | ||
|
|
e14f94ba63 | ||
|
|
06b9bc8293 | ||
|
|
4bc38b1760 | ||
|
|
b36dceb486 | ||
|
|
90cb2faaf9 | ||
|
|
994f9f2a1b | ||
|
|
62f3f5394e | ||
|
|
cda26947f2 | ||
|
|
1b72e65a1d | ||
|
|
67ded6e502 | ||
|
|
a9923e19e0 | ||
|
|
8cdcd99eb3 | ||
|
|
086a178341 | ||
|
|
07f74e372b | ||
|
|
5824981e2b | ||
|
|
da2ca2690d | ||
|
|
f4f18e913a | ||
|
|
2aa8865b23 | ||
|
|
e357f42b81 | ||
|
|
4b6731786d | ||
|
|
f7b1eacecb | ||
|
|
37c4c116d0 | ||
|
|
08c02276e5 | ||
|
|
33bdb8d856 | ||
|
|
8a652063c5 | ||
|
|
5121eaa000 | ||
|
|
ff6f86b829 | ||
|
|
be4bb44c40 | ||
|
|
dc9b5fce47 | ||
|
|
5c640d02f7 | ||
|
|
c9b00e6f10 | ||
|
|
be163a88c2 | ||
|
|
8f8ae039d4 | ||
|
|
cb53ff67ee | ||
|
|
bc2e031b0c | ||
|
|
b9736b9ac7 | ||
|
|
be519f9bc1 | ||
|
|
5b524bfce9 | ||
|
|
fa63871da7 | ||
|
|
91c2ceb5b1 | ||
|
|
63f950f9db | ||
|
|
d52d49974f | ||
|
|
d153cc2eba | ||
|
|
232aaf2ca8 | ||
|
|
351349ec1f | ||
|
|
dcd713f222 | ||
|
|
14623ed4fa | ||
|
|
2c0da281bd | ||
|
|
bd7a145afa | ||
|
|
73e4298b75 | ||
|
|
ad1f3c179c | ||
|
|
f525ce3c27 | ||
|
|
6ff9c3e7a8 | ||
|
|
46b12d4a58 | ||
|
|
3ccb9b149e | ||
|
|
d26c31e6d1 | ||
|
|
ed3ced42b3 | ||
|
|
8e173b1a85 | ||
|
|
77661bf0b6 | ||
|
|
9211c8a395 | ||
|
|
9fb7f0ca77 | ||
|
|
da2ebdca23 | ||
|
|
d87e64f73b | ||
|
|
80f6da7949 | ||
|
|
b0690c4cdc | ||
|
|
c8d653b39b | ||
|
|
c6147d020e | ||
|
|
e2d4367d7a | ||
|
|
8497dad06a | ||
|
|
6423b7c7f8 | ||
|
|
90c2f97c5b | ||
|
|
3b3ef470b0 | ||
|
|
c4ec979f78 | ||
|
|
46049bf30d | ||
|
|
8adbe8cc90 | ||
|
|
a9a25e4b1e | ||
|
|
6b0b2a3511 | ||
|
|
b1357a6ffe | ||
|
|
cbe46cdba2 | ||
|
|
303defe24d | ||
|
|
115952a132 | ||
|
|
3e2ebf3823 | ||
|
|
d115ffb144 | ||
|
|
74502e60a3 | ||
|
|
eb0d0d9ebc | ||
|
|
887caed2aa | ||
|
|
b5fcdb2b5a | ||
|
|
490bac500e | ||
|
|
91ad5e6596 | ||
|
|
8ead213bdf | ||
|
|
9b1ad202a9 | ||
|
|
8e67f5db23 | ||
|
|
3d5459b8d2 | ||
|
|
d496877a53 | ||
|
|
872c6e715c | ||
|
|
78af83d2e3 | ||
|
|
67be9de316 | ||
|
|
8b175211ea | ||
|
|
90ae6803a4 | ||
|
|
f0b0694842 | ||
|
|
6db2234851 | ||
|
|
43519505bc | ||
|
|
6961254e57 | ||
|
|
6811463343 | ||
|
|
3df66b1353 | ||
|
|
4a7ea387a0 | ||
|
|
55f7f82a42 | ||
|
|
c90649fc1e | ||
|
|
8c881d153c | ||
|
|
5bb4e374b4 | ||
|
|
e9eaa62d89 | ||
|
|
c91063d6f3 | ||
|
|
4a49196bcc | ||
|
|
5aad13e635 | ||
|
|
5499ca54af | ||
|
|
7833e1b668 | ||
|
|
7442d10e68 | ||
|
|
d846ba2007 | ||
|
|
fc3799c791 | ||
|
|
083f0937e2 | ||
|
|
cf7a04ea15 | ||
|
|
710f95794c | ||
|
|
7bd1161abf | ||
|
|
12eb40614b | ||
|
|
4ef3ad0026 | ||
|
|
b08ea7a55f | ||
|
|
58990ebe8d | ||
|
|
c720696781 | ||
|
|
f515de54ad | ||
|
|
8d3ac08a37 | ||
|
|
48da8bccd8 | ||
|
|
1011162bb1 | ||
|
|
5e3be3ec42 | ||
|
|
10c0328bf2 | ||
|
|
baa0fdd331 | ||
|
|
27cc22dc69 | ||
|
|
a739dd8fe3 | ||
|
|
eae40f4286 | ||
|
|
aafcb5dcb6 | ||
|
|
cda1571d9d | ||
|
|
5d14bd5809 |
No files matched your search
+31
-1
@@ -9,7 +9,10 @@
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/config/system/settings.php
|
||||
# The whole directory, not just the two known files: editor backups like
|
||||
# additional.php.save would otherwise slip into a careless `git add -A`
|
||||
# (settings.php holds credentials).
|
||||
/config/system/
|
||||
auth.json
|
||||
|
||||
# --------------------------------------------------
|
||||
@@ -40,6 +43,7 @@ auth.json
|
||||
# --------------------------------------------------
|
||||
/public/fileadmin/
|
||||
/public/uploads/
|
||||
/public/_frontend/
|
||||
|
||||
# --------------------------------------------------
|
||||
# Projektspezifisch ignoriert
|
||||
@@ -117,3 +121,29 @@ CLAUDE.local.md
|
||||
|
||||
# TYPO3 auto-generated public assets
|
||||
/public/_assets_install/
|
||||
|
||||
# local developer scratch notes
|
||||
.dev-notes.md
|
||||
|
||||
# Agency product workbooks - working copies synced from Nextcloud
|
||||
# (Material/Products). The manifest IS tracked: it documents which
|
||||
# workbook feeds which product record.
|
||||
migrations/products_xlsx/*
|
||||
!migrations/products_xlsx/manifest.json
|
||||
/daten.md
|
||||
|
||||
# Workbook inboxes: filled customer/agency workbooks live in the Nextcloud
|
||||
migrations/submarkets_xlsx_in/*
|
||||
migrations/subsolutions_xlsx_in/*
|
||||
migrations/solutiongroups_xlsx_in/*
|
||||
migrations/product_images_in/
|
||||
migrations/product_images_in2/
|
||||
migrations/markethero_xlsx_in/
|
||||
migrations/product_categories_xlsx_in/
|
||||
|
||||
# generated internal report (php migrations/build_product_audit.php)
|
||||
public/product-audit/
|
||||
migrations/datapath_stories_in/
|
||||
|
||||
migrations/product_overviews_xlsx_in/
|
||||
migrations/product_images_pending/
|
||||
@@ -1,26 +1,170 @@
|
||||
## Site Sets
|
||||
<div align="center">
|
||||
|
||||
# VITEC Platform
|
||||
|
||||
### Headless TYPO3 v14 + React — the VITEC website
|
||||
|
||||
TYPO3 runs the editorial backend and serves content as **JSON**; a decoupled
|
||||
**React** front end renders it. This repository is the TYPO3 Composer distribution.
|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||

|
||||
-61DAFB?logo=react&logoColor=black)
|
||||

|
||||

|
||||
|
||||
// Site Sets
|
||||
<f:if condition="{site.settings.vitec.debugMode}">
|
||||
<div class="debug-info">
|
||||
Debug mode is enabled
|
||||
{f:debug(data)}
|
||||
</div>
|
||||
</f:if>
|
||||
|
||||
<?php
|
||||
$debugMode = $request->getAttribute('site')->getSettings()->get('vitec.debugMode');
|
||||
---
|
||||
|
||||
> **Overview**
|
||||
> This is a TYPO3 CMS base distribution configured for **headless** operation. Editors
|
||||
> work in the TYPO3 backend; every page is exposed as a JSON document (no server‑side
|
||||
> HTML). The compiled React application in [`public/_frontend/`](public/_frontend/)
|
||||
> consumes that JSON. All custom domain logic — content elements, plugins, layout
|
||||
> containers and the JSON renderers — lives in the **[`evomedien/vitec`](packages/vitec/)**
|
||||
> extension.
|
||||
|
||||
## Table of contents
|
||||
|
||||
# Custom Frames:
|
||||
/usr/home/vitecevo/public_html/live/packages/vitec/Configuration/TCA/Overrides/sys_category.php
|
||||
- [How it fits together](#how-it-fits-together)
|
||||
- [Tech stack](#tech-stack)
|
||||
- [Repository layout](#repository-layout)
|
||||
- [Requirements](#requirements)
|
||||
- [Getting started](#getting-started)
|
||||
- [Configuration](#configuration)
|
||||
- [Everyday commands](#everyday-commands)
|
||||
- [Deployment notes](#deployment-notes)
|
||||
- [Documentation](#documentation)
|
||||
- [License](#license)
|
||||
|
||||
## How it fits together
|
||||
|
||||
```
|
||||
┌────────────┐ edits ┌─────────────────────┐ JSON per page ┌────────────────┐
|
||||
│ Editors │ ─────────────▶ │ TYPO3 v14 backend │ ────────────────▶ │ React (Vite) │
|
||||
│ (backend) │ │ headless: 1 │ /page → { … } │ public/_front │
|
||||
└────────────┘ └─────────────────────┘ └────────────────┘
|
||||
│
|
||||
▼
|
||||
evomedien/vitec extension
|
||||
(content elements · plugins · JSON-LD)
|
||||
```
|
||||
|
||||
https://www.figma.com/design/0EZHIXmmRoIRk0BljKzhXq/VITEC?node-id=61-220&p=f
|
||||
The heavy lifting — the JSON envelope, per‑plugin renderers, layout containers and
|
||||
schema.org structured data — is documented in the extension’s architecture spec:
|
||||
**[`packages/vitec/Documentation/Headless-JSON-Architecture.md`](packages/vitec/Documentation/Headless-JSON-Architecture.md)**.
|
||||
|
||||
## Tech stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| CMS / backend | TYPO3 CMS `^14.3` (headless) |
|
||||
| Language / runtime | PHP `8.4` |
|
||||
| Dependency management | Composer (base distribution) |
|
||||
| Headless rendering | `friendsoftypo3/headless`, `nb-headless-content-blocks` |
|
||||
| Content modelling | `friendsoftypo3/content-blocks`, `b13/container` |
|
||||
| News | `georgringer/news` |
|
||||
| Custom logic | `evomedien/vitec` (see [`packages/vitec/`](packages/vitec/)) |
|
||||
| Front end | React, built with Vite → `public/_frontend/` |
|
||||
|
||||
vitecdev
|
||||
17ZJfy75sJoU68J6
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
live/
|
||||
├── config/
|
||||
│ ├── sites/vitec/config.yaml # site config (headless: 1, Sets, routing)
|
||||
│ └── system/settings.php # TYPO3 system config (git-ignored — secrets)
|
||||
├── packages/
|
||||
│ └── vitec/ # the custom extension (see its own README)
|
||||
├── public/
|
||||
│ ├── index.php # TYPO3 front controller
|
||||
│ ├── _frontend/ # compiled React app (Vite build)
|
||||
│ ├── _assets/ # published extension assets
|
||||
│ ├── fileadmin/ typo3/ …
|
||||
├── var/ # caches, logs (git-ignored)
|
||||
├── vendor/ # Composer dependencies (git-ignored)
|
||||
├── Documentation/ # project-level docs
|
||||
├── composer.json / composer.lock
|
||||
├── auth.json # Composer auth for private repos (git-ignored)
|
||||
└── fix-asset-perms.sh # deploy helper — fixes asset file permissions
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- **PHP 8.4**
|
||||
- **Composer 2**
|
||||
- **MySQL / MariaDB** (database)
|
||||
- **Node.js** (only to build the front end)
|
||||
- Access to the private `t3planet` Composer repository (via `auth.json`)
|
||||
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
# 1. Install PHP dependencies (requires a valid auth.json for the private repo)
|
||||
composer install
|
||||
|
||||
# 2. Configure the system (database, encryption key, …)
|
||||
# config/system/settings.php is git-ignored; create it via the Install Tool
|
||||
# or copy it from a secure source.
|
||||
|
||||
# 3. Apply the database schema
|
||||
vendor/bin/typo3 database:updateschema "*.add,*.change"
|
||||
|
||||
# 4. Warm up
|
||||
vendor/bin/typo3 cache:flush
|
||||
```
|
||||
|
||||
The document root is **`public/`**. Point the web server there; `public/index.php` is
|
||||
the TYPO3 entry point.
|
||||
|
||||
## Configuration
|
||||
|
||||
- **Headless mode & routing** — `config/sites/vitec/config.yaml`:
|
||||
```yaml
|
||||
base: /
|
||||
headless: 1
|
||||
dependencies:
|
||||
- friendsoftypo3/headless
|
||||
- friendsoftypo3/headless-mixed
|
||||
- nb-headless-content-blocks/headless-content-blocks
|
||||
- georgringer/news
|
||||
```
|
||||
- **System settings & secrets** — `config/system/settings.php` and `auth.json` are
|
||||
**git-ignored**. Never commit database credentials or Composer tokens.
|
||||
|
||||
## Everyday commands
|
||||
|
||||
```bash
|
||||
vendor/bin/typo3 cache:flush # clear all caches
|
||||
vendor/bin/typo3 database:updateschema "*.add,*.change" # apply schema changes
|
||||
bash fix-asset-perms.sh # fix asset permissions (see below)
|
||||
```
|
||||
|
||||
## Deployment notes
|
||||
|
||||
- **Document root:** `public/`.
|
||||
- **Asset permissions:** when files are deployed over an SSHFS/Windows mount they can
|
||||
land with a too‑restrictive mode (no “other” read), which makes the web server return
|
||||
**403** for backend icons/CSS/JS. Run [`fix-asset-perms.sh`](fix-asset-perms.sh) on the
|
||||
server after deploying assets (it sets files to `644` / directories to `755` under
|
||||
`public/_assets` and the extension’s `Resources/Public`, then flushes caches).
|
||||
- **Never commit** `config/system/settings.php` or `auth.json` (already git-ignored).
|
||||
|
||||
## Documentation
|
||||
|
||||
| Document | Purpose |
|
||||
|---|---|
|
||||
| [`packages/vitec/README.md`](packages/vitec/README.md) | The custom extension — features & content elements |
|
||||
| [`packages/vitec/Documentation/Headless-JSON-Architecture.md`](packages/vitec/Documentation/Headless-JSON-Architecture.md) | Authoritative headless architecture & JSON interface spec |
|
||||
| [`Documentation/CustomFrameClasses.md`](Documentation/CustomFrameClasses.md) | Custom frame classes |
|
||||
| [`VITEC-ContentBlocks-Setup.md`](VITEC-ContentBlocks-Setup.md) | Content Blocks setup notes |
|
||||
|
||||
## License
|
||||
|
||||
[GPL‑2.0‑or‑later](LICENSE) — © evomedien.
|
||||
|
||||
<div align="center"><sub>VITEC · TYPO3 headless backend + React front end</sub></div>
|
||||
@@ -15,13 +15,18 @@
|
||||
"sort-packages": true
|
||||
},
|
||||
"require": {
|
||||
"apache-solr-for-typo3/solr": "^14.0.2",
|
||||
"b13/container": "^3.1",
|
||||
"b13/typo3-updater": "^1.1",
|
||||
"evomedien/evo-megamenu-json": "^1.0.0",
|
||||
"evomedien/fe-comment": "^1.0.0",
|
||||
"evomedien/vitec": "^1.0.1",
|
||||
"friendsoftypo3/content-blocks": "^2.4",
|
||||
"friendsoftypo3/headless": "^5.0@rc",
|
||||
"georgringer/news": "^14.0",
|
||||
"netzbewegung/nb-headless-content-blocks": "^0.0.23",
|
||||
"phpoffice/phpspreadsheet": "^5.9",
|
||||
"plan2net/backend-category-hierarchy": "^14.0",
|
||||
"typo3/cms-backend": "^14.3",
|
||||
"typo3/cms-belog": "^14.3",
|
||||
"typo3/cms-beuser": "^14.3",
|
||||
@@ -40,6 +45,7 @@
|
||||
"typo3/cms-install": "^14.3",
|
||||
"typo3/cms-reactions": "^14.3",
|
||||
"typo3/cms-recycler": "^14.3",
|
||||
"typo3/cms-redirects": "^14.3",
|
||||
"typo3/cms-rte-ckeditor": "^14.3",
|
||||
"typo3/cms-scheduler": "^14.3",
|
||||
"typo3/cms-seo": "^14.3",
|
||||
|
||||
Generated
+1653
-639
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,4 @@
|
||||
base: /
|
||||
base: 'https://dev.vitec.com/'
|
||||
dependencies:
|
||||
- typo3/fluid-styled-content
|
||||
- typo3/fluid-styled-content-css
|
||||
@@ -6,7 +6,6 @@ dependencies:
|
||||
- typo3/form
|
||||
- typo3/seo-sitemap
|
||||
- friendsoftypo3/headless
|
||||
- friendsoftypo3/headless-mixed
|
||||
- nb-headless-content-blocks/headless-content-blocks
|
||||
- evomedien/vitecset
|
||||
- vitec/content-blocks-bundle
|
||||
@@ -20,6 +19,14 @@ dependencies:
|
||||
- georgringer/news-sitemap
|
||||
- georgringer/news-recordlinks
|
||||
- vitec/columns
|
||||
- apache-solr-for-typo3/solr
|
||||
- apache-solr-for-typo3/solr-open-search
|
||||
- apache-solr-for-typo3/solr-stylesheets
|
||||
- apache-solr-for-typo3/solr-bootstrap-css
|
||||
- evomedien/megamenu
|
||||
- evomedien/fe-comment
|
||||
- vitec/quotation
|
||||
- vitec/featured-content
|
||||
frontendBase: ''
|
||||
headless: 1
|
||||
languages:
|
||||
@@ -31,8 +38,26 @@ languages:
|
||||
locale: en_US.UTF-8
|
||||
navigationTitle: English
|
||||
flag: us
|
||||
solr_core_read: core_en
|
||||
rootPageId: 1
|
||||
routeEnhancers:
|
||||
# Pretty, resolvable URLs for ALL XML-sitemap parts. The typo3/seo-sitemap
|
||||
# set maps only "pages"; links to the other parts were generated as
|
||||
# ?sitemap=<name> query URLs that the renderer cannot resolve (it reads
|
||||
# tx_seo[sitemap]) - the index then returned itself. SEO audit 2026-11-09.
|
||||
Sitemap:
|
||||
type: Simple
|
||||
routePath: 'sitemap-type/{sitemap}'
|
||||
aspects:
|
||||
sitemap:
|
||||
type: StaticValueMapper
|
||||
map:
|
||||
pages: pages
|
||||
news: news
|
||||
products: products
|
||||
stories: stories
|
||||
_arguments:
|
||||
sitemap: 'tx_seo/sitemap'
|
||||
ProductPlugin:
|
||||
type: Extbase
|
||||
limitToPages:
|
||||
@@ -61,7 +86,8 @@ routeEnhancers:
|
||||
extension: News
|
||||
plugin: Pi1
|
||||
routes:
|
||||
- routePath: '/{news-title}'
|
||||
-
|
||||
routePath: '/{news-title}'
|
||||
_controller: 'News::detail'
|
||||
_arguments:
|
||||
news-title: news
|
||||
@@ -69,4 +95,38 @@ routeEnhancers:
|
||||
aspects:
|
||||
news-title:
|
||||
type: NewsTitle
|
||||
UsecaseDetail:
|
||||
type: Extbase
|
||||
limitToPages:
|
||||
- 4
|
||||
- 26
|
||||
- 25
|
||||
extension: Vitec
|
||||
plugin: Usecaseshow
|
||||
routes:
|
||||
-
|
||||
routePath: '/{usecase_slug}'
|
||||
_controller: 'Usecase::show'
|
||||
_arguments:
|
||||
usecase_slug: usecase
|
||||
defaultController: 'Usecase::show'
|
||||
requirements:
|
||||
usecase_slug: '[a-zA-Z0-9\-]+'
|
||||
aspects:
|
||||
usecase_slug:
|
||||
type: PersistedAliasMapper
|
||||
tableName: tx_vitec_domain_model_usecase
|
||||
routeFieldName: slug
|
||||
solr_enabled_read: true
|
||||
solr_host_read: vitecsolr.evomedien.de
|
||||
solr_host_write: localhost
|
||||
solr_password_read: '%env(SOLR_PASSWORD)%'
|
||||
solr_path_read: /
|
||||
solr_path_write: /
|
||||
solr_port_read: '443'
|
||||
solr_port_write: '8983'
|
||||
solr_scheme_read: https
|
||||
solr_scheme_write: ''
|
||||
solr_use_write_connection: false
|
||||
solr_username_read: '%env(SOLR_USERNAME)%'
|
||||
websiteTitle: ''
|
||||
@@ -1,4 +1,27 @@
|
||||
vitec.debugMode: true
|
||||
my.example.setting: Test
|
||||
menu.footer.pageUids: '1,2,3'
|
||||
menu.meta.pageUids: '5'
|
||||
megamenu.contentUid: 10366
|
||||
vitec.storyDetailPid: 26
|
||||
felogin.pid: '520'
|
||||
felogin.view.templateRootPath: 'EXT:headless/Resources/Private/FeLogin/Templates/'
|
||||
felogin.view.partialRootPath: 'EXT:headless/Resources/Private/FeLogin/Partials/'
|
||||
felogin.view.layoutRootPath: 'EXT:headless/Resources/Private/FeLogin/Layouts/'
|
||||
seo.site.canonicalBase: 'https://www.vitec.com'
|
||||
seo.site.name: VITEC
|
||||
seo.organization.name: VITEC
|
||||
seo.organization.legalName: 'VITEC SA'
|
||||
seo.organization.logoUrl: /fileadmin/vitec-brand/vitec-logo.png
|
||||
seo.organization.sameAs: 'https://www.linkedin.com/company/vitec-multimedia/, https://www.youtube.com/user/vitecmm'
|
||||
seo.organization.phone: '+33 1 46 73 06 06'
|
||||
seo.organization.street: '99 rue Pierre Sémard'
|
||||
seo.organization.postalCode: '92320'
|
||||
seo.organization.locality: Châtillon
|
||||
seo.organization.country: FR
|
||||
vitec.ssr.enabled: true
|
||||
|
||||
# Shell-Seiten (Produkt-/News-/Story-Detail-Container) nicht in die XML-Sitemap
|
||||
# aufnehmen; 106 = alter /en/news-Ast (noindex-Duplikate). SEO-Audit 2026-11-09.
|
||||
seo.sitemap.pages.excludePagesRecursive: '10,21,26,106'
|
||||
vitec.ssr.alertEmails:
|
||||
- info@evomedien.de
|
||||
+10
-3
@@ -16,9 +16,16 @@ set -u
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
echo "[perms] Project root: $(pwd)"
|
||||
echo "[perms] Setting files 644 / directories 755 under Resources/Public + _assets ..."
|
||||
find public/_assets packages/vitec/Resources/Public -type f -exec chmod 644 {} \; 2>/dev/null
|
||||
find public/_assets packages/vitec/Resources/Public -type d -exec chmod 755 {} \; 2>/dev/null
|
||||
# public/fileadmin as a whole, not just icons: every editorial upload and every
|
||||
# importer output lands there (success_stories_import, blog-import, downloads,
|
||||
# user_upload/products ...) and hits the same 403 otherwise. Found 2026-09-11,
|
||||
# when the imported success-story images stayed invisible in the megamenu.
|
||||
# public/product-audit: generated report page (build_product_audit.php), same 403 trap
|
||||
TARGETS="public/_assets public/fileadmin public/product-audit packages/vitec/Resources/Public"
|
||||
|
||||
echo "[perms] Setting files 644 / directories 755 under $TARGETS ..."
|
||||
find $TARGETS -type f -exec chmod 644 {} \; 2>/dev/null
|
||||
find $TARGETS -type d -exec chmod 755 {} \; 2>/dev/null
|
||||
|
||||
echo "[perms] Flushing TYPO3 caches ..."
|
||||
if [ -x vendor/bin/typo3 ]; then
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Puts the vitec_productcheck content element on the page /products/product-check
|
||||
// (uid 607) once. Idempotent: a second run reports the existing element.
|
||||
// Run on dedi: php migrations/add_product_check_element_20261112.php
|
||||
error_reporting(E_ALL); ini_set('display_errors', '1');
|
||||
$pageUid = 607;
|
||||
$root = null; $dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
|
||||
if ($root === null) { exit("no root\n"); }
|
||||
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
||||
|
||||
$page = $pdo->query("SELECT uid, title, slug FROM pages WHERE uid = $pageUid AND deleted = 0")->fetch();
|
||||
if ($page === false) { exit("page $pageUid not found\n"); }
|
||||
echo "page #{$page['uid']} {$page['title']} ({$page['slug']})\n";
|
||||
|
||||
$existing = $pdo->query("SELECT uid, hidden FROM tt_content WHERE pid = $pageUid AND CType = 'vitec_productcheck' AND deleted = 0")->fetch();
|
||||
if ($existing !== false) { echo "element already there: #{$existing['uid']} (hidden={$existing['hidden']})\n"; exit(0); }
|
||||
|
||||
$now = time();
|
||||
$stmt = $pdo->prepare('INSERT INTO tt_content (pid, tstamp, crdate, sorting, CType, colPos, header, header_layout, sys_language_uid, hidden, deleted)
|
||||
VALUES (:pid, :now, :now, 256, :ctype, 0, :header, 0, 0, 0, 0)');
|
||||
$stmt->execute([':pid' => $pageUid, ':now' => $now, ':ctype' => 'vitec_productcheck', ':header' => 'Product Content Check']);
|
||||
echo "inserted tt_content #" . $pdo->lastInsertId() . " (vitec_productcheck, colPos 0)\n";
|
||||
Binary file not shown.
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Fill empty seotitle/seometa product fields from a scraped mapping file
|
||||
* (migrations/seo_meta_vitec.json - source: the live vitec.com product
|
||||
* pages, 2026-09-04). Fill-up ONLY: fields that already hold a value are
|
||||
* never overwritten.
|
||||
*
|
||||
* Dry run: php migrations/apply_seo_meta.php
|
||||
* Write: php migrations/apply_seo_meta.php --apply
|
||||
*/
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$map = json_decode((string)file_get_contents(__DIR__ . '/seo_meta_vitec.json'), true);
|
||||
if (!is_array($map)) {
|
||||
exit("seo_meta_vitec.json missing or invalid.\n");
|
||||
}
|
||||
unset($map['_quelle']);
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$select = $pdo->prepare("SELECT uid, title, seotitle, seometa FROM tx_vitec_domain_model_product WHERE deleted = 0 AND slug = ?");
|
||||
$update = $pdo->prepare("UPDATE tx_vitec_domain_model_product SET seotitle = ?, seometa = ?, tstamp = ? WHERE uid = ?");
|
||||
|
||||
$written = 0;
|
||||
foreach ($map as $slug => $meta) {
|
||||
$select->execute([$slug]);
|
||||
$product = $select->fetch();
|
||||
if (!$product) {
|
||||
echo "FEHLT $slug - kein Produkt\n";
|
||||
continue;
|
||||
}
|
||||
$newTitle = trim((string)$product['seotitle']) === '' ? trim((string)($meta['seotitle'] ?? '')) : (string)$product['seotitle'];
|
||||
$newMeta = trim((string)$product['seometa']) === '' ? trim((string)($meta['seometa'] ?? '')) : (string)$product['seometa'];
|
||||
$changes = [];
|
||||
if ($newTitle !== (string)$product['seotitle']) {
|
||||
$changes[] = 'seotitle';
|
||||
}
|
||||
if ($newMeta !== (string)$product['seometa']) {
|
||||
$changes[] = 'seometa';
|
||||
}
|
||||
if ($changes === []) {
|
||||
echo "ok {$product['title']} - beide Felder bereits gefuellt oder keine Daten\n";
|
||||
continue;
|
||||
}
|
||||
printf("%s %s (uid %d): %s\n", $apply ? 'WRITE ' : 'plan ', $product['title'], $product['uid'], implode(' + ', $changes));
|
||||
if ($apply) {
|
||||
$update->execute([$newTitle, $newMeta, time(), (int)$product['uid']]);
|
||||
}
|
||||
$written++;
|
||||
}
|
||||
printf("\n%d Produkt(e) %s.%s\n", $written, $apply ? 'geschrieben' : 'geplant', $apply ? ' Cache leeren: vendor/bin/typo3 cache:flush' : ' Mit --apply schreiben.');
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Builds the internal product-asset audit page: one row per product record
|
||||
* with thumbnail and badges for everything we track (hero, photos, texts,
|
||||
* resources, datasheets, solutions, flags). Output is a static page under
|
||||
* the dev host's basic auth:
|
||||
*
|
||||
* public/product-audit/index.html -> https://dev.vitec.com/product-audit/
|
||||
*
|
||||
* Plain PDO against config/system/settings.php (pattern
|
||||
* migrations/check_bg.php), re-run any time the data changes:
|
||||
*
|
||||
* php migrations/build_product_audit.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
// ---- data -----------------------------------------------------------------
|
||||
$products = $pdo->query(
|
||||
"SELECT uid, title, slug, teaser, subtitle, description, description2, highlights,
|
||||
capabilities, applications, resources, resources_layout, datasheets, solutions,
|
||||
hidden, legacy, supportproduct, subproduct, hideonwebsite, hideonproducts, showdatapath
|
||||
FROM tx_vitec_domain_model_product WHERE deleted = 0 ORDER BY title"
|
||||
)->fetchAll();
|
||||
|
||||
$refs = $pdo->query(
|
||||
"SELECT r.uid_foreign, r.fieldname, COUNT(*) AS cnt, MIN(r.sorting_foreign) AS minsort
|
||||
FROM sys_file_reference r
|
||||
WHERE r.deleted = 0 AND r.hidden = 0 AND r.tablenames = 'tx_vitec_domain_model_product'
|
||||
AND r.fieldname IN ('heroimage','productimage','ogimage','videofile')
|
||||
GROUP BY r.uid_foreign, r.fieldname"
|
||||
)->fetchAll();
|
||||
$refCount = [];
|
||||
foreach ($refs as $r) {
|
||||
$refCount[(int)$r['uid_foreign']][$r['fieldname']] = (int)$r['cnt'];
|
||||
}
|
||||
|
||||
// first thumbnail per product: prefer a product photo, fall back to the hero
|
||||
$thumbRows = $pdo->query(
|
||||
"SELECT r.uid_foreign, r.fieldname, f.identifier
|
||||
FROM sys_file_reference r JOIN sys_file f ON f.uid = r.uid_local
|
||||
WHERE r.deleted = 0 AND r.hidden = 0 AND r.tablenames = 'tx_vitec_domain_model_product'
|
||||
AND r.fieldname IN ('heroimage','productimage')
|
||||
ORDER BY r.uid_foreign, FIELD(r.fieldname,'productimage','heroimage'), r.sorting_foreign"
|
||||
)->fetchAll();
|
||||
$thumb = [];
|
||||
foreach ($thumbRows as $r) {
|
||||
$uid = (int)$r['uid_foreign'];
|
||||
if (!isset($thumb[$uid])) {
|
||||
$thumb[$uid] = '/fileadmin' . $r['identifier'];
|
||||
}
|
||||
}
|
||||
|
||||
// ---- helpers ----------------------------------------------------------------
|
||||
function h(string $v): string
|
||||
{
|
||||
return htmlspecialchars($v, ENT_QUOTES | ENT_HTML5);
|
||||
}
|
||||
function badge(string $label, bool $ok, string $detail = ''): string
|
||||
{
|
||||
$cls = $ok ? 'ok' : 'miss';
|
||||
$txt = $label . ($detail !== '' ? ' ' . $detail : '');
|
||||
return '<span class="b ' . $cls . '">' . h($txt) . '</span>';
|
||||
}
|
||||
|
||||
// ---- stats ------------------------------------------------------------------
|
||||
$total = count($products);
|
||||
$stat = ['hero' => 0, 'photo' => 0, 'teaser' => 0, 'highlights' => 0, 'description' => 0, 'resources' => 0, 'datasheets' => 0, 'solutions' => 0];
|
||||
foreach ($products as $p) {
|
||||
$uid = (int)$p['uid'];
|
||||
if (($refCount[$uid]['heroimage'] ?? 0) > 0) { $stat['hero']++; }
|
||||
if (($refCount[$uid]['productimage'] ?? 0) > 0) { $stat['photo']++; }
|
||||
if (trim((string)$p['teaser']) !== '') { $stat['teaser']++; }
|
||||
if (trim((string)$p['highlights']) !== '') { $stat['highlights']++; }
|
||||
if (trim((string)$p['description']) !== '') { $stat['description']++; }
|
||||
if ((int)$p['resources'] > 0) { $stat['resources']++; }
|
||||
if ((int)$p['datasheets'] > 0) { $stat['datasheets']++; }
|
||||
if ((int)$p['solutions'] > 0) { $stat['solutions']++; }
|
||||
}
|
||||
|
||||
// ---- rows -------------------------------------------------------------------
|
||||
$rowsHtml = '';
|
||||
foreach ($products as $p) {
|
||||
$uid = (int)$p['uid'];
|
||||
$hero = $refCount[$uid]['heroimage'] ?? 0;
|
||||
$photos = $refCount[$uid]['productimage'] ?? 0;
|
||||
$og = $refCount[$uid]['ogimage'] ?? 0;
|
||||
$video = $refCount[$uid]['videofile'] ?? 0;
|
||||
$hasTeaser = trim((string)$p['teaser']) !== '';
|
||||
$hasHighlights = trim((string)$p['highlights']) !== '';
|
||||
$hasDescription = trim((string)$p['description']) !== '';
|
||||
|
||||
$classes = ['row'];
|
||||
if ($hero === 0) { $classes[] = 'f-nohero'; }
|
||||
if ($photos === 0) { $classes[] = 'f-nophoto'; }
|
||||
if (!$hasTeaser) { $classes[] = 'f-noteaser'; }
|
||||
if (!$hasHighlights) { $classes[] = 'f-nohighlights'; }
|
||||
if ((int)$p['legacy'] === 1) { $classes[] = 'f-legacy'; }
|
||||
if ((int)$p['hidden'] === 1 || (int)$p['hideonwebsite'] === 1) { $classes[] = 'f-hidden'; }
|
||||
|
||||
$flags = [];
|
||||
foreach (['hidden' => 'hidden', 'legacy' => 'legacy', 'supportproduct' => 'support', 'subproduct' => 'sub', 'hideonwebsite' => 'hideWeb', 'hideonproducts' => 'hideList', 'showdatapath' => 'datapath'] as $col => $label) {
|
||||
if ((int)$p[$col] === 1) {
|
||||
$flags[] = '<span class="b flag">' . $label . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
$img = isset($thumb[$uid])
|
||||
? '<img loading="lazy" src="' . h($thumb[$uid]) . '" alt="">'
|
||||
: '<div class="noimg">no image</div>';
|
||||
$slug = trim((string)$p['slug'], '/');
|
||||
|
||||
$rowsHtml .= '<div class="' . implode(' ', $classes) . '">'
|
||||
. '<div class="thumb">' . $img . '</div>'
|
||||
. '<div class="info">'
|
||||
. '<div class="t"><a href="/product/' . h($slug) . '" target="_blank">' . h((string)$p['title']) . '</a>'
|
||||
. ' <span class="uid">#' . $uid . '</span></div>'
|
||||
. '<div class="badges">'
|
||||
. badge('Hero', $hero > 0, $hero > 1 ? '(' . $hero . '!)' : '')
|
||||
. badge('Photos', $photos > 0, (string)($photos ?: ''))
|
||||
. badge('Teaser', $hasTeaser)
|
||||
. badge('Description', $hasDescription)
|
||||
. badge('Highlights', $hasHighlights)
|
||||
. badge('Resources', (int)$p['resources'] > 0, (string)((int)$p['resources'] ?: ''))
|
||||
. badge('Datasheets', (int)$p['datasheets'] > 0, (string)((int)$p['datasheets'] ?: ''))
|
||||
. badge('Solutions', (int)$p['solutions'] > 0, (string)((int)$p['solutions'] ?: ''))
|
||||
. badge('OG', $og > 0)
|
||||
. badge('Video', $video > 0)
|
||||
. '<span class="b lay">' . h((string)$p['resources_layout']) . '</span>'
|
||||
. implode('', $flags)
|
||||
. '</div></div></div>' . "\n";
|
||||
}
|
||||
|
||||
// ---- page -------------------------------------------------------------------
|
||||
$generated = date('Y-m-d H:i');
|
||||
$statLine = sprintf(
|
||||
'Hero %d/%d · Photos %d/%d · Teaser %d/%d · Description %d/%d · Highlights %d/%d · Resources %d/%d · Datasheets %d/%d · Solutions %d/%d',
|
||||
$stat['hero'], $total, $stat['photo'], $total, $stat['teaser'], $total, $stat['description'], $total,
|
||||
$stat['highlights'], $total, $stat['resources'], $total, $stat['datasheets'], $total, $stat['solutions'], $total
|
||||
);
|
||||
|
||||
$html = '<!doctype html><html lang="en"><head><meta charset="utf-8">'
|
||||
. '<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
. '<meta name="robots" content="noindex,nofollow">'
|
||||
. '<title>VITEC Product Audit</title><style>'
|
||||
. ':root{--ok:#1a7f37;--miss:#c0392b;--line:#e3e5e8;color-scheme:light}'
|
||||
. 'body{font:14px/1.45 system-ui,sans-serif;margin:0;background:#f6f7f9;color:#1c1e21}'
|
||||
. 'header{position:sticky;top:0;background:#fff;border-bottom:1px solid var(--line);padding:12px 20px;z-index:2}'
|
||||
. 'h1{font-size:17px;margin:0 0 4px}.stats{color:#555;font-size:12.5px}'
|
||||
. '.filters{margin-top:8px;display:flex;gap:6px;flex-wrap:wrap}'
|
||||
. '.filters button{border:1px solid var(--line);background:#fff;border-radius:14px;padding:3px 11px;font-size:12.5px;cursor:pointer}'
|
||||
. '.filters button.on{background:#26358c;color:#fff;border-color:#26358c}'
|
||||
. 'main{max-width:1200px;margin:14px auto;padding:0 20px}'
|
||||
. '.row{display:flex;gap:14px;background:#fff;border:1px solid var(--line);border-radius:8px;margin-bottom:8px;padding:10px;align-items:center}'
|
||||
. '.row.hide{display:none}'
|
||||
. '.thumb{flex:0 0 150px;height:84px;display:flex;align-items:center;justify-content:center;background:#fafbfc;border-radius:6px;overflow:hidden}'
|
||||
. '.thumb img{max-width:100%;max-height:100%;object-fit:contain}'
|
||||
. '.noimg{color:#b23;font-size:12px}'
|
||||
. '.t{font-weight:600;margin-bottom:5px}.t a{color:#1c1e21;text-decoration:none}.t a:hover{text-decoration:underline}'
|
||||
. '.uid{color:#999;font-weight:400;font-size:12px}'
|
||||
. '.badges{display:flex;flex-wrap:wrap;gap:4px}'
|
||||
. '.b{font-size:11.5px;border-radius:10px;padding:1px 8px;border:1px solid var(--line)}'
|
||||
. '.b.ok{background:#e8f5ec;color:var(--ok);border-color:#bfe3c8}'
|
||||
. '.b.miss{background:#fdecea;color:var(--miss);border-color:#f3c3bd}'
|
||||
. '.b.flag{background:#fff7e0;color:#8a6d00;border-color:#eadfa8}'
|
||||
. '.b.lay{background:#eef1fb;color:#26358c;border-color:#ccd4f0}'
|
||||
. '</style></head><body>'
|
||||
. '<header><h1>VITEC Product Audit <span class="uid">(' . $total . ' records, generated ' . $generated . ')</span></h1>'
|
||||
. '<div class="stats">' . h($statLine) . '</div>'
|
||||
. '<div class="filters">'
|
||||
. '<button class="on" data-f="">all</button>'
|
||||
. '<button data-f="f-nohero">no hero</button>'
|
||||
. '<button data-f="f-nophoto">no photos</button>'
|
||||
. '<button data-f="f-noteaser">no teaser</button>'
|
||||
. '<button data-f="f-nohighlights">no highlights</button>'
|
||||
. '<button data-f="f-legacy">legacy</button>'
|
||||
. '<button data-f="f-hidden">hidden</button>'
|
||||
. '</div></header><main>' . "\n" . $rowsHtml . '</main>'
|
||||
. '<script>document.querySelectorAll(".filters button").forEach(b=>b.addEventListener("click",()=>{'
|
||||
. 'document.querySelectorAll(".filters button").forEach(x=>x.classList.remove("on"));b.classList.add("on");'
|
||||
. 'const f=b.dataset.f;document.querySelectorAll(".row").forEach(r=>{r.classList.toggle("hide",f!==""&&!r.classList.contains(f));});'
|
||||
. '}));</script></body></html>';
|
||||
|
||||
$outDir = $root . '/public/product-audit';
|
||||
if (!is_dir($outDir)) {
|
||||
mkdir($outDir, 0775, true);
|
||||
}
|
||||
file_put_contents($outDir . '/index.html', $html);
|
||||
echo "geschrieben: public/product-audit/index.html ($total Produkte)\n";
|
||||
echo "-> https://dev.vitec.com/product-audit/\n";
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// READ-ONLY: hero references + sys_file rows of the three products that got a banner on 2026-11-10
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
||||
foreach ([25, 24, 22] as $uid) {
|
||||
$p = $pdo->query("SELECT uid, slug FROM tx_vitec_domain_model_product WHERE uid = $uid")->fetch();
|
||||
echo "\n=== #{$p['uid']} {$p['slug']}\n";
|
||||
foreach ($pdo->query("SELECT r.uid ref, r.uid_local, r.hidden, f.identifier, f.tstamp FROM sys_file_reference r JOIN sys_file f ON f.uid = r.uid_local WHERE r.deleted = 0 AND r.tablenames = 'tx_vitec_domain_model_product' AND r.fieldname = 'heroimage' AND r.uid_foreign = $uid ORDER BY r.sorting_foreign") as $r) {
|
||||
echo " heroimage ref#{$r['ref']} file#{$r['uid_local']} {$r['identifier']} (file tstamp " . date('Y-m-d H:i', (int)$r['tstamp']) . ")" . ($r['hidden'] ? ' HIDDEN' : '') . "\n";
|
||||
}
|
||||
foreach ($pdo->query("SELECT uid, identifier, tstamp FROM sys_file WHERE identifier LIKE '/product_import/{$p['slug']}/%'") as $f) {
|
||||
echo " sys_file#{$f['uid']} {$f['identifier']} (" . date('Y-m-d H:i', (int)$f['tstamp']) . ")\n";
|
||||
}
|
||||
}
|
||||
echo "\n=== feature icons prism/sff ===\n";
|
||||
foreach ($pdo->query("SELECT f.product, f.title, (SELECT COUNT(*) FROM sys_file_reference r WHERE r.deleted = 0 AND r.tablenames = 'tx_vitec_product_feature' AND r.fieldname = 'icon' AND r.uid_foreign = f.uid) icons FROM tx_vitec_product_feature f WHERE f.deleted = 0 AND f.product IN (29, 30) ORDER BY f.product, f.sorting") as $r) {
|
||||
echo " #{$r['product']} \"{$r['title']}\" icons={$r['icons']}\n";
|
||||
}
|
||||
echo "DONE\n";
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose: why does a VITEC container emit an empty `background`?
|
||||
*
|
||||
* READ-ONLY, plain PDO against config/system/settings.php - same pattern as
|
||||
* migrations/export_news.php, so it needs no TYPO3 bootstrap and cannot be
|
||||
* affected by any cache.
|
||||
*
|
||||
* Usage: php migrations/check_bg.php [uid] (default uid 729)
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$uid = (int)($argv[1] ?? 729);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo "=== 1) Do the three columns exist? ===\n";
|
||||
$cols = $pdo->query("SHOW COLUMNS FROM tt_content LIKE 'tx_vitec_bg_%'")->fetchAll();
|
||||
if ($cols === []) {
|
||||
echo " NONE. database:updateschema has not created them.\n";
|
||||
} else {
|
||||
foreach ($cols as $c) {
|
||||
echo sprintf(" %-24s %-18s default=%s\n", $c['Field'], $c['Type'], var_export($c['Default'], true));
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== 2) The record itself (uid $uid) ===\n";
|
||||
// Column list built from section 1 rather than hard-coded, so every tx_vitec_bg_*
|
||||
// field added later shows up here without the script needing an edit.
|
||||
$select = array_merge(
|
||||
['uid', 'pid', 'CType', 'deleted', 'hidden', 'sys_language_uid', 'l18n_parent'],
|
||||
array_column($cols, 'Field')
|
||||
);
|
||||
$row = $pdo->query('SELECT ' . implode(', ', $select) . " FROM tt_content WHERE uid = $uid")->fetch();
|
||||
if (!$row) {
|
||||
echo " no tt_content row with uid $uid\n";
|
||||
} else {
|
||||
foreach ($row as $k => $v) {
|
||||
echo sprintf(" %-24s %s\n", $k, var_export($v, true));
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== 3) File references pointing at this record ===\n";
|
||||
$refs = $pdo->query(
|
||||
"SELECT r.uid, r.tablenames, r.fieldname, r.uid_local, r.uid_foreign, r.deleted, r.hidden, r.sorting_foreign,
|
||||
f.identifier, f.name, f.mime_type
|
||||
FROM sys_file_reference r LEFT JOIN sys_file f ON f.uid = r.uid_local
|
||||
WHERE r.uid_foreign = $uid AND r.tablenames = 'tt_content'"
|
||||
)->fetchAll();
|
||||
if ($refs === []) {
|
||||
echo " NONE - nothing references this record. The image was not saved.\n";
|
||||
} else {
|
||||
foreach ($refs as $r) {
|
||||
echo sprintf(" ref %-6s field=%-22s file=%s deleted=%s hidden=%s\n",
|
||||
$r['uid'], $r['fieldname'], (string)$r['identifier'], $r['deleted'], $r['hidden']);
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== 4) Which fieldnames does tt_content use at all? (top 15) ===\n";
|
||||
foreach ($pdo->query("SELECT fieldname, COUNT(*) c FROM sys_file_reference WHERE tablenames='tt_content' AND deleted=0 GROUP BY fieldname ORDER BY c DESC LIMIT 15")->fetchAll() as $r) {
|
||||
echo sprintf(" %-28s %d\n", $r['fieldname'], $r['c']);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Diagnose: state of one download record (uid as argv). READ-ONLY.
|
||||
error_reporting(E_ALL); ini_set('display_errors', '1');
|
||||
$uid = (int)($argv[1] ?? 0);
|
||||
$root = null; $dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
|
||||
if ($root === null) { exit("no root\n"); }
|
||||
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
||||
$r = $pdo->query("SELECT uid, pid, title, slug, hidden, deleted, starttime, endtime, private_download, file, fileprefix, useolddl FROM tx_vitec_domain_model_download WHERE uid = $uid")->fetch();
|
||||
print_r($r ?: "not found\n");
|
||||
if ($r) {
|
||||
foreach ($pdo->query("SELECT r.uid, r.uid_local, f.identifier FROM sys_file_reference r LEFT JOIN sys_file f ON f.uid = r.uid_local WHERE r.deleted = 0 AND r.tablenames = 'tx_vitec_domain_model_download' AND r.uid_foreign = $uid") as $ref) { print_r($ref); }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** Read-only: show the duplicate solution page branches (2026-09-04). */
|
||||
$root = null; $dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
|
||||
if ($root === null) { exit("no root\n"); }
|
||||
$s = require $root . '/config/system/settings.php';
|
||||
$db = $s['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
||||
$uids = [481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500];
|
||||
$rows = $pdo->query('SELECT uid, pid, title, slug, hidden, crdate, sorting FROM pages WHERE uid IN (' . implode(',', $uids) . ') ORDER BY pid, sorting')->fetchAll();
|
||||
$parents = [];
|
||||
foreach ($rows as $r) { $parents[(int)$r['pid']] = true; }
|
||||
foreach (array_keys($parents) as $pid) {
|
||||
$p = $pdo->query("SELECT uid, pid, title, slug, hidden, crdate FROM pages WHERE uid = $pid")->fetch();
|
||||
printf("ELTERN %d | pid %d | %s | /%s | %s| angelegt %s\n", $p['uid'], $p['pid'], $p['title'], trim((string)$p['slug'],'/'), $p['hidden'] ? 'HIDDEN ' : '', date('Y-m-d H:i', (int)$p['crdate']));
|
||||
// Kinderzahl + Inhalt vorhanden?
|
||||
foreach ($rows as $r) {
|
||||
if ((int)$r['pid'] !== (int)$p['uid']) { continue; }
|
||||
$ce = (int)$pdo->query('SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND pid = ' . (int)$r['uid'])->fetchColumn();
|
||||
printf(" %d | %-42s | /%s | %s| %d CE | angelegt %s\n", $r['uid'], $r['title'], trim((string)$r['slug'],'/'), $r['hidden'] ? 'HIDDEN ' : '', $ce, date('Y-m-d H:i', (int)$r['crdate']));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Diagnose: all fe_comment summaries and items of one FE user, including
|
||||
* deleted ones, with timestamps. Notes are shortened, no secrets printed.
|
||||
* php migrations/check_fecomment_user.php 2
|
||||
*/
|
||||
$feUser = (int)($argv[1] ?? 0);
|
||||
if ($feUser <= 0) {
|
||||
fwrite(STDERR, "Aufruf: php migrations/check_fecomment_user.php <fe_users-uid>\n");
|
||||
exit(1);
|
||||
}
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO('mysql:host=' . $db['host'] . ';dbname=' . $db['dbname'] . ';charset=utf8mb4', $db['user'], $db['password']);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$summaries = $pdo->prepare(
|
||||
"SELECT uid, status, deleted, FROM_UNIXTIME(crdate) AS created, FROM_UNIXTIME(tstamp) AS changed,
|
||||
FROM_UNIXTIME(NULLIF(sent_at,0)) AS sent, recipients, CHAR_LENGTH(general_notes) AS notes_len
|
||||
FROM tx_fecomment_summary WHERE fe_user = ? ORDER BY uid"
|
||||
);
|
||||
$summaries->execute([$feUser]);
|
||||
$items = $pdo->prepare(
|
||||
"SELECT uid, deleted, FROM_UNIXTIME(crdate) AS created, FROM_UNIXTIME(tstamp) AS changed, url,
|
||||
CHAR_LENGTH(note) AS note_len
|
||||
FROM tx_fecomment_feedback WHERE summary = ? ORDER BY sorting"
|
||||
);
|
||||
foreach ($summaries->fetchAll(PDO::FETCH_ASSOC) as $s) {
|
||||
echo "Summary {$s['uid']} | status={$s['status']} deleted={$s['deleted']} | erstellt {$s['created']} | geaendert {$s['changed']}"
|
||||
. ($s['sent'] ? " | gesendet {$s['sent']}" : '') . " | recipients=[{$s['recipients']}] | general_notes " . (int)$s['notes_len'] . " Zeichen\n";
|
||||
$items->execute([(int)$s['uid']]);
|
||||
foreach ($items->fetchAll(PDO::FETCH_ASSOC) as $i) {
|
||||
echo " Item {$i['uid']} | deleted={$i['deleted']} | erstellt {$i['created']} | geaendert {$i['changed']} | note " . (int)$i['note_len'] . " Zeichen | {$i['url']}\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* Diagnose: warum schlaegt der FE-Login fehl?
|
||||
* Aufruf: php migrations/check_feuser.php <username> '<passwort>'
|
||||
* Gibt KEINE Geheimnisse aus - nur Statusfelder und ein verify-Ergebnis.
|
||||
*/
|
||||
if ($argc < 2) {
|
||||
fwrite(STDERR, "Aufruf: php migrations/check_feuser.php <username> ['<passwort>']\n");
|
||||
exit(1);
|
||||
}
|
||||
$username = $argv[1];
|
||||
$password = $argv[2] ?? null;
|
||||
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
'mysql:host=' . $db['host'] . ';dbname=' . $db['dbname'] . ';charset=utf8mb4',
|
||||
$db['user'],
|
||||
$db['password']
|
||||
);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT uid, pid, username, usergroup, disable, deleted, starttime, endtime, password
|
||||
FROM fe_users WHERE username = ?'
|
||||
);
|
||||
$stmt->execute([$username]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$rows) {
|
||||
echo "KEIN fe_users-Datensatz mit username='$username' gefunden (auch kein geloeschter).\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$now = time();
|
||||
foreach ($rows as $r) {
|
||||
echo "--- fe_users uid={$r['uid']} ---\n";
|
||||
echo 'pid: ' . $r['pid'] . ($r['pid'] == 520 ? ' (ok)' : ' <-- NICHT 520! Login-POST braucht pid=' . $r['pid']) . "\n";
|
||||
echo 'usergroup: "' . $r['usergroup'] . '"' . (in_array('1', array_map('trim', explode(',', (string)$r['usergroup'])), true) ? ' (enthaelt Gruppe 1, ok)' : ' <-- Gruppe 1 FEHLT') . "\n";
|
||||
echo 'deleted: ' . $r['deleted'] . ($r['deleted'] ? ' <-- GELOESCHT' : ' (ok)') . "\n";
|
||||
echo 'disable: ' . $r['disable'] . ($r['disable'] ? ' <-- DEAKTIVIERT' : ' (ok)') . "\n";
|
||||
echo 'starttime: ' . $r['starttime'] . ($r['starttime'] > $now ? ' <-- liegt in der ZUKUNFT' : ' (ok)') . "\n";
|
||||
echo 'endtime: ' . $r['endtime'] . (($r['endtime'] && $r['endtime'] < $now) ? ' <-- ABGELAUFEN' : ' (ok)') . "\n";
|
||||
$hash = (string)$r['password'];
|
||||
$prefix = substr($hash, 0, strpos($hash, '$', 1) ?: 8);
|
||||
echo 'hash-typ: ' . ($prefix !== '' ? $prefix : '(leer!)') . "\n";
|
||||
if ($password !== null) {
|
||||
if (str_starts_with($hash, '$argon2') || str_starts_with($hash, '$2y$')) {
|
||||
echo 'passwort: ' . (password_verify($password, $hash) ? 'VERIFIZIERT - stimmt mit dem Hash ueberein' : 'FALSCH - stimmt NICHT mit dem Hash ueberein') . "\n";
|
||||
} else {
|
||||
echo "passwort: Hash-Format unbekannt, kein lokaler Verify moeglich\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppe 1 existiert und ist nicht versteckt/geloescht?
|
||||
$g = $pdo->query('SELECT uid, pid, title, hidden, deleted FROM fe_groups WHERE uid = 1')->fetch(PDO::FETCH_ASSOC);
|
||||
echo "--- fe_groups uid=1 ---\n";
|
||||
echo $g
|
||||
? "title: \"{$g['title']}\" pid: {$g['pid']} hidden: {$g['hidden']} deleted: {$g['deleted']}\n"
|
||||
: "EXISTIERT NICHT!\n";
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose: state of the /products grid wrap (vitec:wrap-cards-grid run,
|
||||
* 2026-10-27). Cards and containers on pid 447 with colPos /
|
||||
* tx_container_parent / sorting / hidden / deleted.
|
||||
*
|
||||
* READ-ONLY. Usage: php migrations/check_grid_447.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo "=== all vitec_card / vitec_cols_33_33_33 / vitec_introparagraph on pid 447 ===\n";
|
||||
$rows = $pdo->query(
|
||||
"SELECT uid, CType, LEFT(header,32) h, colPos, tx_container_parent par, sorting, hidden, deleted
|
||||
FROM tt_content WHERE pid = 447 AND CType IN ('vitec_card','vitec_cols_33_33_33','vitec_introparagraph')
|
||||
ORDER BY deleted, colPos, sorting, uid"
|
||||
)->fetchAll();
|
||||
foreach ($rows as $r) {
|
||||
printf(" #%-6d %-22s col=%-4d par=%-6d sort=%-6d hid=%d del=%d %s\n",
|
||||
$r['uid'], $r['CType'], $r['colPos'], $r['par'], $r['sorting'], $r['hidden'], $r['deleted'], $r['h']);
|
||||
}
|
||||
echo "\ncounts: " . count($rows) . " rows\n";
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose wie check_grid_447.php, aber mit Seiten-Parameter: Karten,
|
||||
* Grid-Container und Intros einer Seite mit colPos / tx_container_parent /
|
||||
* sorting / hidden / deleted.
|
||||
*
|
||||
* READ-ONLY. Usage: php migrations/check_grid_page.php 569
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$pid = (int)($argv[1] ?? 0);
|
||||
if ($pid <= 0) {
|
||||
exit("Usage: php migrations/check_grid_page.php <pid>\n");
|
||||
}
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo "=== vitec_card / vitec_cols_* / vitec_introparagraph auf pid $pid ===\n";
|
||||
$rows = $pdo->query(
|
||||
"SELECT uid, CType, LEFT(header,32) h, colPos, tx_container_parent par, sorting, hidden, deleted
|
||||
FROM tt_content WHERE pid = $pid AND (CType IN ('vitec_card','vitec_introparagraph') OR CType LIKE 'vitec_cols_%')
|
||||
ORDER BY deleted, colPos, sorting, uid"
|
||||
)->fetchAll();
|
||||
foreach ($rows as $r) {
|
||||
printf(" #%-6d %-22s col=%-4d par=%-6d sort=%-6d hid=%d del=%d %s\n",
|
||||
$r['uid'], $r['CType'], $r['colPos'], $r['par'], $r['sorting'], $r['hidden'], $r['deleted'], $r['h']);
|
||||
}
|
||||
echo "\ncounts: " . count($rows) . " rows\n";
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Read-only: is the evo_megamenu_json schema in place?
|
||||
* Checks the four menu tables and the inline column on tt_content.
|
||||
*
|
||||
* Usage: php migrations/check_megamenu_schema.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("Could not locate project root above " . __DIR__ . "\n"); }
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$tables = [
|
||||
'tx_evomegamenujson_entry',
|
||||
'tx_evomegamenujson_column',
|
||||
'tx_evomegamenujson_item',
|
||||
'tx_evomegamenujson_teaser',
|
||||
];
|
||||
$missing = 0;
|
||||
foreach ($tables as $table) {
|
||||
$exists = (bool)$pdo->query("SHOW TABLES LIKE " . $pdo->quote($table))->fetchColumn();
|
||||
$count = $exists ? (int)$pdo->query("SELECT COUNT(*) FROM `$table`")->fetchColumn() : 0;
|
||||
printf("%-32s %s%s\n", $table, $exists ? 'OK' : 'FEHLT', $exists ? " ($count Datensaetze)" : '');
|
||||
$missing += $exists ? 0 : 1;
|
||||
}
|
||||
|
||||
$col = $pdo->query("SHOW COLUMNS FROM tt_content LIKE 'tx_evomegamenujson_entries'")->fetch();
|
||||
printf("%-32s %s\n", 'tt_content.tx_..._entries', $col ? 'OK' : 'FEHLT');
|
||||
$missing += $col ? 0 : 1;
|
||||
|
||||
$ce = (int)$pdo->query("SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND CType = 'evomegamenujson_megamenu'")->fetchColumn();
|
||||
printf("%-32s %d\n", 'Megamenu-Elemente angelegt', $ce);
|
||||
|
||||
echo "\n" . ($missing === 0
|
||||
? "Schema vollstaendig - Plugin kann angelegt werden.\n"
|
||||
: "$missing Objekt(e) fehlen - Schema-Update noetig.\n");
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose: the six old-taxonomy category stubs under /products
|
||||
* (#451-454, #456, #458) - their own content, their child pages (family
|
||||
* level) and those children's content/state. Basis for the
|
||||
* hide/move/redirect decision (2026-10-27).
|
||||
*
|
||||
* READ-ONLY. Usage: php migrations/check_old_stubs.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
function tree(PDO $pdo, int $pid, int $depth): void
|
||||
{
|
||||
$rows = $pdo->query("SELECT uid, title, slug, doktype, layout, hidden, nav_hide, sorting
|
||||
FROM pages WHERE deleted = 0 AND pid = $pid ORDER BY sorting")->fetchAll();
|
||||
foreach ($rows as $r) {
|
||||
$ce = (int)$pdo->query("SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND hidden = 0 AND pid = {$r['uid']}")->fetchColumn();
|
||||
printf("%s#%-5d dok=%d lay=%-2d hid=%d nav=%d ce=%-3d %-55s %s\n",
|
||||
str_repeat(' ', $depth), $r['uid'], $r['doktype'], $r['layout'], $r['hidden'], $r['nav_hide'], $ce, $r['slug'], $r['title']);
|
||||
tree($pdo, (int)$r['uid'], $depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ([451, 452, 453, 454, 456, 458] as $stub) {
|
||||
$r = $pdo->query("SELECT uid, title, slug, hidden, nav_hide FROM pages WHERE uid = $stub")->fetch();
|
||||
$ce = (int)$pdo->query("SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND hidden = 0 AND pid = $stub")->fetchColumn();
|
||||
printf("STUB #%-4d hid=%d nav=%d ce=%-3d %-50s %s\n", $r['uid'], $r['hidden'], $r['nav_hide'], $ce, $r['slug'], $r['title']);
|
||||
tree($pdo, $stub, 1);
|
||||
echo "\n";
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose (2026-11-10): state of the 20 product overview pages before the
|
||||
* "Enriched" workbook import, plus product slugs / solution pages needed for
|
||||
* banner, icon and solution-card mapping. READ-ONLY, plain PDO.
|
||||
*
|
||||
* php migrations/check_overview_pages_20261110.php > migrations/check_overview_out.txt
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$slugs = ['avedia', 'ez-tv', 'x-player-end-points', 'x-view-integrated-displays', 'aetria', 'operator-workstations',
|
||||
'appliances', 'modular-systems', 'rugged-appliances', 'avedia-modular-system', 'channellink-ip-gateway',
|
||||
'prism-transcoder', 'aligo', 'vsn-series-video-wall-controllers', 'video-wall-management-software',
|
||||
'vms-plugins-and-integrations', 'x-series-multi-display-processors', 'image-graphics-cards',
|
||||
'vision-video-capture-cards', 'activesqx'];
|
||||
|
||||
$ce = $pdo->prepare('SELECT uid, colPos, CType, header, header_layout, vitec_layout_variant, vitec_primary_cta, vitec_primary_cta_label, tx_container_parent, sorting, hidden, LENGTH(bodytext) blen, LEFT(bodytext, 90) bshort FROM tt_content WHERE deleted = 0 AND pid = ? AND sys_language_uid = 0 ORDER BY colPos, sorting');
|
||||
$kids = $pdo->prepare("SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND tx_container_parent = ? AND CType = 'vitec_card'");
|
||||
foreach ($slugs as $slug) {
|
||||
$p = $pdo->prepare('SELECT uid, title, nav_title, layout, seo_title, description, keywords, abstract FROM pages WHERE deleted = 0 AND slug = ? AND sys_language_uid = 0');
|
||||
$p->execute(['/products/' . $slug]);
|
||||
$page = $p->fetch();
|
||||
echo "\n=== /products/$slug ";
|
||||
if (!$page) { echo "NOT FOUND\n"; continue; }
|
||||
printf("#%d \"%s\" nav=\"%s\" layout=%s\n seo_title=%s | desc=%s | kw=%s\n", $page['uid'], $page['title'], $page['nav_title'], $page['layout'], $page['seo_title'], mb_substr((string)$page['description'], 0, 80), $page['keywords']);
|
||||
$ce->execute([$page['uid']]);
|
||||
foreach ($ce->fetchAll() as $r) {
|
||||
$n = '';
|
||||
if (str_starts_with($r['CType'], 'vitec_cols')) { $kids->execute([$r['uid']]); $n = ' cards=' . $kids->fetchColumn(); }
|
||||
if ((int)$r['tx_container_parent'] > 0 && $r['CType'] === 'vitec_card') { continue; }
|
||||
printf(" col%-3s s%-6d #%-6d %-22s %-12s %-10s \"%s\" len=%d%s %s%s\n", $r['colPos'], $r['sorting'], $r['uid'], $r['CType'], $r['vitec_layout_variant'], $r['vitec_primary_cta_label'], $r['header'], $r['blen'], $n, (int)$r['tx_container_parent'] > 0 ? ' IN#' . $r['tx_container_parent'] : '', $r['hidden'] ? ' HIDDEN' : '');
|
||||
if ($r['blen'] > 0 && !str_starts_with($r['CType'], 'vitec_cols')) { echo " > " . str_replace("\n", ' ', (string)$r['bshort']) . "\n"; }
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== products (slug | title) matching banner/icon deliveries ===\n";
|
||||
foreach ($pdo->query("SELECT uid, slug, title, hidden FROM tx_vitec_domain_model_product WHERE deleted = 0 AND (slug LIKE '%mges%' OR slug LIKE '%ace%' OR slug LIKE '%chassis%' OR slug LIKE '%prism%' OR slug LIKE '%diamond%' OR slug LIKE '%avedia%') ORDER BY slug") as $r) {
|
||||
printf(" #%-4d %-45s %s%s\n", $r['uid'], $r['slug'], $r['title'], $r['hidden'] ? ' (hidden)' : '');
|
||||
}
|
||||
|
||||
echo "\n=== pages under /solutions (2 levels) ===\n";
|
||||
$root = $pdo->query("SELECT uid FROM pages WHERE deleted = 0 AND slug = '/solutions'")->fetchColumn();
|
||||
$q = $pdo->prepare('SELECT uid, title, slug, doktype, hidden FROM pages WHERE deleted = 0 AND pid = ? AND sys_language_uid = 0 ORDER BY sorting');
|
||||
$q->execute([$root]);
|
||||
foreach ($q->fetchAll() as $g) {
|
||||
printf(" #%-4d %-55s %s%s\n", $g['uid'], $g['title'], $g['slug'], $g['hidden'] ? ' (hidden)' : '');
|
||||
$q2 = $pdo->prepare('SELECT uid, title, slug, hidden FROM pages WHERE deleted = 0 AND pid = ? AND sys_language_uid = 0 ORDER BY sorting');
|
||||
$q2->execute([$g['uid']]);
|
||||
foreach ($q2->fetchAll() as $s) {
|
||||
printf(" #%-4d %-55s %s%s\n", $s['uid'], $s['title'], $s['slug'], $s['hidden'] ? ' (hidden)' : '');
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== products with feature cards (count) for prism/sff/avedia chassis ===\n";
|
||||
foreach ($pdo->query("SELECT p.uid, p.slug, (SELECT COUNT(*) FROM tx_vitec_product_feature f WHERE f.deleted = 0 AND f.product = p.uid) n FROM tx_vitec_domain_model_product p WHERE p.deleted = 0 AND (p.slug LIKE '%prism%' OR p.slug LIKE '%chassis%')") as $r) {
|
||||
printf(" #%-4d %-45s features=%d\n", $r['uid'], $r['slug'], $r['n']);
|
||||
}
|
||||
echo "\nDONE\n";
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Comparison report: download records vs. the delivered product workbooks
|
||||
* (request Olli 2026-10-28). Per product: linked downloads
|
||||
* (tx_vitec_product_download_mm, uid_local = product), datasheet coverage
|
||||
* (download title suffix "datasheet" or a Datasheet category), the manual
|
||||
* Resources-tab selection (tx_vitec_product_datasheet_mm) - matched against
|
||||
* the workbook coverage from migrations/products_xlsx/manifest.json.
|
||||
*
|
||||
* READ-ONLY. Usage: php migrations/check_product_downloads.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
// --- workbook coverage from the manifest ------------------------------------
|
||||
$manifest = json_decode((string)file_get_contents($root . '/migrations/products_xlsx/manifest.json'), true) ?: [];
|
||||
$workbookSlugs = [];
|
||||
foreach (($manifest['workbooks'] ?? []) as $file => $entry) {
|
||||
if (!empty($entry['product'])) {
|
||||
$workbookSlugs[(string)$entry['product']] = basename((string)$file);
|
||||
}
|
||||
}
|
||||
|
||||
// --- data ---------------------------------------------------------------------
|
||||
$products = $pdo->query('SELECT uid, title, slug, hidden, legacy, supportproduct, subproduct FROM tx_vitec_domain_model_product WHERE deleted = 0 ORDER BY title')->fetchAll();
|
||||
$downloads = $pdo->query('SELECT uid, title, hidden FROM tx_vitec_domain_model_download WHERE deleted = 0')->fetchAll();
|
||||
$downloadTitle = array_column($downloads, 'title', 'uid');
|
||||
|
||||
$linksByProduct = [];
|
||||
$linkedDownloadUids = [];
|
||||
foreach ($pdo->query('SELECT uid_local, uid_foreign FROM tx_vitec_product_download_mm') as $r) {
|
||||
$linksByProduct[(int)$r['uid_local']][] = (int)$r['uid_foreign'];
|
||||
$linkedDownloadUids[(int)$r['uid_foreign']] = true;
|
||||
}
|
||||
$sheetsByProduct = [];
|
||||
foreach ($pdo->query('SELECT uid_local, COUNT(*) c FROM tx_vitec_product_datasheet_mm GROUP BY uid_local') as $r) {
|
||||
$sheetsByProduct[(int)$r['uid_local']] = (int)$r['c'];
|
||||
}
|
||||
|
||||
$isDatasheet = static function (string $title): bool {
|
||||
return (bool)preg_match('/data\s*sheet\s*$/i', trim($title));
|
||||
};
|
||||
$docType = static function (string $title): string {
|
||||
foreach (['datasheet', 'data sheet', 'brochure', 'user guide', 'quick start guide', 'application note',
|
||||
'white paper', 'case study', 'success story', 'manual', 'firmware', 'software', 'flyer', 'guide'] as $suffix) {
|
||||
if (preg_match('/' . preg_quote($suffix, '/') . '\s*$/i', trim($title))) {
|
||||
return $suffix;
|
||||
}
|
||||
}
|
||||
return '(no type suffix)';
|
||||
};
|
||||
|
||||
// --- per-product evaluation ------------------------------------------------------
|
||||
$noDownloads = [];
|
||||
$noDatasheet = [];
|
||||
$okCount = 0;
|
||||
$withoutWorkbook = [];
|
||||
foreach ($products as $p) {
|
||||
$uid = (int)$p['uid'];
|
||||
$slug = (string)$p['slug'];
|
||||
$linked = $linksByProduct[$uid] ?? [];
|
||||
$hasSheet = false;
|
||||
foreach ($linked as $dUid) {
|
||||
if ($isDatasheet((string)($downloadTitle[$dUid] ?? ''))) { $hasSheet = true; break; }
|
||||
}
|
||||
$flag = ((int)$p['legacy'] ? ' [legacy]' : '') . ((int)$p['hidden'] ? ' [hidden]' : '')
|
||||
. ((int)$p['supportproduct'] ? ' [support]' : '') . ((int)$p['subproduct'] ? ' [sub]' : '');
|
||||
$line = sprintf('#%-4d %-45s dl=%-2d sheetsTab=%d%s', $uid, $p['title'], count($linked), $sheetsByProduct[$uid] ?? 0, $flag);
|
||||
if (!isset($workbookSlugs[$slug])) {
|
||||
$withoutWorkbook[] = $line;
|
||||
}
|
||||
if (isset($workbookSlugs[$slug])) {
|
||||
if ($linked === []) { $noDownloads[] = $line; }
|
||||
elseif (!$hasSheet) { $noDatasheet[] = $line; }
|
||||
else { $okCount++; }
|
||||
}
|
||||
}
|
||||
|
||||
// --- orphan downloads ---------------------------------------------------------------
|
||||
$orphans = [];
|
||||
$orphanTypes = [];
|
||||
foreach ($downloads as $d) {
|
||||
if (!isset($linkedDownloadUids[(int)$d['uid']])) {
|
||||
$type = $docType((string)$d['title']);
|
||||
$orphanTypes[$type] = ($orphanTypes[$type] ?? 0) + 1;
|
||||
$orphans[] = $d['title'];
|
||||
}
|
||||
}
|
||||
|
||||
printf("Produkte: %d (davon %d mit Workbook) · Downloads: %d (davon %d verlinkt, %d verwaist)\n\n",
|
||||
count($products), count($workbookSlugs), count($downloads), count($linkedDownloadUids), count($orphans));
|
||||
|
||||
echo "=== A) Workbook-Produkte OHNE verknuepfte Downloads (" . count($noDownloads) . ") ===\n";
|
||||
foreach ($noDownloads as $l) { echo " $l\n"; }
|
||||
|
||||
echo "\n=== B) Workbook-Produkte mit Downloads, aber OHNE Datasheet (" . count($noDatasheet) . ") ===\n";
|
||||
foreach ($noDatasheet as $l) { echo " $l\n"; }
|
||||
|
||||
echo "\n=== C) Workbook-Produkte mit Datasheet-Abdeckung: $okCount ===\n";
|
||||
|
||||
echo "\n=== D) Produkte OHNE Workbook (" . count($withoutWorkbook) . ") - zur Einordnung ===\n";
|
||||
foreach ($withoutWorkbook as $l) { echo " $l\n"; }
|
||||
|
||||
arsort($orphanTypes);
|
||||
echo "\n=== E) Verwaiste Downloads nach Typ ===\n";
|
||||
foreach ($orphanTypes as $type => $n) { printf(" %-22s %d\n", $type, $n); }
|
||||
echo "\n Beispiele (max 30):\n";
|
||||
foreach (array_slice($orphans, 0, 30) as $t) { echo " - $t\n"; }
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Read-only: dump the current product structure - the sys_category tree and
|
||||
* every product record with its category relations - to compare against the
|
||||
* agency's "Merged site map" V2 (Products tab, columns J/K/L).
|
||||
*
|
||||
* Usage: php migrations/check_product_structure.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
// --- sys_category tree ------------------------------------------------------
|
||||
$cats = $pdo->query(
|
||||
"SELECT uid, parent, title, hidden, sorting FROM sys_category WHERE deleted = 0 ORDER BY parent, sorting"
|
||||
)->fetchAll();
|
||||
$byParent = [];
|
||||
foreach ($cats as $cat) {
|
||||
$byParent[(int)$cat['parent']][] = $cat;
|
||||
}
|
||||
|
||||
// product count per category (MM to our product table)
|
||||
$counts = [];
|
||||
foreach ($pdo->query(
|
||||
"SELECT mm.uid_local AS cat, COUNT(*) AS n
|
||||
FROM sys_category_record_mm mm
|
||||
JOIN tx_vitec_domain_model_product p ON p.uid = mm.uid_foreign AND p.deleted = 0
|
||||
WHERE mm.tablenames = 'tx_vitec_domain_model_product' AND mm.fieldname = 'categories'
|
||||
GROUP BY mm.uid_local"
|
||||
) as $row) {
|
||||
$counts[(int)$row['cat']] = (int)$row['n'];
|
||||
}
|
||||
|
||||
echo "=== sys_category tree (uid | title | hidden? | #products) ===\n";
|
||||
$printTree = function (int $parent, int $depth) use (&$printTree, $byParent, $counts): void {
|
||||
foreach ($byParent[$parent] ?? [] as $cat) {
|
||||
$uid = (int)$cat['uid'];
|
||||
printf(
|
||||
"%s%d | %s%s%s\n",
|
||||
str_repeat(' ', $depth),
|
||||
$uid,
|
||||
$cat['title'],
|
||||
$cat['hidden'] ? ' | HIDDEN' : '',
|
||||
isset($counts[$uid]) ? ' | ' . $counts[$uid] . ' products' : ''
|
||||
);
|
||||
$printTree($uid, $depth + 1);
|
||||
}
|
||||
};
|
||||
$printTree(0, 0);
|
||||
|
||||
// --- products with their categories ----------------------------------------
|
||||
$products = $pdo->query(
|
||||
"SELECT uid, title, slug, hidden, legacy, subproduct, supportproduct
|
||||
FROM tx_vitec_domain_model_product WHERE deleted = 0 ORDER BY title"
|
||||
)->fetchAll();
|
||||
|
||||
$catTitles = [];
|
||||
foreach ($cats as $cat) {
|
||||
$catTitles[(int)$cat['uid']] = $cat['title'];
|
||||
}
|
||||
$prodCats = [];
|
||||
foreach ($pdo->query(
|
||||
"SELECT uid_foreign AS product, uid_local AS cat
|
||||
FROM sys_category_record_mm
|
||||
WHERE tablenames = 'tx_vitec_domain_model_product' AND fieldname = 'categories'"
|
||||
) as $row) {
|
||||
$prodCats[(int)$row['product']][] = $catTitles[(int)$row['cat']] ?? ('?' . $row['cat']);
|
||||
}
|
||||
|
||||
echo "\n=== products (uid | title | slug | flags | categories) ===\n";
|
||||
foreach ($products as $p) {
|
||||
$uid = (int)$p['uid'];
|
||||
$flags = [];
|
||||
foreach (['hidden', 'legacy', 'subproduct', 'supportproduct'] as $flag) {
|
||||
if ($p[$flag]) {
|
||||
$flags[] = $flag;
|
||||
}
|
||||
}
|
||||
printf(
|
||||
"%d | %s | %s | %s | %s\n",
|
||||
$uid,
|
||||
$p['title'],
|
||||
$p['slug'],
|
||||
$flags === [] ? '-' : implode(',', $flags),
|
||||
isset($prodCats[$uid]) ? implode(' + ', $prodCats[$uid]) : 'KEINE KATEGORIE'
|
||||
);
|
||||
}
|
||||
printf("\n%d categories, %d products total\n", count($cats), count($products));
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Read-only: print the FlexForm settings (categories, allproducts, layout)
|
||||
* of every vitec_productlist content element, plus which of the configured
|
||||
* category uids still exist and how many products they carry.
|
||||
*
|
||||
* Usage: php migrations/check_productlist_ces.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$ces = $pdo->query(
|
||||
"SELECT uid, pid, header, hidden, pi_flexform FROM tt_content
|
||||
WHERE CType = 'vitec_productlist' AND deleted = 0 ORDER BY pid, sorting"
|
||||
)->fetchAll();
|
||||
|
||||
$flexValue = function (string $xml, string $field): string {
|
||||
if (preg_match('/<field index="settings\.' . preg_quote($field, '/') . '">.*?<value index="vDEF">(.*?)<\/value>/s', $xml, $m)) {
|
||||
return trim($m[1]);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
foreach ($ces as $ce) {
|
||||
printf("CE %d (pid %d)%s | header: %s\n", $ce['uid'], $ce['pid'], $ce['hidden'] ? ' HIDDEN' : '', $ce['header'] ?: '-');
|
||||
$cats = $flexValue((string)$ce['pi_flexform'], 'categories');
|
||||
printf(" categories: %s | allproducts: %s | layout: %s\n",
|
||||
$cats === '' ? '(leer)' : $cats,
|
||||
$flexValue((string)$ce['pi_flexform'], 'allproducts') ?: '0',
|
||||
$flexValue((string)$ce['pi_flexform'], 'layout') ?: '-'
|
||||
);
|
||||
foreach (array_filter(array_map('intval', explode(',', $cats))) as $catUid) {
|
||||
$cat = $pdo->query("SELECT title, deleted, hidden FROM sys_category WHERE uid = " . $catUid)->fetch();
|
||||
$count = (int)$pdo->query(
|
||||
"SELECT COUNT(DISTINCT p.uid) FROM sys_category_record_mm mm
|
||||
JOIN tx_vitec_domain_model_product p ON p.uid = mm.uid_foreign AND p.deleted = 0
|
||||
WHERE mm.uid_local = $catUid AND mm.tablenames = 'tx_vitec_domain_model_product' AND mm.fieldname = 'categories'"
|
||||
)->fetchColumn();
|
||||
printf(" cat %d: %s | %d products\n",
|
||||
$catUid,
|
||||
$cat ? ($cat['title'] . ($cat['deleted'] ? ' [DELETED]' : '') . ($cat['hidden'] ? ' [HIDDEN]' : '')) : 'EXISTIERT NICHT',
|
||||
$count
|
||||
);
|
||||
}
|
||||
}
|
||||
echo count($ces) . " productlist elements total\n";
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose: where does the /products page (#447) content actually live?
|
||||
* The portfolio dry-run found ZERO visible elements on pid 447 although the
|
||||
* JSON payload renders ~40. Suspicion: content_from_pid or elements on
|
||||
* another pid (10331/10368 = payload samples).
|
||||
*
|
||||
* READ-ONLY, plain PDO (pattern migrations/check_bg.php).
|
||||
* Usage: php migrations/check_products_page.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo "=== page 447 ===\n";
|
||||
$p = $pdo->query('SELECT uid, title, slug, doktype, layout, content_from_pid, l10n_parent, sys_language_uid FROM pages WHERE uid = 447')->fetch();
|
||||
print_r($p);
|
||||
|
||||
echo "\n=== tt_content on pid 447 (any state) ===\n";
|
||||
foreach ($pdo->query('SELECT colPos, CType, COUNT(*) c, SUM(hidden) hid, SUM(deleted) del FROM tt_content WHERE pid = 447 GROUP BY colPos, CType ORDER BY colPos') as $r) {
|
||||
printf(" colPos %-4s %-24s n=%-3d hidden=%d deleted=%d\n", $r['colPos'], $r['CType'], $r['c'], $r['hid'], $r['del']);
|
||||
}
|
||||
|
||||
echo "\n=== sample payload elements ===\n";
|
||||
foreach ([10331, 10368, 10306] as $uid) {
|
||||
$r = $pdo->query("SELECT uid, pid, CType, colPos, tx_container_parent, hidden, deleted, sys_language_uid FROM tt_content WHERE uid = $uid")->fetch();
|
||||
print_r($r);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose part 2: page 448 (where the rendered /products elements live)
|
||||
* plus every page whose slug starts with /products, and the visible
|
||||
* element summary of 448.
|
||||
*
|
||||
* READ-ONLY. Usage: php migrations/check_products_page2.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo "=== pages with slug like /products% (incl. deleted/hidden) ===\n";
|
||||
foreach ($pdo->query("SELECT uid, pid, title, slug, doktype, layout, hidden, deleted, sorting FROM pages WHERE slug LIKE '/products%' ORDER BY uid") as $r) {
|
||||
printf(" #%-4d pid=%-4d dok=%d lay=%-2d hid=%d del=%d sort=%-5d %-42s %s\n",
|
||||
$r['uid'], $r['pid'], $r['doktype'], $r['layout'], $r['hidden'], $r['deleted'], $r['sorting'], $r['slug'], $r['title']);
|
||||
}
|
||||
|
||||
echo "\n=== page 448 ===\n";
|
||||
print_r($pdo->query('SELECT uid, pid, title, slug, doktype, layout, hidden, deleted, content_from_pid FROM pages WHERE uid = 448')->fetch());
|
||||
|
||||
echo "\n=== visible tt_content on pid 448 (top level) ===\n";
|
||||
foreach ($pdo->query('SELECT colPos, CType, COUNT(*) c FROM tt_content WHERE pid = 448 AND deleted = 0 AND hidden = 0 AND tx_container_parent = 0 GROUP BY colPos, CType ORDER BY colPos') as $r) {
|
||||
printf(" colPos %-4s %-24s n=%d\n", $r['colPos'], $r['CType'], $r['c']);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Diagnose: what do the solution records really contain (all versions)?
|
||||
* php migrations/check_solution_fields.php 50 1
|
||||
*/
|
||||
$uids = array_map('intval', array_slice($argv, 1)) ?: [50, 1];
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO('mysql:host=' . $db['host'] . ';dbname=' . $db['dbname'] . ';charset=utf8mb4', $db['user'], $db['password']);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$in = implode(',', $uids);
|
||||
$rows = $pdo->query(
|
||||
"SELECT uid, pid, deleted, hidden, sys_language_uid, l10n_parent, t3ver_oid, t3ver_wsid, t3ver_state,
|
||||
FROM_UNIXTIME(tstamp) AS changed, title, teaser, LEFT(description, 90) AS description
|
||||
FROM tx_vitec_domain_model_solution
|
||||
WHERE uid IN ($in) OR t3ver_oid IN ($in) OR l10n_parent IN ($in)
|
||||
ORDER BY uid"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$kind = $r['t3ver_oid'] > 0
|
||||
? 'WORKSPACE-VERSION (ws ' . $r['t3ver_wsid'] . ') von uid ' . $r['t3ver_oid']
|
||||
: ($r['l10n_parent'] > 0 ? 'Uebersetzung von uid ' . $r['l10n_parent'] : 'LIVE-Datensatz');
|
||||
echo "--- uid {$r['uid']} | $kind | deleted={$r['deleted']} hidden={$r['hidden']} | geaendert {$r['changed']}\n";
|
||||
echo " title: {$r['title']}\n";
|
||||
echo " teaser: " . ($r['teaser'] === '' ? '(leer)' : $r['teaser']) . "\n";
|
||||
echo " description: " . ($r['description'] === '' || $r['description'] === null ? '(leer)' : $r['description']) . "\n";
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Read-only: dump every solution record (tx_vitec_domain_model_solution) -
|
||||
* for the comparison against the "Generic Solutions" tab of the merged
|
||||
* sitemap (2026-09-04).
|
||||
*
|
||||
* Usage: php migrations/check_solutions.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$rows = $pdo->query(
|
||||
"SELECT uid, title, slug, hidden, detail_page, teaser <> '' AS has_teaser, description <> '' AS has_description
|
||||
FROM tx_vitec_domain_model_solution WHERE deleted = 0 ORDER BY title"
|
||||
)->fetchAll();
|
||||
|
||||
echo "=== solutions (uid | title | slug | flags) ===\n";
|
||||
foreach ($rows as $row) {
|
||||
printf("%3d | %-55s | %-45s | %s%s%s%s\n",
|
||||
$row['uid'], $row['title'], $row['slug'],
|
||||
$row['hidden'] ? 'HIDDEN ' : '',
|
||||
$row['detail_page'] ? ('detail_page=' . $row['detail_page'] . ' ') : '',
|
||||
$row['has_teaser'] ? 'teaser ' : '',
|
||||
$row['has_description'] ? 'desc' : ''
|
||||
);
|
||||
}
|
||||
echo count($rows) . " solutions total\n";
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Diagnose: ordered content elements of one usecase (argv: usecase uid,
|
||||
// default 12 = ADIB) incl. columns children. READ-ONLY.
|
||||
error_reporting(E_ALL); ini_set('display_errors', '1');
|
||||
$ucUid = (int)($argv[1] ?? 12);
|
||||
$root = null; $dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
|
||||
if ($root === null) { exit("no root\n"); }
|
||||
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
||||
$story = $pdo->query("SELECT uid, title, products FROM tx_vitec_domain_model_usecase WHERE uid = $ucUid")->fetch();
|
||||
echo "Story #{$story['uid']} \"{$story['title']}\" products-MM-count-field={$story['products']}\n";
|
||||
echo "verknuepfte Produkte: ";
|
||||
foreach ($pdo->query("SELECT p.uid, p.title FROM tx_vitec_usecase_product_mm mm JOIN tx_vitec_domain_model_product p ON p.uid = mm.uid_foreign WHERE mm.uid_local = $ucUid ORDER BY mm.sorting") as $p) {
|
||||
echo "#{$p['uid']} {$p['title']} | ";
|
||||
}
|
||||
echo "\n\n";
|
||||
function dump(PDO $pdo, string $where, int $depth): void {
|
||||
foreach ($pdo->query("SELECT uid, CType, LEFT(header,45) h, colPos, sorting, hidden, LEFT(REPLACE(REPLACE(COALESCE(bodytext,''),CHAR(10),' '),CHAR(13),''),70) b FROM tt_content WHERE deleted = 0 AND $where ORDER BY sorting") as $r) {
|
||||
printf("%s#%-5d %-20s col=%-4d sort=%-6d hid=%d %-45s | %s\n", str_repeat(' ', $depth), $r['uid'], $r['CType'], $r['colPos'], $r['sorting'], $r['hidden'], $r['h'], $r['b']);
|
||||
dump($pdo, "tx_container_parent = {$r['uid']}", $depth + 1);
|
||||
}
|
||||
}
|
||||
dump($pdo, "tx_vitec_usecase_content = $ucUid AND tx_container_parent = 0", 0);
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Read-only: did the sub-market import attach its files?
|
||||
* Lists sys_file rows under submarket_import/ and every sys_file_reference
|
||||
* on page 443's content elements, plus the elements' file-field counters.
|
||||
*
|
||||
* Usage: php migrations/check_submarket_refs.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("Could not locate project root above " . __DIR__ . "\n"); }
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo "=== sys_file unter submarket_import/ ===\n";
|
||||
foreach ($pdo->query("SELECT uid, identifier, name FROM sys_file WHERE identifier LIKE '/submarket_import/%'") as $row) {
|
||||
echo " #{$row['uid']} {$row['identifier']}\n";
|
||||
}
|
||||
|
||||
echo "\n=== tt_content auf Seite 443 (Slots 110-160): Datei-Zaehler ===\n";
|
||||
foreach ($pdo->query("SELECT uid, CType, colPos, vitec_bgimage, vitec_image, vitec_video_cover FROM tt_content WHERE deleted = 0 AND pid = 443 AND colPos >= 110 ORDER BY colPos") as $row) {
|
||||
echo " #{$row['uid']} {$row['CType']} (colPos {$row['colPos']}): bgimage={$row['vitec_bgimage']} image={$row['vitec_image']} video_cover={$row['vitec_video_cover']}\n";
|
||||
}
|
||||
|
||||
echo "\n=== sys_file_reference auf diese Elemente ===\n";
|
||||
$stmt = $pdo->query("SELECT r.uid, r.uid_local, r.uid_foreign, r.fieldname, r.deleted, r.hidden, f.identifier
|
||||
FROM sys_file_reference r LEFT JOIN sys_file f ON f.uid = r.uid_local
|
||||
WHERE r.tablenames = 'tt_content' AND r.uid_foreign IN (SELECT uid FROM tt_content WHERE pid = 443 AND colPos >= 110)");
|
||||
$found = 0;
|
||||
foreach ($stmt as $row) {
|
||||
$found++;
|
||||
echo " ref #{$row['uid']} -> CE #{$row['uid_foreign']} field {$row['fieldname']} file {$row['identifier']} deleted={$row['deleted']} hidden={$row['hidden']}\n";
|
||||
}
|
||||
if ($found === 0) {
|
||||
echo " KEINE Referenzen gefunden.\n";
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Read-only: TypoScript template records and their static includes.
|
||||
* A static include that pulls headless in AFTER the site sets resets the
|
||||
* whole `page` object and silently drops page-level fields.
|
||||
*
|
||||
* Usage: php migrations/check_typoscript_templates.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$rows = $pdo->query(
|
||||
"SELECT uid, pid, title, root, clear, hidden, deleted, include_static_file, tstamp,
|
||||
LENGTH(config) AS config_len, LENGTH(constants) AS const_len
|
||||
FROM sys_template ORDER BY pid, sorting"
|
||||
)->fetchAll();
|
||||
|
||||
echo "=== sys_template ===\n";
|
||||
foreach ($rows as $r) {
|
||||
printf("uid %d | pid %d | %s%s%s%s | geaendert %s\n",
|
||||
$r['uid'], $r['pid'], $r['title'],
|
||||
$r['root'] ? ' | ROOT' : '',
|
||||
$r['hidden'] ? ' | HIDDEN' : '',
|
||||
$r['deleted'] ? ' | DELETED' : '',
|
||||
date('Y-m-d H:i', (int)$r['tstamp'])
|
||||
);
|
||||
printf(" clear=%s config=%d Zeichen constants=%d Zeichen\n", $r['clear'], $r['config_len'], $r['const_len']);
|
||||
$inc = trim((string)$r['include_static_file']);
|
||||
if ($inc !== '') {
|
||||
echo " include_static_file:\n";
|
||||
foreach (explode(',', $inc) as $i => $one) {
|
||||
printf(" %2d. %s\n", $i + 1, trim($one));
|
||||
}
|
||||
} else {
|
||||
echo " include_static_file: (leer)\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Anything in the config field that resets page?
|
||||
foreach ($rows as $r) {
|
||||
if ((int)$r['config_len'] === 0) { continue; }
|
||||
$cfg = $pdo->query("SELECT config FROM sys_template WHERE uid = " . (int)$r['uid'])->fetchColumn();
|
||||
$hits = [];
|
||||
foreach (preg_split('/\R/', (string)$cfg) as $n => $line) {
|
||||
if (preg_match('/^\s*page\s*[<=]/', $line) || str_contains($line, 'lib.headlessPage')) {
|
||||
$hits[] = ($n + 1) . ': ' . trim($line);
|
||||
}
|
||||
}
|
||||
if ($hits !== []) {
|
||||
echo "\n=== page-Zuweisungen im config von uid {$r['uid']} ===\n" . implode("\n", $hits) . "\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Read-only diagnosis: why do success stories emit empty `markets`?
|
||||
*
|
||||
* Modeled on migrations/check_bg.php - PDO against the credentials from
|
||||
* config/system/settings.php, no TYPO3 bootstrap, SELECT only.
|
||||
*
|
||||
* Run on the server: php migrations/check_usecase_markets.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $db['host'], $db['dbname']),
|
||||
$db['user'],
|
||||
$db['password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
function one(PDO $pdo, string $sql): array
|
||||
{
|
||||
return $pdo->query($sql)->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
function all(PDO $pdo, string $sql): array
|
||||
{
|
||||
return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
echo "=== 1. MM-Tabellen: Zeilen ueberhaupt da? ===\n";
|
||||
foreach (['tx_vitec_usecase_market_mm', 'tx_vitec_usecase_solution_mm'] as $mm) {
|
||||
$n = one($pdo, "SELECT COUNT(*) c FROM {$mm}");
|
||||
echo sprintf("%-32s %s Zeilen\n", $mm, $n['c'] ?? '?');
|
||||
}
|
||||
|
||||
echo "\n=== 2. Waisen: MM -> Markets, die es nicht (mehr) gibt ===\n";
|
||||
echo "verknuepfte Market-uids und ihr Zustand:\n";
|
||||
$rows = all($pdo, "
|
||||
SELECT mm.uid_foreign AS market_uid,
|
||||
COUNT(*) AS stories,
|
||||
MAX(m.uid IS NULL) AS fehlt_ganz,
|
||||
MAX(COALESCE(m.deleted, -1)) AS deleted,
|
||||
MAX(COALESCE(m.hidden, -1)) AS hidden,
|
||||
MAX(COALESCE(m.title, '?')) AS title
|
||||
FROM tx_vitec_usecase_market_mm mm
|
||||
LEFT JOIN tx_vitec_domain_model_market m ON m.uid = mm.uid_foreign
|
||||
GROUP BY mm.uid_foreign
|
||||
ORDER BY mm.uid_foreign
|
||||
");
|
||||
foreach ($rows as $r) {
|
||||
$state = $r['fehlt_ganz'] ? 'FEHLT KOMPLETT' : (($r['deleted'] ?? 0) ? 'GELOESCHT' : ((($r['hidden'] ?? 0) ? 'versteckt' : 'ok')));
|
||||
echo sprintf(" market uid %-5s %-15s %-40s (%s Stories)\n", $r['market_uid'], $state, mb_substr((string)$r['title'], 0, 40), $r['stories']);
|
||||
}
|
||||
|
||||
echo "\n=== 3. Markets-Tabelle: Bestand ===\n";
|
||||
$m = one($pdo, "SELECT COUNT(*) total, SUM(deleted) del, SUM(hidden) hid, MIN(uid) minuid, MAX(uid) maxuid FROM tx_vitec_domain_model_market");
|
||||
echo sprintf("gesamt %s | geloescht %s | versteckt %s | uid-Bereich %s-%s\n", $m['total'], $m['del'], $m['hid'], $m['minuid'], $m['maxuid']);
|
||||
echo "geloeschte Markets (falls vorhanden):\n";
|
||||
foreach (all($pdo, "SELECT uid, title, FROM_UNIXTIME(tstamp) t FROM tx_vitec_domain_model_market WHERE deleted = 1 ORDER BY uid") as $r) {
|
||||
echo sprintf(" uid %-5s %-45s zuletzt %s\n", $r['uid'], mb_substr((string)$r['title'], 0, 45), $r['t']);
|
||||
}
|
||||
|
||||
echo "\n=== 4. sys_category-Zuordnungen der Stories (JSON-Feld `categories`) ===\n";
|
||||
$c = one($pdo, "SELECT COUNT(*) c FROM sys_category_record_mm WHERE tablenames = 'tx_vitec_domain_model_usecase'");
|
||||
echo "sys_category_record_mm fuer usecases: " . ($c['c'] ?? '?') . " Zeilen\n";
|
||||
|
||||
echo "\n=== 5. Stichprobe: 3 Stories mit ihren MM-Zeilen ===\n";
|
||||
foreach (all($pdo, "
|
||||
SELECT u.uid, u.title,
|
||||
(SELECT COUNT(*) FROM tx_vitec_usecase_market_mm mm WHERE mm.uid_local = u.uid) AS mmrows
|
||||
FROM tx_vitec_domain_model_usecase u
|
||||
WHERE u.deleted = 0
|
||||
ORDER BY u.uid LIMIT 3
|
||||
") as $r) {
|
||||
echo sprintf(" story uid %-4s %-40s -> %s MM-Zeile(n)\n", $r['uid'], mb_substr((string)$r['title'], 0, 40), $r['mmrows']);
|
||||
}
|
||||
|
||||
echo "\nFertig - nichts geschrieben.\n";
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose: what does ProductXlsxReader actually extract from a workbook?
|
||||
*
|
||||
* Standalone on purpose - the reader has no TYPO3 dependencies, only
|
||||
* PhpSpreadsheet, so composer's autoloader is all it needs. That makes this the
|
||||
* exact code path the backend module runs, without cache or session in the way.
|
||||
*
|
||||
* Usage: php migrations/check_workbook.php migrations/aligo-test.xlsx
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$file = $argv[1] ?? '';
|
||||
if ($file === '' || !is_file($file)) {
|
||||
exit("Usage: php migrations/check_workbook.php <file.xlsx>\n");
|
||||
}
|
||||
|
||||
$reader = new \Evomedien\Vitec\Import\ProductXlsxReader();
|
||||
$parsed = $reader->parse($file);
|
||||
|
||||
echo "pageType : " . $parsed['pageType'] . "\n";
|
||||
echo "meta : " . json_encode($parsed['meta'], JSON_UNESCAPED_UNICODE) . "\n";
|
||||
echo "seo keys : " . implode(' | ', array_keys($parsed['seo'])) . "\n";
|
||||
echo "components: " . count($parsed['components']) . "\n\n";
|
||||
|
||||
printf("%-3s %-34s %-3s %-28s %-9s %-12s\n", '#', 'component', 'occ', 'title', 'body-len', 'cta-in-body');
|
||||
foreach ($parsed['components'] as $c) {
|
||||
printf(
|
||||
"%-3d %-34s %-3d %-28s %-9d %s\n",
|
||||
$c['order'],
|
||||
mb_substr($c['component'], 0, 34),
|
||||
$c['occurrence'],
|
||||
mb_substr($c['title'], 0, 28),
|
||||
mb_strlen((string)$c['body']),
|
||||
str_contains((string)$c['body'], '| CTA:') ? 'yes' : '-'
|
||||
);
|
||||
}
|
||||
|
||||
echo "\n--- Composed sources (g:) ---\n";
|
||||
foreach (($reader->sources($parsed)['Composed'] ?? []) as $entry) {
|
||||
echo $entry['selector'] . "\n";
|
||||
echo " label: " . $entry['label'] . "\n";
|
||||
echo " html : " . $entry['value'] . "\n\n";
|
||||
}
|
||||
|
||||
echo "\n--- Related Product Cards, raw ---\n";
|
||||
foreach ($parsed['components'] as $c) {
|
||||
if ($c['component'] !== 'Related Product Card') {
|
||||
continue;
|
||||
}
|
||||
echo "title: [" . $c['title'] . "]\n";
|
||||
echo "body : [" . $c['body'] . "]\n";
|
||||
echo "cta : [" . $c['cta'] . "]\n\n";
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Ticket 0010-FE Punkt 1: /markets/broadcast-and-telecoms ist eine Mischseite
|
||||
* (alte Pilotseite): vitec_herosection/Container/text/marketlist-Reste plus
|
||||
* colPos220-Legacy NEBEN dem 27.10.-Import. Ziel: exakt das Schema der zehn
|
||||
* anderen Market-Seiten (colPos1: markethero + introparagraph; colPos0:
|
||||
* marketintro, introparagraph, 2 cards, ctabanner).
|
||||
*
|
||||
* Whitelist = die acht Import-Elemente vom 27.10.; ALLES andere auf der
|
||||
* Seite wird soft-geloescht (deleted=1, wiederherstellbar).
|
||||
*
|
||||
* Plain PDO (Muster migrations/check_bg.php).
|
||||
*
|
||||
* Usage: php migrations/cleanup_broadcast_market_page.php (inspect)
|
||||
* php migrations/cleanup_broadcast_market_page.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
const PAGE_SLUG = '/markets/broadcast-and-telecoms';
|
||||
// 11342-11349 = Import 27.10. (inkl. der beiden HIDDEN-Stubs customerlogos/
|
||||
// usecaselist, die auch die anderen Market-Seiten tragen); 10293 = das alte
|
||||
// "Explore Broadcast & Telecoms"-Band - der Import hat hier KEINEN eigenen
|
||||
// Explore-Introparagraph angelegt, das Element bleibt die Ueberschrift
|
||||
// ueber den Karten (Inspect-Befund 2026-10-28).
|
||||
const KEEP = [11342, 11343, 11344, 11345, 11346, 11347, 11348, 11349, 10293];
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$stmt = $pdo->prepare('SELECT uid FROM pages WHERE deleted = 0 AND slug = ? AND sys_language_uid IN (0,-1)');
|
||||
$stmt->execute([PAGE_SLUG]);
|
||||
$pid = (int)$stmt->fetchColumn();
|
||||
if ($pid === 0) {
|
||||
exit("Seite " . PAGE_SLUG . " nicht gefunden.\n");
|
||||
}
|
||||
echo ($apply ? 'APPLY' : 'INSPECT') . " · Seite #$pid " . PAGE_SLUG . "\n\n";
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT uid, colPos, sorting, CType, header, tx_container_parent, hidden
|
||||
FROM tt_content WHERE deleted = 0 AND pid = ? ORDER BY colPos, sorting'
|
||||
);
|
||||
$stmt->execute([$pid]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$delete = [];
|
||||
printf("%-7s %-7s %-8s %-28s %-9s %-7s %s\n", 'uid', 'colPos', 'parent', 'CType', 'hidden', 'aktion', 'header');
|
||||
foreach ($rows as $row) {
|
||||
$keep = in_array((int)$row['uid'], KEEP, true);
|
||||
if (!$keep) {
|
||||
$delete[] = (int)$row['uid'];
|
||||
}
|
||||
printf("%-7d %-7d %-8d %-28s %-9s %-7s %s\n",
|
||||
$row['uid'], $row['colPos'], $row['tx_container_parent'], $row['CType'],
|
||||
$row['hidden'] ? 'hidden' : '-', $keep ? 'KEEP' : 'DELETE', mb_substr((string)$row['header'], 0, 45));
|
||||
}
|
||||
echo "\n" . count($rows) . " Element(e), davon " . count($delete) . " zu loeschen, " . (count($rows) - count($delete)) . " bleiben.\n";
|
||||
|
||||
$missing = array_diff(KEEP, array_map(static fn(array $r): int => (int)$r['uid'], $rows));
|
||||
if ($missing !== []) {
|
||||
echo "WARNUNG: Whitelist-Elemente fehlen auf der Seite: " . implode(', ', $missing) . " - NICHT anwenden, erst klaeren.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!$apply) {
|
||||
echo "\nInspect only - mit --apply soft-loeschen.\n";
|
||||
exit(0);
|
||||
}
|
||||
if ($delete === []) {
|
||||
echo "Nichts zu tun.\n";
|
||||
exit(0);
|
||||
}
|
||||
$in = implode(',', array_fill(0, count($delete), '?'));
|
||||
$stmt = $pdo->prepare("UPDATE tt_content SET deleted = 1, tstamp = UNIX_TIMESTAMP() WHERE uid IN ($in)");
|
||||
$stmt->execute($delete);
|
||||
echo "Soft-geloescht: " . $stmt->rowCount() . " Element(e). Danach: vendor/bin/typo3 cache:flush\n";
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Fixes from the second Seobility crawl of 2026-11-10 (after the duplicate clean-up).
|
||||
*
|
||||
* A. Card CTAs pointing at hidden/deleted product URLs (internal 301 hops):
|
||||
* ArtioSign/ArtioView cards -> /products/aetria (and the two redirects
|
||||
* follow), X-View self-link cleared, chassis card -> surviving product.
|
||||
* B. News shells still carrying the InfoComm import placeholder text:
|
||||
* #6/#7/#5 are duplicates of real articles (#17/#18/#16) -> deleted + 301;
|
||||
* #4 (EKOM) gets its text from the old /en/news page. #551 is a byte-
|
||||
* identical copy of #550 under a title that does not match the text -> 301.
|
||||
* C. Three typos.
|
||||
* D. Dead vitec.com links in old articles: "https://www.vitec.com/ ",
|
||||
* trailing-dot host, /industries, /green-initiative, two dead PDFs ->
|
||||
* internal targets.
|
||||
* E. SEO titles for the short top-level pages (/solutions "Solutions | VITEC" etc.).
|
||||
* F. Meta descriptions for the nine category pages whose generated text was
|
||||
* under 60 characters ("VITEC Avedia: Avedia Platform.").
|
||||
*
|
||||
* Dry run: php migrations/crawl2_fixes_20261110.php
|
||||
* Write: php migrations/crawl2_fixes_20261110.php --apply (--only=A,B / --skip=F)
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
$only = []; $skip = [];
|
||||
foreach ($argv as $arg) {
|
||||
if (str_starts_with($arg, '--only=')) { $only = array_map('trim', explode(',', strtoupper(substr($arg, 7)))); }
|
||||
if (str_starts_with($arg, '--skip=')) { $skip = array_map('trim', explode(',', strtoupper(substr($arg, 7)))); }
|
||||
}
|
||||
$runs = static fn(string $s): bool => (!$only || in_array($s, $only, true)) && !in_array($s, $skip, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
$now = time();
|
||||
$purgePaths = [];
|
||||
$plain = static fn(?string $h): string => trim((string)preg_replace('/\s+/', ' ', html_entity_decode(strip_tags((string)$h), ENT_QUOTES, 'UTF-8')));
|
||||
|
||||
// --- helpers -----------------------------------------------------------------
|
||||
|
||||
$hasRedirect = $pdo->prepare("SELECT uid, target FROM sys_redirect WHERE deleted = 0 AND source_path = ? LIMIT 1");
|
||||
$insertRedirect = $pdo->prepare(
|
||||
"INSERT INTO sys_redirect (pid, createdon, updatedon, source_host, source_path, target, target_statuscode)
|
||||
VALUES (0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '*', ?, ?, 301)"
|
||||
);
|
||||
$addRedirect = static function (string $source, string $target) use ($pdo, $hasRedirect, $insertRedirect, $apply, &$purgePaths): void {
|
||||
$purgePaths[$source] = true;
|
||||
$hasRedirect->execute([$source]);
|
||||
$existing = $hasRedirect->fetch();
|
||||
if ($existing) {
|
||||
if ($existing['target'] === $target) { return; }
|
||||
echo " redirect $source: Ziel {$existing['target']} -> $target\n";
|
||||
if ($apply) { $pdo->prepare("UPDATE sys_redirect SET target = ?, updatedon = UNIX_TIMESTAMP() WHERE uid = ?")->execute([$target, (int)$existing['uid']]); }
|
||||
return;
|
||||
}
|
||||
echo " redirect $source -> $target\n";
|
||||
if ($apply) { $insertRedirect->execute([$source, $target]); }
|
||||
};
|
||||
$retarget = static function (string $oldTarget, string $newTarget) use ($pdo, $apply): void {
|
||||
$st = $pdo->prepare("SELECT uid, source_path FROM sys_redirect WHERE deleted = 0 AND target = ?");
|
||||
$st->execute([$oldTarget]);
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
echo " redirect #{$r['uid']} {$r['source_path']}: Ziel $oldTarget -> $newTarget\n";
|
||||
if ($apply) { $pdo->prepare("UPDATE sys_redirect SET target = ?, updatedon = UNIX_TIMESTAMP() WHERE uid = ?")->execute([$newTarget, (int)$r['uid']]); }
|
||||
}
|
||||
};
|
||||
$softDelete = static function (string $table, int $uid) use ($pdo, $apply, $now): void {
|
||||
echo " loeschen $table #$uid\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE $table SET deleted = 1, tstamp = ? WHERE uid = ?")->execute([$now, $uid]);
|
||||
$pdo->prepare("DELETE FROM sys_category_record_mm WHERE tablenames = ? AND uid_foreign = ?")->execute([$table, $uid]);
|
||||
}
|
||||
};
|
||||
$newsRow = static function (int $uid) use ($pdo): ?array {
|
||||
$st = $pdo->prepare("SELECT uid, title, path_segment, type, LENGTH(bodytext) AS bl FROM tx_news_domain_model_news WHERE deleted = 0 AND uid = ?");
|
||||
$st->execute([$uid]);
|
||||
$r = $st->fetch();
|
||||
return $r ?: null;
|
||||
};
|
||||
$updateField = static function (string $table, int $uid, string $field, string $value) use ($pdo, $apply, $now): void {
|
||||
if ($apply) { $pdo->prepare("UPDATE $table SET $field = ?, tstamp = ? WHERE uid = ?")->execute([$value, $now, $uid]); }
|
||||
};
|
||||
// Replace patterns in a text field of every matching row; prints each change.
|
||||
$replaceIn = static function (string $table, string $field, array $pairs, string $label) use ($pdo, $apply, $now, &$purgePaths): int {
|
||||
// pair = [needle, replacement, isRegex, likeNeedle (plain text for the SQL prefilter; required for regex pairs)]
|
||||
$like = implode(' OR ', array_fill(0, count($pairs), "$field LIKE ?"));
|
||||
$params = array_map(fn($p) => '%' . ($p[3] ?? $p[0]) . '%', $pairs);
|
||||
$extra = $table === 'tx_news_domain_model_news' ? ', path_segment AS ref' : ($table === 'tt_content' ? ', pid AS ref' : ', uid AS ref');
|
||||
$st = $pdo->prepare("SELECT uid, $field AS v $extra FROM $table WHERE deleted = 0 AND ($like)");
|
||||
$st->execute($params);
|
||||
$n = 0;
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
$new = (string)$r['v'];
|
||||
foreach ($pairs as [$needle, $replacement, $isRegex]) {
|
||||
$new = $isRegex ? (string)preg_replace($needle, $replacement, $new) : str_replace($needle, $replacement, $new);
|
||||
}
|
||||
if ($new === (string)$r['v']) { continue; }
|
||||
$n++;
|
||||
echo " $label: $table #{$r['uid']} ({$r['ref']})\n";
|
||||
if ($table === 'tx_news_domain_model_news') { $purgePaths['/news/article/' . $r['ref']] = true; }
|
||||
if ($apply) { $pdo->prepare("UPDATE $table SET $field = ?, tstamp = ? WHERE uid = ?")->execute([$new, $now, (int)$r['uid']]); }
|
||||
}
|
||||
return $n;
|
||||
};
|
||||
|
||||
// --- A. card CTAs onto hidden/deleted products -----------------------------------
|
||||
|
||||
if ($runs('A')) {
|
||||
echo "\nA. Karten-Buttons auf versteckte/geloeschte Produkt-URLs\n";
|
||||
$ctas = [
|
||||
11744 => ['/product/artiosign', '/products/aetria'],
|
||||
11745 => ['/product/artioview', '/products/aetria'],
|
||||
11649 => ['/product/artiosign', '/products/aetria'],
|
||||
11650 => ['/product/artioview', '/products/aetria'],
|
||||
12179 => ['/product/avedia-modular-chassis-c1101-c1103-c1210-1', '/product/avedia-modular-chassis-c1101-c1103-c1210'],
|
||||
];
|
||||
$ce = $pdo->prepare("SELECT uid, pid, header, vitec_primary_cta, vitec_primary_cta_label FROM tt_content WHERE deleted = 0 AND uid = ?");
|
||||
foreach ($ctas as $uid => [$old, $new]) {
|
||||
$ce->execute([$uid]); $c = $ce->fetch();
|
||||
if (!$c) { continue; }
|
||||
if (trim((string)$c['vitec_primary_cta']) !== $old) { echo " CE #$uid: CTA ist '{$c['vitec_primary_cta']}', erwartet '$old' - uebersprungen\n"; continue; }
|
||||
echo " CE #$uid (Seite {$c['pid']}, \"{$c['header']}\"): $old -> $new\n";
|
||||
$updateField('tt_content', $uid, 'vitec_primary_cta', $new);
|
||||
$pageSlug = $pdo->query("SELECT slug FROM pages WHERE uid = {$c['pid']}")->fetchColumn();
|
||||
if ($pageSlug) { $purgePaths[$pageSlug] = true; }
|
||||
}
|
||||
// X-View card links to the page it sits on: no button.
|
||||
$ce->execute([12133]); $c = $ce->fetch();
|
||||
if ($c && trim((string)$c['vitec_primary_cta']) === '/product/x-view-integrated-displays') {
|
||||
echo " CE #12133 (\"{$c['header']}\"): Selbst-Link entfernt (Button weg)\n";
|
||||
$updateField('tt_content', 12133, 'vitec_primary_cta', '');
|
||||
$updateField('tt_content', 12133, 'vitec_primary_cta_label', '');
|
||||
$purgePaths['/products/x-view-integrated-displays'] = true;
|
||||
}
|
||||
// The two product redirects follow the cards.
|
||||
$addRedirect('/product/artiosign', '/products/aetria');
|
||||
$addRedirect('/product/artioview', '/products/aetria');
|
||||
}
|
||||
|
||||
// --- B. placeholder news shells -----------------------------------------------------
|
||||
|
||||
if ($runs('B')) {
|
||||
echo "\nB. Platzhalter-News (InfoComm-Importtext)\n";
|
||||
$placeholderMarker = 'VITEC will demonstrate its latest enterprise video technologies at InfoComm 2026';
|
||||
$isPlaceholder = static function (int $uid) use ($pdo, $placeholderMarker): bool {
|
||||
$b = (string)$pdo->query("SELECT bodytext FROM tx_news_domain_model_news WHERE uid = $uid")->fetchColumn();
|
||||
return strlen($b) === 1308 && str_contains($b, $placeholderMarker);
|
||||
};
|
||||
// duplicate shell => real article
|
||||
foreach ([6 => 17, 7 => 18, 5 => 16] as $shell => $real) {
|
||||
$s = $newsRow($shell); $r = $newsRow($real);
|
||||
if (!$s || !$r) { echo " #$shell/#$real fehlt, uebersprungen\n"; continue; }
|
||||
if (!$isPlaceholder($shell)) { echo " #$shell ist kein Platzhalter mehr, uebersprungen\n"; continue; }
|
||||
echo " #$shell \"{$s['title']}\" (Platzhalter) -> #$real \"{$r['title']}\" ({$r['bl']} Zeichen)\n";
|
||||
$softDelete('tx_news_domain_model_news', $shell);
|
||||
$addRedirect('/news/article/' . $s['path_segment'], '/news/article/' . $r['path_segment']);
|
||||
$retarget('/news/article/' . $s['path_segment'], '/news/article/' . $r['path_segment']);
|
||||
$purgePaths['/news/article/' . $r['path_segment']] = true;
|
||||
}
|
||||
// EKOM: text lives on the old page
|
||||
$s = $newsRow(4);
|
||||
if ($s && $isPlaceholder(4)) {
|
||||
$page = $pdo->query("SELECT uid, slug FROM pages WHERE deleted = 0 AND slug LIKE '%/news/vitec-partners-with-ekom%' ORDER BY uid LIMIT 1")->fetch();
|
||||
$html = '';
|
||||
if ($page) {
|
||||
foreach ($pdo->query("SELECT CType, bodytext FROM tt_content WHERE deleted = 0 AND hidden = 0 AND pid = {$page['uid']} AND CType IN ('text','textmedia','html') AND bodytext <> '' ORDER BY sorting") as $c) {
|
||||
$part = (string)$c['bodytext'];
|
||||
if (str_contains($part, 'vitec.com/ ')) { $part = str_replace('https://www.vitec.com/ ', '<a href="/">www.vitec.com</a>', $part); }
|
||||
$html .= $part . "\n";
|
||||
}
|
||||
}
|
||||
$len = mb_strlen($plain($html));
|
||||
if ($len >= 500) {
|
||||
echo " #4 \"{$s['title']}\": Text von Seite #{$page['uid']} ({$page['slug']}) uebernommen, $len Zeichen\n";
|
||||
$updateField('tx_news_domain_model_news', 4, 'bodytext', trim($html));
|
||||
$purgePaths['/news/article/' . $s['path_segment']] = true;
|
||||
} else {
|
||||
echo " #4: keine alte Seite mit Text gefunden ($len Zeichen) - bleibt\n";
|
||||
}
|
||||
}
|
||||
// byte-identical Datapath pair: keep the one whose title matches the text
|
||||
$a = $newsRow(551); $b = $newsRow(550);
|
||||
if ($a && $b) {
|
||||
$ba = $pdo->query("SELECT bodytext FROM tx_news_domain_model_news WHERE uid = 551")->fetchColumn();
|
||||
$bb = $pdo->query("SELECT bodytext FROM tx_news_domain_model_news WHERE uid = 550")->fetchColumn();
|
||||
if ($ba === $bb) {
|
||||
echo " #551 \"{$a['title']}\" = Textkopie von #550 \"{$b['title']}\" -> weg\n";
|
||||
$softDelete('tx_news_domain_model_news', 551);
|
||||
$addRedirect('/news/article/' . $a['path_segment'], '/news/article/' . $b['path_segment']);
|
||||
$retarget('/news/article/' . $a['path_segment'], '/news/article/' . $b['path_segment']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- C. typos ------------------------------------------------------------------------
|
||||
|
||||
if ($runs('C')) {
|
||||
echo "\nC. Tippfehler\n";
|
||||
$replaceIn('tx_news_domain_model_news', 'bodytext', [
|
||||
['/\bsponser\b/', 'sponsor', true, 'sponser'],
|
||||
['/\bSponser\b/', 'Sponsor', true, 'Sponser'],
|
||||
['/\breconnaisance\b/', 'reconnaissance', true, 'reconnaisance'],
|
||||
['/\bReconnaisance\b/', 'Reconnaissance', true, 'Reconnaisance'],
|
||||
['/\bauxillary\b/', 'auxiliary', true, 'auxillary'],
|
||||
['/\bAuxillary\b/', 'Auxiliary', true, 'Auxillary'],
|
||||
], 'Tippfehler');
|
||||
}
|
||||
|
||||
// --- D. dead vitec.com links -----------------------------------------------------------
|
||||
|
||||
if ($runs('D')) {
|
||||
echo "\nD. Tote vitec.com-Links\n";
|
||||
$pairs = [
|
||||
['https://www.vitec.com/ ', '<a href="/">www.vitec.com</a>', false],
|
||||
['~https?://www\.vitec\.com\.(?=[/"#\s<])/?~', '/', true, 'www.vitec.com.'],
|
||||
['https://www.vitec.com/green-initiative', '/insights/sustainability', false],
|
||||
['https://www.vitec.com/industries/military/government', '/markets/defense-and-security', false],
|
||||
['https://www.vitec.com/industries', '/markets', false],
|
||||
['~https?://www\.vitec\.com/fileadmin/downloads/Company_Downloads/Use_Cases/Northwestern_University_USE_CASE_WebVersion\.pdf~', '/success-stories/northwestern-university', true, 'Northwestern_University_USE_CASE'],
|
||||
['~https?://datapathdocuments\.co\.uk/wp-content/uploads/Datapath-Fx4_datasheet_EN\.pdf~', '/product/fx4', true, 'Datapath-Fx4_datasheet'],
|
||||
];
|
||||
$n = $replaceIn('tx_news_domain_model_news', 'bodytext', $pairs, 'Link');
|
||||
$n += $replaceIn('tt_content', 'bodytext', $pairs, 'Link');
|
||||
echo " $n Datensaetze\n";
|
||||
}
|
||||
|
||||
// --- E. SEO titles for short top-level pages -------------------------------------------
|
||||
|
||||
if ($runs('E')) {
|
||||
echo "\nE. Seitentitel der Hauptseiten\n";
|
||||
$titles = [
|
||||
'/solutions' => 'Video Solutions for Control Rooms, Streaming & Signage',
|
||||
'/products' => 'Products: IPTV, Video Streaming, Video Walls & Capture',
|
||||
'/support' => 'Support, Training & Product Resources',
|
||||
'/datasheets' => 'Datasheets: Encoders, IPTV, Video Walls & Capture Cards',
|
||||
'/contact' => 'Contact VITEC: Sales, Support & Offices Worldwide',
|
||||
'/contact/locations' => 'VITEC Offices & Locations Worldwide',
|
||||
'/success-stories' => 'Success Stories from Broadcast, Defense & Enterprise',
|
||||
];
|
||||
$st = $pdo->prepare("SELECT uid, title, seo_title FROM pages WHERE deleted = 0 AND slug = ?");
|
||||
foreach ($titles as $slug => $seo) {
|
||||
$st->execute([$slug]); $p = $st->fetch();
|
||||
if (!$p) { continue; }
|
||||
if ($p['seo_title'] === $seo) { continue; }
|
||||
printf(" %-20s \"%s\" -> \"%s\"\n", $slug, $p['seo_title'], $seo);
|
||||
$updateField('pages', (int)$p['uid'], 'seo_title', $seo);
|
||||
$purgePaths[$slug] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- F. descriptions for the thinnest category pages -------------------------------------
|
||||
|
||||
if ($runs('F')) {
|
||||
echo "\nF. Meta-Descriptions der kuerzesten Kategorieseiten\n";
|
||||
$desc = [
|
||||
'/products/ez-tv' => 'EZ TV Platform by VITEC: enterprise IPTV and digital signage for live TV, video distribution and targeted content across screens and devices.',
|
||||
'/products/activesqx' => 'ActiveSQX2 by VITEC and Datapath: IP stream decoding card that brings network video sources onto video walls. Specifications, features and datasheets.',
|
||||
'/products/avedia' => 'Avedia Platform by VITEC: one platform for IP video, digital signage and interactive experiences across enterprise networks.',
|
||||
'/products/rf-gateway' => 'VITEC RF gateways for the Avedia Modular System: receive satellite, cable and terrestrial TV and distribute it as IPTV across the network.',
|
||||
'/products/prism-transcoder' => 'PRISM and PRISM SFF by VITEC: broadcast-grade video transcoders for converting, scaling and re-streaming live video, including a compact small-form-factor model.',
|
||||
'/products/channellink-ip-gateway' => 'ChannelLink IP Gateway by VITEC: brings IP video streams into IPTV and digital signage networks, with transcoding for reliable delivery to every screen.',
|
||||
'/products/aligo' => 'Aligo by VITEC: AV over IP and KVM encoders, receivers and workstations (QTX100, TX100, RX100, TX100 SQX) for control rooms and multi-display environments.',
|
||||
'/products/vms-plugins-and-integrations' => 'VMS plugins and integrations for VITEC and Datapath video walls: bring Genetec and Milestone video management systems onto operator displays.',
|
||||
'/products/image-graphics-cards' => 'Datapath Image graphics cards from VITEC (Image 2K, Image 4K, IQS4): multi-output graphics for video walls and multi-display workstations.',
|
||||
];
|
||||
$st = $pdo->prepare("SELECT uid, description FROM pages WHERE deleted = 0 AND slug = ?");
|
||||
foreach ($desc as $slug => $d) {
|
||||
$st->execute([$slug]); $p = $st->fetch();
|
||||
if (!$p || $p['description'] === $d) { continue; }
|
||||
printf(" %-42s (%d -> %d Zeichen)\n alt: %s\n neu: %s\n", $slug, strlen((string)$p['description']), strlen($d), $p['description'], $d);
|
||||
$updateField('pages', (int)$p['uid'], 'description', $d);
|
||||
$purgePaths[$slug] = true;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nSSR-Pfade zum Purgen (" . count($purgePaths) . "):\n " . implode("\n ", array_keys($purgePaths)) . "\n";
|
||||
echo $apply ? "\nFERTIG.\n" : "\n(Trockenlauf, nichts geschrieben)\n";
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Create the ArtioSign and ArtioView product records (decision Olli
|
||||
* 2026-09-28, last two open manifest cases).
|
||||
*
|
||||
* Both arrive HIDDEN: the agency wrote full product workbooks, but neither
|
||||
* product exists in sitemap V2, so Olli publishes them deliberately, not an
|
||||
* import. Category: 76 "Aetria" - the home of their closest neighbour
|
||||
* Aetria WallControl - as a PROVISIONAL placement; moving them is one
|
||||
* checkbox in the backend. pid and sorting are taken from the existing
|
||||
* product records.
|
||||
*
|
||||
* Idempotent: a record whose slug already exists is skipped.
|
||||
*
|
||||
* Usage: php migrations/create_artio_records.php
|
||||
* then: vendor/bin/typo3 cache:flush
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("Could not locate project root above " . __DIR__ . "\n"); }
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
const TABLE = 'tx_vitec_domain_model_product';
|
||||
const CATEGORY_UID = 76; // "Aetria" - provisional, see header
|
||||
|
||||
$products = [
|
||||
['title' => 'ArtioSign', 'slug' => 'artiosign'],
|
||||
['title' => 'ArtioView', 'slug' => 'artioview'],
|
||||
];
|
||||
|
||||
// The product table has NO sorting column (list order comes from the
|
||||
// category tree) - only the pid is taken from the existing records.
|
||||
$ref = $pdo->query('SELECT pid FROM ' . TABLE . ' WHERE deleted = 0 GROUP BY pid ORDER BY COUNT(*) DESC LIMIT 1')->fetch();
|
||||
if (!$ref) { exit("No existing product records to take the pid from.\n"); }
|
||||
$pid = (int)$ref['pid'];
|
||||
|
||||
$catName = $pdo->query('SELECT title FROM sys_category WHERE uid = ' . CATEGORY_UID . ' AND deleted = 0')->fetchColumn();
|
||||
if ($catName === false) { exit("Category " . CATEGORY_UID . " not found - adjust CATEGORY_UID.\n"); }
|
||||
|
||||
foreach ($products as $product) {
|
||||
$exists = $pdo->prepare('SELECT uid FROM ' . TABLE . ' WHERE slug = ? AND deleted = 0');
|
||||
$exists->execute([$product['slug']]);
|
||||
if (($uid = $exists->fetchColumn()) !== false) {
|
||||
echo $product['title'] . ": exists already (uid $uid) - skipped.\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$insert = $pdo->prepare('INSERT INTO ' . TABLE . ' (pid, tstamp, crdate, hidden, title, slug, categories)
|
||||
VALUES (?, ?, ?, 1, ?, ?, 1)');
|
||||
$insert->execute([$pid, time(), time(), $product['title'], $product['slug']]);
|
||||
$uid = (int)$pdo->lastInsertId();
|
||||
|
||||
$mm = $pdo->prepare('INSERT INTO sys_category_record_mm (uid_local, uid_foreign, tablenames, fieldname, sorting, sorting_foreign)
|
||||
VALUES (?, ?, ?, ?, 0, 1)');
|
||||
$mm->execute([CATEGORY_UID, $uid, TABLE, 'categories']);
|
||||
|
||||
echo $product['title'] . ": created uid $uid (pid $pid, HIDDEN, category $catName).\n";
|
||||
}
|
||||
|
||||
echo "Done. Next: vendor/bin/typo3 cache:flush\n";
|
||||
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Duplicate clean-up after the Seobility crawl of 2026-11-10.
|
||||
*
|
||||
* A. Product copy "avedia-modular-chassis-c1101-c1103-c1210-1" (empty) -> deleted, 301 to the original.
|
||||
* B. News duplicates. Where the "-1/-2" copy carries the real text and the
|
||||
* base record is an empty shell or an import placeholder, the shell is
|
||||
* deleted and the copy takes over the clean slug (301 from the "-1" URL).
|
||||
* Records without any content anywhere (#9/#229 global events, #42 DSEI
|
||||
* 2023 pointer) are deleted and their URLs sent to the news overview.
|
||||
* C. Records that are NOT duplicates but carry a "-1/-2" slug from the
|
||||
* import: slug rebuilt from the title, 301 from the old URL.
|
||||
* D. /about (page 7): only an office list, superseded by /insights/who-we-are
|
||||
* and /about/locations -> hidden, 301, two links re-pointed.
|
||||
* E. Stories with copied teaser / SEO description (#11 UAE University,
|
||||
* #30 Kansas City Royals, #35 Minnesota United): texts rebuilt from the
|
||||
* story's own first paragraph.
|
||||
*
|
||||
* Dry run: php migrations/dedupe_20261110.php
|
||||
* Write: php migrations/dedupe_20261110.php --apply
|
||||
* Sections: --only=A,B or --skip=D,E
|
||||
*
|
||||
* Deletions are TYPO3 soft deletes (deleted = 1, recoverable via the recycler).
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
$only = []; $skip = [];
|
||||
foreach ($argv as $arg) {
|
||||
if (str_starts_with($arg, '--only=')) { $only = array_map('trim', explode(',', strtoupper(substr($arg, 7)))); }
|
||||
if (str_starts_with($arg, '--skip=')) { $skip = array_map('trim', explode(',', strtoupper(substr($arg, 7)))); }
|
||||
}
|
||||
$runs = static fn(string $s): bool => (!$only || in_array($s, $only, true)) && !in_array($s, $skip, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
$now = time();
|
||||
$purgePaths = [];
|
||||
|
||||
// --- helpers -----------------------------------------------------------------
|
||||
|
||||
$hasRedirect = $pdo->prepare("SELECT uid, target FROM sys_redirect WHERE deleted = 0 AND source_path = ? LIMIT 1");
|
||||
$insertRedirect = $pdo->prepare(
|
||||
"INSERT INTO sys_redirect (pid, createdon, updatedon, source_host, source_path, target, target_statuscode)
|
||||
VALUES (0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '*', ?, ?, 301)"
|
||||
);
|
||||
$updateRedirectBySource = $pdo->prepare("UPDATE sys_redirect SET target = ?, updatedon = UNIX_TIMESTAMP() WHERE uid = ?");
|
||||
$addRedirect = static function (string $source, string $target) use ($hasRedirect, $insertRedirect, $updateRedirectBySource, $apply, &$purgePaths): void {
|
||||
$purgePaths[$source] = true;
|
||||
$hasRedirect->execute([$source]);
|
||||
$existing = $hasRedirect->fetch();
|
||||
if ($existing) {
|
||||
if ($existing['target'] === $target) { return; }
|
||||
echo " redirect $source: Ziel {$existing['target']} -> $target\n";
|
||||
if ($apply) { $updateRedirectBySource->execute([$target, (int)$existing['uid']]); }
|
||||
return;
|
||||
}
|
||||
echo " redirect $source -> $target\n";
|
||||
if ($apply) { $insertRedirect->execute([$source, $target]); }
|
||||
};
|
||||
|
||||
// Redirects whose TARGET is the URL that goes away get the new target (no 301 chains).
|
||||
$retarget = static function (string $oldTarget, string $newTarget) use ($pdo, $apply): void {
|
||||
$st = $pdo->prepare("SELECT uid, source_path FROM sys_redirect WHERE deleted = 0 AND target = ?");
|
||||
$st->execute([$oldTarget]);
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
echo " redirect #{$r['uid']} {$r['source_path']}: Ziel $oldTarget -> $newTarget\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE sys_redirect SET target = ?, updatedon = UNIX_TIMESTAMP() WHERE uid = ?")->execute([$newTarget, (int)$r['uid']]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$softDelete = static function (string $table, int $uid) use ($pdo, $apply, $now): void {
|
||||
echo " loeschen $table #$uid\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE $table SET deleted = 1, tstamp = ? WHERE uid = ?")->execute([$now, $uid]);
|
||||
$pdo->prepare("DELETE FROM sys_category_record_mm WHERE tablenames = ? AND uid_foreign = ?")->execute([$table, $uid]);
|
||||
}
|
||||
};
|
||||
|
||||
$newsRow = static function (int $uid) use ($pdo): ?array {
|
||||
$st = $pdo->prepare("SELECT uid, title, path_segment, type, hidden, LENGTH(bodytext) AS bl FROM tx_news_domain_model_news WHERE deleted = 0 AND uid = ?");
|
||||
$st->execute([$uid]);
|
||||
$r = $st->fetch();
|
||||
return $r ?: null;
|
||||
};
|
||||
$setSegment = static function (int $uid, string $segment) use ($pdo, $apply, $now): void {
|
||||
echo " news #$uid slug -> $segment\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE tx_news_domain_model_news SET path_segment = ?, tstamp = ? WHERE uid = ?")->execute([$segment, $now, $uid]);
|
||||
}
|
||||
};
|
||||
$slugify = static function (string $t): string {
|
||||
$t = html_entity_decode($t, ENT_QUOTES, 'UTF-8');
|
||||
$t = strtolower(trim((string)preg_replace('/[^A-Za-z0-9]+/', '-', iconv('UTF-8', 'ASCII//TRANSLIT', $t) ?: $t), '-'));
|
||||
return $t;
|
||||
};
|
||||
$plain = static fn(?string $h): string => trim((string)preg_replace('/\s+/', ' ', html_entity_decode(strip_tags((string)$h), ENT_QUOTES, 'UTF-8')));
|
||||
$cut = static function (string $s, int $max): string {
|
||||
if (mb_strlen($s) <= $max) { return $s; }
|
||||
$s = mb_substr($s, 0, $max);
|
||||
$s = preg_replace('/\s+\S*$/u', '', $s) ?? $s;
|
||||
return rtrim($s, ' ,;:-') . '…';
|
||||
};
|
||||
|
||||
$newsOverview = '/insights/news';
|
||||
|
||||
// --- A. product copy -----------------------------------------------------------
|
||||
|
||||
if ($runs('A')) {
|
||||
echo "\nA. Produkt-Kopie\n";
|
||||
$st = $pdo->query("SELECT uid, slug, LENGTH(description) dl FROM tx_vitec_domain_model_product WHERE deleted = 0 AND slug IN ('avedia-modular-chassis-c1101-c1103-c1210', 'avedia-modular-chassis-c1101-c1103-c1210-1')");
|
||||
$p = [];
|
||||
foreach ($st as $r) { $p[$r['slug']] = $r; }
|
||||
$copy = $p['avedia-modular-chassis-c1101-c1103-c1210-1'] ?? null;
|
||||
$base = $p['avedia-modular-chassis-c1101-c1103-c1210'] ?? null;
|
||||
if ($copy && $base && (int)$copy['dl'] === 0) {
|
||||
echo " Produkt #{$copy['uid']} (leer) -> #{$base['uid']}\n";
|
||||
$softDelete('tx_vitec_domain_model_product', (int)$copy['uid']);
|
||||
$addRedirect('/product/' . $copy['slug'], '/product/' . $base['slug']);
|
||||
$purgePaths['/product/' . $base['slug']] = true;
|
||||
} else {
|
||||
echo " nichts zu tun\n";
|
||||
}
|
||||
}
|
||||
|
||||
// --- B. news duplicates ----------------------------------------------------------
|
||||
|
||||
if ($runs('B')) {
|
||||
echo "\nB. News-Dubletten\n";
|
||||
|
||||
// [keep uid, clean segment, [uids to delete], [old segments to redirect]]
|
||||
$takeovers = [
|
||||
[12, 'vitec-to-showcase-expanding-enterprise-iptv-and-digital-signage-innovations-at-infocomm-2026', [1]],
|
||||
[13, 'vitec-and-lg-electronics-to-deliver-large-scale-iptv-and-digital-signage-over-next-generation-private-5g-networks', [2]],
|
||||
[14, 'vitec-to-launch-prism-sff-its-most-compact-transcoder-at-eurosatory-2026', [3]],
|
||||
[258, 'ise-2026-a-milestone-for-vitec-and-datapath', [10, 230]],
|
||||
[21, 'vitec-acquires-datapath-ltd-to-expand-video-capabilities', [263]],
|
||||
[231, 'the-impact-of-remote-work-on-security', [344]],
|
||||
];
|
||||
foreach ($takeovers as [$keepUid, $clean, $deleteUids]) {
|
||||
$keep = $newsRow($keepUid);
|
||||
if ($keep === null) { echo " #$keepUid fehlt, uebersprungen\n"; continue; }
|
||||
echo " behalten #$keepUid ({$keep['bl']} Zeichen) als /news/article/$clean | {$keep['title']}\n";
|
||||
foreach ($deleteUids as $d) {
|
||||
$row = $newsRow($d);
|
||||
if ($row === null) { echo " #$d schon weg\n"; continue; }
|
||||
echo " weg: #$d /news/article/{$row['path_segment']} (type {$row['type']}, {$row['bl']} Zeichen)\n";
|
||||
$softDelete('tx_news_domain_model_news', $d);
|
||||
if ($row['path_segment'] !== $clean) {
|
||||
$addRedirect('/news/article/' . $row['path_segment'], '/news/article/' . $clean);
|
||||
$retarget('/news/article/' . $row['path_segment'], '/news/article/' . $clean);
|
||||
}
|
||||
}
|
||||
if ($keep['path_segment'] !== $clean) {
|
||||
$old = $keep['path_segment'];
|
||||
$setSegment($keepUid, $clean);
|
||||
$addRedirect('/news/article/' . $old, '/news/article/' . $clean);
|
||||
$retarget('/news/article/' . $old, '/news/article/' . $clean);
|
||||
}
|
||||
$purgePaths['/news/article/' . $clean] = true;
|
||||
}
|
||||
|
||||
// Records with no content anywhere.
|
||||
echo " ohne Inhalt (Record + alte Seite leer):\n";
|
||||
foreach ([9, 229] as $d) {
|
||||
$row = $newsRow($d);
|
||||
if ($row === null) { continue; }
|
||||
echo " weg: #$d /news/article/{$row['path_segment']} | {$row['title']}\n";
|
||||
$softDelete('tx_news_domain_model_news', $d);
|
||||
$addRedirect('/news/article/' . $row['path_segment'], $newsOverview);
|
||||
$retarget('/news/article/' . $row['path_segment'], $newsOverview);
|
||||
}
|
||||
$row = $newsRow(42);
|
||||
if ($row !== null) {
|
||||
$alt = $pdo->query("SELECT path_segment FROM tx_news_domain_model_news WHERE deleted = 0 AND uid <> 42 AND type = 0 AND path_segment LIKE '%dsei-2023%' LIMIT 1")->fetchColumn();
|
||||
$target = $alt ? '/news/article/' . $alt : $newsOverview;
|
||||
echo " weg: #42 /news/article/{$row['path_segment']} (Zeiger auf leere Seite 321) -> $target\n";
|
||||
$softDelete('tx_news_domain_model_news', 42);
|
||||
$addRedirect('/news/article/' . $row['path_segment'], $target);
|
||||
$retarget('/news/article/' . $row['path_segment'], $target);
|
||||
}
|
||||
}
|
||||
|
||||
// --- C. wrong "-N" slugs on distinct articles --------------------------------------
|
||||
|
||||
if ($runs('C')) {
|
||||
echo "\nC. Eigene Artikel mit Import-Suffix im Slug\n";
|
||||
foreach ([
|
||||
435 => 'wallcontrol-10-scoops-installation-product-awards-crown',
|
||||
334 => '2023-end-of-year-message-from-ceo-bjorn-krylander',
|
||||
376 => '2022-end-of-year-message-from-ceo-bjorn-krylander',
|
||||
] as $uid => $segment) {
|
||||
$row = $newsRow($uid);
|
||||
if ($row === null || $row['path_segment'] === $segment) { continue; }
|
||||
echo " #$uid | {$row['title']}\n";
|
||||
$old = $row['path_segment'];
|
||||
$setSegment($uid, $segment);
|
||||
$addRedirect('/news/article/' . $old, '/news/article/' . $segment);
|
||||
$retarget('/news/article/' . $old, '/news/article/' . $segment);
|
||||
$purgePaths['/news/article/' . $segment] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- D. /about ------------------------------------------------------------------------
|
||||
|
||||
if ($runs('D')) {
|
||||
echo "\nD. /about (Seite 7) -> /insights/who-we-are\n";
|
||||
$page = $pdo->query("SELECT uid, hidden, slug FROM pages WHERE uid = 7 AND deleted = 0")->fetch();
|
||||
if ($page && (int)$page['hidden'] === 0) {
|
||||
echo " Seite 7 verstecken\n";
|
||||
if ($apply) { $pdo->exec("UPDATE pages SET hidden = 1, tstamp = $now WHERE uid = 7"); }
|
||||
}
|
||||
$addRedirect('/about', '/insights/who-we-are');
|
||||
$addRedirect('/about/', '/insights/who-we-are');
|
||||
foreach ($pdo->query("SELECT uid, pid, bodytext, vitec_button_link, header_link FROM tt_content WHERE deleted = 0 AND uid IN (117, 10291)") as $c) {
|
||||
$changes = [];
|
||||
foreach (['bodytext', 'vitec_button_link', 'header_link'] as $f) {
|
||||
$v = (string)($c[$f] ?? '');
|
||||
$n = (string)preg_replace('/t3:\/\/page\?uid=7(?![0-9])/', 't3://page?uid=550', $v);
|
||||
$n = str_replace(['"/about"', '"/about/"'], '"/insights/who-we-are"', $n);
|
||||
if (in_array($v, ['/about', '/about/', '7'], true)) { $n = $v === '7' ? '550' : 't3://page?uid=550'; }
|
||||
if ($n !== $v) { $changes[$f] = $n; }
|
||||
elseif ($f !== 'bodytext' && $v !== '') { echo " CE #{$c['uid']} $f = $v\n"; }
|
||||
}
|
||||
if ($changes) {
|
||||
echo " CE #{$c['uid']} (Seite {$c['pid']}): " . implode(', ', array_keys($changes)) . " -> who-we-are\n";
|
||||
if ($apply) {
|
||||
$set = implode(', ', array_map(fn($f) => "$f = ?", array_keys($changes)));
|
||||
$pdo->prepare("UPDATE tt_content SET $set, tstamp = $now WHERE uid = ?")->execute([...array_values($changes), (int)$c['uid']]);
|
||||
}
|
||||
} else {
|
||||
echo " CE #{$c['uid']}: kein /about-Link gefunden (manuell pruefen)\n";
|
||||
}
|
||||
}
|
||||
$purgePaths['/'] = true;
|
||||
}
|
||||
|
||||
// --- E. story teasers / descriptions copied from the wrong story -----------------------
|
||||
|
||||
if ($runs('E')) {
|
||||
echo "\nE. Story-Teaser/-Beschreibungen aus dem eigenen Text\n";
|
||||
$firstPara = static function (int $usecase) use ($pdo, $plain): string {
|
||||
$st = $pdo->prepare("SELECT bodytext FROM tt_content WHERE deleted = 0 AND hidden = 0 AND tx_vitec_usecase_content = ? AND bodytext <> '' ORDER BY sorting");
|
||||
$st->execute([$usecase]);
|
||||
$fallback = '';
|
||||
foreach ($st->fetchAll(PDO::FETCH_COLUMN) as $html) {
|
||||
$html = (string)preg_replace('/<(script|style)\b[^>]*>.*?<\/\1>/is', '', (string)$html);
|
||||
$parts = preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $html, $m) ? $m[1] : [$html];
|
||||
foreach ($parts as $p) {
|
||||
$t = $plain($p);
|
||||
if (mb_strlen($t) < 80 || str_contains($t, 'document.') || str_contains($t, 'function (')) { continue; }
|
||||
// Prefer a real sentence over a title-case sub-headline.
|
||||
if (mb_strlen($t) >= 120 && preg_match('/[.!?]$/u', $t) === 1) { return $t; }
|
||||
if ($fallback === '') { $fallback = $t; }
|
||||
}
|
||||
}
|
||||
return $fallback;
|
||||
};
|
||||
// Texts derived from each story's own (correct) teaser or headline; the
|
||||
// automatic first-paragraph pick was tried and read badly.
|
||||
$texts = [
|
||||
11 => ['seo_description' => 'VITEC IPTV and digital signage power streaming and next-generation learning at United Arab Emirates University. Read the success story.'],
|
||||
30 => ['teaser' => 'Reliable, high-quality, low-latency streams and digital signage tools: the Kansas City Royals improve fan engagement with VITEC’s IPTV platform.'],
|
||||
35 => [
|
||||
'teaser' => 'VITEC’s EZ TV IPTV and digital signage solution delivers game-day coverage and advertising opportunities at Minnesota United’s Allianz Field.',
|
||||
'seo_description' => 'VITEC’s EZ TV IPTV and digital signage solution delivers game-day coverage and advertising opportunities at Minnesota United’s Allianz Field.',
|
||||
],
|
||||
];
|
||||
foreach ($texts as $uid => $values) {
|
||||
$st = $pdo->prepare("SELECT uid, title, teaser, seo_description FROM tx_vitec_domain_model_usecase WHERE deleted = 0 AND uid = ?");
|
||||
$st->execute([$uid]);
|
||||
$row = $st->fetch();
|
||||
if (!$row) { continue; }
|
||||
echo " #$uid {$row['title']} (erster Absatz der Story: " . $cut($firstPara($uid), 80) . ")\n";
|
||||
foreach ($values as $f => $v) {
|
||||
echo " $f alt: " . $cut($plain($row[$f]), 90) . "\n $f neu: $v\n";
|
||||
}
|
||||
if ($apply) {
|
||||
$set = implode(', ', array_map(fn($f) => "$f = ?", array_keys($values)));
|
||||
$pdo->prepare("UPDATE tx_vitec_domain_model_usecase SET $set, tstamp = $now WHERE uid = ?")->execute([...array_values($values), $uid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nSSR-Pfade zum Purgen (" . count($purgePaths) . "):\n " . implode("\n ", array_keys($purgePaths)) . "\n";
|
||||
echo $apply ? "\nFERTIG. Danach: vendor/bin/typo3 cache:flush && vendor/bin/typo3 referenceindex:update\n" : "\n(Trockenlauf, nichts geschrieben)\n";
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* One-time repair 2026-10-02: the fe_comment tables were created while the
|
||||
* TCA cache was stale, so TYPO3's schema enrichment (uid/pid/...) never ran
|
||||
* and MySQL cannot add an auto-increment PK afterwards. Both tables are
|
||||
* still EMPTY - drop them and let extension:setup recreate them properly.
|
||||
*
|
||||
* Usage: php migrations/drop_fecomment_tables.php
|
||||
* (then: vendor/bin/typo3 cache:flush && vendor/bin/typo3 extension:setup)
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("Could not locate project root above " . __DIR__ . "\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
foreach (['tx_fecomment_summary', 'tx_fecomment_feedback'] as $table) {
|
||||
$count = 0;
|
||||
try { $count = (int)$pdo->query('SELECT COUNT(*) FROM `' . $table . '`')->fetchColumn(); } catch (\Throwable) { echo "$table: existiert nicht - ok\n"; continue; }
|
||||
if ($count > 0) { exit("$table enthaelt $count Zeilen - ABBRUCH, nicht droppen!\n"); }
|
||||
$pdo->exec('DROP TABLE `' . $table . '`');
|
||||
echo "$table: geloescht (war leer)\n";
|
||||
}
|
||||
echo "Fertig. Jetzt: vendor/bin/typo3 cache:flush && vendor/bin/typo3 extension:setup\n";
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Data points from the frontend answers 0100-FE / 0104-FE / 0108-FE (2026-11-10).
|
||||
*
|
||||
* 1. /solutions: header element #1 "H1 Header" is an import placeholder -> hidden.
|
||||
* 2. Home: #757 vitec_featuredcontent "Featured Product" carries lorem ipsum
|
||||
* and links the empty /product shell -> hidden (placeholder).
|
||||
* 3. Home: #702 vitec_card "Government" linked /markets/defense-and-security
|
||||
* -> /markets/government (where the footer sends "Government").
|
||||
* 4. Home: #699 text, button "Learn more" -> "Explore our solutions".
|
||||
* 5. Story CDC: <h4>Product Highlight</h4> under an h2 -> <h3>.
|
||||
*
|
||||
* Dry run: php migrations/fe_answers_fixes_20261111.php
|
||||
* Write: php migrations/fe_answers_fixes_20261111.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
$now = time();
|
||||
$set = static function (string $table, int $uid, string $field, string $value) use ($pdo, $apply, $now): void {
|
||||
if ($apply) { $pdo->prepare("UPDATE $table SET $field = ?, tstamp = ? WHERE uid = ?")->execute([$value, $now, $uid]); }
|
||||
};
|
||||
$ce = static function (int $uid) use ($pdo): ?array {
|
||||
$st = $pdo->prepare("SELECT uid, pid, CType, header, hidden, bodytext, vitec_primary_cta, vitec_cta FROM tt_content WHERE deleted = 0 AND uid = ?");
|
||||
$st->execute([$uid]);
|
||||
$r = $st->fetch();
|
||||
return $r ?: null;
|
||||
};
|
||||
|
||||
echo "\n1./2. Platzhalter-Elemente verstecken\n";
|
||||
foreach ([1 => 'H1 Header', 757 => 'Featured Product'] as $uid => $expect) {
|
||||
$c = $ce($uid);
|
||||
if (!$c) { echo " #$uid fehlt\n"; continue; }
|
||||
if ($c['header'] !== $expect) { echo " #$uid hat Header '{$c['header']}' (erwartet '$expect') - uebersprungen\n"; continue; }
|
||||
if ((int)$c['hidden'] === 1) { echo " #$uid schon versteckt\n"; continue; }
|
||||
echo " #$uid {$c['CType']} \"{$c['header']}\" auf Seite {$c['pid']}: hidden = 1\n";
|
||||
$set('tt_content', $uid, 'hidden', '1');
|
||||
}
|
||||
|
||||
echo "\n3. Karte 'Government' (#702)\n";
|
||||
$c = $ce(702);
|
||||
if ($c && $c['vitec_primary_cta'] === 't3://page?uid=37') {
|
||||
echo " #702 CTA t3://page?uid=37 (/markets/defense-and-security) -> t3://page?uid=526 (/markets/government)\n";
|
||||
$set('tt_content', 702, 'vitec_primary_cta', 't3://page?uid=526');
|
||||
} else {
|
||||
echo " #702: CTA ist '" . ($c['vitec_primary_cta'] ?? '?') . "' - uebersprungen\n";
|
||||
}
|
||||
|
||||
echo "\n4. Text #699: Button-Text\n";
|
||||
$c = $ce(699);
|
||||
if ($c && str_contains((string)$c['bodytext'], '>Learn more</a>')) {
|
||||
echo " #699: 'Learn more' -> 'Explore our solutions'\n";
|
||||
$set('tt_content', 699, 'bodytext', str_replace('>Learn more</a>', '>Explore our solutions</a>', (string)$c['bodytext']));
|
||||
} else {
|
||||
echo " #699: kein 'Learn more'-Button gefunden - uebersprungen\n";
|
||||
}
|
||||
|
||||
echo "\n5. Story CDC: h4 'Product Highlight' -> h3\n";
|
||||
$pattern = '~<h4(\s[^>]*)?>(\s*Product Highlight\s*)</h4>~i';
|
||||
$n = 0;
|
||||
foreach ($pdo->query("SELECT c.uid, u.slug, c.bodytext FROM tt_content c LEFT JOIN tx_vitec_domain_model_usecase u ON u.uid = c.tx_vitec_usecase_content WHERE c.deleted = 0 AND c.bodytext LIKE '%Product Highlight%'")->fetchAll() as $r) {
|
||||
$new = (string)preg_replace($pattern, '<h3$1>$2</h3>', (string)$r['bodytext'], -1, $count);
|
||||
if ($count === 0) { continue; }
|
||||
$n++;
|
||||
echo " tt_content #{$r['uid']} (Story " . ($r['slug'] ?? '-') . "): $count x h4 -> h3\n";
|
||||
$set('tt_content', (int)$r['uid'], 'bodytext', $new);
|
||||
}
|
||||
// The story text may also live on the record itself.
|
||||
$cols = $pdo->query("SHOW COLUMNS FROM tx_vitec_domain_model_usecase")->fetchAll(PDO::FETCH_COLUMN);
|
||||
foreach ($cols as $col) {
|
||||
if (!in_array($col, ['description', 'teaser', 'subtitle'], true)) { continue; }
|
||||
foreach ($pdo->query("SELECT uid, slug, $col AS v FROM tx_vitec_domain_model_usecase WHERE deleted = 0 AND $col LIKE '%Product Highlight%'")->fetchAll() as $r) {
|
||||
$new = (string)preg_replace($pattern, '<h3$1>$2</h3>', (string)$r['v'], -1, $count);
|
||||
if ($count === 0) { continue; }
|
||||
$n++;
|
||||
echo " usecase #{$r['uid']} ({$r['slug']}) Feld $col: $count x h4 -> h3\n";
|
||||
$set('tx_vitec_domain_model_usecase', (int)$r['uid'], $col, $new);
|
||||
}
|
||||
}
|
||||
// Story sections live in the content-block collection table vitec_items.
|
||||
foreach ($pdo->query("SELECT uid, text FROM vitec_items WHERE deleted = 0 AND text LIKE '%Product Highlight%'")->fetchAll() as $r) {
|
||||
$new = (string)preg_replace($pattern, '<h3$1>$2</h3>', (string)$r['text'], -1, $count);
|
||||
if ($count === 0) { continue; }
|
||||
$n++;
|
||||
echo " vitec_items #{$r['uid']}: $count x h4 -> h3\n";
|
||||
$set('vitec_items', (int)$r['uid'], 'text', $new);
|
||||
}
|
||||
if ($n === 0) { echo " nichts gefunden (Text liegt woanders)\n"; }
|
||||
|
||||
echo "\nSSR-Pfade: / /solutions /success-stories/centers-for-disease-control-cdc\n";
|
||||
echo $apply ? "FERTIG.\n" : "(Trockenlauf, nichts geschrieben)\n";
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# fe_comment API-Testkette (braucht eingeloggte Session aus fec_login_test.sh)
|
||||
# Aufruf: bash migrations/fec_api_test.sh
|
||||
set -e
|
||||
cd /usr/www/users/vitecevo/live
|
||||
BASE=https://dev.vitec.com
|
||||
JAR=/tmp/fec.jar
|
||||
[ -s "$JAR" ] || { echo "Cookie-Jar fehlt - erst migrations/fec_login_test.sh laufen lassen."; exit 1; }
|
||||
|
||||
echo '--- 1) Feedback anlegen (mit Sanitizer-Probe: script-Tag muss verschwinden, strong bleiben) ---'
|
||||
curl -sS -b "$JAR" -H 'Content-Type: application/json' \
|
||||
-d '{"url":"https://dev.vitec.com/testseite","note":"<p>Erster Test <script>alert(1)</script> mit <strong>fett</strong></p>"}' \
|
||||
"$BASE/api/fe-comment/feedback" | tee /tmp/fec_fb.json; echo
|
||||
|
||||
FB=$(php -r '$j=json_decode(file_get_contents("/tmp/fec_fb.json"),true);echo (int)($j["uid"]??0);')
|
||||
echo "Feedback-UID: $FB"
|
||||
[ "$FB" -gt 0 ] || { echo "Anlegen fehlgeschlagen."; exit 1; }
|
||||
|
||||
echo '--- 2) Feedback aendern ---'
|
||||
curl -sS -b "$JAR" -H 'Content-Type: application/json' \
|
||||
-d '{"note":"<p>Geaenderte Notiz, <em>kursiv</em>.</p>"}' \
|
||||
"$BASE/api/fe-comment/feedback/$FB"; echo
|
||||
|
||||
echo '--- 3) Datei-Upload (Test-PNG vom Server) ---'
|
||||
curl -sS -b "$JAR" \
|
||||
-F "file=@public/testimages/placeholder_1920x600.png;type=image/png" \
|
||||
-F "feedback=$FB" \
|
||||
"$BASE/api/fe-comment/upload"; echo
|
||||
|
||||
echo '--- 4) General Notes an der Summary ---'
|
||||
curl -sS -b "$JAR" -H 'Content-Type: application/json' \
|
||||
-d '{"generalNotes":"<p>Bitte alles einmal durchsehen.</p>","recipients":[]}' \
|
||||
"$BASE/api/fe-comment/summary"; echo
|
||||
|
||||
echo '--- 5) Gesamtzustand ---'
|
||||
curl -sS -b "$JAR" "$BASE/api/fe-comment/state"; echo
|
||||
|
||||
read -rp "6) Submit ausloesen (Mail an die mailTo-Adresse)? [j/N] " yn
|
||||
if [[ "$yn" =~ ^[jJyY] ]]; then
|
||||
curl -sS -b "$JAR" -X POST "$BASE/api/fe-comment/submit"; echo
|
||||
echo '--- state nach Submit (Summary sollte weg/neu sein) ---'
|
||||
curl -sS -b "$JAR" "$BASE/api/fe-comment/state"; echo
|
||||
fi
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
# fe_comment Isolationstest (2026-11-03): zwei FE-User NACHEINANDER einloggen
|
||||
# und pruefen, dass /api/fe-comment/state UND das USER_INT-Feld auf
|
||||
# /_json/ticket-overview jedem User nur die EIGENEN Tickets zeigen.
|
||||
# Aufruf: bash migrations/fec_isolation_test.sh <user1> <user2>
|
||||
# (fragt beide Passwoerter unsichtbar ab)
|
||||
set -e
|
||||
cd /usr/www/users/vitecevo/live
|
||||
BASE=https://dev.vitec.com
|
||||
|
||||
U1=${1:?Aufruf: fec_isolation_test.sh <user1> <user2>}
|
||||
U2=${2:?Aufruf: fec_isolation_test.sh <user1> <user2>}
|
||||
|
||||
extract_token() {
|
||||
php -r '
|
||||
$j = json_decode(file_get_contents($argv[1]), true);
|
||||
$f = "";
|
||||
$w = function ($n) use (&$w, &$f) {
|
||||
if (is_array($n)) {
|
||||
if (($n["name"] ?? "") === "__RequestToken" && isset($n["value"])) {
|
||||
$f = (string)$n["value"];
|
||||
}
|
||||
foreach ($n as $c) { $w($c); }
|
||||
} elseif (is_string($n) && $f === "") {
|
||||
if (preg_match("/name=\"__RequestToken\" value=\"([^\"]+)\"/", $n, $m)) {
|
||||
$f = $m[1];
|
||||
}
|
||||
}
|
||||
};
|
||||
$w($j);
|
||||
echo $f;
|
||||
' "$1"
|
||||
}
|
||||
|
||||
login() { # $1=user $2=pass $3=jar
|
||||
rm -f "$3"
|
||||
curl -sS -c "$3" -o /tmp/fec_form.json "$BASE/_json/fe-user-profile"
|
||||
TOKEN=$(extract_token /tmp/fec_form.json)
|
||||
if [ -z "$TOKEN" ]; then echo "KEIN __RequestToken gefunden"; exit 1; fi
|
||||
curl -sS -b "$3" -c "$3" -o /tmp/fec_login.json \
|
||||
--data-urlencode "user=$1" \
|
||||
--data-urlencode "pass=$2" \
|
||||
--data-urlencode "logintype=login" \
|
||||
--data-urlencode "pid=520" \
|
||||
--data-urlencode "__RequestToken=$TOKEN" \
|
||||
"$BASE/_json/fe-user-profile"
|
||||
if ! grep -q fe_typo_user "$3"; then
|
||||
echo "LOGIN FEHLGESCHLAGEN fuer $1 (kein fe_typo_user-Cookie)"; exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
summarize_state() { # stdin = state json, $1 = label
|
||||
php -r '
|
||||
$j = json_decode(stream_get_contents(STDIN), true);
|
||||
printf("%-28s allowed=%s user=%s summaryUid=%s items=%d\n",
|
||||
$argv[1],
|
||||
var_export($j["allowed"] ?? null, true),
|
||||
$j["user"]["name"] ?? "-",
|
||||
isset($j["summary"]["uid"]) ? (string)$j["summary"]["uid"] : "-",
|
||||
is_array($j["summary"]["items"] ?? null) ? count($j["summary"]["items"]) : 0);
|
||||
foreach (($j["summary"]["items"] ?? []) as $i) {
|
||||
printf(" item #%d %s\n", $i["uid"], mb_substr($i["url"], 0, 70));
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
summarize_overview() { # stdin = page json, $1 = label
|
||||
php -r '
|
||||
$j = json_decode(stream_get_contents(STDIN), true);
|
||||
$fo = null;
|
||||
foreach (($j["content"]["colPos0"] ?? []) as $el) {
|
||||
if (($el["type"] ?? "") === "fecomment_overview") {
|
||||
$fo = $el["content"]["feedbackOverview"] ?? null;
|
||||
}
|
||||
}
|
||||
if ($fo === null) { echo $argv[1] . " KEIN fecomment_overview-Element gefunden\n"; exit; }
|
||||
printf("%-28s allowed=%s user=%s summaryUid=%s items=%d\n",
|
||||
$argv[1],
|
||||
var_export($fo["allowed"] ?? null, true),
|
||||
$fo["user"]["name"] ?? "-",
|
||||
isset($fo["summary"]["uid"]) ? (string)$fo["summary"]["uid"] : "-",
|
||||
is_array($fo["summary"]["items"] ?? null) ? count($fo["summary"]["items"]) : 0);
|
||||
' "$1"
|
||||
}
|
||||
|
||||
check_user() { # $1=user $2=jar
|
||||
echo "=== $1 ==="
|
||||
curl -sS -b "$2" "$BASE/api/fe-comment/state" | summarize_state "state-API"
|
||||
curl -sS -b "$2" "$BASE/_json/ticket-overview" | summarize_overview "ticket-overview (USER_INT)"
|
||||
echo
|
||||
}
|
||||
|
||||
read -rsp "Passwort fuer $U1: " P1; echo
|
||||
read -rsp "Passwort fuer $U2: " P2; echo
|
||||
|
||||
login "$U1" "$P1" /tmp/fec_u1.jar
|
||||
login "$U2" "$P2" /tmp/fec_u2.jar
|
||||
|
||||
# Testticket als U1 anlegen, damit es etwas zu sehen (bzw. NICHT zu sehen) gibt
|
||||
echo "--- lege Testticket als $U1 an ---"
|
||||
TICKET=$(curl -sS -b /tmp/fec_u1.jar -H 'Content-Type: application/json' \
|
||||
-d '{"url":"/isolation-test","note":"<p>Isolationstest - bitte ignorieren</p>"}' \
|
||||
"$BASE/api/fe-comment/feedback" | php -r '
|
||||
$j = json_decode(stream_get_contents(STDIN), true);
|
||||
echo (int)($j["uid"] ?? 0);')
|
||||
echo "Testticket uid=$TICKET"
|
||||
if [ "$TICKET" = "0" ]; then echo "ANLEGEN FEHLGESCHLAGEN"; exit 1; fi
|
||||
echo
|
||||
|
||||
# beide Sessions leben gleichzeitig - genau wie zwei Browser
|
||||
check_user "$U1" /tmp/fec_u1.jar
|
||||
check_user "$U2" /tmp/fec_u2.jar
|
||||
|
||||
# Gegenprobe: darf U2 das Ticket von U1 veraendern? (erwartet: Fehler)
|
||||
echo "--- Gegenprobe: $U2 versucht Ticket #$TICKET von $U1 zu loeschen ---"
|
||||
curl -sS -b /tmp/fec_u2.jar -X POST "$BASE/api/fe-comment/feedback/$TICKET/delete"; echo
|
||||
echo "(erwartet: {\"error\":\"feedback not found or not yours\"})"
|
||||
echo
|
||||
|
||||
# aufraeumen: U1 loescht sein eigenes Testticket
|
||||
echo "--- aufraeumen: $U1 loescht Testticket #$TICKET ---"
|
||||
curl -sS -b /tmp/fec_u1.jar -X POST "$BASE/api/fe-comment/feedback/$TICKET/delete"; echo
|
||||
echo
|
||||
|
||||
echo "=== anonym (ohne Session) ==="
|
||||
curl -sS "$BASE/api/fe-comment/state" | summarize_state "state-API"
|
||||
curl -sS "$BASE/_json/ticket-overview" | summarize_overview "ticket-overview (USER_INT)"
|
||||
echo
|
||||
echo "ERWARTUNG: $U1 sieht 1 Item (das Testticket), $U2 sieht 0 Items,"
|
||||
echo "die Gegenprobe-Loeschung durch $U2 schlaegt fehl, anonym ist allowed=false."
|
||||
rm -f /tmp/fec_u1.jar /tmp/fec_u2.jar /tmp/fec_form.json /tmp/fec_login.json
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# fe_comment Login-Testkette: GET (Nonce+Token) -> Login-POST -> state
|
||||
# Aufruf: bash migrations/fec_login_test.sh (fragt das Passwort unsichtbar ab)
|
||||
set -e
|
||||
cd /usr/www/users/vitecevo/live
|
||||
BASE=https://dev.vitec.com
|
||||
|
||||
read -rsp "Passwort fuer joe: " PASS; echo
|
||||
|
||||
rm -f /tmp/fec.jar
|
||||
curl -sS -c /tmp/fec.jar -o /tmp/fec_form.json "$BASE/_json/fe-user-profile"
|
||||
|
||||
TOKEN=$(php -r '
|
||||
$j = json_decode(file_get_contents("/tmp/fec_form.json"), true);
|
||||
$f = "";
|
||||
$w = function ($n) use (&$w, &$f) {
|
||||
if (is_array($n)) {
|
||||
// Variante 1: strukturiertes headless-Formular {name: "__RequestToken", value: "..."}
|
||||
if (($n["name"] ?? "") === "__RequestToken" && isset($n["value"])) {
|
||||
$f = (string)$n["value"];
|
||||
}
|
||||
foreach ($n as $c) { $w($c); }
|
||||
} elseif (is_string($n) && $f === "") {
|
||||
// Variante 2: felogin rendert klassisches HTML als String im JSON
|
||||
if (preg_match("/name=\"__RequestToken\" value=\"([^\"]+)\"/", $n, $m)) {
|
||||
$f = $m[1];
|
||||
}
|
||||
}
|
||||
};
|
||||
$w($j);
|
||||
echo $f;
|
||||
')
|
||||
|
||||
echo "Nonce-Cookies im Jar: $(grep -c typo3nonce /tmp/fec.jar || true)"
|
||||
echo "Token-Laenge: ${#TOKEN}"
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "KEIN __RequestToken im Seiten-JSON gefunden."
|
||||
echo "Vorkommen von 'RequestToken' im JSON (zur Diagnose):"
|
||||
grep -o 'RequestToken[^"]*"[^"]*"' /tmp/fec_form.json | head -n 3 || echo "(keine)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -sS -b /tmp/fec.jar -c /tmp/fec.jar -o /tmp/fec_login.json \
|
||||
--data-urlencode "user=joe" \
|
||||
--data-urlencode "pass=$PASS" \
|
||||
--data-urlencode "logintype=login" \
|
||||
--data-urlencode "pid=520" \
|
||||
--data-urlencode "__RequestToken=$TOKEN" \
|
||||
"$BASE/_json/fe-user-profile"
|
||||
|
||||
echo "Session-Cookies (fe_typo_user): $(grep -c fe_typo_user /tmp/fec.jar || true)"
|
||||
echo '--- state ---'
|
||||
curl -sS -b /tmp/fec.jar "$BASE/api/fe-comment/state"; echo
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
/**
|
||||
* Test data for the sub-solution pipeline: fills the generated workbook
|
||||
* "Control Room Platforms SOC NOC Command Centres" with realistic copy
|
||||
* (based on vitec.com product content) and real vitec.com images, and
|
||||
* drops it into the import inbox.
|
||||
*
|
||||
* Starts from the FRESH generated file (named ranges intact), never from
|
||||
* an Excel-saved copy. Stories/products are picked from the workbook's own
|
||||
* dropdown lists, so every value is a valid DB title.
|
||||
*
|
||||
* php migrations/fill_test_subsolution.php
|
||||
*/
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
$source = __DIR__ . '/subsolutions_xlsx_out/Control Room & Command Centre Solutions/Control Room Platforms SOC NOC Command Centres.xlsx';
|
||||
$target = __DIR__ . '/subsolutions_xlsx_in/Control Room Platforms SOC NOC Command Centres.xlsx';
|
||||
|
||||
if (!is_file($source)) {
|
||||
fwrite(STDERR, "Source workbook not found: $source\n(run vendor/bin/typo3 vitec:generate-subsolution-workbooks first)\n");
|
||||
exit(1);
|
||||
}
|
||||
if (is_file($target)) {
|
||||
copy($target, $target . '.bak');
|
||||
echo "Backup of the existing inbox file: " . basename($target) . ".bak\n";
|
||||
}
|
||||
|
||||
$spreadsheet = IOFactory::load($source);
|
||||
|
||||
// ---- dropdown lists from the workbook itself ------------------------------
|
||||
$lists = $spreadsheet->getSheetByName('Lists');
|
||||
$column = static function (string $col) use ($lists): array {
|
||||
$values = [];
|
||||
for ($r = 1; $r <= $lists->getHighestRow(); $r++) {
|
||||
$v = trim((string)$lists->getCell($col . $r)->getValue());
|
||||
if ($v !== '') {
|
||||
$values[] = $v;
|
||||
}
|
||||
}
|
||||
return $values;
|
||||
};
|
||||
$stories = $column('A');
|
||||
$products = $column('B');
|
||||
|
||||
/** First list entry matching each keyword, in keyword order, no duplicates. */
|
||||
$pick = static function (array $list, array $keywords, int $max) : array {
|
||||
$chosen = [];
|
||||
foreach ($keywords as $keyword) {
|
||||
foreach ($list as $entry) {
|
||||
if (stripos($entry, $keyword) !== false && !in_array($entry, $chosen, true)) {
|
||||
$chosen[] = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (count($chosen) >= $max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $chosen;
|
||||
};
|
||||
|
||||
$storyPicks = $pick($stories, ['National Defense', 'Defense Visual', 'ISR Video Mission', 'Disease Control', 'Parliament', 'Police', 'Government'], 3);
|
||||
$productPicks = $pick($products, ['VSN V3', 'VSNMicro', 'VSNMini', 'Aligo', 'QTX100', 'WallControl', 'Fx4'], 4);
|
||||
$featured = array_shift($productPicks) ?? '';
|
||||
|
||||
// ---- content ----------------------------------------------------------------
|
||||
$values = [
|
||||
'hero_subline' => 'Unified, low-latency video visualisation for security, network and command operations - built for 24/7 reliability.',
|
||||
'hero_cta_label' => 'Talk to an expert',
|
||||
'hero_cta_url' => '/contact',
|
||||
'hero_bgimage_url' => 'https://www.vitec.com/fileadmin/user_upload/gov_16-9.jpg',
|
||||
|
||||
'intro_headline' => 'Every source, every screen, one operational picture',
|
||||
'intro_text' => 'Security operations centres, network operations centres and command centres all face the same challenge: operators must see and act on many live sources at once - CCTV, IP streams, dashboards, sensor data and broadcast feeds - without delay and without interruption. VITEC control room platforms bring these sources together on configurable video walls and operator workstations. VSN Series controllers render any mix of HDMI, DisplayPort, SDI and IP inputs, Aligo extends the wall to remote positions with visually lossless 4K and sub-millisecond latency, and WallControl 10 lets operators rearrange layouts in seconds as situations evolve.',
|
||||
'intro_image_url' => 'https://www.vitec.com/fileadmin/user_upload/control-room_control-launch.png',
|
||||
|
||||
'flow_headline' => 'Built for mission-critical operations',
|
||||
'flow_subtext' => 'VITEC control room platforms are designed for environments where every second counts and downtime is not an option. Key advantages include:',
|
||||
|
||||
'flow_card1_icon' => 'capture',
|
||||
'flow_card1_title' => 'Ingest',
|
||||
'flow_card1_front' => 'Bring every source onto the wall: HDMI, DisplayPort, SDI and IP streams.',
|
||||
'flow_card1_back' => 'Vision and ActiveSQX capture cards ingest local and networked sources side by side, so CCTV, PCs, dashboards and broadcast feeds share one display surface.',
|
||||
|
||||
'flow_card2_icon' => 'view',
|
||||
'flow_card2_title' => 'Visualise',
|
||||
'flow_card2_front' => 'Build any wall layout and change it in seconds with WallControl 10.',
|
||||
'flow_card2_back' => 'Operators drag and drop sources, recall saved layouts per shift or incident, and scale from a single screen to walls of 24 displays and beyond.',
|
||||
|
||||
'flow_card3_icon' => 'distribute',
|
||||
'flow_card3_title' => 'Extend',
|
||||
'flow_card3_front' => 'Take the wall to every operator position with Aligo AVoIP.',
|
||||
'flow_card3_back' => 'Visually lossless 4K60 over 10Gb networks with sub-millisecond latency, plus KVM control of remote sources - ideal for supervisor desks and overflow rooms.',
|
||||
|
||||
'flow_card4_icon' => 'respond',
|
||||
'flow_card4_title' => 'Stay resilient',
|
||||
'flow_card4_front' => 'Rack-mounted VSN V3 controllers with dual-redundant power for 24/7 duty.',
|
||||
'flow_card4_back' => 'Hardware-based rendering keeps playback smooth and synchronised, while redundant power supplies keep the picture up when it matters most.',
|
||||
|
||||
'testi_headline' => 'Trusted in control rooms worldwide',
|
||||
|
||||
'prod_featured' => $featured,
|
||||
|
||||
'insights_text1' => 'Modern control rooms are moving from fixed matrix switching to flexible, IP-based visualisation - for faster situational awareness, simpler expansion and lower operating costs.',
|
||||
'insights_text2' => 'See how organisations in defense, government and critical infrastructure use VITEC technology to keep operators informed and in control:',
|
||||
'insights_cta_label' => 'Explore success stories',
|
||||
'insights_cta_target' => '/success-stories',
|
||||
|
||||
'seo_title' => 'Control Room Platforms for SOC, NOC & Command Centres | VITEC',
|
||||
'seo_description' => 'Unify CCTV, IP streams and data on resilient video walls: VITEC VSN controllers, Aligo AVoIP and WallControl 10 for SOC, NOC and command centres.',
|
||||
];
|
||||
foreach ($storyPicks as $i => $story) {
|
||||
$values['testi_story_' . ($i + 1)] = $story;
|
||||
}
|
||||
foreach ($productPicks as $i => $product) {
|
||||
$values['prod_card_' . ($i + 1)] = $product;
|
||||
}
|
||||
|
||||
// ---- write via named ranges -------------------------------------------------
|
||||
$missing = [];
|
||||
foreach ($values as $name => $value) {
|
||||
$range = $spreadsheet->getNamedRange($name);
|
||||
if ($range === null || $range->getWorksheet() === null) {
|
||||
$missing[] = $name;
|
||||
continue;
|
||||
}
|
||||
$cell = str_replace('$', '', (string)$range->getValue());
|
||||
$cell = str_contains($cell, '!') ? substr($cell, strpos($cell, '!') + 1) : $cell;
|
||||
$range->getWorksheet()->getCell($cell)->setValue($value);
|
||||
}
|
||||
|
||||
@mkdir(dirname($target), 0775, true);
|
||||
(new Xlsx($spreadsheet))->save($target);
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
@chmod($target, 0664);
|
||||
|
||||
echo "Written: " . $target . "\n";
|
||||
echo "Featured product: " . ($featured !== '' ? $featured : '(none found!)') . "\n";
|
||||
echo "Product cards: " . ($productPicks !== [] ? implode(' | ', $productPicks) : '(none found)') . "\n";
|
||||
echo "Case studies: " . ($storyPicks !== [] ? implode(' | ', $storyPicks) : '(none found)') . "\n";
|
||||
if ($missing !== []) {
|
||||
echo "WARNING - named ranges not found: " . implode(', ', $missing) . "\n";
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* fix_colombia_quote.php (2026-11-04, Ticket 0038-FE Punkt 3)
|
||||
*
|
||||
* the-national-police-of-colombia: das lose Zitat-Element #12037 traegt nur
|
||||
* die ROLLE ("Director of the Engineering Department of Cosmos Sistemas
|
||||
* Integrado"), waehrend der Usecase-Record quote_person gefuellt, quote_role
|
||||
* aber leer hat. Fix: Element soft-loeschen (+ Inline-Zaehler), quote_role
|
||||
* am Record setzen.
|
||||
*
|
||||
* php migrations/fix_colombia_quote.php # INSPECT (default)
|
||||
* php migrations/fix_colombia_quote.php --apply # schreibt
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $db['host'], $db['dbname']),
|
||||
$db['user'],
|
||||
$db['password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$role = 'Director of the Engineering Department of Cosmos Sistemas Integrado';
|
||||
|
||||
echo $apply ? "APPLY\n\n" : "INSPECT - nichts wird geschrieben (--apply zum Schreiben)\n\n";
|
||||
|
||||
$story = $pdo->query(
|
||||
"SELECT uid, title, quote_person, quote_role, content_elements FROM tx_vitec_domain_model_usecase
|
||||
WHERE deleted = 0 AND slug = 'the-national-police-of-colombia'"
|
||||
)->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$story) {
|
||||
echo "ABBRUCH: Story the-national-police-of-colombia nicht gefunden.\n";
|
||||
exit(1);
|
||||
}
|
||||
printf(
|
||||
"Story #%d \"%s\": quote_person=\"%s\" quote_role=\"%s\" content_elements=%d\n",
|
||||
$story['uid'], $story['title'], $story['quote_person'], $story['quote_role'], $story['content_elements']
|
||||
);
|
||||
|
||||
$element = $pdo->query(
|
||||
'SELECT uid, pid, CType, deleted, tx_vitec_usecase_content, bodytext, header FROM tt_content WHERE uid = 12037'
|
||||
)->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$element) {
|
||||
echo "Element #12037 existiert nicht.\n";
|
||||
exit(1);
|
||||
}
|
||||
printf(
|
||||
"Element #12037: CType=%s deleted=%d usecase=%d text=\"%s\"\n",
|
||||
$element['CType'], $element['deleted'], $element['tx_vitec_usecase_content'],
|
||||
mb_substr(trim(strip_tags((string)$element['bodytext'] . ' ' . (string)$element['header'])), 0, 90)
|
||||
);
|
||||
if ((int)$element['tx_vitec_usecase_content'] !== (int)$story['uid']) {
|
||||
echo "ABBRUCH: Element #12037 haengt nicht an dieser Story.\n";
|
||||
exit(1);
|
||||
}
|
||||
if ((int)$element['deleted'] === 1) {
|
||||
echo "Element ist bereits geloescht.\n";
|
||||
}
|
||||
|
||||
if (!$apply) {
|
||||
echo "\nWuerde: Element #12037 loeschen, content_elements neu zaehlen, quote_role setzen auf:\n \"$role\"\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if ((int)$element['deleted'] === 0) {
|
||||
$pdo->prepare('UPDATE tt_content SET deleted = 1, tstamp = ? WHERE uid = 12037')->execute([time()]);
|
||||
echo "Element #12037 geloescht.\n";
|
||||
}
|
||||
$count = (int)$pdo->query(
|
||||
'SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND tx_vitec_usecase_content = ' . (int)$story['uid']
|
||||
)->fetchColumn();
|
||||
$pdo->prepare('UPDATE tx_vitec_domain_model_usecase SET content_elements = ?, quote_role = ?, tstamp = ? WHERE uid = ?')
|
||||
->execute([$count, $role, time(), (int)$story['uid']]);
|
||||
printf("Record aktualisiert: quote_role gesetzt, content_elements = %d.\n", $count);
|
||||
echo "Danach: vendor/bin/typo3 cache:flush\n";
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Fix dead old-site links inside news bodytexts (SEO follow-up 2026-11-10).
|
||||
*
|
||||
* The imported Datapath articles and the 17 new SEO articles carry hrefs
|
||||
* from the OLD datapath.com information architecture: /datapath-products/…,
|
||||
* old /markets/… market names and /video-wall-solutions/…. None of these
|
||||
* paths exist on the relaunch site (404 on every page view).
|
||||
*
|
||||
* 1. Rewrites every known old href inside tx_news_domain_model_news
|
||||
* bodytext (and teaser, defensively) onto the matching new product
|
||||
* detail page (/product/<slug>), product category page
|
||||
* (/products/<slug>), market or solution page.
|
||||
* 2. Queues a 301 sys_redirect for every old path (with and without
|
||||
* trailing slash) so external backlinks land correctly once the
|
||||
* datapath.com domains point at this site.
|
||||
*
|
||||
* Dry run: php migrations/fix_datapath_links_20261110.php
|
||||
* Write: php migrations/fix_datapath_links_20261110.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
|
||||
/* Old path (no trailing slash) => new target path. */
|
||||
$mapping = [
|
||||
// Aetria
|
||||
'/datapath-products/aetria-control-room-solutions' => '/products/aetria',
|
||||
'/datapath-products/aetria-control-room-solutions/aetria-overview' => '/products/aetria',
|
||||
'/datapath-products/aetria-control-room-solutions/aetria-network-manager' => '/product/aetria-network-manager',
|
||||
'/datapath-products/aetria-control-room-solutions/aetria-touch' => '/product/aetria-touch',
|
||||
'/datapath-products/aetria-control-room-solutions/aetria-wallcontrol' => '/product/aetria-wallcontrol',
|
||||
'/datapath-products/aetria-control-room-solutions/operator-workstations' => '/products/operator-workstations',
|
||||
// AVoIP / KVM
|
||||
'/datapath-products/arqa-avoip-with-kvm' => '/product/arqa',
|
||||
'/datapath-products/video-distribution-kvm' => '/products/av-over-ip-and-kvm',
|
||||
'/datapath-products/video-distribution-kvm/aligo-4k-avoip-with-kvm' => '/products/aligo',
|
||||
'/datapath-products/video-distribution-kvm/aligo-qtx100-quad-transmitter' => '/product/qtx100',
|
||||
'/datapath-products/video-distribution-kvm/aligo-rx100-receiver' => '/product/rx100',
|
||||
'/datapath-products/video-distribution-kvm/aligo-tx100-transmitter' => '/product/tx100',
|
||||
// Capture & graphics
|
||||
'/datapath-products/graphics-products' => '/products/capture-and-graphics',
|
||||
'/datapath-products/graphics-products/image2k' => '/product/image-2k',
|
||||
'/datapath-products/graphics-products/image4k' => '/product/image-4k',
|
||||
'/datapath-products/sqx-ip-capture' => '/products/activesqx',
|
||||
'/datapath-products/vision-range' => '/products/vision-video-capture-cards',
|
||||
'/datapath-products/video-capture-cards/capture-card-features' => '/products/vision-video-capture-cards',
|
||||
'/datapath-products/video-capture-cards/vision-range/visionrgb-e1s' => '/product/visionrgb-e1s',
|
||||
'/datapath-products/video-capture-cards/vision-range/visionsd41s' => '/product/visionsd4-1s',
|
||||
'/datapath-products/video-capture-cards/vision-range/visionsdi2' => '/products/vision-video-capture-cards',
|
||||
'/datapath-products/video-capture-cards/visionav-range' => '/product/visionav',
|
||||
'/datapath-products/video-capture-cards/visionav-range/visionhd4' => '/product/visionhd4',
|
||||
'/datapath-products/video-capture-cards/visionsc-range' => '/products/vision-video-capture-cards',
|
||||
'/datapath-products/video-capture-cards/visionsc-range/visionsc-dp2' => '/product/visionsc-dp2',
|
||||
'/datapath-products/video-capture-cards/visionsc-range/visionsc-hd4' => '/product/visionsc-hd4',
|
||||
'/datapath-products/video-capture-cards/visionsc-range/visionsc-uhd2' => '/product/visionsc-uhd2',
|
||||
// Multi-display / Fx4 / Hx4
|
||||
'/datapath-products/multi-display-controllers/datapath-fx4' => '/product/fx4',
|
||||
'/datapath-products/multi-display-controllers/datapath-fx4-sdi' => '/product/fx4-sdi',
|
||||
'/datapath-products/multi-display-controllers/datapath-hx4' => '/product/hx4',
|
||||
'/datapath-products/multi-display-controllers/multi-display-overview' => '/products/x-series-multi-display-processors',
|
||||
'/datapath-products/multi-display-controllers/video-wall-designer-software' => '/products/video-wall-management-software',
|
||||
// Video walls / VSN / WallControl
|
||||
'/datapath-products/video-wall-control/industrial-wall-controllers' => '/products/vsn-series-video-wall-controllers',
|
||||
'/datapath-products/video-wall-control/video-wall-controllers' => '/products/vsn-series-video-wall-controllers',
|
||||
'/datapath-products/video-wall-control/vsn-11-series' => '/products/vsn-series-video-wall-controllers',
|
||||
'/datapath-products/video-wall-control/vsn-9-series' => '/products/vsn-series-video-wall-controllers',
|
||||
'/datapath-products/video-wall-controllers' => '/products/video-wall-controllers-and-management',
|
||||
'/datapath-products/video-wall-controllers/datapath-fx4' => '/product/fx4',
|
||||
'/datapath-products/video-wall-controllers/video-wall-software/wallcontrol-10-software' => '/product/wall-control-10',
|
||||
'/datapath-products/video-wall-controllers/vsn-series' => '/products/vsn-series-video-wall-controllers',
|
||||
'/datapath-products/video-wall-management-software' => '/products/video-wall-management-software',
|
||||
'/datapath-products/video-wall-software/wallcontrol-10-software' => '/product/wall-control-10',
|
||||
// Accessories (no equivalent records - portfolio page)
|
||||
'/datapath-products/components-accessories/components-accessories-overview/displayport-accessories' => '/products',
|
||||
'/datapath-products/components-accessories/pcie-backplanes/express11-g3' => '/products',
|
||||
// Old market / solution names
|
||||
'/markets/airport-and-aviation' => '/markets/transport/airports',
|
||||
'/markets/corporate' => '/markets/corporation',
|
||||
'/markets/emergency-operation-center' => '/markets/public-safety/emergency-operation-centres',
|
||||
'/markets/events-and-experiences' => '/markets/sports-and-entertainment/events-and-experiences',
|
||||
'/markets/military-control-room' => '/markets/defense-and-security',
|
||||
'/markets/network-operation-center' => '/solutions/control-room-command-centre-solutions/control-room-platforms-soc-noc-command-centres',
|
||||
'/markets/retail-and-hospitality' => '/markets/hospitality',
|
||||
'/markets/security-and-surveillance' => '/solutions/security-surveillance-solutions',
|
||||
'/markets/traffic-management-control-centers' => '/markets/government/traffic-and-smart-mobility',
|
||||
'/markets/utilities' => '/markets/critical-national-infrastructure/energy-and-utilities',
|
||||
'/video-wall-solutions' => '/solutions/video-wall-data-visualisation',
|
||||
'/video-wall-solutions/retail-events-and-experiences' => '/markets/sports-and-entertainment/events-and-experiences',
|
||||
'/video-wall-solutions/video-wall-for-security-suites' => '/solutions/security-surveillance-solutions',
|
||||
];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Sanity: every target must exist (page slug, or product slug). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
$pageBySlug = $pdo->prepare("SELECT uid FROM pages WHERE deleted = 0 AND hidden = 0 AND slug = ? LIMIT 1");
|
||||
$productBySlug = $pdo->prepare("SELECT uid FROM tx_vitec_domain_model_product WHERE deleted = 0 AND hidden = 0 AND hideonwebsite = 0 AND slug = ? LIMIT 1");
|
||||
$missing = 0;
|
||||
foreach (array_unique(array_values($mapping)) as $target) {
|
||||
if (str_starts_with($target, '/product/')) {
|
||||
$productBySlug->execute([substr($target, strlen('/product/'))]);
|
||||
if ((int)$productBySlug->fetchColumn() > 0) { continue; }
|
||||
} else {
|
||||
$pageBySlug->execute([$target]);
|
||||
if ((int)$pageBySlug->fetchColumn() > 0) { continue; }
|
||||
}
|
||||
echo " !! Ziel fehlt: $target\n";
|
||||
$missing++;
|
||||
}
|
||||
if ($missing > 0) {
|
||||
echo "$missing Ziele nicht gefunden - Mapping pruefen.\n";
|
||||
if ($apply) { exit("Abbruch.\n"); }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 1. Rewrite hrefs in news bodytext/teaser */
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "1. Links in News-Texten ersetzen:\n";
|
||||
$findNews = $pdo->prepare("SELECT COUNT(*) FROM tx_news_domain_model_news WHERE deleted = 0 AND (bodytext LIKE ? OR teaser LIKE ?)");
|
||||
$fixNews = $pdo->prepare(
|
||||
"UPDATE tx_news_domain_model_news
|
||||
SET bodytext = REPLACE(bodytext, ?, ?), teaser = REPLACE(teaser, ?, ?), tstamp = UNIX_TIMESTAMP()
|
||||
WHERE deleted = 0 AND (bodytext LIKE ? OR teaser LIKE ?)"
|
||||
);
|
||||
$totalHits = 0;
|
||||
foreach ($mapping as $old => $new) {
|
||||
// Both href variants: with and without trailing slash; plus the one
|
||||
// broken absolute form (http://datapath-products/...).
|
||||
$variants = ['href="' . $old . '/"', 'href="' . $old . '"', 'href="http:/' . $old . '/"'];
|
||||
foreach ($variants as $needle) {
|
||||
$like = '%' . $needle . '%';
|
||||
$findNews->execute([$like, $like]);
|
||||
$count = (int)$findNews->fetchColumn();
|
||||
if ($count === 0) { continue; }
|
||||
$totalHits += $count;
|
||||
echo " $needle -> href=\"$new\" ($count Record(s))\n";
|
||||
if ($apply) {
|
||||
$fixNews->execute([$needle, 'href="' . $new . '"', $needle, 'href="' . $new . '"', $like, $like]);
|
||||
}
|
||||
}
|
||||
}
|
||||
echo " Summe Record-Treffer: $totalHits\n";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 2. 301 redirects for the old paths */
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "2. Redirects anlegen:\n";
|
||||
$hasRedirect = $pdo->prepare("SELECT COUNT(*) FROM sys_redirect WHERE deleted = 0 AND source_path = ?");
|
||||
$insertRedirect = $pdo->prepare(
|
||||
"INSERT INTO sys_redirect (pid, createdon, updatedon, source_host, source_path, target, target_statuscode)
|
||||
VALUES (0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '*', ?, ?, 301)"
|
||||
);
|
||||
$newRedirects = 0;
|
||||
foreach ($mapping as $old => $new) {
|
||||
foreach ([$old, $old . '/'] as $source) {
|
||||
$hasRedirect->execute([$source]);
|
||||
if ((int)$hasRedirect->fetchColumn() > 0) { continue; }
|
||||
$newRedirects++;
|
||||
if ($apply) {
|
||||
$insertRedirect->execute([$source, $new]);
|
||||
}
|
||||
}
|
||||
}
|
||||
echo " $newRedirects neue Redirects (mit + ohne Slash; bestehende unangetastet)\n";
|
||||
|
||||
echo $apply ? "FERTIG. Cache flushen.\n" : "DRY RUN beendet.\n";
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Ticket 0014-FE Punkt 2: doppelt kodierte HTML-Entities in tt_content
|
||||
* (z. B. "VITEC&apos;s" im JSON). Ursache: die Produkt-Importer
|
||||
* escapten mit ENT_QUOTES|ENT_HTML5 (' -> '), die RTE-Persistenz
|
||||
* escapte das & erneut. Die Importer schreiben seit 2026-10-28 literal
|
||||
* (ENT_NOQUOTES); dieses Skript repariert den Bestand um EINE
|
||||
* Escaping-Stufe: &apos; -> ' usw.
|
||||
*
|
||||
* Plain PDO (Muster migrations/check_bg.php).
|
||||
*
|
||||
* Usage: php migrations/fix_double_entities.php (Scan/Report)
|
||||
* php migrations/fix_double_entities.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
// Stufe 1: doppelt kodierte Entities um eine Stufe reduzieren.
|
||||
// Stufe 2 (Befund 2026-10-28: Scan fand KEINE Doubles in der DB - die DB
|
||||
// traegt ' EINFACH kodiert, erst die Render-Pipeline escapet das &
|
||||
// erneut): Apostroph-Entities literal machen, dann kann nichts mehr doppeln.
|
||||
const PATTERNS = ['&apos;', '&quot;', '&#039;', '&amp;', '&nbsp;', ''', '''];
|
||||
const REPLACEMENTS = [''', '"', ''', '&', ' ', "'", "'"];
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo ($apply ? 'APPLY' : 'SCAN') . "\n";
|
||||
|
||||
$like = implode(' OR ', array_map(static fn(string $p): string => "bodytext LIKE '%" . $p . "%'", PATTERNS));
|
||||
$rows = $pdo->query(
|
||||
"SELECT c.uid, c.pid, p.slug, c.CType, c.bodytext FROM tt_content c
|
||||
LEFT JOIN pages p ON p.uid = c.pid
|
||||
WHERE c.deleted = 0 AND ($like) ORDER BY p.slug, c.uid"
|
||||
)->fetchAll();
|
||||
|
||||
$fixed = 0;
|
||||
foreach ($rows as $row) {
|
||||
$new = str_replace(PATTERNS, REPLACEMENTS, (string)$row['bodytext']);
|
||||
$hits = [];
|
||||
foreach (PATTERNS as $pattern) {
|
||||
$n = substr_count((string)$row['bodytext'], $pattern);
|
||||
if ($n > 0) {
|
||||
$hits[] = $pattern . 'x' . $n;
|
||||
}
|
||||
}
|
||||
printf("#%-6d %-28s %-45s %s\n", $row['uid'], $row['CType'], (string)$row['slug'], implode(' ', $hits));
|
||||
if ($apply) {
|
||||
$pdo->prepare('UPDATE tt_content SET bodytext = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?')
|
||||
->execute([$new, (int)$row['uid']]);
|
||||
$fixed++;
|
||||
}
|
||||
}
|
||||
echo "\n" . count($rows) . " Element(e) betroffen" . ($apply ? ", $fixed repariert. Danach: vendor/bin/typo3 cache:flush" : ". Mit --apply reparieren.") . "\n";
|
||||
if (!$apply && $rows !== []) {
|
||||
echo "HINWEIS: Ersetzt wird genau EINE Escaping-Stufe (&apos; -> ' usw.).\n";
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Repairs the record links the success-story import wrote into RTE bodies:
|
||||
* it used t3://record?identifier=tx_vitec_download&uid=N (the TABLE name),
|
||||
* but the configured link handler key - and therefore the only identifier
|
||||
* config.recordLinks resolves - is `download`. Result: raw t3://record
|
||||
* hrefs leaked into the JSON payload ("Download Success Story" buttons,
|
||||
* found 2026-10-28).
|
||||
*
|
||||
* This migration rewrites identifier=tx_vitec_download -> identifier=download
|
||||
* in tt_content.bodytext (plain and &-encoded variants). One-time,
|
||||
* idempotent, dry-run by default.
|
||||
*
|
||||
* Usage: php migrations/fix_download_record_links.php (dry run)
|
||||
* php migrations/fix_download_record_links.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$needle = 'identifier=tx_vitec_download';
|
||||
$rows = $pdo->query(
|
||||
"SELECT uid, pid, CType, LEFT(header, 40) h FROM tt_content
|
||||
WHERE deleted = 0 AND bodytext LIKE '%identifier=tx_vitec_download%' ORDER BY pid, uid"
|
||||
)->fetchAll();
|
||||
|
||||
echo ($apply ? "APPLY\n" : "DRY-RUN\n");
|
||||
echo count($rows) . " element(s) carry the legacy identifier:\n";
|
||||
foreach ($rows as $r) {
|
||||
printf(" #%-6d pid=%-5d %-18s %s\n", $r['uid'], $r['pid'], $r['CType'], $r['h']);
|
||||
}
|
||||
if (!$apply) {
|
||||
exit("DRY RUN - add --apply to rewrite identifier=tx_vitec_download -> identifier=download.\n");
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE tt_content SET bodytext = REPLACE(bodytext, 'identifier=tx_vitec_download', 'identifier=download'),
|
||||
tstamp = UNIX_TIMESTAMP() WHERE uid = ?"
|
||||
);
|
||||
foreach ($rows as $r) {
|
||||
$stmt->execute([(int)$r['uid']]);
|
||||
}
|
||||
echo count($rows) . " element(s) rewritten. Flush the cache, then re-check a story payload.\n";
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Diagnose + repair: the /events redirect answers 307 instead of 301 after
|
||||
* vitec:build-content-pages (2026-10-27). Suspicion: EXT:redirects
|
||||
* auto-created a second sys_redirect row for the slug change (TYPO3 default
|
||||
* 307) that shadows the command's own 301 row.
|
||||
*
|
||||
* Plain PDO against config/system/settings.php (pattern migrations/check_bg.php).
|
||||
*
|
||||
* Usage: php migrations/fix_events_redirect.php (show rows)
|
||||
* php migrations/fix_events_redirect.php --apply (keep ONE 301, delete the rest)
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo "=== sys_redirect rows for /events (and children) ===\n";
|
||||
$rows = $pdo->query(
|
||||
"SELECT uid, deleted, source_host, source_path, target, target_statuscode, createdon, creation_type
|
||||
FROM sys_redirect WHERE source_path LIKE '/events%' ORDER BY uid"
|
||||
)->fetchAll();
|
||||
foreach ($rows as $r) {
|
||||
echo sprintf(" #%-5d del=%d %s %s -> %s [%d] creation_type=%s created=%s\n",
|
||||
$r['uid'], $r['deleted'], $r['source_host'], $r['source_path'], $r['target'],
|
||||
$r['target_statuscode'], (string)($r['creation_type'] ?? '?'),
|
||||
date('Y-m-d H:i', (int)$r['createdon']));
|
||||
}
|
||||
if ($rows === []) {
|
||||
exit(" none - nothing to fix.\n");
|
||||
}
|
||||
|
||||
$live = array_values(array_filter($rows, fn(array $r): bool => (int)$r['deleted'] === 0 && $r['source_path'] === '/events'));
|
||||
if (count($live) === 0) {
|
||||
exit("\nNo live /events row - check manually.\n");
|
||||
}
|
||||
|
||||
// keep the command's own row (source_host '*', target t3://page) as 301,
|
||||
// delete every other live /events row
|
||||
$keep = null;
|
||||
foreach ($live as $r) {
|
||||
if ($r['source_host'] === '*' && str_starts_with((string)$r['target'], 't3://page')) {
|
||||
$keep = $r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$keep ??= $live[0];
|
||||
|
||||
echo "\nPlan: keep #{$keep['uid']} (force 301), delete " . (count($live) - 1) . " other live row(s)\n";
|
||||
if (!$apply) {
|
||||
exit("DRY RUN - add --apply to write.\n");
|
||||
}
|
||||
|
||||
$pdo->prepare('UPDATE sys_redirect SET target_statuscode = 301 WHERE uid = ?')->execute([(int)$keep['uid']]);
|
||||
foreach ($live as $r) {
|
||||
if ((int)$r['uid'] !== (int)$keep['uid']) {
|
||||
$pdo->prepare('UPDATE sys_redirect SET deleted = 1 WHERE uid = ?')->execute([(int)$r['uid']]);
|
||||
echo " deleted #{$r['uid']}\n";
|
||||
}
|
||||
}
|
||||
echo "Done. Flush the cache (redirects are cached), then re-test /events.\n";
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Footer columns (pid 19, html elements #132-#135, #119): align the old-site
|
||||
* link texts with the relaunch structure.
|
||||
*
|
||||
* The footer HTML still carries the old website's wording ("Venues",
|
||||
* "Military", "VITEC OG", "PCIe Encoder" …) although every href points at
|
||||
* the new pages since seo_fixes_20261109. This migration, per internal link:
|
||||
*
|
||||
* 1. sets the link text (and title attribute) to the target page's
|
||||
* navigation title (nav_title, falling back to title);
|
||||
* 2. drops leaf entries that only duplicate an earlier entry of the same
|
||||
* column (several old items collapsed onto one new page);
|
||||
* 3. drops the orphaned catch-all entries that point at the bare /markets
|
||||
* or /products overview ("Healthcare", "Telemetry" - no equivalent page);
|
||||
* 4. removes list containers left empty by 2./3.
|
||||
*
|
||||
* External links (LinkedIn, YouTube, HelpDesk) and t3:// links stay as they
|
||||
* are. Headings (<h4>) stay.
|
||||
*
|
||||
* Dry run: php migrations/fix_footer_link_texts_20261110.php
|
||||
* Write: php migrations/fix_footer_link_texts_20261110.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
|
||||
$pageTitle = $pdo->prepare("SELECT title, nav_title FROM pages WHERE deleted = 0 AND slug = ? LIMIT 1");
|
||||
$resolveTitle = static function (string $href) use ($pageTitle): ?string {
|
||||
$pageTitle->execute([$href]);
|
||||
$row = $pageTitle->fetch();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
$t = trim((string)($row['nav_title'] ?? '')) ?: trim((string)($row['title'] ?? ''));
|
||||
return $t !== '' ? $t : null;
|
||||
};
|
||||
|
||||
$dropOverviewHrefs = ['/markets', '/products'];
|
||||
|
||||
// Product detail pages are records, not pages.
|
||||
$productTitle = $pdo->prepare("SELECT title FROM tx_vitec_domain_model_product WHERE deleted = 0 AND hidden = 0 AND slug = ? LIMIT 1");
|
||||
$resolveAnyTitle = static function (string $href) use ($resolveTitle, $productTitle): ?string {
|
||||
if (str_starts_with($href, '/product/')) {
|
||||
$productTitle->execute([substr($href, strlen('/product/'))]);
|
||||
$t = trim((string)($productTitle->fetchColumn() ?: ''));
|
||||
return $t !== '' ? $t : null;
|
||||
}
|
||||
return $resolveTitle($href);
|
||||
};
|
||||
|
||||
/*
|
||||
* Editorial overrides where the mechanical "target page title" is the wrong
|
||||
* wording or the earlier redirect mapping landed on a too-generic page.
|
||||
* Keyed by element uid; matched by old link text (decoded) or by href.
|
||||
*/
|
||||
$overrides = [
|
||||
133 => [
|
||||
// Solutions column: "Digital Signage" is a solution, not the product category
|
||||
['matchHref' => '/products/iptv-and-digital-signage', 'href' => '/solutions/digital-signage-content-delivery'],
|
||||
],
|
||||
134 => [
|
||||
// Fx4 is a product of its own, not the whole video-wall category
|
||||
['matchText' => 'Fx4 Series', 'href' => '/product/fx4', 'text' => 'Fx4'],
|
||||
// the gateways/transcoding parent has its own category page
|
||||
['matchText' => 'IP/RF Gateways & Transcoders', 'href' => '/products/gateways-and-transcoding'],
|
||||
],
|
||||
135 => [
|
||||
// sits directly below the external "Log in to HelpDesk" link
|
||||
['matchHref' => '/support/login-to-helpdesk', 'text' => 'HelpDesk Access'],
|
||||
],
|
||||
];
|
||||
|
||||
$elements = $pdo->query(
|
||||
"SELECT uid, bodytext FROM tt_content WHERE pid = 19 AND deleted = 0 AND CType = 'html' ORDER BY colPos, sorting"
|
||||
)->fetchAll();
|
||||
|
||||
$update = $pdo->prepare("UPDATE tt_content SET bodytext = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?");
|
||||
|
||||
foreach ($elements as $element) {
|
||||
$uid = (int)$element['uid'];
|
||||
$html = (string)$element['bodytext'];
|
||||
$log = [];
|
||||
|
||||
// --- 1. rename internal links -----------------------------------------
|
||||
$seen = [];
|
||||
$elementOverrides = $overrides[$uid] ?? [];
|
||||
$html = preg_replace_callback(
|
||||
'#<a\b([^>]*?)href="(/[^"]*)"([^>]*)>([^<]*)</a>#s',
|
||||
static function (array $m) use ($resolveAnyTitle, &$log, &$seen, $elementOverrides): string {
|
||||
$href = $m[2];
|
||||
$oldText = trim(strip_tags($m[4]));
|
||||
$oldDecoded = html_entity_decode($oldText, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$forcedText = null;
|
||||
foreach ($elementOverrides as $o) {
|
||||
$hit = (isset($o['matchText']) && $o['matchText'] === $oldDecoded)
|
||||
|| (isset($o['matchHref']) && $o['matchHref'] === $href);
|
||||
if ($hit) {
|
||||
if (!empty($o['href']) && $o['href'] !== $href) {
|
||||
$log[] = sprintf(' > [%s] Ziel %s -> %s', $oldText, $href, $o['href']);
|
||||
$href = $o['href'];
|
||||
}
|
||||
$forcedText = $o['text'] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$newTitle = $forcedText ?? $resolveAnyTitle($href);
|
||||
$seen[$href] = ($seen[$href] ?? 0) + 1;
|
||||
if ($newTitle === null) {
|
||||
$log[] = sprintf(' ? [%s] -> %s (keine Seite, unveraendert)', $oldText, $href);
|
||||
return $m[0];
|
||||
}
|
||||
$escaped = htmlspecialchars($newTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8', false);
|
||||
$attrs = $m[1] . 'href="' . $href . '"' . $m[3];
|
||||
if (preg_match('/\stitle="[^"]*"/', $attrs)) {
|
||||
$attrs = preg_replace('/\stitle="[^"]*"/', ' title="' . $escaped . '"', $attrs, 1);
|
||||
} else {
|
||||
$attrs .= ' title="' . $escaped . '"';
|
||||
}
|
||||
if ($oldDecoded !== $newTitle) {
|
||||
$log[] = sprintf(' ~ [%s] -> [%s] (%s)', $oldText, $newTitle, $href);
|
||||
}
|
||||
return '<a' . $attrs . '>' . $escaped . '</a>';
|
||||
},
|
||||
$html
|
||||
) ?? $html;
|
||||
|
||||
// --- 2.+3. drop duplicate leaf items and overview catch-alls -------------
|
||||
// Parent items (a link followed by a nested <ul>) always stay; a leaf
|
||||
// pointing at the same page as a parent or an earlier leaf is a duplicate.
|
||||
$kept = [];
|
||||
// Link texts are plain ([^<]*), so neither pattern can span from a parent
|
||||
// link into its nested list.
|
||||
preg_match_all('#<li>\s*<a\b[^>]*href="(/[^"]*)"[^>]*>[^<]*</a>\s*<ul>#s', $html, $parents);
|
||||
foreach ($parents[1] as $parentHref) {
|
||||
$kept[$parentHref] = true;
|
||||
}
|
||||
$html = preg_replace_callback(
|
||||
'#<li>\s*<a\b[^>]*href="(/[^"]*)"[^>]*>([^<]*)</a>\s*</li>\s*#s',
|
||||
static function (array $m) use (&$kept, &$log, $dropOverviewHrefs): string {
|
||||
$href = $m[1];
|
||||
$text = trim(strip_tags($m[2]));
|
||||
if (in_array($href, $dropOverviewHrefs, true)) {
|
||||
$log[] = sprintf(' - [%s] -> %s (Uebersichts-Platzhalter entfernt)', $text, $href);
|
||||
return '';
|
||||
}
|
||||
if (isset($kept[$href])) {
|
||||
$log[] = sprintf(' - [%s] -> %s (Dublette entfernt)', $text, $href);
|
||||
return '';
|
||||
}
|
||||
$kept[$href] = true;
|
||||
return $m[0];
|
||||
},
|
||||
$html
|
||||
) ?? $html;
|
||||
// --- 4. empty lists ------------------------------------------------------
|
||||
$before = '';
|
||||
while ($before !== $html) {
|
||||
$before = $html;
|
||||
$html = preg_replace('#<ul>\s*</ul>#s', '', $html) ?? $html;
|
||||
}
|
||||
|
||||
$changed = $html !== (string)$element['bodytext'];
|
||||
echo "\n##### tt_content #$uid " . ($changed ? '(AENDERUNGEN)' : '(unveraendert)') . "\n";
|
||||
foreach ($log as $line) {
|
||||
echo $line . "\n";
|
||||
}
|
||||
if ($changed && $apply) {
|
||||
$update->execute([$html, $uid]);
|
||||
echo " gespeichert\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo $apply ? "\nFERTIG. Cache flushen + SSR purgen (Direkt-SQL loest keinen Auto-Purge aus).\n" : "\nDRY RUN beendet.\n";
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Ticket 0014-FE, Folge-Befund: pid 569 (/products/graphics-and-multi-display)
|
||||
* nach dem IQS4-Kategorie-Re-Import. Container und ihre Kinder sind intakt,
|
||||
* aber: 8 Duplikat-Karten top-level in colPos0 (11853-11860) und die Grids
|
||||
* stehen falsch relativ zu ihren Intros (11840 vor 11629, 11841 vor 11632).
|
||||
*
|
||||
* Reparatur: Duplikate soft-loeschen, colPos0 in Sektions-Folge neu sortieren.
|
||||
*
|
||||
* Usage: php migrations/fix_graphics_grid.php (inspect)
|
||||
* php migrations/fix_graphics_grid.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
const PID = 569;
|
||||
const DUPLICATES = [11853, 11854, 11855, 11856, 11857, 11858, 11859, 11860];
|
||||
// Intro -> Grid(s): Why VITEC -> 11838 (3 Benefit-Karten) + 11839 (1),
|
||||
// Finder-Intro + Comparison-Text, Products-Intro -> 11840 (IGC + IQS4),
|
||||
// Solutions-Intro -> 11841 (2 Solution-Karten)
|
||||
const ORDER = [11620, 11621, 11622, 11838, 11839, 11627, 11628, 11629, 11840, 11632, 11841];
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo ($apply ? 'APPLY' : 'INSPECT') . " · pid " . PID . "\n\n";
|
||||
|
||||
$rows = $pdo->query('SELECT uid, CType, colPos, LEFT(header,40) h FROM tt_content WHERE deleted = 0 AND pid = ' . PID)->fetchAll();
|
||||
$byUid = [];
|
||||
foreach ($rows as $row) {
|
||||
$byUid[(int)$row['uid']] = $row;
|
||||
}
|
||||
foreach (array_merge(ORDER, DUPLICATES) as $uid) {
|
||||
if (!isset($byUid[$uid])) {
|
||||
exit("ABBRUCH: uid $uid fehlt auf der Seite - erst klaeren.\n");
|
||||
}
|
||||
}
|
||||
$tail = [];
|
||||
foreach ($pdo->query('SELECT uid, CType, LEFT(header,40) h FROM tt_content WHERE deleted = 0 AND pid = ' . PID . ' AND colPos = 0 ORDER BY sorting, uid') as $row) {
|
||||
$uid = (int)$row['uid'];
|
||||
if (!in_array($uid, ORDER, true) && !in_array($uid, DUPLICATES, true)) {
|
||||
$tail[] = $uid;
|
||||
echo "tail: #$uid {$row['CType']} \"{$row['h']}\"\n";
|
||||
}
|
||||
}
|
||||
echo "\n1) " . count(DUPLICATES) . " Duplikat-Karten soft-loeschen: " . implode(',', DUPLICATES) . "\n";
|
||||
echo "2) colPos0 neu sortieren:\n";
|
||||
$sequence = array_merge(ORDER, $tail);
|
||||
foreach ($sequence as $i => $uid) {
|
||||
$r = $byUid[$uid];
|
||||
printf(" %3d. #%-6d %-22s %s\n", $i + 1, $uid, $r['CType'], $r['h']);
|
||||
}
|
||||
if (!$apply) {
|
||||
echo "\nInspect only - mit --apply schreiben.\n";
|
||||
exit(0);
|
||||
}
|
||||
$pdo->exec('UPDATE tt_content SET deleted = 1, tstamp = UNIX_TIMESTAMP() WHERE uid IN (' . implode(',', DUPLICATES) . ')');
|
||||
$sorting = 0;
|
||||
$stmt = $pdo->prepare('UPDATE tt_content SET sorting = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?');
|
||||
foreach ($sequence as $uid) {
|
||||
$sorting += 256;
|
||||
$stmt->execute([$sorting, $uid]);
|
||||
}
|
||||
echo "\nGeschrieben. Danach: vendor/bin/typo3 cache:flush\n";
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* One-shot repair (2026-09-04): the first vitec:import-product-images run
|
||||
* wrote its sys_file_reference rows with fieldname 'images' - the JSON key -
|
||||
* but the TCA field (and everything that queries it) is 'productimage'.
|
||||
* Re-files those references and refreshes the products' inline counter.
|
||||
*
|
||||
* Usage: php migrations/fix_image_reference_fieldname.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$wrong = (int)$pdo->query(
|
||||
"SELECT COUNT(*) FROM sys_file_reference
|
||||
WHERE deleted = 0 AND tablenames = 'tx_vitec_domain_model_product' AND fieldname = 'images'"
|
||||
)->fetchColumn();
|
||||
echo "Misfiled references (fieldname 'images'): $wrong\n";
|
||||
|
||||
if ($wrong > 0) {
|
||||
$pdo->exec(
|
||||
"UPDATE sys_file_reference SET fieldname = 'productimage'
|
||||
WHERE deleted = 0 AND tablenames = 'tx_vitec_domain_model_product' AND fieldname = 'images'"
|
||||
);
|
||||
echo "Re-filed to 'productimage'.\n";
|
||||
}
|
||||
|
||||
$updated = $pdo->exec(
|
||||
"UPDATE tx_vitec_domain_model_product p
|
||||
SET p.productimage = (
|
||||
SELECT COUNT(*) FROM sys_file_reference r
|
||||
WHERE r.deleted = 0 AND r.uid_foreign = p.uid
|
||||
AND r.tablenames = 'tx_vitec_domain_model_product' AND r.fieldname = 'productimage'
|
||||
)
|
||||
WHERE p.deleted = 0"
|
||||
);
|
||||
echo "Inline counters refreshed on $updated product(s). Flush the cache: vendor/bin/typo3 cache:flush\n";
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Repair for the 2026-10-27 --delete-dropped run: the RENAME_MAP entries
|
||||
* 'Government' -> 'Government & Agencies' and 'Education' -> 'Education
|
||||
* Communications' are NOT idempotent (the old names double as the NEW main
|
||||
* market names). The re-run therefore renamed the MAIN records (#50/#20,
|
||||
* stories and teaser travelled along), created fresh empty "Government" /
|
||||
* "Education" records, and pass 4 deleted the original (empty) sub-market
|
||||
* records as unclaimed.
|
||||
*
|
||||
* This script swaps everything back:
|
||||
* - #<main> "Government & Agencies" -> title Government, slug government,
|
||||
* detail_page = page /markets/government
|
||||
* - the empty record titled "Government" (created today) -> title
|
||||
* Government & Agencies, slug government-agencies, detail_page =
|
||||
* page /markets/government/government-and-agencies
|
||||
* - same pair for Education / Education Communications
|
||||
*
|
||||
* Plain PDO (pattern migrations/check_bg.php). Dry-run by default.
|
||||
*
|
||||
* Usage: php migrations/fix_market_rename_regression.php [--apply]
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
function pageUid(PDO $pdo, string $slug): int
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT uid FROM pages WHERE deleted = 0 AND slug = ?');
|
||||
$stmt->execute([$slug]);
|
||||
return (int)($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
function record(PDO $pdo, string $title): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT uid, title, slug, detail_page,
|
||||
(SELECT COUNT(*) FROM tx_vitec_usecase_market_mm mm WHERE mm.uid_foreign = m.uid) AS stories
|
||||
FROM tx_vitec_domain_model_market m WHERE deleted = 0 AND title = ?');
|
||||
$stmt->execute([$title]);
|
||||
$rows = $stmt->fetchAll();
|
||||
if (count($rows) !== 1) {
|
||||
echo " !! " . count($rows) . " live record(s) titled \"$title\" - manual check needed\n";
|
||||
return null;
|
||||
}
|
||||
return $rows[0];
|
||||
}
|
||||
|
||||
$pairs = [
|
||||
['mainTitle' => 'Government', 'mainSlug' => 'government', 'mainPage' => '/markets/government',
|
||||
'subTitle' => 'Government & Agencies', 'subSlug' => 'government-agencies', 'subPage' => '/markets/government/government-and-agencies'],
|
||||
['mainTitle' => 'Education', 'mainSlug' => 'education', 'mainPage' => '/markets/education',
|
||||
'subTitle' => 'Education Communications', 'subSlug' => 'education-communications', 'subPage' => '/markets/education/education-communications'],
|
||||
];
|
||||
|
||||
$updates = [];
|
||||
foreach ($pairs as $pair) {
|
||||
echo "=== {$pair['mainTitle']} ===\n";
|
||||
// the record carrying the stories currently holds the SUB title
|
||||
$hijacked = record($pdo, $pair['subTitle']); // ex-main, renamed today
|
||||
$fresh = record($pdo, $pair['mainTitle']); // empty, created today
|
||||
if ($hijacked === null || $fresh === null) {
|
||||
continue;
|
||||
}
|
||||
$mainPageUid = pageUid($pdo, $pair['mainPage']);
|
||||
$subPageUid = pageUid($pdo, $pair['subPage']);
|
||||
if (!$mainPageUid || !$subPageUid) {
|
||||
echo " !! page lookup failed ({$pair['mainPage']} -> $mainPageUid, {$pair['subPage']} -> $subPageUid)\n";
|
||||
continue;
|
||||
}
|
||||
echo sprintf(" #%d \"%s\" (%d stories, slug %s, detail_page %d) -> BACK TO \"%s\" / %s / page %d\n",
|
||||
$hijacked['uid'], $hijacked['title'], $hijacked['stories'], $hijacked['slug'], $hijacked['detail_page'],
|
||||
$pair['mainTitle'], $pair['mainSlug'], $mainPageUid);
|
||||
echo sprintf(" #%d \"%s\" (%d stories, slug %s, detail_page %d) -> BECOMES \"%s\" / %s / page %d\n",
|
||||
$fresh['uid'], $fresh['title'], $fresh['stories'], $fresh['slug'], $fresh['detail_page'],
|
||||
$pair['subTitle'], $pair['subSlug'], $subPageUid);
|
||||
if ((int)$fresh['stories'] > 0) {
|
||||
echo " !! the fresh record unexpectedly has stories - aborting this pair\n";
|
||||
continue;
|
||||
}
|
||||
$updates[] = [(int)$hijacked['uid'], $pair['mainTitle'], $pair['mainSlug'], $mainPageUid];
|
||||
$updates[] = [(int)$fresh['uid'], $pair['subTitle'], $pair['subSlug'], $subPageUid];
|
||||
}
|
||||
|
||||
if (!$apply) {
|
||||
exit("\nDRY RUN - add --apply to write.\n");
|
||||
}
|
||||
if ($updates === []) {
|
||||
exit("\nNothing to update.\n");
|
||||
}
|
||||
$stmt = $pdo->prepare('UPDATE tx_vitec_domain_model_market SET title = ?, slug = ?, detail_page = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?');
|
||||
foreach ($updates as [$uid, $title, $slug, $pageUid]) {
|
||||
$stmt->execute([$title, $slug, $pageUid, $uid]);
|
||||
echo " updated #$uid -> \"$title\"\n";
|
||||
}
|
||||
echo "Done. Flush the cache, then verify the /markets marketlist and the story badges.\n";
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* fix_overview_hero_refs.php (2026-11-03)
|
||||
*
|
||||
* Repariert die Hero-Platzhalter-Referenzen der 20 Produkt-Übersichtsseiten:
|
||||
* vitec:build-product-overviews --apply hatte die sys_file_reference-Zeilen
|
||||
* mit fieldname 'hero_bgimage' angelegt - der Content-Block vitec/herosection
|
||||
* hat aber prefixFields, die echte Spalte/der echte Fieldname ist
|
||||
* 'vitec_hero_bgimage' (das JSON streift den Praefix nur ab). Ergebnis:
|
||||
* Referenzen existieren, rendern aber nicht.
|
||||
*
|
||||
* Dieses Skript biegt die vorhandenen Referenzen um (fieldname-Update) und
|
||||
* setzt das Zaehlfeld tt_content.vitec_hero_bgimage - KEIN Neuanlegen.
|
||||
*
|
||||
* php migrations/fix_overview_hero_refs.php # INSPECT (default)
|
||||
* php migrations/fix_overview_hero_refs.php --apply # schreibt
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $db['host'], $db['dbname']),
|
||||
$db['user'],
|
||||
$db['password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
echo $apply ? "APPLY\n\n" : "INSPECT - nichts wird geschrieben (--apply zum Schreiben)\n\n";
|
||||
|
||||
// Zielspalte muss existieren, sonst stimmt die Diagnose nicht
|
||||
$col = (int)$pdo->query(
|
||||
"SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tt_content' AND COLUMN_NAME = 'vitec_hero_bgimage'"
|
||||
)->fetchColumn();
|
||||
echo 'tt_content.vitec_hero_bgimage vorhanden: ' . ($col > 0 ? 'ja' : 'NEIN') . "\n";
|
||||
if ($col === 0) {
|
||||
echo "ABBRUCH: Zielspalte fehlt - Feldname im Content Block pruefen.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// die falsch benannten Referenzen, nur an vitec_herosection-Elementen
|
||||
$refs = $pdo->query(
|
||||
"SELECT r.uid AS ref_uid, r.uid_foreign AS content_uid, r.uid_local AS file_uid,
|
||||
c.pid, c.CType, f.identifier
|
||||
FROM sys_file_reference r
|
||||
JOIN tt_content c ON c.uid = r.uid_foreign
|
||||
LEFT JOIN sys_file f ON f.uid = r.uid_local
|
||||
WHERE r.deleted = 0 AND r.tablenames = 'tt_content' AND r.fieldname = 'hero_bgimage'
|
||||
AND c.deleted = 0 AND c.CType = 'vitec_herosection'
|
||||
ORDER BY c.pid"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo count($refs) . " Referenz(en) mit fieldname 'hero_bgimage' an vitec_herosection:\n";
|
||||
foreach ($refs as $ref) {
|
||||
printf(
|
||||
" ref #%d -> content #%d (pid %d) file %s\n",
|
||||
$ref['ref_uid'],
|
||||
$ref['content_uid'],
|
||||
$ref['pid'],
|
||||
(string)$ref['identifier']
|
||||
);
|
||||
}
|
||||
|
||||
// Sicherheitsnetz: Elemente, die BEIDE Varianten tragen (sollte es nicht geben)
|
||||
$both = $pdo->query(
|
||||
"SELECT uid_foreign FROM sys_file_reference
|
||||
WHERE deleted = 0 AND tablenames = 'tt_content' AND fieldname = 'vitec_hero_bgimage'
|
||||
AND uid_foreign IN (SELECT uid_foreign FROM sys_file_reference
|
||||
WHERE deleted = 0 AND tablenames = 'tt_content' AND fieldname = 'hero_bgimage')"
|
||||
)->fetchAll(PDO::FETCH_COLUMN);
|
||||
if ($both !== []) {
|
||||
echo "ACHTUNG: Elemente mit korrekter UND falscher Referenz: " . implode(', ', $both) . " - die falschen wuerden geloescht statt umbenannt.\n";
|
||||
}
|
||||
|
||||
if (!$apply) {
|
||||
echo "\nINSPECT-Ende.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$renamed = 0;
|
||||
$deleted = 0;
|
||||
foreach ($refs as $ref) {
|
||||
if (in_array((string)$ref['content_uid'], array_map('strval', $both), true)) {
|
||||
$pdo->prepare('UPDATE sys_file_reference SET deleted = 1, tstamp = ? WHERE uid = ?')
|
||||
->execute([time(), (int)$ref['ref_uid']]);
|
||||
$deleted++;
|
||||
continue;
|
||||
}
|
||||
$pdo->prepare("UPDATE sys_file_reference SET fieldname = 'vitec_hero_bgimage', tstamp = ? WHERE uid = ?")
|
||||
->execute([time(), (int)$ref['ref_uid']]);
|
||||
$renamed++;
|
||||
}
|
||||
|
||||
// Zaehlfeld pro betroffenem Element nachziehen
|
||||
$contentUids = array_values(array_unique(array_map(static fn(array $r): int => (int)$r['content_uid'], $refs)));
|
||||
foreach ($contentUids as $uid) {
|
||||
$count = (int)$pdo->query(
|
||||
"SELECT COUNT(*) FROM sys_file_reference
|
||||
WHERE deleted = 0 AND tablenames = 'tt_content' AND fieldname = 'vitec_hero_bgimage' AND uid_foreign = " . $uid
|
||||
)->fetchColumn();
|
||||
$pdo->prepare('UPDATE tt_content SET vitec_hero_bgimage = ?, tstamp = ? WHERE uid = ?')
|
||||
->execute([$count, time(), $uid]);
|
||||
}
|
||||
|
||||
printf("\nFertig: %d Referenz(en) umbenannt, %d geloescht, %d Zaehlfeld(er) gesetzt.\n", $renamed, $deleted, count($contentUids));
|
||||
echo "Danach: vendor/bin/typo3 cache:flush\n";
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Ticket 0014-FE Punkt 1: /products (pid 447) ohne Grid-Container im Payload.
|
||||
*
|
||||
* Befund (check_grid_447.php, 2026-10-28): der Portfolio-Re-Run (IQS4) hat
|
||||
* die 15 vitec_cols_33_33_33-Container VERSTECKT (hidden=1, uid 11794-11808;
|
||||
* ihre 38 Original-Karten stecken intakt und bebildert darin), 38 neue
|
||||
* Duplikat-Karten top-level in colPos0 angelegt (11861-11898) und die
|
||||
* Sortierung neu verteilt - die versteckten Container stehen dadurch an
|
||||
* falschen Positionen.
|
||||
*
|
||||
* Reparatur: Container einblenden, Duplikate soft-loeschen, colPos0 in der
|
||||
* P5-Reihenfolge (Intro -> Grid(s) -> naechstes Intro, danach Banner/FAQ in
|
||||
* bisheriger Folge) mit Stride 256 neu sortieren.
|
||||
*
|
||||
* Usage: php migrations/fix_products_grid.php (inspect)
|
||||
* php migrations/fix_products_grid.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
const PID = 447;
|
||||
const CONTAINERS = [11794, 11795, 11796, 11797, 11798, 11799, 11800, 11801, 11802, 11803, 11804, 11805, 11806, 11807, 11808];
|
||||
const DUPLICATES = [
|
||||
11861, 11862, 11863, 11864, 11865, 11866, 11867, 11868, 11869, 11870,
|
||||
11871, 11872, 11873, 11874, 11875, 11876, 11877, 11878, 11879, 11880,
|
||||
11881, 11882, 11883, 11884, 11885, 11886, 11887, 11888, 11889, 11890,
|
||||
11891, 11892, 11893, 11894, 11895, 11896, 11897, 11898,
|
||||
];
|
||||
// Zielreihenfolge colPos0: Sektions-Intro, danach seine Grids (Zuordnung
|
||||
// ueber die Container-Kinder verifiziert, check_grid_447-Ausgabe 2026-10-28)
|
||||
const ORDER = [
|
||||
11741, 11808, 11807, // IPTV & Digital Signage (5 Karten -> 2 Grids)
|
||||
11747, 11806, 11805, 11804, // Video Encoding & Streaming (7 -> 3)
|
||||
11755, 11803, // Gateways & Transcoding (3 -> 1)
|
||||
11759, 11802, 11801, // Control Room Platforms (6 -> 2)
|
||||
11766, 11800, // AV over IP & KVM (2 -> 1)
|
||||
11769, 11799, 11798, // Video Wall & Display Processing (4 -> 2)
|
||||
11774, 11797, // Graphics & Multi-Display (2 -> 1)
|
||||
11777, 11796, 11795, // Video Capture (6 -> 2)
|
||||
11784, 11794, // Infrastructure & Accessories (3 -> 1)
|
||||
];
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo ($apply ? 'APPLY' : 'INSPECT') . " · pid " . PID . "\n\n";
|
||||
|
||||
// Sanity: alle ORDER-/CONTAINER-uids existieren undeleted auf der Seite
|
||||
$rows = $pdo->query('SELECT uid, CType, colPos, hidden, LEFT(header,40) h FROM tt_content WHERE deleted = 0 AND pid = ' . PID)->fetchAll();
|
||||
$byUid = [];
|
||||
foreach ($rows as $row) {
|
||||
$byUid[(int)$row['uid']] = $row;
|
||||
}
|
||||
foreach (array_merge(ORDER, DUPLICATES) as $uid) {
|
||||
if (!isset($byUid[$uid])) {
|
||||
exit("ABBRUCH: uid $uid fehlt auf der Seite (geloescht/verschoben?) - erst klaeren.\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Rest von colPos0 (Banner, FAQ, ...) hinten anhaengen, bisherige Folge
|
||||
$tail = [];
|
||||
foreach ($pdo->query('SELECT uid, CType, LEFT(header,40) h FROM tt_content WHERE deleted = 0 AND pid = ' . PID . ' AND colPos = 0 ORDER BY sorting, uid') as $row) {
|
||||
$uid = (int)$row['uid'];
|
||||
if (!in_array($uid, ORDER, true) && !in_array($uid, DUPLICATES, true)) {
|
||||
$tail[] = $uid;
|
||||
echo "tail: #$uid {$row['CType']} \"{$row['h']}\"\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n1) " . count(CONTAINERS) . " Container einblenden (hidden=0): " . implode(',', CONTAINERS) . "\n";
|
||||
echo "2) " . count(DUPLICATES) . " Duplikat-Karten soft-loeschen: " . implode(',', DUPLICATES) . "\n";
|
||||
echo "3) colPos0 neu sortieren: " . count(ORDER) . " Elemente in P5-Folge + " . count($tail) . " Tail-Element(e)\n";
|
||||
$sequence = array_merge(ORDER, $tail);
|
||||
foreach ($sequence as $i => $uid) {
|
||||
$r = $byUid[$uid];
|
||||
printf(" %3d. #%-6d %-22s %s\n", $i + 1, $uid, $r['CType'], $r['h']);
|
||||
}
|
||||
|
||||
if (!$apply) {
|
||||
echo "\nInspect only - mit --apply schreiben.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$pdo->exec('UPDATE tt_content SET hidden = 0, tstamp = UNIX_TIMESTAMP() WHERE uid IN (' . implode(',', CONTAINERS) . ')');
|
||||
$pdo->exec('UPDATE tt_content SET deleted = 1, tstamp = UNIX_TIMESTAMP() WHERE uid IN (' . implode(',', DUPLICATES) . ')');
|
||||
$sorting = 0;
|
||||
$stmt = $pdo->prepare('UPDATE tt_content SET sorting = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?');
|
||||
foreach ($sequence as $uid) {
|
||||
$sorting += 256;
|
||||
$stmt->execute([$sorting, $uid]);
|
||||
}
|
||||
echo "\nGeschrieben. Danach: vendor/bin/typo3 cache:flush\n";
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Ticket 0017-FE Punkt 1 + 2 (Altbestand vitec.com-Import, gegen das
|
||||
* Original auf www.vitec.com verifiziert, 2026-10-29):
|
||||
*
|
||||
* 1) CDC (centers-for-disease-control-cdc): die Background-vitec_columns
|
||||
* (#355) traegt links den Text des Crowne Plaza Muscat OCEC und rechts
|
||||
* dessen Logo. Das Original hat dort CDC-Copy. -> linkes Text-Item auf
|
||||
* die Original-Absaetze setzen, rechtes Logo-Item soft-loeschen
|
||||
* (CDC-Bild kann redaktionell nachgelegt werden).
|
||||
*
|
||||
* 2) "Key Feature"-textmedia mit identischem Hotel-/Signage-Text in 5
|
||||
* Stories. Abgleich mit dem Original:
|
||||
* - parramatta-eels #218: Text ist DORT ECHT -> behalten
|
||||
* - belgrade #253, isr #266, meydan #206: Original hat KEIN
|
||||
* Key Feature -> Element soft-loeschen
|
||||
* - proteo #234: Original hat MIL-spec-Bullets -> bodytext ersetzen
|
||||
*
|
||||
* Usage: php migrations/fix_story_content_errors.php (inspect)
|
||||
* php migrations/fix_story_content_errors.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
const CDC_COLUMNS_UID = 355;
|
||||
const DELETE_ELEMENTS = [253, 266, 206];
|
||||
const PROTEO_UID = 234;
|
||||
const KEEP_CHECK = 218;
|
||||
|
||||
const CDC_BACKGROUND = '<p>When it comes mitigating major threats to public health, it is imperative for organizations like the Centers for Disease Control (CDC) to keep their finger on the pulse of global developments that require a coordinated national or global response.</p>'
|
||||
. '<p>In addition to being ardent consumers of news and information, doctors, scientists, and other healthcare experts at the CDC produce a tremendous amount of original content for both internal and external consumption. This makes the agency one of the most video-centric departments in the federal government.</p>';
|
||||
|
||||
const PROTEO_KEYFEATURE = '<ul>'
|
||||
. '<li>MIL-spec ruggedization HEVC & H.264 Encoder</li>'
|
||||
. '<li>Low latency HD/SD Streaming</li>'
|
||||
. '<li>Low bandwidth HEVC Streaming</li>'
|
||||
. '<li>Overlay for video input identification and timestamping</li>'
|
||||
. '<li>KLV / STANAG metadata processing for ISR</li>'
|
||||
. '</ul>';
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo ($apply ? 'APPLY' : 'INSPECT') . "\n";
|
||||
|
||||
// --- 1) CDC-Spalten-Items ----------------------------------------------------
|
||||
// Collection-Tabellen generisch ueber foreign_table_parent_uid finden
|
||||
// (die Content-Blocks-Felder heissen hier unprefixed: column/text/item_type)
|
||||
$itemTables = $pdo->query(
|
||||
"SELECT DISTINCT TABLE_NAME FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = 'foreign_table_parent_uid'"
|
||||
)->fetchAll(PDO::FETCH_COLUMN);
|
||||
echo "\n--- 1) CDC #" . CDC_COLUMNS_UID . " · Kandidaten-Tabellen: " . implode(', ', $itemTables) . "\n";
|
||||
$fixedLeft = false;
|
||||
$deletedRight = false;
|
||||
foreach ($itemTables as $table) {
|
||||
$cols = $pdo->query("SELECT COLUMN_NAME FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = " . $pdo->quote($table))->fetchAll(PDO::FETCH_COLUMN);
|
||||
$textCol = in_array('vitec_text', $cols, true) ? 'vitec_text' : (in_array('text', $cols, true) ? 'text' : null);
|
||||
$colCol = in_array('vitec_column', $cols, true) ? 'vitec_column' : (in_array('column', $cols, true) ? 'column' : null);
|
||||
if ($textCol === null || $colCol === null) {
|
||||
continue;
|
||||
}
|
||||
$items = $pdo->query("SELECT * FROM `$table` WHERE deleted = 0 AND foreign_table_parent_uid = " . CDC_COLUMNS_UID . " ORDER BY sorting")->fetchAll();
|
||||
if ($items === []) {
|
||||
continue;
|
||||
}
|
||||
echo " Tabelle: $table (textCol=$textCol, colCol=$colCol)\n";
|
||||
foreach ($items as $item) {
|
||||
$text = (string)$item[$textCol];
|
||||
printf(" item #%d col=%s type=%s text=\"%s\"\n",
|
||||
$item['uid'], $item[$colCol] ?? '?',
|
||||
$item['vitec_item_type'] ?? ($item['item_type'] ?? '?'),
|
||||
mb_substr(trim(strip_tags($text)), 0, 70));
|
||||
$isHotelText = str_contains($text, 'Crowne Plaza');
|
||||
$column = (string)($item[$colCol] ?? 'left');
|
||||
if ($isHotelText && $textCol !== null) {
|
||||
echo " -> linkes Text-Item: Hotel-Text wird durch CDC-Background ersetzt\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE `$table` SET `$textCol` = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?")
|
||||
->execute([CDC_BACKGROUND, (int)$item['uid']]);
|
||||
$fixedLeft = true;
|
||||
}
|
||||
} elseif ($column === 'right') {
|
||||
echo " -> rechtes Item (Hotel-Logo): wird soft-geloescht\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE `$table` SET deleted = 1, tstamp = UNIX_TIMESTAMP() WHERE uid = ?")
|
||||
->execute([(int)$item['uid']]);
|
||||
$deletedRight = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // erste Tabelle mit Items reicht
|
||||
}
|
||||
if ($apply) {
|
||||
echo $fixedLeft ? " Text ersetzt.\n" : " WARNUNG: kein Hotel-Text-Item gefunden!\n";
|
||||
echo $deletedRight ? " Logo-Item geloescht.\n" : " Hinweis: kein rechtes Item geloescht.\n";
|
||||
}
|
||||
|
||||
// --- 2) Key-Feature-Elemente -------------------------------------------------
|
||||
echo "\n--- 2) Key-Feature-Elemente ---\n";
|
||||
$in = implode(',', array_merge(DELETE_ELEMENTS, [PROTEO_UID, KEEP_CHECK]));
|
||||
foreach ($pdo->query("SELECT uid, CType, LEFT(header,30) h, LEFT(bodytext,70) b, deleted FROM tt_content WHERE uid IN ($in)") as $row) {
|
||||
$uid = (int)$row['uid'];
|
||||
$action = in_array($uid, DELETE_ELEMENTS, true) ? 'LOESCHEN (Original hat kein Key Feature)'
|
||||
: ($uid === PROTEO_UID ? 'bodytext -> MIL-spec-Bullets (Original)' : 'BEHALTEN (Text im Original echt)');
|
||||
printf(" #%-4d %-12s \"%s\" \"%s\"%s -> %s\n", $uid, $row['CType'], $row['h'],
|
||||
trim(strip_tags((string)$row['b'])), $row['deleted'] ? ' [DELETED]' : '', $action);
|
||||
}
|
||||
if ($apply) {
|
||||
$pdo->exec('UPDATE tt_content SET deleted = 1, tstamp = UNIX_TIMESTAMP() WHERE uid IN (' . implode(',', DELETE_ELEMENTS) . ')');
|
||||
$pdo->prepare('UPDATE tt_content SET bodytext = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?')
|
||||
->execute([PROTEO_KEYFEATURE, PROTEO_UID]);
|
||||
echo " 3 geloescht, Proteo ersetzt.\n";
|
||||
}
|
||||
|
||||
echo $apply ? "\nFertig. Danach: vendor/bin/typo3 cache:flush\n" : "\nInspect only - mit --apply schreiben.\n";
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Ticket 0017-FE Punkt 4: drei Alt-Stories tragen Platzhalter in
|
||||
* quote/quote_person/quote_role (africa-hall: "Here Fu Manchu lalala.",
|
||||
* bayer-uk: "This is a testimonial", meydan-racecourse: der Feldhilfetext).
|
||||
* Auf Testimonial-Karten wuerden die erscheinen -> Felder leeren.
|
||||
*
|
||||
* Usage: php migrations/fix_story_placeholder_quotes.php (inspect)
|
||||
* php migrations/fix_story_placeholder_quotes.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
const SLUGS = ['africa-hall', 'bayer-uk', 'meydan-racecourse'];
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo ($apply ? 'APPLY' : 'INSPECT') . "\n";
|
||||
$in = implode(',', array_fill(0, count(SLUGS), '?'));
|
||||
$stmt = $pdo->prepare("SELECT uid, slug, LEFT(quote,60) q, quote_person, quote_role
|
||||
FROM tx_vitec_domain_model_usecase WHERE deleted = 0 AND slug IN ($in)");
|
||||
$stmt->execute(SLUGS);
|
||||
$rows = $stmt->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
printf("#%-4d %-20s quote=\"%s\" person=\"%s\" role=\"%s\" -> leeren\n",
|
||||
$row['uid'], $row['slug'], $row['q'], $row['quote_person'], $row['quote_role']);
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE tx_vitec_domain_model_usecase
|
||||
SET quote = '', quote_person = '', quote_role = '', tstamp = UNIX_TIMESTAMP() WHERE uid = ?")
|
||||
->execute([(int)$row['uid']]);
|
||||
}
|
||||
}
|
||||
echo count($rows) . " Record(s)" . ($apply ? " geleert. Danach cache:flush." : ". Mit --apply leeren.") . "\n";
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Ticket 0017-FE Punkt 5b: Videos als echte vitec_video-Elemente.
|
||||
*
|
||||
* - betfred-hq: "The Result" endet mit "<p>Case Study Video</p>" + Link
|
||||
* auf youtube nw52kbYpF3A -> Absaetze strippen, vitec_video danach.
|
||||
* - kanha-shanti-vanam: "The Result" traegt "<p>Short Videos</p><ul>"
|
||||
* mit zwei YouTube-Links (WD0dHmqN4s0, C8FzHR5aUAI) -> strippen,
|
||||
* zwei vitec_video danach.
|
||||
* - atlanta-hawks: html-Element #326 traegt den escapten iframe
|
||||
* (-ds3p9jb5F0) -> Element loeschen, vitec_video an seiner Position.
|
||||
*
|
||||
* Einfuegung direkt per SQL (pid/colPos 999/tx_vitec_usecase_content/
|
||||
* sorting zwischen den Nachbarn); der Inline-Zaehler content_elements am
|
||||
* Usecase-Record wird mitgezogen.
|
||||
*
|
||||
* Usage: php migrations/fix_story_videos.php (inspect)
|
||||
* php migrations/fix_story_videos.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo ($apply ? 'APPLY' : 'INSPECT') . "\n";
|
||||
|
||||
$col = $pdo->query("SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tt_content' AND COLUMN_NAME = 'vitec_youtube_code'")->fetchColumn();
|
||||
if ((int)$col !== 1) {
|
||||
exit("ABBRUCH: Spalte tt_content.vitec_youtube_code nicht gefunden.\n");
|
||||
}
|
||||
|
||||
function storyUid(PDO $pdo, string $slug): int
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT uid FROM tx_vitec_domain_model_usecase WHERE deleted = 0 AND slug = ?');
|
||||
$stmt->execute([$slug]);
|
||||
return (int)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
/** sorting-Wert direkt nach $anchorUid; bei zu kleinem Abstand werden die Kinder vorher mit Stride 256 neu nummeriert. */
|
||||
function sortingAfter(PDO $pdo, int $storyUid, int $anchorUid, bool $apply): int
|
||||
{
|
||||
$children = $pdo->query("SELECT uid, sorting FROM tt_content
|
||||
WHERE deleted = 0 AND tx_vitec_usecase_content = $storyUid ORDER BY sorting, uid")->fetchAll();
|
||||
$index = array_search($anchorUid, array_map(static fn(array $r): int => (int)$r['uid'], $children), true);
|
||||
if ($index === false) {
|
||||
throw new RuntimeException("Anker #$anchorUid nicht unter den Kindern von Story #$storyUid");
|
||||
}
|
||||
$anchorSort = (int)$children[$index]['sorting'];
|
||||
$nextSort = isset($children[$index + 1]) ? (int)$children[$index + 1]['sorting'] : $anchorSort + 512;
|
||||
if ($nextSort - $anchorSort >= 4) {
|
||||
return $anchorSort + intdiv($nextSort - $anchorSort, 2);
|
||||
}
|
||||
// zu eng: komplett neu nummerieren und erneut rechnen
|
||||
if ($apply) {
|
||||
$sorting = 0;
|
||||
$stmt = $pdo->prepare('UPDATE tt_content SET sorting = ? WHERE uid = ?');
|
||||
foreach ($children as $child) {
|
||||
$sorting += 256;
|
||||
$stmt->execute([$sorting, (int)$child['uid']]);
|
||||
}
|
||||
}
|
||||
return ($index + 1) * 256 + 128;
|
||||
}
|
||||
|
||||
function insertVideo(PDO $pdo, int $storyUid, int $pid, int $sorting, string $header, string $code, bool $apply): void
|
||||
{
|
||||
printf(" + vitec_video \"%s\" code=%s sorting=%d\n", $header, $code, $sorting);
|
||||
if (!$apply) {
|
||||
return;
|
||||
}
|
||||
$pdo->prepare("INSERT INTO tt_content
|
||||
(pid, CType, colPos, tx_vitec_usecase_content, sorting, header, vitec_youtube_code, crdate, tstamp)
|
||||
VALUES (?, 'vitec_video', 999, ?, ?, ?, ?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
|
||||
->execute([$pid, $storyUid, $sorting, $header, $code]);
|
||||
$pdo->exec("UPDATE tx_vitec_domain_model_usecase
|
||||
SET content_elements = (SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND tx_vitec_usecase_content = $storyUid),
|
||||
tstamp = UNIX_TIMESTAMP()
|
||||
WHERE uid = $storyUid");
|
||||
}
|
||||
|
||||
function stripFromBody(PDO $pdo, int $elementUid, string $pattern, bool $apply): bool
|
||||
{
|
||||
$body = (string)$pdo->query("SELECT bodytext FROM tt_content WHERE uid = $elementUid")->fetchColumn();
|
||||
$clean = (string)preg_replace($pattern, '', $body);
|
||||
if ($clean === $body) {
|
||||
echo " ! Strip-Muster traf NICHT in #$elementUid - nichts geaendert\n";
|
||||
return false;
|
||||
}
|
||||
echo " ~ #$elementUid: Video-Absaetze aus bodytext entfernt\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare('UPDATE tt_content SET bodytext = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?')
|
||||
->execute([trim($clean), $elementUid]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Betfred -----------------------------------------------------------------
|
||||
echo "\n--- betfred-hq ---\n";
|
||||
$uid = storyUid($pdo, 'betfred-hq');
|
||||
$pid = (int)$pdo->query("SELECT pid FROM tx_vitec_domain_model_usecase WHERE uid = $uid")->fetchColumn();
|
||||
if (stripFromBody($pdo, 11910, '#<p>\s*Case Study Video\s*</p>\s*<p>[^<]*nw52kbYpF3A[^<]*</p>#u', $apply)) {
|
||||
insertVideo($pdo, $uid, $pid, sortingAfter($pdo, $uid, 11910, $apply), 'Case Study Video', 'nw52kbYpF3A', $apply);
|
||||
}
|
||||
|
||||
// --- Kanha Shanti Vanam --------------------------------------------------------
|
||||
echo "\n--- kanha-shanti-vanam ---\n";
|
||||
$uid = storyUid($pdo, 'kanha-shanti-vanam-meditation-center');
|
||||
$pid = (int)$pdo->query("SELECT pid FROM tx_vitec_domain_model_usecase WHERE uid = $uid")->fetchColumn();
|
||||
if (stripFromBody($pdo, 11964, '#<p>\s*Short Videos\s*</p>\s*<ul>\s*<li>[^<]*WD0dHmqN4s0[^<]*</li>\s*<li>[^<]*C8FzHR5aUAI[^<]*</li>\s*</ul>#u', $apply)) {
|
||||
$sorting = sortingAfter($pdo, $uid, 11964, $apply);
|
||||
insertVideo($pdo, $uid, $pid, $sorting, 'Short Videos', 'WD0dHmqN4s0', $apply);
|
||||
insertVideo($pdo, $uid, $pid, $sorting + 1, '', 'C8FzHR5aUAI', $apply);
|
||||
}
|
||||
|
||||
// --- Atlanta Hawks --------------------------------------------------------------
|
||||
echo "\n--- atlanta-hawks ---\n";
|
||||
$uid = storyUid($pdo, 'atlanta-hawks');
|
||||
$old = $pdo->query('SELECT uid, pid, sorting, bodytext FROM tt_content WHERE uid = 326 AND deleted = 0')->fetch();
|
||||
if ($old && str_contains((string)$old['bodytext'], '-ds3p9jb5F0')) {
|
||||
echo " ~ html #326 (escapter iframe) wird geloescht, vitec_video an seiner Position\n";
|
||||
if ($apply) {
|
||||
$pdo->exec('UPDATE tt_content SET deleted = 1, tstamp = UNIX_TIMESTAMP() WHERE uid = 326');
|
||||
}
|
||||
insertVideo($pdo, $uid, (int)$old['pid'], (int)$old['sorting'], '', '-ds3p9jb5F0', $apply);
|
||||
} else {
|
||||
echo " ! html #326 nicht gefunden oder ohne erwarteten iframe - uebersprungen\n";
|
||||
}
|
||||
|
||||
echo $apply ? "\nFertig. Danach: vendor/bin/typo3 cache:flush\n" : "\nInspect only - mit --apply schreiben.\n";
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* fix_why_band_position.php (2026-11-04)
|
||||
*
|
||||
* Verschiebt das "Why VITEC?"-Band (introparagraph + folgende icon_card-
|
||||
* Elemente aus vitec:build-why-vitec-bands) auf den 7 Produkt-Uebersichts-
|
||||
* seiten VOR das "Product range"-Intro. Der Band-Lauf hatte es dahinter
|
||||
* einsortiert und damit die Ueberschrift von ihren Produktkarten getrennt
|
||||
* (Arne, 0042-FE Abschnitt 3).
|
||||
*
|
||||
* Soll: Hero -> Why-VITEC-Band -> "Product range" -> Produktkarten.
|
||||
*
|
||||
* php migrations/fix_why_band_position.php # INSPECT (default)
|
||||
* php migrations/fix_why_band_position.php --apply # schreibt
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $db['host'], $db['dbname']),
|
||||
$db['user'],
|
||||
$db['password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
echo $apply ? "APPLY\n\n" : "INSPECT - nichts wird geschrieben (--apply zum Schreiben)\n\n";
|
||||
|
||||
$slugs = [
|
||||
'/products/activesqx',
|
||||
'/products/aetria',
|
||||
'/products/aligo',
|
||||
'/products/avedia-modular-system',
|
||||
'/products/image-graphics-cards',
|
||||
'/products/operator-workstations',
|
||||
'/products/prism-transcoder',
|
||||
];
|
||||
|
||||
$moved = 0;
|
||||
foreach ($slugs as $slug) {
|
||||
$page = $pdo->prepare('SELECT uid, title FROM pages WHERE deleted = 0 AND slug = ?');
|
||||
$page->execute([$slug]);
|
||||
$page = $page->fetch(PDO::FETCH_ASSOC);
|
||||
if ($page === false) {
|
||||
echo "$slug: Seite NICHT gefunden - uebersprungen\n\n";
|
||||
continue;
|
||||
}
|
||||
$pid = (int)$page['uid'];
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT uid, CType, header, sorting, hidden FROM tt_content
|
||||
WHERE pid = ? AND deleted = 0 AND colPos = 0
|
||||
AND (tx_container_parent IS NULL OR tx_container_parent = 0)
|
||||
ORDER BY sorting, uid'
|
||||
);
|
||||
$stmt->execute([$pid]);
|
||||
$elements = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Band finden: "Why VITEC?"-Intro + direkt folgende vitec_card-Elemente
|
||||
$bandStart = null;
|
||||
foreach ($elements as $i => $el) {
|
||||
if ($el['CType'] === 'vitec_introparagraph' && trim((string)$el['header']) === 'Why VITEC?') {
|
||||
$bandStart = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($bandStart === null) {
|
||||
echo "$slug (pid $pid): kein 'Why VITEC?'-Intro - uebersprungen\n\n";
|
||||
continue;
|
||||
}
|
||||
$bandEnd = $bandStart;
|
||||
while (isset($elements[$bandEnd + 1]) && $elements[$bandEnd + 1]['CType'] === 'vitec_card') {
|
||||
$bandEnd++;
|
||||
}
|
||||
|
||||
// Ziel: das erste andere Intro ("Product range")
|
||||
$target = null;
|
||||
foreach ($elements as $i => $el) {
|
||||
if ($i >= $bandStart && $i <= $bandEnd) {
|
||||
continue;
|
||||
}
|
||||
if ($el['CType'] === 'vitec_introparagraph') {
|
||||
$target = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($target === null) {
|
||||
echo "$slug (pid $pid): kein zweites Intro gefunden - uebersprungen\n\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "$slug (pid $pid) - Band: Elemente " . ($bandEnd - $bandStart + 1)
|
||||
. " (#" . $elements[$bandStart]['uid'] . "..#" . $elements[$bandEnd]['uid'] . ")"
|
||||
. ", Ziel-Intro: #" . $elements[$target]['uid'] . " \"" . $elements[$target]['header'] . "\"\n";
|
||||
|
||||
if ($target > $bandEnd) {
|
||||
echo " Band steht bereits VOR dem Ziel-Intro - nichts zu tun\n\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// neue Reihenfolge: Band raus, vor das Ziel-Intro wieder rein
|
||||
$band = array_slice($elements, $bandStart, $bandEnd - $bandStart + 1);
|
||||
$rest = array_merge(array_slice($elements, 0, $bandStart), array_slice($elements, $bandEnd + 1));
|
||||
$newOrder = [];
|
||||
foreach ($rest as $el) {
|
||||
if ((int)$el['uid'] === (int)$elements[$target]['uid']) {
|
||||
foreach ($band as $b) {
|
||||
$newOrder[] = $b;
|
||||
}
|
||||
}
|
||||
$newOrder[] = $el;
|
||||
}
|
||||
|
||||
echo " Neue Reihenfolge:\n";
|
||||
$sorting = 0;
|
||||
foreach ($newOrder as $el) {
|
||||
$sorting += 256;
|
||||
printf(" %5d #%-6d %-24s %s\n", $sorting, $el['uid'], $el['CType'], (string)$el['header']);
|
||||
if ($apply) {
|
||||
$pdo->prepare('UPDATE tt_content SET sorting = ? WHERE uid = ?')->execute([$sorting, (int)$el['uid']]);
|
||||
}
|
||||
}
|
||||
$moved++;
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo ($apply ? "FERTIG" : "WUERDE") . ": $moved Seite(n) umsortiert.\n";
|
||||
if ($apply && $moved > 0) {
|
||||
echo "Danach: vendor/bin/typo3 cache:flush\n";
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* The product group pages (two middle levels of the /products tree) were
|
||||
* turned into folders by the editor on 2026-11-10 (decision confirmed by
|
||||
* Harry 2026-11-11: intentional). Their former URLs answered 404 since
|
||||
* then (Seobility crawl 3: 8 "technische Probleme"). Every folder under
|
||||
* /products gets a 301 to /products; the content elements on the folders
|
||||
* stay untouched.
|
||||
*
|
||||
* Dry run: php migrations/folder_pages_redirects_20261111.php
|
||||
* Write: php migrations/folder_pages_redirects_20261111.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
|
||||
$hasRedirect = $pdo->prepare("SELECT uid, target FROM sys_redirect WHERE deleted = 0 AND source_path = ? LIMIT 1");
|
||||
$insertRedirect = $pdo->prepare(
|
||||
"INSERT INTO sys_redirect (pid, createdon, updatedon, source_host, source_path, target, target_statuscode)
|
||||
VALUES (0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '*', ?, ?, 301)"
|
||||
);
|
||||
$target = '/products';
|
||||
$paths = [];
|
||||
$n = 0;
|
||||
foreach ($pdo->query("SELECT uid, slug, title, doktype FROM pages WHERE deleted = 0 AND hidden = 0 AND doktype = 254 AND slug LIKE '/products/%' ORDER BY slug") as $p) {
|
||||
$ce = (int)$pdo->query("SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND hidden = 0 AND pid = {$p['uid']}")->fetchColumn();
|
||||
foreach ([$p['slug'], $p['slug'] . '/'] as $source) {
|
||||
$hasRedirect->execute([$source]);
|
||||
$existing = $hasRedirect->fetch();
|
||||
if ($existing) {
|
||||
printf(" #%-4d %-55s (%2d CEs) %s: Redirect existiert -> %s\n", $p['uid'], $source, $ce, $p['title'], $existing['target']);
|
||||
continue;
|
||||
}
|
||||
printf(" #%-4d %-55s (%2d CEs) %s -> %s\n", $p['uid'], $source, $ce, $p['title'], $target);
|
||||
$n++;
|
||||
if ($apply) { $insertRedirect->execute([$source, $target]); }
|
||||
}
|
||||
$paths[] = $p['slug'];
|
||||
// Older redirects that point AT this folder (t3://page?uid=… or its slug)
|
||||
// would now end in a 404 chain: send them to /products as well.
|
||||
$st = $pdo->prepare("SELECT uid, source_path, target FROM sys_redirect WHERE deleted = 0 AND (target = ? OR target = ? OR target LIKE ?)");
|
||||
$st->execute(['t3://page?uid=' . $p['uid'], $p['slug'], 't3://page?uid=' . $p['uid'] . '&%']);
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
printf(" redirect #%d %s: Ziel %s -> %s\n", $r['uid'], $r['source_path'], $r['target'], $target);
|
||||
if ($apply) { $pdo->prepare("UPDATE sys_redirect SET target = ?, updatedon = UNIX_TIMESTAMP() WHERE uid = ?")->execute([$target, (int)$r['uid']]); }
|
||||
}
|
||||
}
|
||||
echo "\n$n Redirect(s) " . ($apply ? 'angelegt' : 'wuerden angelegt') . ".\n";
|
||||
echo "SSR-Pfade:\n " . implode("\n ", $paths) . "\n";
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Follow-up to folder_pages_redirects_20261111: a product folder that holds
|
||||
* exactly one visible overview page sends its visitors to that page instead
|
||||
* of /products (RF Gateway -> Avedia Modular System, AV over IP & KVM -> Aligo).
|
||||
* Applies to the folder's own redirects and to older redirects that were
|
||||
* pointed at /products in the previous step because they targeted the folder.
|
||||
*
|
||||
* Dry run: php migrations/folder_redirect_targets_20261111.php
|
||||
* Write: php migrations/folder_redirect_targets_20261111.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
|
||||
// Older redirects (sys_redirect uid) that targeted the folder before the previous step.
|
||||
$legacyByFolder = [566 => [34, 124, 125], 590 => [63, 66]];
|
||||
$n = 0;
|
||||
foreach ($pdo->query("SELECT uid, slug, title FROM pages WHERE deleted = 0 AND hidden = 0 AND doktype = 254 AND slug LIKE '/products/%' ORDER BY slug") as $folder) {
|
||||
$children = $pdo->query("SELECT uid, slug, title FROM pages WHERE deleted = 0 AND hidden = 0 AND doktype = 1 AND pid = {$folder['uid']}")->fetchAll();
|
||||
if (count($children) !== 1) { continue; }
|
||||
$target = $children[0]['slug'];
|
||||
printf(" Ordner #%d %s -> einzige Seite #%d %s\n", $folder['uid'], $folder['slug'], $children[0]['uid'], $target);
|
||||
$uids = $legacyByFolder[(int)$folder['uid']] ?? [];
|
||||
$st = $pdo->prepare("SELECT uid, source_path, target FROM sys_redirect WHERE deleted = 0 AND target = '/products' AND (source_path = ? OR source_path = ?" . ($uids ? ' OR uid IN (' . implode(',', array_map('intval', $uids)) . ')' : '') . ")");
|
||||
$st->execute([$folder['slug'], $folder['slug'] . '/']);
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
printf(" redirect #%-4d %-55s /products -> %s\n", $r['uid'], $r['source_path'], $target);
|
||||
$n++;
|
||||
if ($apply) { $pdo->prepare("UPDATE sys_redirect SET target = ?, updatedon = UNIX_TIMESTAMP() WHERE uid = ?")->execute([$target, (int)$r['uid']]); }
|
||||
}
|
||||
}
|
||||
echo "\n$n Redirect(s) " . ($apply ? 'umgebogen' : 'wuerden umgebogen') . ".\n";
|
||||
+114435
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* News articles that carry the same text as a success story get the story
|
||||
* as canonical (field tx_news_domain_model_news.vitec_canonical_usecase,
|
||||
* evaluated by VitecMetaHandler). The article stays in the news list.
|
||||
*
|
||||
* Pairs from the Seobility duplicate-content report of 2026-11-10.
|
||||
*
|
||||
* Dry run: php migrations/news_story_canonical_20261110.php
|
||||
* Write: php migrations/news_story_canonical_20261110.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
|
||||
// news uid => usecase (story) uid
|
||||
$pairs = [
|
||||
361 => 56, // Denver Sportsbook Bar & Grill
|
||||
121 => 24, // European Space Agency
|
||||
164 => 26, // Feature Story News
|
||||
336 => 62, // Kyndryl Cyber Operations Center
|
||||
388 => 74, // Leading Video Capture for the Medical Market
|
||||
239 => 66, // RRS Sir David Attenborough
|
||||
158 => 46, // Sacramento Kings
|
||||
310 => 68, // SENTRY Video Walls – 2024 Olympic Games
|
||||
290 => 72, // UK Mission Critical Control Room
|
||||
389 => 59, // Intercity Istanbul Park
|
||||
436 => 64, // NHS tribute onto Parliament
|
||||
395 => 54, // Bahrain International Circuit
|
||||
327 => 60, // ITV News Headquarters
|
||||
];
|
||||
|
||||
$news = $pdo->prepare("SELECT uid, title, path_segment, vitec_canonical_usecase FROM tx_news_domain_model_news WHERE deleted = 0 AND uid = ?");
|
||||
$story = $pdo->prepare("SELECT uid, title, slug, hidden FROM tx_vitec_domain_model_usecase WHERE deleted = 0 AND uid = ?");
|
||||
$update = $pdo->prepare("UPDATE tx_news_domain_model_news SET vitec_canonical_usecase = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?");
|
||||
$set = 0;
|
||||
$paths = [];
|
||||
foreach ($pairs as $newsUid => $storyUid) {
|
||||
$news->execute([$newsUid]); $n = $news->fetch();
|
||||
$story->execute([$storyUid]); $s = $story->fetch();
|
||||
if (!$n || !$s) { echo " #$newsUid -> #$storyUid: Datensatz fehlt, uebersprungen\n"; continue; }
|
||||
if ((int)$s['hidden'] === 1) { echo " #$newsUid -> #$storyUid: Story versteckt, uebersprungen\n"; continue; }
|
||||
$state = (int)$n['vitec_canonical_usecase'] === $storyUid ? 'schon gesetzt' : 'setzen';
|
||||
printf(" news #%-4d %-70s -> story #%-3d /success-stories/%s [%s]\n", $newsUid, mb_substr($n['title'], 0, 70), $storyUid, $s['slug'], $state);
|
||||
$paths[] = '/news/article/' . $n['path_segment'];
|
||||
if ($state === 'setzen') {
|
||||
$set++;
|
||||
if ($apply) { $update->execute([$storyUid, $newsUid]); }
|
||||
}
|
||||
}
|
||||
echo "\n$set Zuordnung(en) " . ($apply ? 'geschrieben' : 'wuerden geschrieben') . ".\n";
|
||||
echo "SSR-Pfade:\n " . implode("\n ", $paths) . "\n";
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# Placeholder flag for the content checks (2026-11-13): permissions, syntax check,
|
||||
# cache flush and SSR purge of the three check pages; "commit" as first argument
|
||||
# commits the two services afterwards.
|
||||
set -e
|
||||
cd /usr/www/users/vitecevo/live
|
||||
SRV=packages/vitec/Classes/Service
|
||||
chmod 644 $SRV/PageCheckService.php $SRV/ProductCheckService.php migrations/pagecheck_placeholder_20261113.sh
|
||||
php -l $SRV/PageCheckService.php
|
||||
php -l $SRV/ProductCheckService.php
|
||||
if [ "$1" = "commit" ]; then
|
||||
git add $SRV/PageCheckService.php $SRV/ProductCheckService.php migrations/pagecheck_placeholder_20261113.sh
|
||||
git commit -q -F migrations/pagecheck_placeholder_20261113.msg
|
||||
git log --oneline -1
|
||||
exit 0
|
||||
fi
|
||||
vendor/bin/typo3 cache:flush
|
||||
vendor/bin/typo3 vitec:ssr-purge /markets/market-check /solutions/solution-check /products/product-check
|
||||
vendor/bin/typo3 vitec:ssr-warm /markets/market-check /solutions/solution-check /products/product-check
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
# vitec_pagecheck (2026-11-13): permissions, syntax check, cache flush, page setup (dry-run unless --apply).
|
||||
set -e
|
||||
cd /usr/www/users/vitecevo/live
|
||||
EXT=packages/vitec
|
||||
CFG=$EXT/Configuration
|
||||
chmod 644 $EXT/Classes/Command/CreateCheckPagesCommand.php $EXT/Classes/Service/PageCheckService.php $EXT/Classes/UserFunc/PageCheckJsonRenderer.php \
|
||||
$CFG/FlexForms/Pagecheck.xml $CFG/Services.yaml $CFG/Sets/Vitecset/setup.typoscript $CFG/TCA/Overrides/tt_content.php $CFG/page.tsconfig migrations/pagecheck_setup_20261113.sh
|
||||
php -l $EXT/Classes/Service/PageCheckService.php
|
||||
php -l $EXT/Classes/UserFunc/PageCheckJsonRenderer.php
|
||||
php -l $EXT/Classes/Command/CreateCheckPagesCommand.php
|
||||
php -l $CFG/TCA/Overrides/tt_content.php
|
||||
vendor/bin/typo3 cache:flush
|
||||
vendor/bin/typo3 vitec:pagecheck-pages "$@"
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,339 @@
|
||||
{
|
||||
"_doku": "Workbook -> Produkt-Zuordnung fuer vitec:import-product-workbooks. product = Slug des Zieldatensatzes; family = K-Ebene (Familienseite, wird NIE importiert); product null = ungeklaert (Kommando meldet und ueberspringt). Erzeugt 2026-09-24 aus der Analyse vom 18.09. + Sitemap-V2 Spalte K; von Hand pflegbar.",
|
||||
"workbooks": {
|
||||
"AV over IP and KVM Product Pages/Aligo_Product Page Content.xlsx": {
|
||||
"product": "aligo"
|
||||
},
|
||||
"AV over IP and KVM Product Pages/Arqa_Product Page Content.xlsx": {
|
||||
"product": "arqa"
|
||||
},
|
||||
"AV over IP and KVM Product Pages/QTX100_Product Page Content.xlsx": {
|
||||
"product": "qtx100"
|
||||
},
|
||||
"AV over IP and KVM Product Pages/RX1-C_Product Page Content.xlsx": {
|
||||
"product": "rx1c"
|
||||
},
|
||||
"AV over IP and KVM Product Pages/RX1-F_Product Page Content.xlsx": {
|
||||
"product": "rx1f"
|
||||
},
|
||||
"AV over IP and KVM Product Pages/RX100_Product Page Content.xlsx": {
|
||||
"product": "rx100"
|
||||
},
|
||||
"AV over IP and KVM Product Pages/TX1-C_Product Page Content.xlsx": {
|
||||
"product": "tx1c"
|
||||
},
|
||||
"AV over IP and KVM Product Pages/TX1-F_Product Page Content.xlsx": {
|
||||
"product": "tx1f"
|
||||
},
|
||||
"AV over IP and KVM Product Pages/TX100 SQX_Product Page Content.xlsx": {
|
||||
"product": "tx100-sqx"
|
||||
},
|
||||
"AV over IP and KVM Product Pages/TX100_Product Page Content.xlsx": {
|
||||
"product": "tx100"
|
||||
},
|
||||
"Control Room Platforms Product Pages/Aetria Command Center_Product Page Content.xlsx": {
|
||||
"product": "aetria-command-center"
|
||||
},
|
||||
"Control Room Platforms Product Pages/Aetria Network Manager_Product Page Content.xlsx": {
|
||||
"product": "aetria-network-manager"
|
||||
},
|
||||
"Control Room Platforms Product Pages/Aetria Touch_Product Page Content.xlsx": {
|
||||
"product": "aetria-touch"
|
||||
},
|
||||
"Control Room Platforms Product Pages/Aetria WallControl_Product Page Content.xlsx": {
|
||||
"product": "aetria-wallcontrol"
|
||||
},
|
||||
"Control Room Platforms Product Pages/Aetria Workstation_Product Page Content.xlsx": {
|
||||
"product": "aetria-workstation"
|
||||
},
|
||||
"Control Room Platforms Product Pages/Aetria_Product Page Content.xlsx": {
|
||||
"product": "aetria"
|
||||
},
|
||||
"Gateways and Transcoding Product Pages/IP Gateways_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: ChannelLink (IP to IP gateways) - Familien-Seite /gateways-and-transcoding/ip-gateways/"
|
||||
},
|
||||
"Gateways and Transcoding Product Pages/PRISM Transcoder_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: PRISM Transcoder"
|
||||
},
|
||||
"Gateways and Transcoding Product Pages/PRISM_1RU_Rack_Mount_Transcoder.xlsx": {
|
||||
"product": "prism-1ru-rack-mount-transcoder"
|
||||
},
|
||||
"Gateways and Transcoding Product Pages/RF Gateways_Product Page Content.xlsx": {
|
||||
"product": "45-series-rf-gateways",
|
||||
"note": "manuell 2026-09-24, PRUEFEN: einziger RF-Gateway-Record; Workbook-URL ist aber der Familien-Pfad"
|
||||
},
|
||||
"Gateways and Transcoding Product Pages/SFF_PRISM.xlsx": {
|
||||
"product": "sff-prism-small-format-appliance-transcoder"
|
||||
},
|
||||
"Graphics and Multi-Display Product Pages/IQS4 (4K splitter)_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: IQS4 (4K splitter)"
|
||||
},
|
||||
"Graphics and Multi-Display Product Pages/Image Graphics Cards (multi-output GPU cards)_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: Image Graphics Cards (multi-output GPU cards)"
|
||||
},
|
||||
"Graphics and Multi-Display Product Pages/Image_2K.xlsx": {
|
||||
"product": "image-2k"
|
||||
},
|
||||
"Graphics and Multi-Display Product Pages/Image_4K.xlsx": {
|
||||
"product": "image-4k"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/Aligo Workstation.xlsx": {
|
||||
"product": "aligo-workstation"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/ArtioSign_Product Page Content.xlsx": {
|
||||
"product": "artiosign",
|
||||
"note": "Record angelegt 2026-09-28 (versteckt, Kategorie Aetria provisorisch) - Entscheidung Olli"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/ArtioView_Product Page Content.xlsx": {
|
||||
"product": "artioview",
|
||||
"note": "Record angelegt 2026-09-28 (versteckt, Kategorie Aetria provisorisch) - Entscheidung Olli"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/Avedia End-Points_Product Page Content.xlsx": {
|
||||
"product": "avedia-end-points"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/Avedia Platform_Product Page Content.xlsx": {
|
||||
"product": "avedia-platform-iptv-signage-cmsserver"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/Avedia_Modular_System.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: Avedia Modular System"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/Avedia_Product_Page_Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: Avedia Platform (URL /iptv-and-digital-signage/avedia/); Produkt-Workbook ist Avedia Platform_Product Page Content"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/ChannelLink_Product Page Content.xlsx": {
|
||||
"product": "channellink"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/EZ TV End-Points.xlsx": {
|
||||
"product": "ez-tv-end-points-ep6"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/EZ TV Platform CMS-Server.xlsx": {
|
||||
"product": "ez-tv-platform-iptv-signage-cmsserver",
|
||||
"note": "manuell 2026-09-24: das eigentliche Produkt-Workbook (URL .../ez-tv-platform-cms-server/)"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/EZ TV Platform.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: EZ TV Platform (URL /platforms-and-end-points/ez-tv-platform/); Auto-Titel-Match auf den Produkt-Record war FALSCH, korrigiert 2026-09-24"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/Media Players_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "Sammelseite (K-Ebene) - Entscheidung Olli 2026-09-28"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/Operator Workstations.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: Operator Workstations"
|
||||
},
|
||||
"IPTV and Digital Signage Product Pages/X-Player End-Points.xlsx": {
|
||||
"product": "x-player-end-points-xp23-xp25-xp26"
|
||||
},
|
||||
"Infrastructure and Accessories Product Pages/Accessories & Cables.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: Accessories & Cables"
|
||||
},
|
||||
"Infrastructure and Accessories Product Pages/Chassis Systems.xlsx": {
|
||||
"family": true,
|
||||
"note": "Sammelseite (K-Ebene) - Entscheidung Olli 2026-09-28"
|
||||
},
|
||||
"Infrastructure and Accessories Product Pages/Express Backplanes.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: Express Backplanes"
|
||||
},
|
||||
"Video Capture Product Pages/ActiveSQX.xlsx": {
|
||||
"product": "activesqx"
|
||||
},
|
||||
"Video Capture Product Pages/ActiveSQX2.xlsx": {
|
||||
"product": "activesqx2"
|
||||
},
|
||||
"Video Capture Product Pages/Vision.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: Vision (DVI & SDI capture cards)"
|
||||
},
|
||||
"Video Capture Product Pages/VisionAV-HD.xlsx": {
|
||||
"product": "visionav-hd"
|
||||
},
|
||||
"Video Capture Product Pages/VisionAV-SDI.xlsx": {
|
||||
"product": "visionav-sdi"
|
||||
},
|
||||
"Video Capture Product Pages/VisionAV.xlsx": {
|
||||
"product": "visionav"
|
||||
},
|
||||
"Video Capture Product Pages/VisionHD4.xlsx": {
|
||||
"product": "visionhd4"
|
||||
},
|
||||
"Video Capture Product Pages/VisionIO-SDI4.xlsx": {
|
||||
"product": "visionio-sd14",
|
||||
"note": "Dev-Name kanonisch (OCR-Abweichung SDI4/SD14), Zuordnung Olli 2026-09-28"
|
||||
},
|
||||
"Video Capture Product Pages/VisionIO-XD2.xlsx": {
|
||||
"product": "visionio-xd2"
|
||||
},
|
||||
"Video Capture Product Pages/VisionIO.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: VisionIO (real-time capture + overlay)"
|
||||
},
|
||||
"Video Capture Product Pages/VisionLC-HD.xlsx": {
|
||||
"product": "visionlc-hd"
|
||||
},
|
||||
"Video Capture Product Pages/VisionLC-HD2.xlsx": {
|
||||
"product": "visionlc-hd2"
|
||||
},
|
||||
"Video Capture Product Pages/VisionLC-SDI.xlsx": {
|
||||
"product": "visionlc-sdi"
|
||||
},
|
||||
"Video Capture Product Pages/VisionLC.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: VisionLC (low-profile capture cards)"
|
||||
},
|
||||
"Video Capture Product Pages/VisionRGB-E1S.xlsx": {
|
||||
"product": "visionrgb-e1s"
|
||||
},
|
||||
"Video Capture Product Pages/VisionRGB-E2S.xlsx": {
|
||||
"product": "visionrgb-e2s"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSC (scalable capture & processing)_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: VisionSC (scalable capture & processing)"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSC-A2.xlsx": {
|
||||
"product": "visionsc-a2"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSC-DP2.xlsx": {
|
||||
"product": "visionsc-dp2"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSC-HD4Plus.xlsx": {
|
||||
"product": "visionsc-hd4"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSC-S2.xlsx": {
|
||||
"product": "visionsc-s2"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSC-SDI4.xlsx": {
|
||||
"product": "visionsc-sd14",
|
||||
"note": "Dev-Name kanonisch (OCR-Abweichung SDI4/SD14), Zuordnung Olli 2026-09-28"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSC-UHD2.xlsx": {
|
||||
"product": "visionsc-uhd2"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSD12.xlsx": {
|
||||
"product": "visionsd12"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSD4Plus1S.xlsx": {
|
||||
"product": "visionsd4-1s"
|
||||
},
|
||||
"Video Capture Product Pages/VisionSD8.xlsx": {
|
||||
"product": "visionsd8"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/38-Series Encoders_Product Page Content.xlsx": {
|
||||
"product": "38-series-encoders-modular-blade-systems"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/Diamond_IP_OG.xlsx": {
|
||||
"product": "diamond-ip-og-ip-to-ip-encoder"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGES6000.xlsx": {
|
||||
"product": "mges6000-broadcast-quality-hdsd-h264-quad-input-iptv-encoder"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGES7000.xlsx": {
|
||||
"product": "mges7000-high-density-4kuhdhd-hevc-h264-iptv-encoding-blade"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW Ace Decoder.xlsx": {
|
||||
"product": "mgw-ace-decoder"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW Ace_Product Page Content.xlsx": {
|
||||
"product": "mgw-ace-encoder",
|
||||
"note": "manuell 2026-09-24: Encoder (Hero: ultra-low latency HEVC encoding); Praefix-Match war doppeldeutig (encoder/decoder)"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW Diamond OG.xlsx": {
|
||||
"product": "mgw-diamond-og-sdi-composite-blade-encoder",
|
||||
"note": "uid 19, Titel exakt - geklaert Olli 2026-09-28"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW Diamond-H_Product Page Content.xlsx": {
|
||||
"product": "mgw-diamond-h-hdmi-encoder"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW Diamond-Hx OG.xlsx": {
|
||||
"product": "mgw-diamond-hx-og"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW Diamond_Product Page Content.xlsx": {
|
||||
"product": "mgw-diamond-sdi-encoder"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW Pico_Product Page Content.xlsx": {
|
||||
"product": "mgw-pico-portable-encoder"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW_Ace_Decoder_OG.xlsx": {
|
||||
"product": "mgw-ace-decoder-og-ultra-low-latency-blade-encoder"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW_Blade_System.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: MGW Blade System"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW_Diamond_Hx_OG.xlsx": {
|
||||
"product": "mgw-diamond-hx-og"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/MGW_Diamond_Plus_OG.xlsx": {
|
||||
"product": "mgw-diamond-og-sdi-blade-encoder",
|
||||
"note": "uid 20 MGW Diamond+ OG, Slug ohne plus ist dev-kanonisch - geklaert Olli 2026-09-28"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/PCIe Encoders_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "Sammelseite (K-Ebene) - Entscheidung Olli 2026-09-28"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/VITEC OG_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: VITEC OG Modular System (zweites Workbook derselben Familie neben VITEC_OG_Modular_System.xlsx)"
|
||||
},
|
||||
"Video Encoding and Streaming Product Pages/VITEC_OG_Modular_System.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: VITEC OG Modular System"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/Fx4.xlsx": {
|
||||
"product": "fx4"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/Fx4_HDR.xlsx": {
|
||||
"product": "fx4-hdr"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/Fx4_SDI.xlsx": {
|
||||
"product": "fx4-sdi"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/Genetec.xlsx": {
|
||||
"product": "genetec"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/Hx4.xlsx": {
|
||||
"product": "hx4"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/Milestone.xlsx": {
|
||||
"product": "milestone"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/VMS Plugins & Integrations_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: VMS Plugins & Integrations"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/VSN V3_Product Page Content.xlsx": {
|
||||
"product": "vsn-v3"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/VSNMicro 600_Product Page Content.xlsx": {
|
||||
"product": "vsnmicro-600"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/VSNMini 300_Product Page Content.xlsx": {
|
||||
"product": "vsnmini-300"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/VSN_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: VSN"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/Video Wall Management Software_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: Video Wall Management Software"
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/WallControl_10.xlsx": {
|
||||
"product": null,
|
||||
"note": "GEPARKT 2026-09-28 (Revision der Zuordnung auf aetria-wallcontrol): Dry-Run zeigte, dass aetria-wallcontrol bereits vom Workbook Aetria WallControl (Control Room Platforms) befuellt ist - WallControl_10 haette es ueberschrieben. Eigenes Produkt oder verwerfen: Olli entscheidet."
|
||||
},
|
||||
"Video Wall and Display Processing Product Pages/X-Series (Multi-display Processors)_Product Page Content.xlsx": {
|
||||
"family": true,
|
||||
"note": "K-Ebene: X-Series (Multi-display Processors)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* One-time rebuild of tx_vitec_product_related_text (2026-09-28).
|
||||
*
|
||||
* The table stored the per-pair related-product card copy with a composite
|
||||
* primary key and no uid/pid - fine for the import module, invisible to
|
||||
* FormEngine. To surface the texts as an inline list on the product record
|
||||
* the table needs a proper uid PK and a pid, and MySQL cannot grow an
|
||||
* auto_increment PK onto an existing composite-key table through the TYPO3
|
||||
* schema compare - hence this manual rebuild.
|
||||
*
|
||||
* Copies every row, backfilling:
|
||||
* pid = the owning product's pid
|
||||
* sorting = the pair's tx_vitec_product_related_mm sorting (0 when the
|
||||
* relation has meanwhile been removed - such orphan texts are
|
||||
* kept; they become active again if the relation returns)
|
||||
* crdate = old tstamp
|
||||
*
|
||||
* Idempotent: exits when the uid column already exists.
|
||||
*
|
||||
* Usage: php migrations/rebuild_related_text_table.php
|
||||
* (BEFORE vendor/bin/typo3 extension:setup)
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("Could not locate project root above " . __DIR__ . "\n"); }
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$table = 'tx_vitec_product_related_text';
|
||||
|
||||
$cols = $pdo->query('SHOW COLUMNS FROM `' . $table . '`')->fetchAll();
|
||||
$names = array_column($cols, 'Field');
|
||||
if (in_array('uid', $names, true)) {
|
||||
exit("$table already has a uid column - nothing to do.\n");
|
||||
}
|
||||
|
||||
$count = (int)$pdo->query('SELECT COUNT(*) FROM `' . $table . '`')->fetchColumn();
|
||||
echo "Rebuilding $table ($count rows) ...\n";
|
||||
|
||||
$pdo->exec('DROP TABLE IF EXISTS `' . $table . '_new`');
|
||||
$pdo->exec('CREATE TABLE `' . $table . '_new` (
|
||||
uid int(11) NOT NULL auto_increment,
|
||||
pid int(11) DEFAULT 0 NOT NULL,
|
||||
product_uid int(11) DEFAULT 0 NOT NULL,
|
||||
related_uid int(11) DEFAULT 0 NOT NULL,
|
||||
sorting int(11) DEFAULT 0 NOT NULL,
|
||||
cardtext text,
|
||||
tstamp int(11) DEFAULT 0 NOT NULL,
|
||||
crdate int(11) DEFAULT 0 NOT NULL,
|
||||
PRIMARY KEY (uid),
|
||||
UNIQUE KEY pair (product_uid, related_uid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
|
||||
|
||||
$copied = $pdo->exec('INSERT INTO `' . $table . '_new`
|
||||
(pid, product_uid, related_uid, sorting, cardtext, tstamp, crdate)
|
||||
SELECT COALESCE(p.pid, 0), t.product_uid, t.related_uid,
|
||||
COALESCE(mm.sorting, 0), t.cardtext, t.tstamp, t.tstamp
|
||||
FROM `' . $table . '` t
|
||||
LEFT JOIN tx_vitec_domain_model_product p ON p.uid = t.product_uid
|
||||
LEFT JOIN tx_vitec_product_related_mm mm
|
||||
ON mm.uid_local = t.product_uid AND mm.uid_foreign = t.related_uid');
|
||||
|
||||
$pdo->exec('RENAME TABLE `' . $table . '` TO `' . $table . '_old`, `' . $table . '_new` TO `' . $table . '`');
|
||||
$pdo->exec('DROP TABLE `' . $table . '_old`');
|
||||
|
||||
echo "Done: $copied rows copied, old table dropped.\n";
|
||||
echo "Next: vendor/bin/typo3 extension:setup && vendor/bin/typo3 cache:flush\n";
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* One-shot: clear the SAVED field mapping of the product-text import
|
||||
* (tx_vitec_import_mapping, model "product_xlsx") so the corrected
|
||||
* DEFAULT_MAPPING seed applies again.
|
||||
*
|
||||
* Deliberately clears ONLY the `mapping` column - the same row also stores
|
||||
* `record_aliases` (the editor's manual related-product matches), and those
|
||||
* must survive.
|
||||
*
|
||||
* Usage: php migrations/reset_product_mapping.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$row = $pdo->query("SELECT uid, mapping, record_aliases FROM tx_vitec_import_mapping WHERE model = 'product_xlsx'")->fetch();
|
||||
if (!$row) {
|
||||
exit("No row for model 'product_xlsx' - nothing to clear, defaults apply already.\n");
|
||||
}
|
||||
|
||||
echo "Row uid " . $row['uid'] . "\n";
|
||||
echo " mapping (cleared) : " . ($row['mapping'] ?: '(empty)') . "\n";
|
||||
echo " aliases (KEPT) : " . ($row['record_aliases'] ?: '(empty)') . "\n";
|
||||
|
||||
$pdo->prepare("UPDATE tx_vitec_import_mapping SET mapping = '', tstamp = ? WHERE model = 'product_xlsx'")
|
||||
->execute([time()]);
|
||||
|
||||
echo "Done - the saved field mapping is cleared, the corrected defaults apply on the next preview.\n";
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* One-shot: clear the subproduct flag on ALL product records.
|
||||
*
|
||||
* The flag was set on every classic product on 2026-09-04 while K-level
|
||||
* entries still existed as records. Same-day model change: families are
|
||||
* content pages, the record table holds only classic products - and the
|
||||
* product list renderer has always EXCLUDED subproduct=1 (its original,
|
||||
* editorial meaning), which emptied every product list on /products.
|
||||
* Before 2026-09-04 no record carried the flag, so 0 across the board
|
||||
* restores the original state.
|
||||
*
|
||||
* Usage: php migrations/reset_subproduct_flags.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$count = (int)$pdo->query(
|
||||
"SELECT COUNT(*) FROM tx_vitec_domain_model_product WHERE deleted = 0 AND subproduct = 1"
|
||||
)->fetchColumn();
|
||||
echo "Records with subproduct=1: " . $count . "\n";
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE tx_vitec_domain_model_product SET subproduct = 0, tstamp = ? WHERE deleted = 0 AND subproduct = 1");
|
||||
$stmt->execute([time()]);
|
||||
|
||||
echo "Cleared " . $stmt->rowCount() . " flag(s). Flush the cache: vendor/bin/typo3 cache:flush\n";
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Retires the six old-taxonomy category stubs under /products (decision
|
||||
* Olli 2026-10-27; diagnosis check_old_stubs.php: all six are EMPTY -
|
||||
* no content elements, no child pages, so nothing needs moving):
|
||||
*
|
||||
* soft-delete the stub page and queue a 301 sys_redirect onto its new
|
||||
* category equivalent. Guards: a stub with children or visible content
|
||||
* is skipped and reported; an existing redirect for the path is kept.
|
||||
*
|
||||
* Usage: php migrations/retire_old_category_stubs.php (dry run)
|
||||
* php migrations/retire_old_category_stubs.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$map = [
|
||||
'/products/platforms-and-end-points' => '/products/iptv-and-digital-signage',
|
||||
'/products/ip-video-streaming' => '/products/video-encoding-and-streaming',
|
||||
'/products/control-room-solutions' => '/products/control-room-platforms',
|
||||
'/products/avoip-distribution-kvm' => '/products/av-over-ip-and-kvm',
|
||||
'/products/graphics-multi-display' => '/products/graphics-and-multi-display',
|
||||
'/products/infrastructure-accessories' => '/products/infrastructure-and-accessories',
|
||||
];
|
||||
|
||||
echo ($apply ? "APPLY\n" : "DRY-RUN\n");
|
||||
foreach ($map as $oldSlug => $newSlug) {
|
||||
$stub = $pdo->prepare('SELECT uid, title FROM pages WHERE deleted = 0 AND slug = ?');
|
||||
$stub->execute([$oldSlug]);
|
||||
$stub = $stub->fetch();
|
||||
if (!$stub) {
|
||||
echo " = $oldSlug: already gone\n";
|
||||
continue;
|
||||
}
|
||||
$uid = (int)$stub['uid'];
|
||||
$children = (int)$pdo->query("SELECT COUNT(*) FROM pages WHERE deleted = 0 AND pid = $uid")->fetchColumn();
|
||||
$content = (int)$pdo->query("SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND pid = $uid")->fetchColumn();
|
||||
if ($children > 0 || $content > 0) {
|
||||
echo " !! #$uid $oldSlug has $children child page(s) / $content element(s) - SKIPPED, decide manually\n";
|
||||
continue;
|
||||
}
|
||||
$target = $pdo->prepare('SELECT uid FROM pages WHERE deleted = 0 AND slug = ?');
|
||||
$target->execute([$newSlug]);
|
||||
$targetUid = (int)($target->fetchColumn() ?: 0);
|
||||
if ($targetUid === 0) {
|
||||
echo " !! target $newSlug not found - #$uid skipped\n";
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM sys_redirect WHERE deleted = 0 AND source_path = ?');
|
||||
$stmt->execute([$oldSlug]);
|
||||
$hasRedirect = (int)$stmt->fetchColumn();
|
||||
echo " ~ DELETE #$uid \"{$stub['title']}\" ($oldSlug) -> 301 to $newSlug (#$targetUid)" . ($hasRedirect ? ' [redirect exists, kept]' : '') . "\n";
|
||||
if (!$apply) {
|
||||
continue;
|
||||
}
|
||||
$pdo->prepare('UPDATE pages SET deleted = 1, tstamp = UNIX_TIMESTAMP() WHERE uid = ?')->execute([$uid]);
|
||||
if (!$hasRedirect) {
|
||||
$pdo->prepare(
|
||||
"INSERT INTO sys_redirect (pid, createdon, updatedon, source_host, source_path, target, target_statuscode)
|
||||
VALUES (0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '*', ?, ?, 301)"
|
||||
)->execute([$oldSlug, 't3://page?uid=' . $targetUid]);
|
||||
}
|
||||
}
|
||||
echo $apply ? "Done. Flush the cache (redirects are cached).\n" : "DRY RUN - add --apply to write.\n";
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Scan: all text-element headers inside usecase content, grouped. READ-ONLY.
|
||||
error_reporting(E_ALL); ini_set('display_errors', '1');
|
||||
$root = null; $dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
|
||||
if ($root === null) { exit("no root\n"); }
|
||||
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
||||
echo "=== Header von text-Elementen in Story-Content (gruppiert) ===\n";
|
||||
foreach ($pdo->query("SELECT TRIM(header) h, COUNT(*) c FROM tt_content WHERE deleted = 0 AND tx_vitec_usecase_content > 0 AND CType = 'text' AND TRIM(COALESCE(header,'')) <> '' GROUP BY TRIM(header) ORDER BY c DESC, h") as $r) {
|
||||
printf(" %3d x %s\n", $r['c'], $r['h']);
|
||||
}
|
||||
echo "\n=== CTypes in Story-Content gesamt ===\n";
|
||||
foreach ($pdo->query("SELECT CType, COUNT(*) c FROM tt_content WHERE deleted = 0 AND tx_vitec_usecase_content > 0 GROUP BY CType ORDER BY c DESC") as $r) {
|
||||
printf(" %3d x %s\n", $r['c'], $r['CType']);
|
||||
}
|
||||
|
||||
echo "\n=== Header von Kind-Elementen in Story-vitec_columns ===\n";
|
||||
foreach ($pdo->query(
|
||||
"SELECT TRIM(c.header) h, c.CType, COUNT(*) n FROM tt_content c
|
||||
WHERE c.deleted = 0 AND TRIM(COALESCE(c.header,'')) <> ''
|
||||
AND c.tx_container_parent IN (
|
||||
SELECT p.uid FROM tt_content p
|
||||
WHERE p.deleted = 0 AND p.tx_vitec_usecase_content > 0 AND p.CType = 'vitec_columns')
|
||||
GROUP BY TRIM(c.header), c.CType ORDER BY n DESC, h"
|
||||
) as $r) {
|
||||
printf(" %3d x %-14s %s\n", $r['n'], $r['CType'], $r['h']);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SEO fixes (audit 2026-11-09):
|
||||
*
|
||||
* 1. Clear SELF-referencing canonical_link values. The page imports wrote
|
||||
* "t3://page?uid=<self>" / "<self>" into 161 pages; TYPO3 emits a
|
||||
* self-canonical anyway, and the pages sitemap EXCLUDES every page with
|
||||
* a canonical_link (additionalWhere) - which is why markets, solutions,
|
||||
* support, contact, insights and even the home page were missing from
|
||||
* sitemap.xml.
|
||||
* 2. no_index = 1 for technical pages (search, megamenu, footer shells...)
|
||||
* and the imported old news pages under /en/news + /news (duplicates of
|
||||
* the EXT:news articles).
|
||||
* 3. 301 redirects for old-site URLs onto their new equivalents - the
|
||||
* footer link targets plus every old news page that matches an EXT:news
|
||||
* article by slug segment.
|
||||
* 4. Rewrites the old-site hrefs inside tt_content bodytext (footer html
|
||||
* columns) onto the new targets, so no page carries dead internal links.
|
||||
*
|
||||
* Dry run: php migrations/seo_fixes_20261109.php
|
||||
* Write: php migrations/seo_fixes_20261109.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 1. Self-canonicals */
|
||||
/* ------------------------------------------------------------------ */
|
||||
// Comparison in PHP: CONCAT() in SQL trips over mixed column collations.
|
||||
$selfCanonical = [];
|
||||
foreach ($pdo->query("SELECT uid, canonical_link FROM pages WHERE deleted = 0 AND canonical_link <> ''") as $row) {
|
||||
$uid = (int)$row['uid'];
|
||||
$link = trim((string)$row['canonical_link']);
|
||||
if ($link === 't3://page?uid=' . $uid || $link === (string)$uid) {
|
||||
$selfCanonical[] = $uid;
|
||||
}
|
||||
}
|
||||
echo "1. Self-Canonicals leeren: " . count($selfCanonical) . " Seiten\n";
|
||||
if ($apply && $selfCanonical !== []) {
|
||||
$pdo->exec(
|
||||
"UPDATE pages SET canonical_link = '', tstamp = UNIX_TIMESTAMP()
|
||||
WHERE uid IN (" . implode(',', $selfCanonical) . ")"
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 2. no_index for technical + old news pages */
|
||||
/* ------------------------------------------------------------------ */
|
||||
$technicalUids = [12, 16, 19, 106, 107, 448, 450, 516, 517, 519, 521, 523, 607];
|
||||
$in = implode(',', $technicalUids);
|
||||
$cnt = (int)$pdo->query("SELECT COUNT(*) FROM pages WHERE deleted = 0 AND no_index = 0 AND (uid IN ($in) OR pid = 106)")->fetchColumn();
|
||||
echo "2. no_index=1 setzen: $cnt Seiten (technische Seiten + alte News-Seiten unter #106)\n";
|
||||
if ($apply && $cnt > 0) {
|
||||
$pdo->exec("UPDATE pages SET no_index = 1, tstamp = UNIX_TIMESTAMP() WHERE deleted = 0 AND no_index = 0 AND (uid IN ($in) OR pid = 106)");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 3a. Old-site URL mapping (footer targets) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
$mapping = [
|
||||
'/about/careers' => '/insights/careers',
|
||||
'/about/company' => '/insights/who-we-are',
|
||||
'/about/events' => '/insights/events',
|
||||
'/about/locations' => '/contact/locations',
|
||||
'/about/news' => '/insights/news',
|
||||
'/about/sustainability' => '/insights/sustainability',
|
||||
'/about/who-we-are' => '/insights/who-we-are',
|
||||
'/about/why-vitec' => '/insights/who-we-are',
|
||||
'/downloads' => '/datasheets',
|
||||
'/markets/accommodation' => '/markets/hospitality',
|
||||
'/markets/broadcast' => '/markets/broadcast-and-telecoms',
|
||||
'/markets/corporate' => '/markets/corporation',
|
||||
'/markets/healthcare' => '/markets',
|
||||
'/markets/military' => '/markets/defense-and-security',
|
||||
'/markets/sports' => '/markets/sports-and-entertainment',
|
||||
'/markets/venues' => '/markets/sports-and-entertainment',
|
||||
'/products/encoders-decoders' => '/products/video-encoding-and-streaming',
|
||||
'/products/encoders-decoders/appliances' => '/products/appliances',
|
||||
'/products/encoders-decoders/modular-systems' => '/products/modular-systems',
|
||||
'/products/encoders-decoders/modular-systems/avedia-encoders' => '/products/avedia-modular-system',
|
||||
'/products/encoders-decoders/modular-systems/iptv-blade-system' => '/products/avedia-modular-system',
|
||||
'/products/encoders-decoders/modular-systems/vitec-og' => '/products/modular-systems',
|
||||
'/products/encoders-decoders/pcie-encoder' => '/products/activesqx',
|
||||
'/products/encoders-decoders/rugged' => '/products/rugged-appliances',
|
||||
'/products/ip/rf-gateways-transcoders' => '/products/rf-gateway',
|
||||
'/products/ip/rf-gateways-transcoders/ip-gateways' => '/products/channellink-ip-gateway',
|
||||
'/products/ip/rf-gateways-transcoders/prism-transcoder' => '/products/prism-transcoder',
|
||||
'/products/ip/rf-gateways-transcoders/rf-gateways' => '/products/rf-gateway',
|
||||
'/products/iptv-digital-signage-platform' => '/products/iptv-and-digital-signage',
|
||||
'/products/iptv-digital-signage-platform/all-in-one-platform' => '/products/avedia',
|
||||
'/products/iptv-digital-signage-platform/digital-signage' => '/products/iptv-and-digital-signage',
|
||||
'/products/iptv-digital-signage-platform/end-points' => '/products/end-points',
|
||||
'/products/iptv-digital-signage-platform/integrated-displays' => '/products/x-view-integrated-displays',
|
||||
'/products/iptv-digital-signage-platform/interactive-tv-portal' => '/products/ez-tv',
|
||||
'/products/iptv-digital-signage-platform/video-wall-controllers' => '/products/video-wall-controllers-and-management',
|
||||
'/products/iptv-digital-signage-platform/video-wall-controllers/aligo' => '/products/aligo',
|
||||
'/products/iptv-digital-signage-platform/video-wall-controllers/fx4-series' => '/products/video-wall-controllers-and-management',
|
||||
'/products/iptv-digital-signage-platform/video-wall-controllers/vsn-series' => '/products/vsn-series-video-wall-controllers',
|
||||
'/products/iptv-digital-signage-platform/x-player' => '/products/x-player-end-points',
|
||||
'/products/services' => '/support',
|
||||
'/products/services/deployment-services' => '/support/commissioning',
|
||||
'/products/services/digital-signage-and-design-services' => '/support',
|
||||
'/products/services/training' => '/support/training',
|
||||
'/products/telemetry' => '/products',
|
||||
'/solutions/contribution-remote-production' => '/solutions/video-capture-encoding-processing/contribution-remote-production',
|
||||
'/solutions/iptv-distribution' => '/solutions/video-distribution-streaming/iptv-distribution',
|
||||
'/solutions/situational-awareness-and-isr' => '/solutions/real-time-monitoring-situational-awareness',
|
||||
'/solutions/video-streaming' => '/solutions/video-distribution-streaming',
|
||||
'/support/material-compliance' => '/support/materials-compliance',
|
||||
'/support/request-helpdesk-access' => '/support/login-to-helpdesk',
|
||||
'/en/news' => '/insights/news',
|
||||
];
|
||||
|
||||
// Resolve every target slug to a page uid (sanity: fail loudly on typos).
|
||||
$targetUids = [];
|
||||
$stmt = $pdo->prepare("SELECT uid FROM pages WHERE deleted = 0 AND hidden = 0 AND slug = ? LIMIT 1");
|
||||
$missing = 0;
|
||||
foreach (array_unique(array_values($mapping)) as $target) {
|
||||
$stmt->execute([$target]);
|
||||
$uid = (int)$stmt->fetchColumn();
|
||||
if ($uid <= 0) {
|
||||
echo " !! Zielseite fehlt: $target\n";
|
||||
$missing++;
|
||||
continue;
|
||||
}
|
||||
$targetUids[$target] = $uid;
|
||||
}
|
||||
if ($missing > 0 && $apply) {
|
||||
exit("Abbruch: $missing Zielseiten nicht gefunden - Mapping pruefen.\n");
|
||||
}
|
||||
|
||||
echo "3a. Redirects Alt-URLs -> neue Seiten:\n";
|
||||
$hasRedirect = $pdo->prepare("SELECT COUNT(*) FROM sys_redirect WHERE deleted = 0 AND source_path = ?");
|
||||
$insertRedirect = $pdo->prepare(
|
||||
"INSERT INTO sys_redirect (pid, createdon, updatedon, source_host, source_path, target, target_statuscode)
|
||||
VALUES (0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '*', ?, ?, 301)"
|
||||
);
|
||||
$newRedirects = 0;
|
||||
foreach ($mapping as $old => $new) {
|
||||
if (!isset($targetUids[$new])) { continue; }
|
||||
$hasRedirect->execute([$old]);
|
||||
if ((int)$hasRedirect->fetchColumn() > 0) { continue; }
|
||||
$newRedirects++;
|
||||
if ($apply) {
|
||||
$insertRedirect->execute([$old, 't3://page?uid=' . $targetUids[$new]]);
|
||||
}
|
||||
}
|
||||
echo " $newRedirects neue Redirects (bestehende unangetastet)\n";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 3b. Old imported news pages -> EXT:news articles (slug match) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "3b. Alte News-Seiten -> /news/article/<slug>:\n";
|
||||
$oldNewsPages = $pdo->query(
|
||||
"SELECT uid, slug FROM pages WHERE deleted = 0 AND (pid = 106 OR uid = 107) AND slug <> ''"
|
||||
)->fetchAll();
|
||||
$newsBySegment = $pdo->prepare(
|
||||
"SELECT uid FROM tx_news_domain_model_news WHERE deleted = 0 AND hidden = 0 AND path_segment = ? LIMIT 1"
|
||||
);
|
||||
$matched = 0; $unmatched = [];
|
||||
foreach ($oldNewsPages as $page) {
|
||||
$segment = basename((string)$page['slug']);
|
||||
$newsBySegment->execute([$segment]);
|
||||
$newsUid = (int)$newsBySegment->fetchColumn();
|
||||
if ($newsUid <= 0) {
|
||||
$unmatched[] = $page['slug'];
|
||||
continue;
|
||||
}
|
||||
$hasRedirect->execute([$page['slug']]);
|
||||
if ((int)$hasRedirect->fetchColumn() > 0) { continue; }
|
||||
$matched++;
|
||||
if ($apply) {
|
||||
$insertRedirect->execute([(string)$page['slug'], '/news/article/' . $segment]);
|
||||
}
|
||||
}
|
||||
echo " $matched Redirects auf Artikel, " . count($unmatched) . " ohne Artikel-Match (bleiben als noindex-Seiten erreichbar)\n";
|
||||
foreach ($unmatched as $slug) {
|
||||
echo " kein Match: $slug\n";
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 4. Dead hrefs inside tt_content bodytext */
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "4. Alt-Links in Inhalten ersetzen:\n";
|
||||
$findContent = $pdo->prepare("SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND bodytext LIKE ?");
|
||||
$fixContent = $pdo->prepare("UPDATE tt_content SET bodytext = REPLACE(bodytext, ?, ?), tstamp = UNIX_TIMESTAMP() WHERE deleted = 0 AND bodytext LIKE ?");
|
||||
$replaced = 0;
|
||||
foreach ($mapping as $old => $new) {
|
||||
$needle = 'href="' . $old . '"';
|
||||
$findContent->execute(['%' . $needle . '%']);
|
||||
$count = (int)$findContent->fetchColumn();
|
||||
if ($count === 0) { continue; }
|
||||
echo " $needle -> href=\"$new\" ($count Element(e))\n";
|
||||
$replaced += $count;
|
||||
if ($apply) {
|
||||
$fixContent->execute([$needle, 'href="' . $new . '"', '%' . $needle . '%']);
|
||||
}
|
||||
}
|
||||
echo " Summe: $replaced Element-Treffer\n";
|
||||
|
||||
echo $apply ? "FERTIG. Cache flushen (Redirects + Seiten sind gecacht).\n" : "DRY RUN beendet.\n";
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"_quelle": "vitec.com Produktseiten, Scrape 2026-09-04. Nur Produkt-Detailseiten; Kategorieseiten-Metas (ChannelLink, 45-series, PRISM, 38-Series) bewusst ausgelassen (zu generisch). 'VITEC - '-Praefix entfernt.",
|
||||
"mgw-diamond-sdi-encoder": {
|
||||
"seotitle": "MGW Diamond - 4K and Multi-Channel SD/HD HEVC Encoder",
|
||||
"seometa": "MGW Diamond is a small, power-efficient quad channel HD or one channel 4K HEVC video encoder ideal for field-based applications. It features a powerful encoding engine with the ability to output up to eight streams simultaneously."
|
||||
},
|
||||
"mgw-ace-encoder": {
|
||||
"seotitle": "MGW Ace Encoder Compact HEVC (H.265) Hardware Encoder",
|
||||
"seometa": "MGW Ace Encoder is the world's first HEVC / H.265 hardware encoder in a professional grade portable streaming appliance. Powered by VITEC HEVC GEN2+ encoder, it delivers industry's best video quality with up to 50% bandwidth savings compared to H.264 and Ultra Low Latency streaming down to 16ms glass-to-glass."
|
||||
},
|
||||
"mgw-ace-decoder": {
|
||||
"seotitle": "MGW Ace Decoder - Professional Portable HEVC & H.264 Decoder",
|
||||
"seometa": "MGW Ace Decoder is a professional grade, high performance IP decoder supporting the bandwidth efficient HEVC/H.265 and H.264/AVC compression standards, with 4:2:2 10-bit decoding from IP or DVB-ASI, genlock support and 4K-ready 12G-SDI and HDMI outputs."
|
||||
},
|
||||
"mgw-pico-portable-encoder": {
|
||||
"seotitle": "MGW Pico Encoder - Ultra-Compact, Low Latency H.264 Encoding & Streaming Appliance",
|
||||
"seometa": "The MGW Pico Encoder is the world's smallest H.264 HD/SD portable encoding appliance. With 3G/HD/SD-SDI and Composite inputs, low power consumption and robust industrial design, the appliance is ideal for any video field-based streaming application."
|
||||
},
|
||||
"mgw-diamond-og-sdi-composite-blade-encoder": {
|
||||
"seotitle": "MGW Diamond OG - 4K and Multi-Channel SD/HD HEVC VITEC OG Encoder Card",
|
||||
"seometa": "MGW Diamond OG is a small, power-efficient quad channel HD or one channel 4K HEVC video encoder card for the openGear ecosystem, with a powerful encoding engine able to output up to eight streams simultaneously."
|
||||
},
|
||||
"mgw-diamond-og-sdi-blade-encoder": {
|
||||
"seotitle": "MGW Diamond+ OG Encoder - Multi-codec, Broadcast-grade 4K/Multichannel HD VITEC OG Encoder Card",
|
||||
"seometa": "MGW Diamond+ OG is a broadcast grade HEVC, H.264 and MPEG-2 IP encoder that is ideal for contribution or point-to-point streaming applications and compatible with the openGear ecosystem for seamless integration."
|
||||
},
|
||||
"mgw-diamond-hx-og": {
|
||||
"seotitle": "MGW Diamond-Hx OG Encoder 4K & Multi-Channel SD/HD HDMI VITEC OG Encoder Card",
|
||||
"seometa": "openGear 4K HEVC video encoder card that is ideal for IPTV distribution or Direct-to-Web applications, featuring a powerful encoding engine with the ability to deliver up to four streams simultaneously."
|
||||
},
|
||||
"mgw-ace-decoder-og-ultra-low-latency-blade-encoder": {
|
||||
"seotitle": "MGW Ace Decoder OG - Professional HEVC & H.264 openGear Decoder Card",
|
||||
"seometa": "MGW Ace Decoder OG is a professional grade, high performance IP decoder card supporting HEVC/H.265 and H.264/AVC, with 4:2:2 10-bit decoding from IP or DVB-ASI, genlock support and 4K-ready outputs for the openGear ecosystem."
|
||||
},
|
||||
"diamond-ip-og-ip-to-ip-encoder": {
|
||||
"seotitle": "Diamond-IP OG Encoder - 4K HEVC encoder with SMPTE ST2110 capture",
|
||||
"seometa": ""
|
||||
},
|
||||
"mges7000-high-density-4kuhdhd-hevc-h264-iptv-encoding-blade": {
|
||||
"seotitle": "MGES 7000 - Market Leading High Density 4K/UHD/HD HEVC & H.264 IPTV Encoding Blade",
|
||||
"seometa": "Featuring the highest density available on the market, VITEC's 4K/UHD/HD HEVC & H.264 IPTV 8-input blade offers real-time hardware encoding with secondary channel, integrated resolution and frame-rate scaling, AES 256/128-bit encryption and low latency mode."
|
||||
},
|
||||
"mges6000-broadcast-quality-hdsd-h264-quad-input-iptv-encoder": {
|
||||
"seotitle": "MGES 6000 - Broadcast-quality HD/SD H.264 Quad-Input IPTV Encoder",
|
||||
"seometa": "The MGES-6000 Blade provides real-time hardware encoding of HD and SD video for all IPTV applications, with best-in-class picture quality, a secondary low-res channel, streaming to up to seven IP destinations per port and optional AES-256/128-bit encryption."
|
||||
},
|
||||
"x-player-end-points-xp23-xp25-xp26": {
|
||||
"seotitle": "VITEC X-Player End-Points",
|
||||
"seometa": "High-performance IP video & digital signage playback devices"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Fixes from the Seobility crawl of 2026-11-10 (1,094 pages).
|
||||
*
|
||||
* 1. Placeholder SEO title "Title for search engines [seo_title]" on /solutions.
|
||||
* 2. News records whose title/path_segment were saved twice in a row.
|
||||
* 3. News path_segments that contain slashes (imported from old URL paths).
|
||||
* 4. Typos reported by the crawler (content tables).
|
||||
* 5. Second wave of dead old-site links: /resource-center, /services,
|
||||
* /contact-sales, /aetria-use-cases, /video-wall-solutions, absolute
|
||||
* datapath.co.uk / datapath-us.com links and tracking-parameter variants.
|
||||
* 6. "http://www.vitec.com./" (trailing dot) links.
|
||||
* 7. Redirects: record shells (/product, /news/article, /success-stories/
|
||||
* story) onto their overviews, plus a few orphaned product URLs.
|
||||
* 8. Meta description for /datasheets.
|
||||
* 9. WordPress remnants inside the 17 SEO articles (lazy-load placeholder
|
||||
* images, leftover script fragment, wp-/data- attributes, bare text).
|
||||
* 10. News records of type "internal page" pointing at the old /en/news
|
||||
* pages: their body lives only on those pages (which now redirect onto
|
||||
* the empty article URLs). The page content is copied into the record,
|
||||
* images become fal_media, the record becomes a regular article, and
|
||||
* every old page gets its 301.
|
||||
*
|
||||
* Dry run: php migrations/seo_report_fixes_20261110.php
|
||||
* Write: php migrations/seo_report_fixes_20261110.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
|
||||
$now = time();
|
||||
|
||||
$hasRedirect = $pdo->prepare("SELECT COUNT(*) FROM sys_redirect WHERE deleted = 0 AND source_path = ?");
|
||||
$insertRedirect = $pdo->prepare(
|
||||
"INSERT INTO sys_redirect (pid, createdon, updatedon, source_host, source_path, target, target_statuscode)
|
||||
VALUES (0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '*', ?, ?, 301)"
|
||||
);
|
||||
$redirectsAdded = 0;
|
||||
$addRedirect = static function (string $source, string $target) use ($hasRedirect, $insertRedirect, $apply, &$redirectsAdded): void {
|
||||
$hasRedirect->execute([$source]);
|
||||
if ((int)$hasRedirect->fetchColumn() > 0) {
|
||||
return;
|
||||
}
|
||||
$redirectsAdded++;
|
||||
if ($apply) {
|
||||
$insertRedirect->execute([$source, $target]);
|
||||
}
|
||||
};
|
||||
$pageBySlug = $pdo->prepare("SELECT uid FROM pages WHERE deleted = 0 AND hidden = 0 AND slug = ? LIMIT 1");
|
||||
$productBySlug = $pdo->prepare("SELECT uid FROM tx_vitec_domain_model_product WHERE deleted = 0 AND hidden = 0 AND slug = ? LIMIT 1");
|
||||
$newsBySegment = $pdo->prepare("SELECT uid FROM tx_news_domain_model_news WHERE deleted = 0 AND path_segment = ? LIMIT 1");
|
||||
$targetExists = static function (string $target) use ($pageBySlug, $productBySlug, $newsBySegment): bool {
|
||||
if (str_starts_with($target, '/product/')) {
|
||||
$productBySlug->execute([substr($target, 9)]);
|
||||
return (int)$productBySlug->fetchColumn() > 0;
|
||||
}
|
||||
if (str_starts_with($target, '/news/article/')) {
|
||||
$newsBySegment->execute([substr($target, 14)]);
|
||||
return (int)$newsBySegment->fetchColumn() > 0;
|
||||
}
|
||||
$pageBySlug->execute([$target]);
|
||||
return (int)$pageBySlug->fetchColumn() > 0;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n1. Platzhalter-SEO-Titel/-Beschreibungen\n";
|
||||
$rows = $pdo->query("SELECT uid, slug, seo_title FROM pages WHERE deleted = 0 AND (seo_title LIKE 'Title for search engines%' OR description LIKE 'Description for search engines%')")->fetchAll();
|
||||
foreach ($rows as $r) {
|
||||
echo " #{$r['uid']} {$r['slug']}: seo_title/description geleert\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE pages SET seo_title = IF(seo_title LIKE 'Title for search engines%', '', seo_title), description = IF(description LIKE 'Description for search engines%', '', description), tstamp = ? WHERE uid = ?")->execute([$now, $r['uid']]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n2. News mit verdoppeltem Titel / path_segment\n";
|
||||
$halve = static function (string $s): ?string {
|
||||
$len = mb_strlen($s);
|
||||
if ($len < 20 || $len % 2 !== 0) { return null; }
|
||||
$h = mb_substr($s, 0, intdiv($len, 2));
|
||||
return $h === mb_substr($s, intdiv($len, 2)) ? $h : null;
|
||||
};
|
||||
foreach ($pdo->query("SELECT uid, title, path_segment FROM tx_news_domain_model_news WHERE deleted = 0 AND CHAR_LENGTH(title) > 40") as $r) {
|
||||
$newTitle = $halve((string)$r['title']);
|
||||
if ($newTitle === null) { continue; }
|
||||
$seg = (string)$r['path_segment'];
|
||||
$newSeg = $seg;
|
||||
// doubled slug: "a-b-ca-b-c" -> "a-b-c"
|
||||
$segLen = strlen($seg);
|
||||
if ($segLen % 2 === 0 && substr($seg, 0, intdiv($segLen, 2)) === substr($seg, intdiv($segLen, 2))) {
|
||||
$newSeg = substr($seg, 0, intdiv($segLen, 2));
|
||||
}
|
||||
echo " #{$r['uid']}: Titel -> [$newTitle]" . ($newSeg !== $seg ? ", Slug -> $newSeg (+Redirect)" : '') . "\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE tx_news_domain_model_news SET title = ?, path_segment = ?, tstamp = ? WHERE uid = ?")->execute([$newTitle, $newSeg, $now, $r['uid']]);
|
||||
}
|
||||
if ($newSeg !== $seg) {
|
||||
$addRedirect('/news/article/' . $seg, '/news/article/' . $newSeg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n3. News path_segment mit Slash\n";
|
||||
foreach ($pdo->query("SELECT uid, path_segment FROM tx_news_domain_model_news WHERE deleted = 0 AND path_segment LIKE '%/%'") as $r) {
|
||||
$old = (string)$r['path_segment'];
|
||||
$new = basename($old);
|
||||
$newsBySegment->execute([$new]);
|
||||
$taken = (int)$newsBySegment->fetchColumn();
|
||||
if ($taken > 0 && $taken !== (int)$r['uid']) {
|
||||
$new .= '-' . $r['uid'];
|
||||
}
|
||||
echo " #{$r['uid']}: $old -> $new (+Redirect)\n";
|
||||
if ($apply) {
|
||||
$pdo->prepare("UPDATE tx_news_domain_model_news SET path_segment = ?, tstamp = ? WHERE uid = ?")->execute([$new, $now, $r['uid']]);
|
||||
}
|
||||
$addRedirect('/news/article/' . $old, '/news/article/' . $new);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n4. Tippfehler\n";
|
||||
$typos = [
|
||||
'continously' => 'continuously', 'continous' => 'continuous', 'indpendent' => 'independent',
|
||||
'aircrafts' => 'aircraft', 'sponser' => 'sponsor', 'auxillary' => 'auxiliary',
|
||||
'processer' => 'processor', 'accomodation' => 'accommodation',
|
||||
'Accomodation' => 'Accommodation', 'Continous' => 'Continuous',
|
||||
];
|
||||
$typoTargets = [
|
||||
'tt_content' => ['bodytext', 'header'],
|
||||
'tx_news_domain_model_news' => ['bodytext', 'teaser', 'title'],
|
||||
'tx_vitec_domain_model_usecase' => ['description', 'teaser'],
|
||||
];
|
||||
foreach ($typos as $wrong => $right) {
|
||||
foreach ($typoTargets as $table => $fields) {
|
||||
foreach ($fields as $field) {
|
||||
$n = (int)$pdo->query("SELECT COUNT(*) FROM $table WHERE deleted = 0 AND $field LIKE " . $pdo->quote('%' . $wrong . '%'))->fetchColumn();
|
||||
if ($n === 0) { continue; }
|
||||
echo " $wrong -> $right: $table.$field ($n)\n";
|
||||
if ($apply) {
|
||||
$pdo->exec("UPDATE $table SET $field = REPLACE($field, " . $pdo->quote($wrong) . ", " . $pdo->quote($right) . "), tstamp = $now WHERE deleted = 0 AND $field LIKE " . $pdo->quote('%' . $wrong . '%'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n5. Tote Alt-Links (zweite Welle, inkl. datapath.co.uk / utm-Varianten)\n";
|
||||
$awardSeg = 'aetria-award-wins-set-the-tone-for-datapaths-best-ever-ise-week';
|
||||
$newsBySegment->execute([$awardSeg]);
|
||||
$awardTarget = (int)$newsBySegment->fetchColumn() > 0 ? '/news/article/' . $awardSeg : '/insights/news';
|
||||
$mapping2 = [
|
||||
'/resource-center/legacy-products' => '/products/capture-and-graphics',
|
||||
'/resource-center/legacy-products/imagedp4' => '/products/image-graphics-cards',
|
||||
'/resource-center/legacy-products/datapath-x4' => '/products/video-wall-controllers-and-management',
|
||||
'/resource-center/legacy-products/datapath-dl8' => '/products/video-wall-controllers-and-management',
|
||||
'/legacy-products/imagedp4-plus' => '/products/image-graphics-cards',
|
||||
'/resource-center/get-support' => '/support',
|
||||
'/services/premium-service-package' => '/support/support-plans',
|
||||
'/services/training' => '/support/training',
|
||||
'/contact-sales' => '/contact/contact-form',
|
||||
'/aetria-use-cases/oil-and-gas-control-room' => '/markets/critical-national-infrastructure/energy-and-utilities',
|
||||
'/aetria-use-cases/energy-trading-and-dispatch' => '/markets/critical-national-infrastructure/energy-and-utilities',
|
||||
'/aetria-use-cases/cctv-for-university-campus' => '/markets/education',
|
||||
'/video-wall-solutions/commercial-video-walls' => '/solutions/video-wall-data-visualisation',
|
||||
'/video-wall-solutions/control-rooms' => '/solutions/control-room-command-centre-solutions',
|
||||
'/video-wall-solutions/projection-mapping' => '/solutions/video-wall-data-visualisation',
|
||||
'/video-wall-solutions/video-wall-for-security-suites' => '/solutions/security-surveillance-solutions',
|
||||
'/video-wall-solutions' => '/solutions/video-wall-data-visualisation',
|
||||
'/insights/success-stories' => '/success-stories',
|
||||
'/datapath-products' => '/products',
|
||||
'/datapath-products/aetria-control-room-solutions' => '/products/aetria',
|
||||
'/datapath-products/aetria-control-room-solutions/aetria-overview' => '/products/aetria',
|
||||
'/datapath-products/aetria-control-room-solutions/aetria-command-center' => '/product/aetria-command-center',
|
||||
'/datapath-products/aetria-control-room-solutions/aetria-workstation' => '/product/aetria-workstation',
|
||||
'/datapath-products/aetria-control-room-solutions/aetria-wallcontrol' => '/product/aetria-wallcontrol',
|
||||
'/datapath-products/video-wall-controllers/vsn-v3' => '/product/vsn-v3',
|
||||
'/datapath-products/video-wall-controllers/vsn' => '/products/vsn-series-video-wall-controllers',
|
||||
'/datapath-products/video-wall-controllers/vsn-series' => '/products/vsn-series-video-wall-controllers',
|
||||
'/datapath-products/video-wall-controllers/vsnmicro-600' => '/product/vsnmicro-600',
|
||||
'/datapath-products/video-wall-controllers/datapath-fx4' => '/product/fx4',
|
||||
'/datapath-products/video-distribution-kvm/aligo-4k-avoip-with-kvm' => '/products/aligo',
|
||||
'/datapath-products/video-distribution-kvm' => '/products/av-over-ip-and-kvm',
|
||||
'/datapath-products/video-capture-cards/visionsc-range/visionsc-uhd2' => '/product/visionsc-uhd2',
|
||||
'/datapath-products/video-capture-cards/visionsc-range/visionsc-hd4' => '/product/visionsc-hd4',
|
||||
'/datapath-products/video-capture-cards/visionsc-range/visionsc-s2' => '/product/visionsc-s2',
|
||||
'/datapath-products/video-wall-management-software/wallcontrol-10-software' => '/product/wall-control-10',
|
||||
'/datapath-products/graphics-products/image2k' => '/product/image-2k',
|
||||
'/datapath-products/components-accessories/rack-mounting-kits' => '/products',
|
||||
'/datapath-products/sqx-ip-capture/active-sqx' => '/product/activesqx',
|
||||
'/datapath-products/video-wall-control' => '/products/video-wall-controllers-and-management',
|
||||
'/' . $awardSeg => $awardTarget,
|
||||
];
|
||||
$missing = 0;
|
||||
foreach (array_unique(array_values($mapping2)) as $t) {
|
||||
if (!$targetExists($t)) { echo " !! Ziel fehlt: $t\n"; $missing++; }
|
||||
}
|
||||
if ($missing > 0 && $apply) { exit("Abbruch: Ziele fehlen.\n"); }
|
||||
|
||||
$hostPrefix = '(?:https?://(?:www\.)?datapath(?:-us)?\.(?:co\.uk|com))?';
|
||||
$linkTables = ['tx_news_domain_model_news' => 'bodytext', 'tt_content' => 'bodytext'];
|
||||
$linkHits = 0;
|
||||
foreach ($linkTables as $table => $field) {
|
||||
$select = $pdo->query("SELECT uid, $field AS body FROM $table WHERE deleted = 0 AND ($field LIKE '%datapath%' OR $field LIKE '%/resource-center/%' OR $field LIKE '%/services/%' OR $field LIKE '%/contact-sales%' OR $field LIKE '%/aetria-use-cases/%' OR $field LIKE '%/video-wall-solutions%' OR $field LIKE '%/insights/success-stories%' OR $field LIKE '%/legacy-products/%' OR $field LIKE '%aetria-award-wins%')");
|
||||
$update = $pdo->prepare("UPDATE $table SET $field = ?, tstamp = ? WHERE uid = ?");
|
||||
foreach ($select as $r) {
|
||||
$body = (string)$r['body'];
|
||||
$new = $body;
|
||||
foreach ($mapping2 as $old => $target) {
|
||||
$pattern = '#href="' . $hostPrefix . preg_quote($old, '#') . '/?(?:\?[^"]*)?"#';
|
||||
$new = preg_replace($pattern, 'href="' . $target . '"', $new) ?? $new;
|
||||
}
|
||||
if ($new !== $body) {
|
||||
$linkHits++;
|
||||
echo " $table #{$r['uid']}: Links ersetzt\n";
|
||||
if ($apply) { $update->execute([$new, $now, $r['uid']]); }
|
||||
}
|
||||
}
|
||||
}
|
||||
echo " Records mit ersetzten Links: $linkHits\n";
|
||||
foreach ($mapping2 as $old => $target) {
|
||||
$addRedirect($old, $target);
|
||||
$addRedirect($old . '/', $target);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n6. http://www.vitec.com./ (Punkt-Tippfehler)\n";
|
||||
$n = (int)$pdo->query("SELECT COUNT(*) FROM tx_news_domain_model_news WHERE deleted = 0 AND bodytext LIKE '%www.vitec.com./%'")->fetchColumn();
|
||||
echo " $n Record(s)\n";
|
||||
if ($apply && $n > 0) {
|
||||
$pdo->exec("UPDATE tx_news_domain_model_news SET bodytext = REPLACE(REPLACE(bodytext, 'http://www.vitec.com./', 'https://www.vitec.com/'), 'https://www.vitec.com./', 'https://www.vitec.com/'), tstamp = $now WHERE deleted = 0 AND bodytext LIKE '%www.vitec.com./%'");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n7. Redirects fuer Record-Shells und verwaiste Produkt-URLs\n";
|
||||
foreach ([
|
||||
'/product' => '/products',
|
||||
'/news/article' => '/insights/news',
|
||||
'/success-stories/story' => '/success-stories',
|
||||
'/product/artiosign' => '/products/video-wall-management-software',
|
||||
'/product/artioview' => '/products/video-wall-management-software',
|
||||
'/product/x-view-integrated-displays' => '/products/x-view-integrated-displays',
|
||||
] as $old => $target) {
|
||||
$addRedirect($old, $target);
|
||||
$addRedirect($old . '/', $target);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n8. Meta-Description /datasheets\n";
|
||||
$st = $pdo->prepare("UPDATE pages SET description = ?, tstamp = ? WHERE uid = 17 AND (description IS NULL OR description = '')");
|
||||
$desc = 'Download VITEC and Datapath product datasheets: encoders and decoders, IPTV and digital signage platforms, video wall controllers, capture cards and more - searchable by product and category.';
|
||||
if ($apply) { $st->execute([$desc, $now]); echo " gesetzt (" . $st->rowCount() . ")\n"; } else { echo " wird gesetzt, falls leer\n"; }
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n9. WordPress-Reste in den SEO-Artikeln\n";
|
||||
$seoUids = [232,234,235,236,237,238,240,241,242,243,244,245,246,247,248,249,252];
|
||||
$sel = $pdo->query("SELECT uid, path_segment, bodytext FROM tx_news_domain_model_news WHERE deleted = 0 AND uid IN (" . implode(',', $seoUids) . ")");
|
||||
$upd = $pdo->prepare("UPDATE tx_news_domain_model_news SET bodytext = ?, tstamp = ? WHERE uid = ?");
|
||||
foreach ($sel as $r) {
|
||||
$b = (string)$r['bodytext'];
|
||||
$o = $b;
|
||||
$log = [];
|
||||
$b = preg_replace('#<img[^>]*src="data:image/[^"]*"[^>]*>\s*#i', '', $b, -1, $c1) ?? $b; if ($c1) { $log[] = "$c1 Platzhalter-Bilder"; }
|
||||
$b = preg_replace('#<p>[^<]*getWR360PopupSkin[^<]*</p>\s*#i', '', $b, -1, $c2) ?? $b; if ($c2) { $log[] = "Script-Rest"; }
|
||||
$b = preg_replace('#\s+class="wp-[^"]*"#i', '', $b, -1, $c3) ?? $b; if ($c3) { $log[] = "$c3 wp-Klassen"; }
|
||||
$b = preg_replace('#\s+data-[a-z0-9-]+="[^"]*"#i', '', $b, -1, $c4) ?? $b; if ($c4) { $log[] = "$c4 data-Attribute"; }
|
||||
// bare description text between a product <h3> and the following link
|
||||
$b = preg_replace('#(</h3>)\s*([^<\s][^<]{20,}?)\s*(?=<a |<h[2-4]|<p>|<ul>)#s', '$1<p>$2</p>', $b, -1, $c5) ?? $b; if ($c5) { $log[] = "$c5 nackte Textabsaetze gekapselt"; }
|
||||
$b = preg_replace('#<a([^>]*)>\s*View products?\s*</a>#i', '<a$1>View products</a>', $b, -1, $c6) ?? $b; if ($c6) { $log[] = "$c6 View-products-Links normalisiert"; }
|
||||
if ($b !== $o) {
|
||||
echo " #{$r['uid']} {$r['path_segment']}: " . implode(', ', $log) . "\n";
|
||||
if ($apply) { $upd->execute([$b, $now, $r['uid']]); }
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
echo "\n10. News-Zeiger-Records (type=1) -> Inhalt der alten Seite uebernehmen\n";
|
||||
$allowedTags = '<p><h2><h3><h4><ul><ol><li><strong><em><b><i><a><br><blockquote>';
|
||||
$stripAttrs = static function (string $html): string {
|
||||
// keep href/target on links, drop everything else (style, class, ids)
|
||||
$html = preg_replace_callback('#<a\b([^>]*)>#i', static function (array $m): string {
|
||||
preg_match('/href="([^"]*)"/i', $m[1], $h);
|
||||
preg_match('/target="([^"]*)"/i', $m[1], $t);
|
||||
return '<a' . (isset($h[1]) ? ' href="' . $h[1] . '"' : '') . (isset($t[1]) ? ' target="' . $t[1] . '"' : '') . '>';
|
||||
}, $html) ?? $html;
|
||||
return preg_replace('#<(p|h[2-4]|ul|ol|li|strong|em|b|i|blockquote|br)\b[^>]*>#i', '<$1>', $html) ?? $html;
|
||||
};
|
||||
$collectContent = null;
|
||||
$collectContent = static function (int $pid, int $parent, array &$texts, array &$imageRefs) use ($pdo, &$collectContent, $allowedTags, $stripAttrs): void {
|
||||
$st = $pdo->prepare("SELECT uid, CType, bodytext, header FROM tt_content WHERE pid = ? AND tx_container_parent = ? AND deleted = 0 AND hidden = 0 ORDER BY colPos, sorting");
|
||||
$st->execute([$pid, $parent]);
|
||||
foreach ($st->fetchAll() as $el) {
|
||||
$ctype = (string)$el['CType'];
|
||||
if (str_starts_with($ctype, 'vitec_container') || str_starts_with($ctype, 'vitec_cols')) {
|
||||
$collectContent($pid, (int)$el['uid'], $texts, $imageRefs);
|
||||
continue;
|
||||
}
|
||||
if (in_array($ctype, ['image', 'textpic', 'textmedia'], true)) {
|
||||
$refs = $pdo->prepare("SELECT uid FROM sys_file_reference WHERE tablenames = 'tt_content' AND uid_foreign = ? AND fieldname IN ('image', 'assets', 'media') AND deleted = 0 AND hidden = 0 ORDER BY sorting_foreign");
|
||||
$refs->execute([(int)$el['uid']]);
|
||||
foreach ($refs->fetchAll() as $ref) { $imageRefs[] = (int)$ref['uid']; }
|
||||
}
|
||||
if (in_array($ctype, ['text', 'textpic', 'textmedia'], true)) {
|
||||
$t = trim((string)$el['bodytext']);
|
||||
if ($t !== '') { $texts[] = $stripAttrs(strip_tags($t, $allowedTags)); }
|
||||
continue;
|
||||
}
|
||||
if ($ctype === 'html') {
|
||||
$raw = (string)$el['bodytext'];
|
||||
if (preg_match('#<blockquote[^>]*>(.*?)</blockquote>#is', $raw, $q)) {
|
||||
$cite = '';
|
||||
if (preg_match('#</blockquote>\s*<span[^>]*>(.*?)</span>#is', $raw, $s)) { $cite = trim(strip_tags($s[1])); }
|
||||
$texts[] = '<blockquote><p>' . trim(strip_tags($q[1])) . '</p>' . ($cite !== '' ? '<p><em>' . $cite . '</em></p>' : '') . '</blockquote>';
|
||||
continue;
|
||||
}
|
||||
if (stripos($raw, '<style') !== false || stripos($raw, '<script') !== false || mb_strlen(trim(strip_tags($raw))) < 200) {
|
||||
continue; // layout snippets (buttons, styles)
|
||||
}
|
||||
$texts[] = $stripAttrs(strip_tags($raw, $allowedTags));
|
||||
}
|
||||
}
|
||||
};
|
||||
$pointerRows = $pdo->query("SELECT n.uid, n.pid, n.title, n.path_segment, n.internalurl, n.bodytext, n.fal_media FROM tx_news_domain_model_news n WHERE n.deleted = 0 AND n.type = '1'")->fetchAll();
|
||||
$pageSlugs = $pdo->prepare("SELECT uid, slug, pid FROM pages WHERE uid = ? AND deleted = 0");
|
||||
$maxRefSorting = $pdo->prepare("SELECT COALESCE(MAX(sorting_foreign), 0) FROM sys_file_reference WHERE tablenames = 'tx_news_domain_model_news' AND fieldname = 'fal_media' AND uid_foreign = ?");
|
||||
$copyRef = $pdo->prepare(
|
||||
"INSERT INTO sys_file_reference (pid, tstamp, crdate, uid_local, uid_foreign, tablenames, fieldname, sorting_foreign, title, alternative, description, crop, link)
|
||||
SELECT ?, ?, ?, uid_local, ?, 'tx_news_domain_model_news', 'fal_media', ?, title, alternative, description, crop, link FROM sys_file_reference WHERE uid = ?"
|
||||
);
|
||||
$updNews = $pdo->prepare("UPDATE tx_news_domain_model_news SET bodytext = ?, type = '0', internalurl = '', fal_media = ?, tstamp = ? WHERE uid = ?");
|
||||
$updType = $pdo->prepare("UPDATE tx_news_domain_model_news SET type = '0', internalurl = '', tstamp = ? WHERE uid = ?");
|
||||
$migrated = 0; $typeOnly = 0; $skipped = 0;
|
||||
foreach ($pointerRows as $n) {
|
||||
$pid = 0;
|
||||
if (preg_match('/uid=(\d+)/', (string)$n['internalurl'], $m)) { $pid = (int)$m[1]; } elseif (ctype_digit((string)$n['internalurl'])) { $pid = (int)$n['internalurl']; }
|
||||
$pageSlugs->execute([$pid]);
|
||||
$page = $pageSlugs->fetch();
|
||||
if (!$page || !in_array((int)$page['pid'], [106], true) && (int)$page['uid'] !== 107) {
|
||||
echo " #{$n['uid']}: Ziel-Seite $pid nicht im alten News-Ast - uebersprungen\n";
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
$oldSlug = (string)$page['slug'];
|
||||
$newUrl = '/news/article/' . $n['path_segment'];
|
||||
if (trim((string)$n['bodytext']) !== '') {
|
||||
echo " #{$n['uid']}: hat eigenen Text (" . strlen((string)$n['bodytext']) . " Z.) -> nur type=0" . "\n";
|
||||
$typeOnly++;
|
||||
if ($apply) { $updType->execute([$now, $n['uid']]); }
|
||||
$addRedirect($oldSlug, $newUrl);
|
||||
continue;
|
||||
}
|
||||
$texts = []; $imageRefs = [];
|
||||
$collectContent($pid, 0, $texts, $imageRefs);
|
||||
$body = trim(implode("\n", $texts));
|
||||
$plainLen = mb_strlen(trim(strip_tags($body)));
|
||||
if ($plainLen < 500) {
|
||||
echo " #{$n['uid']}: Seite $pid liefert nur $plainLen Zeichen Text - uebersprungen (bleibt Zeiger)\n";
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
$imageRefs = array_slice(array_unique($imageRefs), 0, 3);
|
||||
// Imported placeholder slugs ("default-<hash>") take the old page's slug.
|
||||
$segment = (string)$n['path_segment'];
|
||||
if (str_starts_with($segment, 'default-')) {
|
||||
$candidate = basename($oldSlug);
|
||||
$newsBySegment->execute([$candidate]);
|
||||
if ((int)$newsBySegment->fetchColumn() === 0) {
|
||||
$segment = $candidate;
|
||||
$newUrl = '/news/article/' . $segment;
|
||||
echo " #{$n['uid']}: Slug default-... -> $segment\n";
|
||||
}
|
||||
}
|
||||
echo " #{$n['uid']} $segment: $plainLen Zeichen aus " . count($texts) . " Elementen, " . count($imageRefs) . " Bild(er) <- $oldSlug\n";
|
||||
$migrated++;
|
||||
if ($apply) {
|
||||
if ($segment !== (string)$n['path_segment']) {
|
||||
$pdo->prepare("UPDATE tx_news_domain_model_news SET path_segment = ? WHERE uid = ?")->execute([$segment, $n['uid']]);
|
||||
}
|
||||
$sorting = 0;
|
||||
$maxRefSorting->execute([$n['uid']]);
|
||||
$sorting = (int)$maxRefSorting->fetchColumn();
|
||||
$existing = (int)$n['fal_media'];
|
||||
foreach ($imageRefs as $refUid) {
|
||||
$sorting += 256;
|
||||
$copyRef->execute([(int)$n['pid'], $now, $now, (int)$n['uid'], $sorting, $refUid]);
|
||||
$existing++;
|
||||
}
|
||||
$updNews->execute([$body, $existing, $now, $n['uid']]);
|
||||
}
|
||||
$addRedirect($oldSlug, $newUrl);
|
||||
}
|
||||
echo " uebernommen: $migrated, nur type=0: $typeOnly, uebersprungen: $skipped\n";
|
||||
|
||||
echo "\nRedirects neu: $redirectsAdded\n";
|
||||
echo $apply ? "FERTIG. cache:flush + SSR purge + warm.\n" : "DRY RUN beendet.\n";
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Meta description for the /solutions overview (page 2) - was the TCA
|
||||
// placeholder text until seo_report_fixes_20261110 cleared it. Fill-up only.
|
||||
$root=null;$dir=__DIR__;for($i=0;$i<5;$i++){if(is_file($dir.'/config/system/settings.php')){$root=$dir;break;}$dir=dirname($dir);}
|
||||
$s=require $root.'/config/system/settings.php';$db=$s['DB']['Connections']['Default'];
|
||||
$pdo=new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',$db['host']??'localhost',$db['port']??3306,$db['dbname']??''),$db['user']??'',$db['password']??'',[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
|
||||
$desc='Explore VITEC video solutions for control rooms and command centres, real-time monitoring, video distribution and streaming, capture and encoding, video walls, digital signage, enterprise communications and security.';
|
||||
$st=$pdo->prepare("UPDATE pages SET description=?, tstamp=UNIX_TIMESTAMP() WHERE uid=2 AND (description IS NULL OR description='')");
|
||||
$st->execute([$desc]);
|
||||
echo "Seite 2 (/solutions): ".$st->rowCount()." Zeile(n) aktualisiert\n";
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Setzt die fehlende Meta-Description der Success-Stories-Uebersicht (Seite 4).
|
||||
// Fill-up only: eine vorhandene Description wird nicht ueberschrieben.
|
||||
$root=null;$dir=__DIR__;for($i=0;$i<5;$i++){if(is_file($dir.'/config/system/settings.php')){$root=$dir;break;}$dir=dirname($dir);}
|
||||
$s=require $root.'/config/system/settings.php';$db=$s['DB']['Connections']['Default'];
|
||||
$pdo=new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',$db['host']??'localhost',$db['port']??3306,$db['dbname']??''),$db['user']??'',$db['password']??'',[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
|
||||
$desc='Discover how organizations worldwide use VITEC and Datapath video solutions: customer success stories from broadcast, defense, enterprise, sports and public safety.';
|
||||
$st=$pdo->prepare("UPDATE pages SET description=?, tstamp=UNIX_TIMESTAMP() WHERE uid=4 AND (description IS NULL OR description='')");
|
||||
$st->execute([$desc]);
|
||||
echo "Seite 4: ".$st->rowCount()." Zeile(n) aktualisiert\n";
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Move the parenthetical descriptor out of product titles into `subtitle`
|
||||
* (decision 2026-09-04): "MGW Diamond-H (HDMI encoder)" becomes
|
||||
* title "MGW Diamond-H" + subtitle "HDMI encoder".
|
||||
*
|
||||
* Rules:
|
||||
* - only titles ENDING in "(...)" are touched; slugs stay as they are
|
||||
* - subtitle empty -> descriptor moves there, title is stripped
|
||||
* - subtitle == descriptor -> title is stripped (subtitle already right)
|
||||
* - subtitle holds other text -> SKIPPED and reported, nothing changes
|
||||
*
|
||||
* Dry run by default: php migrations/split_title_descriptors.php
|
||||
* Write: php migrations/split_title_descriptors.php --apply
|
||||
*/
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$products = $pdo->query(
|
||||
"SELECT uid, title, subtitle FROM tx_vitec_domain_model_product WHERE deleted = 0 ORDER BY title"
|
||||
)->fetchAll();
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE tx_vitec_domain_model_product SET title = ?, subtitle = ?, tstamp = ? WHERE uid = ?"
|
||||
);
|
||||
|
||||
$changed = 0;
|
||||
$skipped = 0;
|
||||
foreach ($products as $product) {
|
||||
if (!preg_match('/^(.*\S)\s*\((.+)\)$/u', trim((string)$product['title']), $m)) {
|
||||
continue;
|
||||
}
|
||||
$newTitle = $m[1];
|
||||
$descriptor = trim($m[2]);
|
||||
$subtitle = trim((string)$product['subtitle']);
|
||||
|
||||
if ($subtitle !== '' && $subtitle !== $descriptor) {
|
||||
printf("SKIP %3d | %s -- subtitle belegt: \"%s\" (Deskriptor waere: \"%s\")\n",
|
||||
$product['uid'], $product['title'], $subtitle, $descriptor);
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
printf("%s %3d | \"%s\" -> title \"%s\" | subtitle \"%s\"\n",
|
||||
$apply ? 'WRITE' : 'plan ', $product['uid'], $product['title'], $newTitle, $descriptor);
|
||||
if ($apply) {
|
||||
$stmt->execute([$newTitle, $descriptor, time(), (int)$product['uid']]);
|
||||
}
|
||||
$changed++;
|
||||
}
|
||||
|
||||
printf("\n%d %s, %d skipped.%s\n",
|
||||
$changed,
|
||||
$apply ? 'written' : 'planned',
|
||||
$skipped,
|
||||
$apply ? ' Flush the cache: vendor/bin/typo3 cache:flush' : ' Run again with --apply to write.'
|
||||
);
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Loaded 100 of 518 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user