Add an own-HTML block, and build the DEEG offers page on the embed block
Umut asked for a block that takes custom HTML, and for an iframe on a new DEEG offers page. Only one of those was missing. New core block "Eigenes HTML" (document schema v24): one property, the markup, up to 20.000 characters, written to the page unchanged. Changing it is reserved for administrators and compared against what is stored - on saving, on saving an area, and on restoring an older revision - exactly like an own script. <script> and <iframe> are refused with the way to the right place instead of being swallowed by the content security policy: scripts belong in the script field, frames in the "Externer Inhalt" block, because only that one asks for consent and puts the host into frame-src (ADR 106). DEEG: /angebote existed as a draft with placeholder text. It is now built like the legal pages - small caps line, large light heading, lead on the tinted band - with the IS24 result list below it and a line pointing to the portal and to the contact form. Navigation and the offers band on the home page point there. The consent question is switched on, so the frame loads only after the visitor agrees, and both the banner and the placeholder carry the site's own shapes and colours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6b1aa50d02
commit
59a40157a4
15 files changed
+465
-8
No files matched your search
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Legt auf der bestehenden DEEG-Installation die Seite „Angebote“ an und verweist von der
|
||||
* Navigation und vom Angebote-Band der Startseite dorthin.
|
||||
*
|
||||
* Die Seite zeigt die Ergebnisliste des DEEG-Portals bei ImmobilienScout24 als eingebetteten
|
||||
* Rahmen – Baustein „Externer Inhalt“, nicht eigenes HTML: nur er fragt vorher um Einwilligung
|
||||
* und trägt den Anbieter in die Sicherheitsrichtlinie der Seite ein. Kopfabschnitt, Schriftgrößen
|
||||
* und Farben sind dieselben wie bei Impressum und Datenschutz (siehe seed-immobilien-deeg.php).
|
||||
*
|
||||
* Läuft nur gegen die DEEG-Datenbank und nur, solange es die Seite noch nicht gibt.
|
||||
*/
|
||||
require dirname(__DIR__).'/config/bootstrap.php';
|
||||
|
||||
use App\Content\DocumentValidator;
|
||||
use App\Content\MediaLibrary;
|
||||
use App\Content\MenuRepository;
|
||||
use App\Content\PageRepository;
|
||||
use App\Content\SiteSettings;
|
||||
use App\Infrastructure\ConnectionFactory;
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
$url = str_replace('%kernel.project_dir%', str_replace('\\', '/', $root), (string) $_SERVER['DATABASE_URL']);
|
||||
if (!str_contains($url, 'immobilien-deeg')) {
|
||||
fwrite(STDERR, "Sicherung: DATABASE_URL muss auf die DEEG-Datenbank zeigen.\n");
|
||||
exit(1);
|
||||
}
|
||||
$db = ConnectionFactory::create($url);
|
||||
$media = new MediaLibrary($db, str_replace('%kernel.project_dir%', $root, (string) $_SERVER['MEDIA_DIRECTORY']));
|
||||
$settings = new SiteSettings($db, $media);
|
||||
$pages = new PageRepository($db, new DocumentValidator(), $settings);
|
||||
$menus = new MenuRepository($db, $settings);
|
||||
|
||||
// Die Seite war im Editor schon angelegt (Starterabschnitt plus Rahmen mit Platzhaltertexten).
|
||||
// Sie wird überschrieben, nicht verdoppelt; der bisherige Stand bleibt als Fassung erhalten und
|
||||
// lässt sich im Editor unter „Fassungen“ zurückholen.
|
||||
$vorhanden = $db->fetchOne('SELECT id FROM page WHERE path = ?', ['angebote']);
|
||||
|
||||
$SCHEMA = 24;
|
||||
$portal = 'https://portal.immobilienscout24.de/ergebnisliste/16200281';
|
||||
$nurUnten = static fn (string $w): array => ['top' => '0px', 'right' => '0px', 'bottom' => $w, 'left' => '0px'];
|
||||
|
||||
// Der Kopf wie auf Impressum und Datenschutz: helle Fläche, Kleinzeile, große leichte Überschrift.
|
||||
$kopf = ['id' => 'angebote-kopf', 'type' => 'section', 'children' => [
|
||||
['id' => 'angebote-label', 'type' => 'text', 'props' => ['text' => 'AKTUELLE OBJEKTE'], 'style' => [
|
||||
'desktop' => ['fontSize' => '12px', 'fontWeight' => '700', 'letterSpacing' => '0.14em', 'lineHeight' => 1.6, 'color' => '@anthrazit', 'margin' => ['bottom' => '25px']],
|
||||
'mobile' => ['fontSize' => '11px', 'margin' => ['bottom' => '18px']],
|
||||
], 'layout' => ['effects' => ['entrance' => 'fade', 'delay' => 'none']]],
|
||||
['id' => 'angebote-h1', 'type' => 'heading', 'props' => ['text' => 'Unsere Angebote', 'level' => 1], 'style' => [
|
||||
'desktop' => ['fontWeight' => '400', 'fontSize' => '56px', 'lineHeight' => 1.1, 'letterSpacing' => '-0.05em', 'margin' => ['bottom' => '22px']],
|
||||
'tablet' => ['fontSize' => '44px'],
|
||||
'mobile' => ['fontSize' => '38px', 'margin' => ['bottom' => '16px']],
|
||||
], 'layout' => ['effects' => ['entrance' => 'rise', 'delay' => 'short']]],
|
||||
['id' => 'angebote-vorspann', 'type' => 'text', 'props' => ['text' => 'Wohnungen, Häuser und Grundstücke aus unserem Bestand in Freudenstadt und Umgebung. Die Liste wird laufend aktualisiert – sehen Sie sich um und sprechen Sie uns an.'], 'style' => [
|
||||
'desktop' => ['maxWidth' => '640px', 'fontSize' => '18px', 'lineHeight' => 1.7, 'color' => '@text', 'margin' => ['bottom' => '0px']],
|
||||
'mobile' => ['fontSize' => '16px'],
|
||||
], 'layout' => ['effects' => ['entrance' => 'rise', 'delay' => 'medium']]],
|
||||
], 'style' => [
|
||||
'desktop' => ['backgroundColor' => '@hell', 'borderWidth' => $nurUnten('1px'), 'borderStyle' => 'solid', 'borderColor' => '@linie-hell', 'padding' => ['top' => '84px', 'bottom' => '72px']],
|
||||
'tablet' => ['padding' => ['top' => '64px', 'bottom' => '56px']],
|
||||
'mobile' => ['padding' => ['top' => '48px', 'bottom' => '40px']],
|
||||
]];
|
||||
|
||||
// Die Liste selbst. Der Rahmen bleibt hoch genug, dass mehrere Objekte ohne Scrollen im Rahmen
|
||||
// sichtbar sind; darunter der Weg zum Portal für alle, die lieber dort suchen.
|
||||
$hinweisText = 'Nichts Passendes dabei? <a href="'.$portal.'" target="_blank" rel="noopener">Alle Objekte direkt bei ImmobilienScout24 ansehen ↗</a> oder <a href="/#kontakt">sprechen Sie uns an</a> – wir kennen den Markt vor Ort und wissen oft von Objekten, bevor sie inseriert sind.';
|
||||
$liste = ['id' => 'angebote-liste', 'type' => 'section', 'children' => [
|
||||
['id' => 'angebote-rahmen', 'type' => 'embed', 'props' => [
|
||||
'group' => 'external',
|
||||
'title' => 'Aktuelle Immobilienangebote der Deeg Hausverwaltung und Immobilien GmbH bei ImmobilienScout24',
|
||||
'provider' => 'ImmobilienScout24',
|
||||
'url' => $portal,
|
||||
'hint' => 'Die Angebotsliste wird von ImmobilienScout24 geladen. Dabei erfährt der Anbieter Ihre IP-Adresse und welche Seite Sie ansehen.',
|
||||
'height' => 1000,
|
||||
], 'style' => ['desktop' => ['margin' => ['bottom' => '26px']]], 'layout' => ['effects' => ['entrance' => 'fade', 'delay' => 'none']],
|
||||
// Der Rahmen selbst und der Platzhalter davor tragen den Zuschnitt der Vorlage: eckig,
|
||||
// helle Fläche, Logo-Blau auf dem Knopf. Was IM Rahmen steht, gehört ImmobilienScout24 -
|
||||
// daran kommt kein CSS dieser Website heran, und das ist keine Einstellung, sondern die
|
||||
// Trennung zwischen zwei Herkünften im Browser.
|
||||
'css' => "& iframe{border:1px solid #dbe1e3;border-radius:0;background:#f5fafd}
|
||||
&.embed-blocked{border:1px solid #dbe1e3;border-radius:0;background:#f5fafd;padding:56px 24px;text-align:left}
|
||||
& .embed-title{font-size:20px;font-weight:700;color:#343a40;margin-bottom:12px}
|
||||
& .embed-hint{max-width:640px;font-size:15px;line-height:1.7;color:#58666d}
|
||||
& .embed-provider{font-size:13px;color:#66747b}
|
||||
& .consent-button{border-radius:0;background:#0076b6;border-color:#0076b6;color:#fff;flex:0 0 auto;padding:16px 24px}
|
||||
& .consent-button:hover{background:#006aa3;border-color:#006aa3}",
|
||||
],
|
||||
['id' => 'angebote-hinweis', 'type' => 'text', 'props' => ['text' => $hinweisText], 'style' => [
|
||||
'desktop' => ['maxWidth' => '760px', 'fontSize' => '16px', 'lineHeight' => 1.7, 'color' => '@text', 'margin' => ['bottom' => '0px']],
|
||||
], 'css' => "& a{color:#006aa3;text-decoration:underline;text-underline-offset:3px}\n& a:hover{color:#343a40}",
|
||||
'layout' => ['effects' => ['entrance' => 'rise', 'delay' => 'short']]],
|
||||
], 'style' => [
|
||||
'desktop' => ['padding' => ['top' => '72px', 'bottom' => '108px']],
|
||||
'tablet' => ['padding' => ['top' => '56px', 'bottom' => '80px']],
|
||||
'mobile' => ['padding' => ['top' => '40px', 'bottom' => '65px']],
|
||||
]];
|
||||
|
||||
$dokumentAngebote = [
|
||||
'schemaVersion' => $SCHEMA,
|
||||
'title' => 'Angebote',
|
||||
'description' => 'Aktuelle Wohnungen, Häuser und Grundstücke der Deeg Hausverwaltung und Immobilien GmbH in Freudenstadt und Umgebung.',
|
||||
'blocks' => [$kopf, $liste],
|
||||
];
|
||||
if (false !== $vorhanden) {
|
||||
$id = (string) $vorhanden;
|
||||
$revision = $pages->saveDraft($id, $dokumentAngebote, (int) $pages->findForEditor($id)['version']);
|
||||
$pages->publish($id, $revision, (int) $pages->findForEditor($id)['version']);
|
||||
echo "Seite überarbeitet und veröffentlicht: /angebote\n";
|
||||
} else {
|
||||
$id = $pages->createPage('angebote', null, 'page', $dokumentAngebote);
|
||||
$seite = $db->fetchAssociative('SELECT draft_revision_id, version FROM page WHERE id = ?', [$id]);
|
||||
$pages->publish($id, $seite['draft_revision_id'], (int) $seite['version']);
|
||||
echo "Seite angelegt und veröffentlicht: /angebote\n";
|
||||
}
|
||||
$db->executeStatement('UPDATE page SET seo_title = ? WHERE id = ?', ['Aktuelle Angebote | Deeg Hausverwaltung und Immobilien GmbH', $id]);
|
||||
|
||||
// „Immobilien“ in der Navigation zeigte auf den Sprungpunkt der Startseite; jetzt gibt es die
|
||||
// Seite, auf die dieser Abschnitt ohnehin nur verwiesen hat.
|
||||
$header = $menus->all()['header'];
|
||||
$eintraege = [];
|
||||
foreach ($header['items'] as $eintrag) {
|
||||
if ('Immobilien' === $eintrag['label']) {
|
||||
$eintrag['pageId'] = $id;
|
||||
$eintrag['anchor'] = '';
|
||||
}
|
||||
$eintraege[] = $eintrag;
|
||||
}
|
||||
$menus->save('header', $header['name'], $eintraege, (int) $header['version']);
|
||||
echo "Navigation: „Immobilien“ zeigt auf /angebote\n";
|
||||
|
||||
// Und der Knopf im Angebote-Band der Startseite, der bisher auf die alte Website führte.
|
||||
$home = $db->fetchAssociative('SELECT id FROM page WHERE path = ?', ['home']);
|
||||
$dokument = $pages->findForEditor($home['id'])['document'];
|
||||
$getroffen = 0;
|
||||
$walk = function (array &$blocks) use (&$walk, &$getroffen): void {
|
||||
foreach ($blocks as &$block) {
|
||||
// Der Knopf im Angebote-Band ist ein Textbaustein mit eigenem CSS, kein Button-Baustein -
|
||||
// deshalb wird die Adresse im Text ersetzt, nicht in den Eigenschaften.
|
||||
foreach (['href', 'text'] as $feld) {
|
||||
$wert = (string) ($block['props'][$feld] ?? '');
|
||||
if ('' !== $wert && str_contains($wert, 'immobiliendeeg.de/immobilien')) {
|
||||
$block['props'][$feld] = str_replace(['https://www.immobiliendeeg.de/immobilien/', 'https://www.immobiliendeeg.de/immobilien'], '/angebote', $wert);
|
||||
unset($block['props']['newTab']);
|
||||
++$getroffen;
|
||||
}
|
||||
}
|
||||
if (isset($block['children']) && is_array($block['children'])) {
|
||||
$walk($block['children']);
|
||||
}
|
||||
}
|
||||
};
|
||||
$walk($dokument['blocks']);
|
||||
if ($getroffen > 0) {
|
||||
$stand = $pages->findForEditor($home['id']);
|
||||
$revision = $pages->saveDraft($home['id'], $dokument, $stand['version']);
|
||||
$pages->publish($home['id'], $revision, $pages->findForEditor($home['id'])['version']);
|
||||
}
|
||||
echo "Startseite: $getroffen Knopf/Knöpfe zeigen jetzt auf /angebote\n";
|
||||
|
||||
// Ein fremder Rahmen ohne Einwilligung lädt beim Seitenaufruf mit und gibt dabei IP-Adresse und
|
||||
// Seite an den Anbieter weiter. Mit der Gruppe „Externe Inhalte“ steht davor der Platzhalter, und
|
||||
// erst der Knopf lädt die Liste. Der Balken trägt die Farben der Website.
|
||||
$aktuell = $settings->load();
|
||||
$settings->save([
|
||||
...$aktuell,
|
||||
// Gespeichert als Liste, erwartet als Textfeld - wie es der Einstellungsbildschirm schickt.
|
||||
'sameAs' => implode("\n", $aktuell['sameAs']),
|
||||
'consentBanner' => true,
|
||||
'consentGroups' => ['external'],
|
||||
'consentShape' => 'eckig',
|
||||
'consentAccent' => '@blau-tief',
|
||||
'consentAccentInk' => '@papier',
|
||||
'consentColor' => '@papier',
|
||||
'consentInk' => '@anthrazit',
|
||||
]);
|
||||
echo "Einwilligung eingeschaltet: Gruppe „Externe Inhalte“, Balken eckig in Logo-Blau\n";
|
||||
@@ -940,6 +940,48 @@ try {
|
||||
await page.locator('.notice.ok').waitFor();
|
||||
checks.push({ name: 'plugin switched on, block inserted from the toolbox, schema panel, assets on the public page, switched off keeps the block', passed: true });
|
||||
|
||||
// --- Der Baustein „Eigenes HTML“ (ADR 106) -------------------------------------------------
|
||||
// Markup des Betreibers, unveraendert bis auf die oeffentliche Seite. Geprueft wird der Weg,
|
||||
// den ein Administrator geht: einfuegen, eintippen, Vorschau, veroeffentlichen - und dass
|
||||
// <script> und <iframe> mit einer Meldung abgewiesen werden statt still zu verschwinden.
|
||||
await page.goto(origin + '/admin/pages/test-home');
|
||||
await treeSelect('headline');
|
||||
await addFromLibrary('[data-add="html"]');
|
||||
await page.locator('#properties-title').filter({ hasText: 'Eigenes HTML' }).waitFor();
|
||||
await page.getByLabel('HTML', { exact: true }).fill('<table class="preise"><tbody><tr><td>Miete</td><td>ab 8 €/m²</td></tr></tbody></table>');
|
||||
// Erst der Fokuswechsel schickt den Wert ins Dokument; die Vorschau folgt kurz darauf.
|
||||
await page.locator('#preview-status').click();
|
||||
await page.waitForFunction(() => Boolean(document.querySelector('#preview').contentDocument.querySelector('.block-html table.preise')), null, { timeout: 20000 });
|
||||
await page.locator('#save').click();
|
||||
await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor();
|
||||
await page.locator('#publish').click(); await page.locator('#confirm-publish').click();
|
||||
await page.locator('#save-status').filter({ hasText: 'Veröffentlicht' }).waitFor();
|
||||
{
|
||||
const html = await (await page.request.get(origin + '/')).text();
|
||||
assert.match(html, /class="block-html"><table class="preise">/, 'Eigenes HTML unverändert auf der Website');
|
||||
// Der Baustein selbst bringt kein Skript auf die Seite - die Tabelle steht da, wie sie
|
||||
// eingegeben wurde, und sonst nichts. (Dass eine Seite ohne Skript weiterhin
|
||||
// script-src 'none' sendet, prüft CmsTest; hier trägt das Plugin von vorhin schon eines bei.)
|
||||
assert.equal(html.includes('<table class="preise"><tbody><tr><td>Miete</td>'), true, 'Markup Zeichen für Zeichen');
|
||||
}
|
||||
for (const [markup, meldung] of [
|
||||
['<script>alert(1)</script>', /Eigenes HTML führt keine Skripte aus/],
|
||||
['<iframe src="https://example.com"></iframe>', /Externer Inhalt/],
|
||||
]) {
|
||||
await page.getByLabel('HTML', { exact: true }).fill(markup);
|
||||
await page.locator('#preview-status').click();
|
||||
await page.locator('#save').click();
|
||||
// Abgewiesen wird schon die Vorschau, und das Speichern meldet es in der Leiste über dem
|
||||
// Editor - die Seite bleibt, wie sie veröffentlicht ist.
|
||||
await page.locator('#message').filter({ hasText: meldung }).waitFor();
|
||||
}
|
||||
await page.getByLabel('HTML', { exact: true }).fill('<p class="notiz">Wieder gut.</p>');
|
||||
await page.locator('#preview-status').click();
|
||||
await page.locator('#save').click();
|
||||
await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor();
|
||||
await page.screenshot({ path: 'reports/editor/own-html.png', fullPage: true });
|
||||
checks.push({ name: 'own HTML block: inserted, previewed, published unchanged, script and iframe refused', passed: true });
|
||||
|
||||
// Every management screen, not just the page list: they carry tables, disclosures and now the
|
||||
// media grid, and none of that was ever checked automatically before.
|
||||
for (const [name, path] of [
|
||||
|
||||
Reference in new issue
Block a user