Skip to content

Commit 83bdfb5

Browse files
committed
1.0.0.beta.17
Schliesst die Tickets: #4,#5,#6,#7,#8,#9,#10
1 parent a70f529 commit 83bdfb5

11 files changed

Lines changed: 1024 additions & 29 deletions

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,45 @@
33

44
Dieses Changelog wird ab Version `1.0.0` neu geführt.
55

6+
## 2026-06-02
7+
8+
### Added
9+
- Neues Fieldset `Sprache kopieren` auf der Einstellungsseite (`settings`) zum Kopieren aller sprachbezogenen JSON-LD Inhalte von einer Quelle in eine Zielsprache.
10+
- Neue Custom-Fieldsets in den Formularen für Organization (`schemas`), WebSite (`global_website`) und LocalBusiness (`global_localbusiness`).
11+
- Je Formular kann ein JSON-Objekt mit zusätzlichen Schema-Eigenschaften hinterlegt werden.
12+
13+
### Security
14+
- Serverseitige JSON-Validierung/Sanitizing für Custom-Angaben hinzugefügt; ungültige Eingaben werden nicht gespeichert.
15+
- Geschützte Schlüssel `@context`, `@type` und `@id` werden nicht überschrieben.
16+
- JSON-LD Ausgabe-Encoder wurde mit `JSON_HEX_*` abgesichert, um Script-Injection über Textinhalte zu verhindern.
17+
- Validierung fängt tiefe/fehlerhafte Verschachtelungen kontrolliert ab (kein ungefangener Runtime-Abbruch im Save-Flow).
18+
19+
### Changed
20+
- Beim Sprachkopieren werden `jsonld_localbusiness_branches` und `jsonld_schemas` inklusive Zuordnungen übernommen.
21+
- Branch-basierte Referenzen werden per ID-Mapping korrekt auf die neuen Ziel-Standort-IDs umgeschrieben (`localbusiness_branch_id`, `localbusiness_branch_ids`, `article_branch_*`).
22+
- Zielsprach-spezifische JSON-LD Konfigurations-Keys werden vor dem Kopieren ersetzt, um eine konsistente 1:1-Übernahme zu gewährleisten.
23+
- Generator merged Custom-Daten jetzt zentral in Organization-, WebSite- und LocalBusiness-Schema.
24+
- WebSite-SearchAction liest kompatibel sowohl `search_action` als auch `potentialAction`.
25+
- LocalBusiness-Custom-Merge auf Generator-Ende verschoben (Parität zur Backend-Vorschau).
26+
- Multi-Domain-Setzung des Hauptstandorts auf aktive Domain begrenzt.
27+
- Entspricht der Umsetzung von GitHub Issue `#10`.
28+
- Status: GitHub Issue `#10` ist geschlossen.
29+
30+
### Fixed
31+
- LocalBusiness-Bilder werden im Frontend wieder korrekt im JSON-LD (`image`) ausgegeben.
32+
- Normalisierung für `images`/`image` aus Branch-Konfiguration ergänzt (CSV/Array, relative Media-Dateien und absolute URLs).
33+
- Backend-Vorschau in `global_localbusiness` korrigiert, damit die Domain-Base-URL in JavaScript korrekt gesetzt wird.
34+
- GitHub: Entspricht dem Abschluss von Issue `#9`.
35+
36+
### GitHub
37+
- Mit den heutigen Änderungen werden die aktuell offenen Tickets geschlossen: `#4`, `#5`, `#6`, `#7`, `#8`, `#9` und `#10`.
38+
39+
## v1.0.0.beta16 (02. Juni 2026)
40+
41+
**UX / Light-Mode Lesbarkeit:**
42+
43+
**GitHub:**
44+
645
## v1.0.0.beta16 (18. Mai 2026)
746

847
**Release-Konsolidierung:**

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,27 @@ Die übrigen Optionen helfen dabei:
6868
- `Validierung` prüft die JSON-Syntax
6969
- `Debug-Modus` hilft bei der Kontrolle während der Entwicklung
7070

