fe_comment v2: FE-user feedback tickets with summary, uploads and API
- Replace the v1 submit endpoint with a full JSON API under
/api/fe-comment/ (state, feedback create/update/delete, uploads,
summary notes/recipients, submit), guarded by frontend login and a
configurable FE user group; unauthorized callers get {"allowed":false}
- New models: tx_fecomment_summary (one open ticket per user, status
open/sent) and tx_fecomment_feedback (url, RTE note, FAL attachments),
both editable in the backend on the ticket storage page
- FeedbackApiMiddleware registered after frontend authentication (the
frontend.user attribute does not exist earlier); uploads land in
fileadmin/fe_comment/summary-<uid>/ with mime/extension check, 20 MB cap
- Submit sends one HTML mail (author block, items with attachment links,
reply-to = FE user mail) to selected recipients with mailTo fallback,
then closes the summary
- felogin wired React-compatible: felogin.pid=520 plus headless FeLogin
JSON templates via site settings - felogin v14 and EXT:headless both
claim templateRootPaths.10, the felogin.view.* settings feed exactly
that index, so the structured JSON form (incl. __RequestToken) wins
- Read the sys_file_reference insert id before syncAttachmentCount():
DBAL throws "no identity value" once the count UPDATE has run
- migrations: drop_fecomment_tables.php (repairs the no-uid tables,
only drops when empty), check_feuser.php (login diagnosis),
fec_login_test.sh / fec_api_test.sh (curl chains incl. CSRF token flow)
This commit is contained in:
1 parent
78af83d2e3
commit
872c6e715c
14 files changed
+842
-95
No files matched your search
@@ -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,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,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,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
|
||||
Reference in new issue
Block a user