fe_comment: widget v2, overview link setting and inline mail images

- Rebuild the widget server-backed: it renders only when GET state
  allows (logged-in FE user of the configured group), so the script
  tag is safe on public pages; form with mini rich text (bold/italic/
  list), file attachment chips and Ctrl+V image paste - a paste lazily
  creates the draft feedback, uploads and inserts the image inline
- Note pad badge mirrors the server item count; the pad button links
  to the overview page via new extension setting overviewPid, exposed
  as overviewUrl in GET state (slug lookup)
- Embed /fileadmin images of notes into the feedback mail as CID
  parts: relative srcs are dead in mail clients and dev sits behind
  basic auth; attachment links stay absolute URLs
- public/widget-test.html: dev test page, doubles as the reference
  implementation of the React login flow (fetch felogin JSON form
  incl. __RequestToken, urlencoded POST to /_json + form.action)
- Docs section 5x (fe_comment v2 incl. CSRF login lesson,
  templateRootPaths.10 collision, cache:flush before extension:setup)
This commit is contained in:
orasche committed 2026-10-02 16:57:52 +02:00
1 parent 872c6e715c
commit d496877a53
6 files changed
+510 -217

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
<!doctype html>
<!-- fe_comment v2 Testseite: FE-Login (Referenz-Implementierung der React-
Strecke) + Widget-Einbindung. Nur fuer Dev-Tests, kann jederzeit weg. -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>fe_comment widget test</title>
<style>
body { font-family: -apple-system, "Segoe UI", Roboto, Arial, sans-serif; max-width: 640px; margin: 40px auto; padding: 0 16px; color: #1c2430; }
h1 { font-size: 20px; }
label { display: block; margin: 10px 0 2px; font-size: 13px; color: #5a6572; }
input { padding: 6px 8px; border: 1px solid #c9d1da; border-radius: 6px; font: inherit; width: 240px; }
button { margin-top: 12px; padding: 7px 14px; border: 1px solid #1f6feb; border-radius: 6px; background: #1f6feb; color: #fff; font: inherit; font-weight: 600; cursor: pointer; }
#status { margin-top: 14px; font-weight: 600; }
.ok { color: #2f9e44; } .err { color: #d9463e; }
p.hint { color: #5a6572; font-size: 13px; }
</style>
</head>
<body>
<h1>fe_comment v2 — Widget-Testseite</h1>
<p class="hint">Diese Seite tut, was spaeter die React-App tut: felogin-Formular
per JSON holen (inkl. CSRF-Token), Login-POST, dann erscheint rechts das
Feedback-Widget (nur fuer berechtigte FE-User).</p>
<div id="login">
<label>User</label><input id="user" autocomplete="username">
<label>Passwort</label><input id="pass" type="password" autocomplete="current-password">
<button id="loginBtn">Login</button>
<button id="logoutBtn" style="background:#f6f8fa;color:#1c2430;border-color:#c9d1da">Logout</button>
</div>
<div id="status"></div>
<script>
(function () {
var statusBox = document.getElementById('status');
function say(text, ok) {
statusBox.textContent = text;
statusBox.className = ok ? 'ok' : 'err';
}
function checkState() {
return fetch('/api/fe-comment/state', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (s) {
if (s.allowed) { say('Eingeloggt als ' + s.user.name + ' - Widget aktiv (rechts).', true); }
else { say('Nicht eingeloggt / nicht berechtigt (allowed:false).', false); }
return s;
});
}
// Die React-Login-Strecke in Kurzform:
// 1. Seite als JSON holen -> felogin-Element -> form (setzt Nonce-Cookie)
// 2. Alle Formfelder + user/pass urlencoded an '/_json' + form.action posten
function login(user, pass) {
return fetch('/_json/fe-user-profile', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (page) {
var el = (page.content.colPos0 || []).filter(function (e) { return e.type === 'felogin_login'; })[0];
if (!el || !el.content || !el.content.data.form) { throw new Error('felogin-Formular nicht im JSON'); }
var form = el.content.data.form;
var body = new URLSearchParams();
form.elements.forEach(function (f) { if (f.name) { body.set(f.name, f.value || ''); } });
body.set('user', user);
body.set('pass', pass);
return fetch('/_json' + form.action, { method: 'POST', body: body, credentials: 'same-origin' });
})
.then(checkState);
}
function logout() {
var body = new URLSearchParams();
body.set('logintype', 'logout');
return fetch('/_json/fe-user-profile', { method: 'POST', body: body, credentials: 'same-origin' })
.then(function () { location.reload(); });
}
document.getElementById('loginBtn').addEventListener('click', function () {
login(document.getElementById('user').value, document.getElementById('pass').value)
.then(function (s) { if (s.allowed) { location.reload(); } })
.catch(function (e) { say('Fehler: ' + e.message, false); });
});
document.getElementById('logoutBtn').addEventListener('click', logout);
checkState();
})();
</script>
<script src="/api/fe-comment/widget.js" defer></script>
</body>
</html>