71+
### 3a. Sprache kopieren (Einstellungen)
72+
73+
Im Bereich `Einstellungen` steht bei mehrsprachigen Projekten die Funktion `Sprache kopieren` zur Verfügung.
74+
75+
Damit kannst du eine vollständige sprachabhängige JSON-LD-Konfiguration von einer Quellsprache in eine Zielsprache übernehmen.
76+
77+
Kopiert werden:
78+
79+
- sprachbezogene globale Konfigurationen (`Organization`, `WebSite`, domain-spezifische Sprach-Keys)
80+
- artikelbezogene Schema-Zuordnungen (`jsonld_schemas`)
81+
- LocalBusiness-Standorte der Sprache (`jsonld_localbusiness_branches`)
82+
83+
Wichtiges Verhalten:
84+
85+
- Bestehende Daten der Zielsprache werden vor dem Kopieren entfernt und anschließend durch die Quell-Daten ersetzt.
86+
- Beim Kopieren der Standorte wird ein internes ID-Mapping erstellt, damit Artikel-Zuordnungen in der Zielsprache auf die neu erzeugten Standort-IDs zeigen.
87+
88+
Empfehlung:
89+
90+
- Die Funktion nur bewusst einsetzen (z. B. initiale Sprachanlage oder kompletter Relaunch einer Sprache), da sie die Zielsprache überschreibt.
91+
7192
### 4. Optional: Dynamische URLs
7293

7394
Wenn das AddOn `URL` installiert ist und dort mindestens ein Profil angelegt wurde, kannst du zusätzlich den Bereich `Dynamische URLs` nutzen.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* JSON-LD Manager: Hide dynamic URLs tab when URL addon/profile is unavailable */
2+
3+
#rex-page-jsonld_manager-dynamic_urls,
4+
.rex-page-jsonld_manager-dynamic_urls,
5+
a[href*="page=jsonld_manager/dynamic_urls"] {
6+
display: none !important;
7+
}

assets/css/jsonld_manager.css

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,62 @@
150150
border-right: 1px solid rgba(0,0,0,0.1);
151151
}
152152

