Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@

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

## v1.0.0.beta.19 (08. Juni 2026)

### Added
- Umfassende Rexstan-Qualitätsrunde über das gesamte Addon durchgeführt und final auf fehlerfreien Stand gebracht.
- Zusätzliche Härtung für JavaScript-Datenübergabe in der URL-Mapping-Konfiguration ergänzt: JSON-Werte werden jetzt vorab berechnet und mit sicheren Fallbacks ausgegeben, damit fehlerhafte Eingabedaten kein ungültiges Inline-Skript erzeugen.

### Changed
- Signaturen, Typangaben und Rückgabetypen in zentralen Klassen und Backend-Seiten konsolidiert, um Laufzeitverhalten und statische Analyse in Einklang zu bringen.
- JSON-Decode-/Encode-Grenzen in mehreren Flows vereinheitlicht und defensiver umgesetzt.
- Interne Generator-Pfade für dynamische JSON-LD-Ausgabe bereinigt und auf konsistente Payload-Erzeugung umgestellt.
- Backend-Controller- und Formularlogik für Artikel-, Dynamic-URL-, WebSite- und LocalBusiness-Seiten strukturell verbessert, ohne öffentliche API zu ändern.

### Fixed
- Mehrere potenzielle Laufzeitprobleme bei gemischten Datentypen behoben (insbesondere bei Mapping-, Config- und Vorschaupfaden).
- Fehlerhafte bzw. fragile JSON-LD-Skriptgenerierung in dynamischen Fällen korrigiert.
- Statische Analysewarnungen zu Array-Shapes, immer-wahren Bedingungen und uneindeutigen Typpfaden in relevanten Dateien beseitigt.
- Kontaktpunkt-Filterung im LocalBusiness-Flow so angepasst, dass nur tatsächlich leere Werte entfernt werden und der Rest stabil bleibt.

### Quality
- Vollständiger Rexstan-Lauf auf Addon-Ebene erfolgreich abgeschlossen: keine verbleibenden Fehler.
- Bestehende Funktionalität wurde dabei rückwärtskompatibel weitergeführt (kein BC-Break in Konfiguration oder Ausgabeformat beabsichtigt).

### Notes
- Diese Version ist ein technischer Stabilisierungs- und Qualitätsrelease, der die Grundlage für die nächsten Feature-Schritte verbessert.

## v1.0.0.beta.18 (08. Juni 2026)

### Fixed
Expand Down
77 changes: 36 additions & 41 deletions boot.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?php

use Url\Url;
use FriendsOfRedaxo\JsonLdManager\Frontend\Renderer;