153+
/* === SETTINGS PAGE (jsonld_manager/settings) === */
154+
.jsonld-template-warning-panel {
155+
border-color: #8a6d3b !important;
156+
}
157+
158+
.jsonld-template-warning-panel > .panel-heading {
159+
background: #f0ad4e !important;
160+
border-color: #eea236 !important;
161+
color: #1f2d3a !important;
162+
}
163+
164+
.jsonld-template-warning-panel > .panel-heading .panel-title {
165+
color: #1f2d3a !important;
166+
}
167+
168+
.jsonld-language-copy-panel .panel-body {
169+
padding: 15px !important;
170+
}
171+
172+
.jsonld-language-copy-panel .form-group {
173+
margin: 0 0 12px 0 !important;
174+
}
175+
176+
.jsonld-language-copy-panel .bootstrap-select {
177+
display: block !important;
178+
width: 100% !important;
179+
}
180+
181+
.jsonld-language-copy-panel select.form-control,
182+
.jsonld-language-copy-panel .bootstrap-select > .dropdown-toggle {
183+
width: 100% !important;
184+
}
185+
186+
.jsonld-language-copy-panel .jsonld-copy-label {
187+
display: block;
188+
font-weight: 600;
189+
margin-bottom: 8px;
190+
text-align: left !important;
191+
}
192+
193+
.jsonld-language-copy-panel .help-block {
194+
margin: 0;
195+
}
196+
197+
.jsonld-language-copy-panel .jsonld-copy-help-block {
198+
margin-bottom: 12px;
199+
}
200+
201+
.jsonld-language-copy-panel .jsonld-copy-meta-group {
202+
margin-bottom: 0 !important;
203+
}
204+
205+
.jsonld-language-copy-panel .jsonld-copy-submit-btn {
206+
margin-top: 0;
207+
}
208+
153209
.jsonld-overview .btn-group .btn:last-child {
154210
border-right: none;
155211
}
@@ -511,3 +567,66 @@
511567
font-family: Monaco, Menlo, monospace;
512568
margin-bottom: 12px;
513569
}
570+
571+
/* === LIGHT MODE READABILITY FIXES === */
572+
/* Fallback mit hoher Priorität: nur im Light-Mode */
573+
body.rex-theme-light .panel textarea.form-control,
574+
body:not(.rex-theme-dark):not(.rex-theme-light) .panel textarea.form-control {
575+
background-color: #f8fafc !important;
576+
color: #25313b !important;
577+
border-color: #cfd6de !important;
578+
}
579+
580+
body.rex-theme-light .panel textarea.form-control::placeholder,
581+
body:not(.rex-theme-dark):not(.rex-theme-light) .panel textarea.form-control::placeholder {
582+
color: #7a8694 !important;
583+
}
584+
585+
body.rex-theme-light .panel textarea.form-control:focus,
586+
body:not(.rex-theme-dark):not(.rex-theme-light) .panel textarea.form-control:focus {
587+
background-color: #ffffff !important;
588+
color: #25313b !important;
589+
border-color: #6fa8d8 !important;
590+
box-shadow: 0 0 0 2px rgba(111, 168, 216, 0.18) !important;
591+
}
592+
593+
body.rex-theme-light .panel textarea.form-control,
594+
body:not(.rex-theme-dark) .panel textarea.form-control {
595+
background-color: #ffffff;
596+
color: #2f3942;
597+
border-color: #cfd6de;
598+
}
599+
600+
body.rex-theme-light .panel textarea.form-control::placeholder,
601+
body:not(.rex-theme-dark) .panel textarea.form-control::placeholder {
602+
color: #7a8694;
603+
}
604+
605+
body.rex-theme-light .panel textarea.form-control:focus,
606+
body:not(.rex-theme-dark) .panel textarea.form-control:focus {
607+
border-color: #6fa8d8;
608+
box-shadow: 0 0 0 2px rgba(111, 168, 216, 0.18);
609+
}
610+
611+
body.rex-theme-light .article-table .article-row td,
612+
body:not(.rex-theme-dark) .article-table .article-row td {
613+
background-color: #f5f8fb;
614+
color: #2f3942;
615+
}
616+
617+
body.rex-theme-light .article-table .article-row:hover td,
618+
body:not(.rex-theme-dark) .article-table .article-row:hover td {
619+
background-color: #e9f1f8;
620+
}
621+
622+
body.rex-theme-light .article-table .article-path,
623+
body:not(.rex-theme-dark) .article-table .article-path {
624+
color: #6c7b89;
625+
}
626+
627+
body.rex-theme-light .article-table .article-row.active td,
628+
body.rex-theme-light .article-table .article-row.active:hover td,
629+
body:not(.rex-theme-dark) .article-table .article-row.active td,
630+
body:not(.rex-theme-dark) .article-table .article-row.active:hover td {
631+
background-color: #2f7fbe !important;
632+
}

lib/CustomJsonLdHelper.php

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
3+
namespace FriendsOfRedaxo\JsonLdManager;
4+
5+
class CustomJsonLdHelper
6+
{
7+
private const MAX_RAW_LENGTH = 30000;
8+
private const MAX_DEPTH = 20;
9+
10+
/**
11+
* @return array{raw:string,data:array,errors:array,warnings:array}
12+
*/
13+
public static function parseCustomObject(string $rawJson): array
14+
{
15+
$rawJson = trim($rawJson);
16+
if ($rawJson === '') {
17+
return [
18+
'raw' => '',
19+
'data' => [],
20+
'errors' => [],
21+
'warnings' => [],
22+
];
23+
}
24+
25+
if (strlen($rawJson) > self::MAX_RAW_LENGTH) {
26+
return [
27+
'raw' => $rawJson,
28+
'data' => [],
29+
'errors' => ['Der Custom-JSON-Text ist zu lang (max. ' . self::MAX_RAW_LENGTH . ' Zeichen).'],
30+
'warnings' => [],
31+
];
32+
}
33+
34+
try {
35+
$decoded = json_decode($rawJson, true, 512, JSON_THROW_ON_ERROR);
36+
} catch (\JsonException $e) {
37+
return [
38+
'raw' => $rawJson,
39+
'data' => [],
40+
'errors' => ['Ungueltiges JSON: ' . $e->getMessage()],
41+
'warnings' => [],
42+
];
43+
}
44+
45+
if (!is_array($decoded) || array_is_list($decoded)) {
46+
return [
47+
'raw' => $rawJson,
48+
'data' => [],
49+
'errors' => ['Custom-Angaben muessen ein JSON-Objekt sein (z. B. {"key": "value"}).'],
50+
'warnings' => [],
51+
];
52+
}
53+
54+
$warnings = [];
55+
try {
56+
$sanitized = self::sanitizeObject($decoded, 0, $warnings);
57+
} catch (\RuntimeException $e) {
58+
return [
59+
'raw' => $rawJson,
60+
'data' => [],
61+
'errors' => [$e->getMessage()],
62+
'warnings' => [],
63+
];
64+
}
65+
66+
return [
67+
'raw' => $rawJson,
68+
'data' => $sanitized,
69+
'errors' => [],
70+
'warnings' => $warnings,
71+
];
72+
}
73+
74+
/**
75+
* @param array<string,mixed> $schema
76+
* @param array<string,mixed> $customData
77+
* @param array<int,string> $protectedKeys
78+
* @return array<string,mixed>
79+
*/
80+
public static function mergeIntoSchema(array $schema, array $customData, array $protectedKeys = ['@context', '@type', '@id']): array
81+
{
82+
foreach ($customData as $key => $value) {
83+
$key = trim((string) $key);
84+
if ($key === '') {
85+
continue;
86+
}
87+
88+
if (in_array($key, $protectedKeys, true)) {
89+
continue;
90+
}
91+
92+
if (
93+
isset($schema[$key])
94+
&& is_array($schema[$key])
95+
&& is_array($value)
96+
&& !array_is_list($schema[$key])
97+
&& !array_is_list($value)
98+
) {
99+
$schema[$key] = self::mergeIntoSchema($schema[$key], $value, $protectedKeys);
100+
continue;
101+
}
102+
103+
$schema[$key] = $value;
104+
}
105+
106+
return $schema;
107+
}
108+
109+
/**
110+
* @param array<string,mixed> $object
111+
* @param array<int,string> $warnings
112+
* @return array<string,mixed>
113+
*/
114+
private static function sanitizeObject(array $object, int $depth, array &$warnings): array
115+
{
116+
if ($depth > self::MAX_DEPTH) {
117+
throw new \RuntimeException('Custom-JSON ist zu tief verschachtelt.');
118+
}
119+
120+
$clean = [];
121+
foreach ($object as $key => $value) {
122+
$key = trim((string) $key);
123+
if ($key === '') {
124+
continue;
125+
}
126+
127+
if ($key === '@context' || $key === '@type' || $key === '@id') {
128+
$warnings[] = 'Der Schluessel "' . $key . '" wird ignoriert und nicht ueberschrieben.';
129+
continue;
130+
}
131+
132+
$clean[$key] = self::sanitizeValue($value, $depth + 1, $warnings);
133+
}
134+
135+
return $clean;
136+
}
137+
138+
/**
139+
* @param mixed $value
140+
* @param array<int,string> $warnings
141+
* @return mixed
142+
*/
143+
private static function sanitizeValue($value, int $depth, array &$warnings)
144+
{
145+
if ($depth > self::MAX_DEPTH) {
146+
throw new \RuntimeException('Custom-JSON ist zu tief verschachtelt.');
147+
}
148+
149+
if (is_array($value)) {
150+
if (array_is_list($value)) {
151+
$list = [];
152+
foreach ($value as $item) {
153+
$list[] = self::sanitizeValue($item, $depth + 1, $warnings);
154+
}
155+
156+
return $list;
157+
}
158+
159+
return self::sanitizeObject($value, $depth + 1, $warnings);
160+
}
161+
162+
if (is_string($value)) {
163+
return trim($value);
164+
}
165+
166+
if (is_int($value) || is_float($value) || is_bool($value) || $value === null) {
167+
return $value;
168+
}
169+
170+
// Sollte mit json_decode(..., true) eigentlich nie auftreten, bleibt als Sicherheitsnetz.
171+
return (string) $value;
172+
}
173+
}

0 commit comments

Comments
 (0)