/**
* JSON-LD Manager AddOn - Boot
*
Expand Down Expand Up @@ -27,17 +30,17 @@

$jsonLdOutput = '';
$dynamicJsonLdOutput = '';

// Prüfe ob es eine dynamische URL ist (URL-Addon)
if (rex_addon::get('url')->isAvailable()) {
try {
$urlManager = \Url\Url::resolveCurrent();
$urlManager = Url::resolveCurrent();

if ($urlManager) {
// Dynamische URL erkannt - JSON-LD für URL-Profil generieren
$profileId = $urlManager->getProfileId();
$dataId = $urlManager->getDatasetId();

if ($profileId && $dataId) {
$dynamicJsonLdOutput = generateDynamicJsonLd($profileId, $dataId);
}
Expand All @@ -46,36 +49,33 @@
// Fehler beim URL-Parsing ignorieren
}
}

// Standard JSON-LD immer zusätzlich ausgeben
$jsonLdOutput .= jsonld_render();
// Dynamisches URL-JSON-LD zusätzlich anhängen (falls vorhanden)
$jsonLdOutput .= $dynamicJsonLdOutput;

// Legacy-Meta-Daten ausgeben (nach letztem <meta ...> im <head>)
$legacyMeta = trim(rex_config::get('jsonld_manager', 'legacy_meta_raw', ''));
if ($legacyMeta !== '') {
// Nur ausgeben, wenn Template erlaubt ist (wie bei JSON-LD)
if (function_exists('jsonld_is_template_output_allowed') && jsonld_is_template_output_allowed($article)) {
// Suche alle <meta ...> im <head>
$headStart = stripos($content, '<head');
$headEnd = stripos($content, '</head>');
if ($headStart !== false && $headEnd !== false && $headEnd > $headStart) {
$headContent = substr($content, $headStart, $headEnd - $headStart);
// Finde alle <meta ...> Tags
preg_match_all('/<meta[^>]*>/i', $headContent, $metaMatches, PREG_OFFSET_CAPTURE);
if (!empty($metaMatches[0])) {
$lastMeta = end($metaMatches[0]);
$insertPos = $headStart + $lastMeta[1] + strlen($lastMeta[0]);
$content = substr($content, 0, $insertPos) . "\n" . $legacyMeta . "\n" . substr($content, $insertPos);
} else {
// Kein <meta> gefunden, vor </head> einfügen
$content = str_replace('</head>', $legacyMeta . "\n</head>", $content);
}
// Suche alle <meta ...> im <head>
$headStart = stripos($content, '<head');
$headEnd = stripos($content, '</head>');
if ($headStart !== false && $headEnd !== false && $headEnd > $headStart) {
$headContent = substr($content, $headStart, $headEnd - $headStart);
// Finde alle <meta ...> Tags
preg_match_all('/<meta[^>]*>/i', $headContent, $metaMatches, PREG_OFFSET_CAPTURE);
if (!empty($metaMatches[0])) {
$lastMeta = end($metaMatches[0]);
$insertPos = $headStart + $lastMeta[1] + strlen($lastMeta[0]);
$content = substr($content, 0, $insertPos) . "\n" . $legacyMeta . "\n" . substr($content, $insertPos);
} else {
// Kein <head> gefunden, vor </head> einfügen
$content = str_replace('</head>', $legacyMeta . "\n</head>", $content);
}
} else {
// Kein <head> gefunden, vor </head> einfügen
$content = str_replace('</head>', $legacyMeta . "\n</head>", $content);
}
}
if (!empty($jsonLdOutput)) {
Expand All @@ -87,7 +87,7 @@
}

// Extension Point für Cache-Invalidierung bei Artikel-Änderungen
rex_extension::register('ART_UPDATED', function($ep) {
rex_extension::register('ART_UPDATED', function($ep): void {
if (class_exists('\FriendsOfRedaxo\JsonLdManager\Frontend\Renderer')) {
$articleId = 0;
$params = $ep->getParams();
Expand All @@ -98,7 +98,7 @@
$articleId = (int) $params['article_id'];
}

\FriendsOfRedaxo\JsonLdManager\Frontend\Renderer::clearCache($articleId > 0 ? $articleId : null);
Renderer::clearCache($articleId > 0 ? $articleId : null);
}
});

Expand All @@ -109,21 +109,18 @@
rex_view::addCssFile(rex_url::addonAssets('jsonld_manager', 'css/jsonld_manager.css'));
rex_view::addJsFile(rex_url::addonAssets('jsonld_manager', 'js/jsonld_manager.js'));
}

$hideDynamicUrlsSubpage = static function (): void {
$filter = static function ($page) {
if (!$page || !method_exists($page, 'getSubpages') || !method_exists($page, 'setSubpages')) {
$filter = static function ($page): void {
if (!$page instanceof rex_be_page) {
return;
}

$subpages = $page->getSubpages();
if (!is_array($subpages)) {
return;
}

foreach ($subpages as $key => $subpage) {
$subpageKey = method_exists($subpage, 'getKey') ? (string) $subpage->getKey() : (string) $key;
$subpageFullKey = method_exists($subpage, 'getFullKey') ? (string) $subpage->getFullKey() : '';
$subpageKey = (string) $subpage->getKey();
$subpageFullKey = (string) $subpage->getFullKey();
if ($subpageKey === 'dynamic_urls' || $subpageFullKey === 'jsonld_manager/dynamic_urls') {
unset($subpages[$key]);
}
Expand All @@ -137,11 +134,9 @@

// Aktuelle Navigation (inkl. Parent) absichern
$current = rex_be_controller::getCurrentPageObject();
if ($current) {
if ($current instanceof rex_be_page) {
$filter($current);
if (method_exists($current, 'getParent')) {
$filter($current->getParent());
}
$filter($current->getParent());
}
};

Expand All @@ -158,13 +153,13 @@
rex_view::addCssFile(rex_url::addonAssets('jsonld_manager', 'css/hide_dynamic_urls_tab.css'));
}

rex_extension::register('PACKAGES_INCLUDED', function() use ($hideDynamicUrlsSubpage, $shouldHideDynamicUrls) {
rex_extension::register('PACKAGES_INCLUDED', function() use ($hideDynamicUrlsSubpage, $shouldHideDynamicUrls): void {
if ($shouldHideDynamicUrls()) {
$hideDynamicUrlsSubpage();
}
});

rex_extension::register('PAGE_PREPARED', function() use ($hideDynamicUrlsSubpage, $shouldHideDynamicUrls) {
rex_extension::register('PAGE_PREPARED', function() use ($hideDynamicUrlsSubpage, $shouldHideDynamicUrls): void {
if (!$shouldHideDynamicUrls()) {
return;
}
Expand All @@ -179,7 +174,7 @@
});
}

rex_extension::register('ART_DELETED', function($ep) {
rex_extension::register('ART_DELETED', function($ep): void {
if (class_exists('\FriendsOfRedaxo\JsonLdManager\Frontend\Renderer')) {
$articleId = 0;
$params = $ep->getParams();
Expand All @@ -190,6 +185,6 @@
$articleId = (int) $params['article_id'];
}

\FriendsOfRedaxo\JsonLdManager\Frontend\Renderer::clearCache($articleId > 0 ? $articleId : null);
Renderer::clearCache($articleId > 0 ? $articleId : null);
}
});
17 changes: 10 additions & 7 deletions lib/CustomJsonLdHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

namespace FriendsOfRedaxo\JsonLdManager;

use JsonException;
use RuntimeException;

class CustomJsonLdHelper
{
private const MAX_RAW_LENGTH = 30000;
private const MAX_DEPTH = 20;

/**
* @return array{raw:string,data:array,errors:array,warnings:array}
* @return array{raw:string,data:array<string, mixed>,errors:array<int, string>,warnings:array<int, string>}
*/
public static function parseCustomObject(string $rawJson): array
{
Expand All @@ -33,7 +36,7 @@ public static function parseCustomObject(string $rawJson): array

try {
$decoded = json_decode($rawJson, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
} catch (JsonException $e) {
return [
'raw' => $rawJson,
'data' => [],
Expand All @@ -54,7 +57,7 @@ public static function parseCustomObject(string $rawJson): array
$warnings = [];
try {
$sanitized = self::sanitizeObject($decoded, 0, $warnings);
} catch (\RuntimeException $e) {
} catch (RuntimeException $e) {
return [
'raw' => $rawJson,
'data' => [],
Expand Down Expand Up @@ -114,7 +117,7 @@ public static function mergeIntoSchema(array $schema, array $customData, array $
private static function sanitizeObject(array $object, int $depth, array &$warnings): array
{
if ($depth > self::MAX_DEPTH) {
throw new \RuntimeException('Custom-JSON ist zu tief verschachtelt.');
throw new RuntimeException('Custom-JSON ist zu tief verschachtelt.');
}

$clean = [];
Expand All @@ -138,12 +141,12 @@ private static function sanitizeObject(array $object, int $depth, array &$warnin
/**
* @param mixed $value
* @param array<int,string> $warnings
* @return mixed
* @return array<string, mixed>|array<int, mixed>|string|int|float|bool|null
*/
private static function sanitizeValue($value, int $depth, array &$warnings)
private static function sanitizeValue($value, int $depth, array &$warnings): array|string|int|float|bool|null
{
if ($depth > self::MAX_DEPTH) {
throw new \RuntimeException('Custom-JSON ist zu tief verschachtelt.');
throw new RuntimeException('Custom-JSON ist zu tief verschachtelt.');
}

if (is_array($value)) {
Expand Down
Loading