diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a4e8bb..fa94989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/boot.php b/boot.php index e720e56..3444ded 100644 --- a/boot.php +++ b/boot.php @@ -1,5 +1,8 @@ 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); } @@ -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 im ) $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 im - $headStart = stripos($content, ''); - if ($headStart !== false && $headEnd !== false && $headEnd > $headStart) { - $headContent = substr($content, $headStart, $headEnd - $headStart); - // Finde alle Tags - preg_match_all('/]*>/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 gefunden, vor einfügen - $content = str_replace('', $legacyMeta . "\n", $content); - } + // Suche alle im + $headStart = stripos($content, ''); + if ($headStart !== false && $headEnd !== false && $headEnd > $headStart) { + $headContent = substr($content, $headStart, $headEnd - $headStart); + // Finde alle Tags + preg_match_all('/]*>/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 gefunden, vor einfügen $content = str_replace('', $legacyMeta . "\n", $content); } + } else { + // Kein gefunden, vor einfügen + $content = str_replace('', $legacyMeta . "\n", $content); } } if (!empty($jsonLdOutput)) { @@ -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(); @@ -98,7 +98,7 @@ $articleId = (int) $params['article_id']; } - \FriendsOfRedaxo\JsonLdManager\Frontend\Renderer::clearCache($articleId > 0 ? $articleId : null); + Renderer::clearCache($articleId > 0 ? $articleId : null); } }); @@ -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]); } @@ -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()); } }; @@ -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; } @@ -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(); @@ -190,6 +185,6 @@ $articleId = (int) $params['article_id']; } - \FriendsOfRedaxo\JsonLdManager\Frontend\Renderer::clearCache($articleId > 0 ? $articleId : null); + Renderer::clearCache($articleId > 0 ? $articleId : null); } }); diff --git a/lib/CustomJsonLdHelper.php b/lib/CustomJsonLdHelper.php index b205234..88d4088 100644 --- a/lib/CustomJsonLdHelper.php +++ b/lib/CustomJsonLdHelper.php @@ -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,errors:array,warnings:array} */ public static function parseCustomObject(string $rawJson): array { @@ -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' => [], @@ -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' => [], @@ -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 = []; @@ -138,12 +141,12 @@ private static function sanitizeObject(array $object, int $depth, array &$warnin /** * @param mixed $value * @param array $warnings - * @return mixed + * @return array|array|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)) { diff --git a/lib/DomainConfig.php b/lib/DomainConfig.php index b7bf960..900a273 100644 --- a/lib/DomainConfig.php +++ b/lib/DomainConfig.php @@ -2,22 +2,30 @@ namespace FriendsOfRedaxo\JsonLdManager; +use rex_addon; +use rex_sql; +use rex; +use rex_sql_exception; +use rex_yrewrite; +use rex_url; + class DomainConfig { private const SESSION_KEY = 'jsonld_manager_active_domain_id'; + /** @return array> */ public static function getDomains(): array { // Prüfen ob YRewrite installiert und aktiv ist - if (!\rex_addon::get('yrewrite')->isAvailable()) { + if (!rex_addon::get('yrewrite')->isAvailable()) { return []; } - + try { - $sql = \rex_sql::factory(); - $sql->setQuery('SELECT id, domain, mount_id, start_id FROM ' . \rex::getTable('yrewrite_domain') . ' ORDER BY domain ASC'); + $sql = rex_sql::factory(); + $sql->setQuery('SELECT id, domain, mount_id, start_id FROM ' . rex::getTable('yrewrite_domain') . ' ORDER BY domain ASC'); return $sql->getArray(); - } catch (\rex_sql_exception $e) { + } catch (rex_sql_exception $e) { // Fallback wenn Tabelle nicht existiert oder leer ist return []; } @@ -33,17 +41,22 @@ public static function getActiveDomainId(): int // Prüfe URL-Parameter $requested = \rex_request('domain_id', 'int', 0); if ($requested > 0 && self::domainExists($requested)) { - if (\rex::isBackend()) { + if (rex::isBackend()) { \rex_set_session(self::SESSION_KEY, $requested); } return $requested; } // Frontend: ohne Session arbeiten (verhindert Fehler bei nicht eingeloggten Besuchern) - if (!\rex::isBackend()) { - if (\rex_addon::get('yrewrite')->isAvailable() && class_exists('rex_yrewrite')) { - $currentDomain = \rex_yrewrite::getCurrentDomain(); - if ($currentDomain && method_exists($currentDomain, 'getId')) { + if (!rex::isBackend()) { + if (rex_addon::get('yrewrite')->isAvailable() && class_exists('rex_yrewrite')) { + $currentDomain = rex_yrewrite::getCurrentDomain(); + if ($currentDomain instanceof \rex_yrewrite_domain) { + $currentDomainId = (int) $currentDomain->getId(); + if ($currentDomainId > 0) { + return $currentDomainId; + } + } elseif (is_object($currentDomain) && method_exists($currentDomain, 'getId')) { $currentDomainId = (int) $currentDomain->getId(); if ($currentDomainId > 0) { return $currentDomainId; @@ -62,7 +75,7 @@ public static function getActiveDomainId(): int $domains = self::getDomains(); if (!empty($domains)) { $fallbackDomainId = (int) $domains[0]['id']; - if (\rex::isBackend()) { + if (rex::isBackend()) { \rex_set_session(self::SESSION_KEY, $fallbackDomainId); } return $fallbackDomainId; @@ -71,31 +84,32 @@ public static function getActiveDomainId(): int return 1; // Notfall-Fallback } + /** @return array|null */ public static function getActiveDomain(): ?array { $activeDomainId = self::getActiveDomainId(); $domains = self::getDomains(); - + foreach ($domains as $domain) { if ((int) $domain['id'] === $activeDomainId) { return $domain; } } - + return null; } public static function domainExists(int $domainId): bool { - if (!\rex_addon::get('yrewrite')->isAvailable()) { + if (!rex_addon::get('yrewrite')->isAvailable()) { return false; } - $sql = \rex_sql::factory(); + $sql = rex_sql::factory(); try { - $sql->setQuery('SELECT id FROM ' . \rex::getTable('yrewrite_domain') . ' WHERE id = ?', [$domainId]); + $sql->setQuery('SELECT id FROM ' . rex::getTable('yrewrite_domain') . ' WHERE id = ?', [$domainId]); return $sql->getRows() > 0; - } catch (\rex_sql_exception $e) { + } catch (rex_sql_exception $e) { return false; } } @@ -126,8 +140,8 @@ public static function renderDomainSelect(int $activeDomainId): string $options .= ''; } - $currentUrl = \rex_url::currentBackendPage(); - + $currentUrl = rex_url::currentBackendPage(); + return '
@@ -167,7 +181,7 @@ public static function getBaseUrl(?int $domainId = null): string { $domain = null; - if (\rex_addon::get('yrewrite')->isAvailable()) { + if (rex_addon::get('yrewrite')->isAvailable()) { $domains = self::getDomains(); if ($domainId !== null) { @@ -189,7 +203,7 @@ public static function getBaseUrl(?int $domainId = null): string return self::normalizeBaseUrl($domain); } - return rtrim((string) \rex::getServer(), '/'); + return rtrim((string) rex::getServer(), '/'); } private static function normalizeBaseUrl(string $domain): string diff --git a/lib/DynamicJsonLd.php b/lib/DynamicJsonLd.php index e4b82b3..5b51d6e 100644 --- a/lib/DynamicJsonLd.php +++ b/lib/DynamicJsonLd.php @@ -5,15 +5,22 @@ /** * Dynamisches JSON-LD für URL-Profile generieren * - * @param int $profileId ID des URL-Profils - * @param int $dataId ID des Datensatzes + * @param int|string $profileId ID des URL-Profils + * @param int|string $dataId ID des Datensatzes * @return string JSON-LD Script oder leer */ -function generateDynamicJsonLd($profileId, $dataId) { +function generateDynamicJsonLd(int|string $profileId, int|string $dataId): string { try { + $profileId = (int) $profileId; + $dataId = (int) $dataId; + + if ($profileId <= 0 || $dataId <= 0) { + return ''; + } + // URL-Profil laden - $profile = rex_sql::factory()->getArray( - 'SELECT * FROM ' . rex::getTable('url_generator_profile') . ' WHERE id = ?', + $profile = \rex_sql::factory()->getArray( + 'SELECT * FROM ' . \rex::getTable('url_generator_profile') . ' WHERE id = ?', [$profileId] ); @@ -23,8 +30,8 @@ function generateDynamicJsonLd($profileId, $dataId) { $profile = $profile[0]; // Mapping für dieses Profil laden - $mapping = rex_sql::factory()->getArray( - 'SELECT * FROM ' . rex::getTable('jsonld_url_profile_mappings') . ' WHERE url_profile_id = ? AND active = 1', + $mapping = \rex_sql::factory()->getArray( + 'SELECT * FROM ' . \rex::getTable('jsonld_url_profile_mappings') . ' WHERE url_profile_id = ? AND active = 1', [$profileId] ); @@ -35,16 +42,20 @@ function generateDynamicJsonLd($profileId, $dataId) { // Datensatz aus YForm-Tabelle laden $yformTableName = null; - if ($profile['table_parameters']) { + if (!empty($profile['table_parameters']) && is_string($profile['table_parameters'])) { $tableParams = json_decode($profile['table_parameters'], true); if ($tableParams && !empty($tableParams['table_name'])) { - $yformTableName = $tableParams['table_name']; + $tableName = $tableParams['table_name']; + if (is_string($tableName)) { + $yformTableName = $tableName; + } } } // Fallback: Korrigiere Tabellenname (entferne 1_xxx_ Prefix) if (!$yformTableName) { - $yformTableName = str_replace('1_xxx_', '', $profile['table_name']); + $tableName = $profile['table_name'] ?? ''; + $yformTableName = str_replace('1_xxx_', '', (string) $tableName); } if (!isValidTableName($yformTableName)) { @@ -52,7 +63,7 @@ function generateDynamicJsonLd($profileId, $dataId) { } // WICHTIG: YForm-Tabellenname bereits mit rex_ Prefix - nicht nochmal durch getTable() hinzufügen! - $dataRow = rex_sql::factory()->getArray( + $dataRow = \rex_sql::factory()->getArray( 'SELECT * FROM ' . $yformTableName . ' WHERE id = ?', [$dataId] ); @@ -63,7 +74,10 @@ function generateDynamicJsonLd($profileId, $dataId) { $dataRow = $dataRow[0]; // Field-Mappings anwenden - $fieldMappings = json_decode($mapping['field_mappings'], true); + $fieldMappings = []; + if (isset($mapping['field_mappings']) && is_string($mapping['field_mappings'])) { + $fieldMappings = json_decode($mapping['field_mappings'], true) ?: []; + } if (!$fieldMappings) { return ''; } @@ -75,7 +89,7 @@ function generateDynamicJsonLd($profileId, $dataId) { ]; // Aktuelle URL als @id verwenden - $requestPath = (string) rex_server('REQUEST_URI', 'string', '/'); + $requestPath = (string) \rex_server('REQUEST_URI', 'string', '/'); $requestPath = '/' . ltrim(parse_url($requestPath, PHP_URL_PATH) ?: '', '/'); $baseUrl = DomainConfig::getBaseUrl(); $schema['@id'] = rtrim($baseUrl, '/') . $requestPath; @@ -98,10 +112,10 @@ function generateDynamicJsonLd($profileId, $dataId) { // Spezial-Behandlung für Bilder if (in_array($schemaProperty, ['image', 'photo']) && !empty($value)) { - if (rex_addon::get('yrewrite')->isAvailable()) { - $schema[$schemaProperty] = rex_yrewrite::getFullPath('/media/' . $value); + if (\rex_addon::get('yrewrite')->isAvailable() && class_exists('rex_yrewrite')) { + $schema[$schemaProperty] = \rex_yrewrite::getFullPath('/media/' . $value); } else { - $schema[$schemaProperty] = rex_url::frontend('media/' . $value); + $schema[$schemaProperty] = \rex_url::frontend('media/' . $value); } } else { $schema[$schemaProperty] = $value; @@ -121,10 +135,10 @@ function generateDynamicJsonLd($profileId, $dataId) { // Spezial-Behandlung für Bilder if (in_array($schemaProperty, ['image', 'photo']) && !empty($value)) { - if (rex_addon::get('yrewrite')->isAvailable()) { - $schema[$schemaProperty] = rex_yrewrite::getFullPath('/media/' . $value); + if (\rex_addon::get('yrewrite')->isAvailable() && class_exists('rex_yrewrite')) { + $schema[$schemaProperty] = \rex_yrewrite::getFullPath('/media/' . $value); } else { - $schema[$schemaProperty] = rex_url::frontend('media/' . $value); + $schema[$schemaProperty] = \rex_url::frontend('media/' . $value); } } else { $schema[$schemaProperty] = $value; @@ -137,21 +151,25 @@ function generateDynamicJsonLd($profileId, $dataId) { $meta = [ 'article_id' => 0, - 'clang_id' => (int) rex_clang::getCurrentId(), + 'clang_id' => (int) \rex_clang::getCurrentId(), 'branch_id' => null, 'types' => [$mapping['schema_type']], 'dynamic_profile_id' => (int) $profileId, 'dynamic_data_id' => (int) $dataId ]; - return JsonLdGenerator::renderPayloadScript( - $schema, - function_exists('jsonld_is_debug_enabled') && jsonld_is_debug_enabled(), - $meta - ); + $payload = JsonLdGenerator::buildPayload([$schema]); + $json = JsonLdGenerator::encodePayload($payload); + + $html = '' . "\n"; + if (function_exists('jsonld_is_debug_enabled') && jsonld_is_debug_enabled() && function_exists('jsonld_render_debug_overlay_script')) { + $html .= jsonld_render_debug_overlay_script($payload, $meta); + } + + return $html; - } catch (Exception $e) { - if (rex::isDebugMode()) { + } catch (\Exception $e) { + if (\rex::isDebugMode()) { return '' . "\n"; } return ''; @@ -164,7 +182,7 @@ function isValidTableName(?string $tableName): bool { } namespace { - function generateDynamicJsonLd($profileId, $dataId) { + function generateDynamicJsonLd(int|string $profileId, int|string $dataId): string { return \FriendsOfRedaxo\JsonLdManager\generateDynamicJsonLd($profileId, $dataId); } } diff --git a/lib/Frontend/Renderer.php b/lib/Frontend/Renderer.php index 0ca38ee..fd6abb5 100644 --- a/lib/Frontend/Renderer.php +++ b/lib/Frontend/Renderer.php @@ -2,6 +2,16 @@ namespace FriendsOfRedaxo\JsonLdManager\Frontend; +use rex_article; +use rex_addon; +use FriendsOfRedaxo\JsonLdManager\JsonLdGenerator; +use rex_cache; +use Exception; +use FriendsOfRedaxo\JsonLdManager\Url\RuleEngine; +use rex_sql; +use rex; +use FriendsOfRedaxo\JsonLdManager\Mapping\DataSourceExtended; + /** * JSON-LD Renderer - Frontend API * @@ -14,34 +24,30 @@ */ class Renderer { - /** - * @var array Cache für generierte JSON-LD Daten - */ - private static $cache = []; - - /** - * @var array Debug-Informationen sammeln - */ - private static $debugInfo = []; - + /** @var array Cache für generierte JSON-LD Daten */ + private static array $cache = []; + + /** @var array> Debug-Informationen sammeln */ + private static array $debugInfo = []; + /** * JSON-LD für aktuelle Seite ausgeben * - * @param string|null $schemaType Spezifischer Schema-Type (optional) - * @param array $additionalData Zusätzliche Daten für Mapping (optional) + * @param string|null $schemaType Spezifischer Schema-Type (optional) + * @param array $additionalData Zusätzliche Daten für Mapping (optional) * @return string JSON-LD als formatierter String */ - public static function render($schemaType = null, $additionalData = []) + public static function render(?string $schemaType = null, array $additionalData = []): string { try { - $article = \rex_article::getCurrent(); + $article = rex_article::getCurrent(); if (!$article) { return ''; } - $addon = \rex_addon::get('jsonld_manager'); + $addon = rex_addon::get('jsonld_manager'); $isDebugMode = self::getSetting($addon, 'debug_mode', false); - $branchIds = \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::resolveBranchIdsForArticle( + $branchIds = JsonLdGenerator::resolveBranchIdsForArticle( (int) $article->getId(), (int) $article->getClangId() ); @@ -52,7 +58,7 @@ public static function render($schemaType = null, $additionalData = []) return self::$cache[$cacheKey]; } - $articleOutput = \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::getArticleOutput( + $articleOutput = JsonLdGenerator::getArticleOutput( (int) $article->getId(), null, $isDebugMode, @@ -71,13 +77,13 @@ public static function render($schemaType = null, $additionalData = []) if (self::getSetting($addon, 'cache_enabled', true)) { self::$cache[$cacheKey] = $output; if (class_exists('\rex_cache')) { - \rex_cache::set('jsonld_manager', 'article_'.$article->getId().'_'.$article->getClangId(), $output); + rex_cache::set('jsonld_manager', 'article_'.$article->getId().'_'.$article->getClangId(), $output); } } if ($isDebugMode) { self::addDebugInfo('render_success', 'JSON-LD erfolgreich generiert', [ - 'items_count' => count($articleOutput['items'] ?? []), + 'items_count' => count($articleOutput['items']), 'output_length' => strlen($output), 'cached' => self::getSetting($addon, 'cache_enabled', true), 'branch_ids' => $articleOutput['branch_ids'], @@ -86,8 +92,8 @@ public static function render($schemaType = null, $additionalData = []) } return $output; - } catch (\Exception $e) { - if (self::getSetting(\rex_addon::get('jsonld_manager'), 'debug_mode', false)) { + } catch (Exception $e) { + if (self::getSetting(rex_addon::get('jsonld_manager'), 'debug_mode', false)) { self::addDebugInfo('render_error', 'JSON-LD Fehler', [ 'error_message' => $e->getMessage(), 'error_file' => $e->getFile(), @@ -98,36 +104,37 @@ public static function render($schemaType = null, $additionalData = []) return ''; } } - + /** * JSON-LD für aktuelle Seite automatisch generieren (für Extension Point) * * @return string JSON-LD Output */ - public static function getCurrentPageJsonLd() + public static function getCurrentPageJsonLd(): string { // URL-Regeln prüfen - $urlRuleData = \FriendsOfRedaxo\JsonLdManager\Url\RuleEngine::matchCurrentUrl(); - + $urlRuleData = RuleEngine::matchCurrentUrl(); + if ($urlRuleData) { return self::render($urlRuleData['schema_type'], $urlRuleData['data']); } - + // Standard-Rendering return self::render(); } - + /** * Schema-Konfigurationen für Artikel ermitteln * * @param int $articleId Artikel-ID * @param int $clangId Sprach-ID * @param string|null $schemaType Gewünschter Schema-Type - * @return array Array von Schema-Konfigurationen + * @return array, priority: mixed}> Array von Schema-Konfigurationen */ - private static function getArticleSchemas($articleId, $clangId, $schemaType = null) + // @phpstan-ignore-next-line bewusst als interne Reserve-API vorhanden + private static function getArticleSchemas(int $articleId, int $clangId, ?string $schemaType = null): array { - $isDebugMode = self::getSetting(\rex_addon::get('jsonld_manager'), 'debug_mode', false); + $isDebugMode = self::getSetting(rex_addon::get('jsonld_manager'), 'debug_mode', false); if ($isDebugMode) { self::addDebugInfo('schema_load', 'Lade Schema-Konfigurationen', [ 'article_id' => $articleId, @@ -135,26 +142,27 @@ private static function getArticleSchemas($articleId, $clangId, $schemaType = nu 'requested_schema_type' => $schemaType ]); } - - $sql = \rex_sql::factory(); - + + $sql = rex_sql::factory(); + $where = 'article_id = ? AND clang_id = ? AND active = 1'; $params = [$articleId, $clangId]; - + if ($schemaType) { $where .= ' AND schema_type = ?'; $params[] = $schemaType; } - + $sql->setQuery(' - SELECT * FROM '.\rex::getTable('jsonld_schemas').' + SELECT * FROM '.rex::getTable('jsonld_schemas').' WHERE '.$where.' ORDER BY priority ASC ', $params); - + $schemas = []; while ($sql->hasNext()) { - $config = json_decode($sql->getValue('config'), true) ?: []; + $configRaw = $sql->getValue('config'); + $config = is_string($configRaw) ? (json_decode($configRaw, true) ?: []) : []; $schemas[] = [ 'id' => $sql->getValue('id'), 'type' => $sql->getValue('schema_type'), @@ -163,17 +171,17 @@ private static function getArticleSchemas($articleId, $clangId, $schemaType = nu ]; $sql->next(); } - + if ($isDebugMode) { self::addDebugInfo('schema_load', 'Schema-Konfigurationen geladen', [ 'found_schemas' => count($schemas), 'schema_types' => array_column($schemas, 'type') ]); } - + return $schemas; } - + /** * Aktive LocalBusiness Branch für Artikel laden * @@ -181,21 +189,24 @@ private static function getArticleSchemas($articleId, $clangId, $schemaType = nu * @param int $clangId Sprach-ID * @return array|null LocalBusiness Schema oder null */ - private static function getActiveLocalBusinessBranch($articleId, $clangId) + /** @return array|null */ + // @phpstan-ignore-next-line bewusst als interne Reserve-API vorhanden + private static function getActiveLocalBusinessBranch(int $articleId, int $clangId): ?array { // Artikel-Schema laden um LocalBusiness Branch ID zu finden - $sql = \rex_sql::factory(); + $sql = rex_sql::factory(); $sql->setQuery(' - SELECT config FROM '.\rex::getTable('jsonld_schemas').' + SELECT config FROM '.rex::getTable('jsonld_schemas').' WHERE article_id = ? AND clang_id = ? AND schema_type = "WebPage" AND active = 1 LIMIT 1 ', [$articleId, $clangId]); - + if ($sql->getRows() === 0) { return null; } - - $config = json_decode($sql->getValue('config'), true) ?: []; + + $configRaw = $sql->getValue('config'); + $config = is_string($configRaw) ? (json_decode($configRaw, true) ?: []) : []; $branchIds = $config['localbusiness_branch_ids'] ?? []; if (!is_array($branchIds)) { $branchIds = $branchIds ? [(int) $branchIds] : []; @@ -204,21 +215,21 @@ private static function getActiveLocalBusinessBranch($articleId, $clangId) $branchIds = [(int) $config['localbusiness_branch_id']]; } $branchId = (int) ($branchIds[0] ?? 0); - + if ($branchId <= 0) { return null; // Keine LocalBusiness Zuordnung } - + // LocalBusiness Branch laden und Status prüfen - $branchSql = \rex_sql::factory(); + $branchSql = rex_sql::factory(); $branchSql->setQuery(' - SELECT branch_name, config FROM '.\rex::getTable('jsonld_localbusiness_branches').' + SELECT branch_name, config FROM '.rex::getTable('jsonld_localbusiness_branches').' WHERE id = ? AND clang_id = ? ', [$branchId, (int) $clangId]); - + if ($branchSql->getRows() === 0) { // Branch nicht gefunden - $isDebugMode = self::getSetting(\rex_addon::get('jsonld_manager'), 'debug_mode', false); + $isDebugMode = self::getSetting(rex_addon::get('jsonld_manager'), 'debug_mode', false); if ($isDebugMode) { self::addDebugInfo('localbusiness_missing', 'LocalBusiness Branch nicht gefunden', [ 'article_id' => $articleId, @@ -231,14 +242,15 @@ private static function getActiveLocalBusinessBranch($articleId, $clangId) } return null; } - - $branchConfig = json_decode($branchSql->getValue('config'), true) ?: []; - + + $branchConfigRaw = $branchSql->getValue('config'); + $branchConfig = is_string($branchConfigRaw) ? (json_decode($branchConfigRaw, true) ?: []) : []; + // Prüfen ob LocalBusiness enabled ist - if (empty($branchConfig['enabled']) || !$branchConfig['enabled']) { + if (($branchConfig['enabled'] ?? false) !== true) { return null; } - + // LocalBusiness Schema aus Branch-Config generieren return [ 'type' => 'LocalBusiness', @@ -249,7 +261,7 @@ private static function getActiveLocalBusinessBranch($articleId, $clangId) 'priority' => 50 // Höhere Priorität als WebPage ]; } - + /** * LocalBusiness Mappings aus Branch-Konfiguration erstellen * @@ -259,29 +271,33 @@ private static function getActiveLocalBusinessBranch($articleId, $clangId) * @param array $branchConfig Branch-Konfiguration * @return array Schema.org LocalBusiness Mappings */ - private static function buildLocalBusinessMappings($branchConfig) + /** + * @param array $branchConfig + * @return array + */ + private static function buildLocalBusinessMappings(array $branchConfig): array { $mappings = [ '@context' => 'https://schema.org', '@type' => $branchConfig['businessType'] ?: 'LocalBusiness' ]; - + if (!empty($branchConfig['name'])) { $mappings['name'] = $branchConfig['name']; } - + if (!empty($branchConfig['slogan'])) { $mappings['slogan'] = $branchConfig['slogan']; } - + if (!empty($branchConfig['telephone'])) { $mappings['telephone'] = $branchConfig['telephone']; } - + if (!empty($branchConfig['priceRange'])) { $mappings['priceRange'] = $branchConfig['priceRange']; } - + // Address $address = []; if (!empty($branchConfig['streetAddress'])) { @@ -296,12 +312,12 @@ private static function buildLocalBusinessMappings($branchConfig) if (!empty($branchConfig['addressCountry'])) { $address['addressCountry'] = $branchConfig['addressCountry']; } - + if (!empty($address)) { $address['@type'] = 'PostalAddress'; $mappings['address'] = $address; } - + // Geo-Koordinaten if (!empty($branchConfig['geo']['latitude']) && !empty($branchConfig['geo']['longitude'])) { $mappings['geo'] = [ @@ -310,12 +326,12 @@ private static function buildLocalBusinessMappings($branchConfig) 'longitude' => $branchConfig['geo']['longitude'] ]; } - + // Öffnungszeiten if (!empty($branchConfig['openingHoursSpecification']) && is_array($branchConfig['openingHoursSpecification'])) { $mappings['openingHoursSpecification'] = $branchConfig['openingHoursSpecification']; } - + // Weitere Properties if (!empty($branchConfig['images'])) { $images = array_filter(array_map('trim', explode(',', $branchConfig['images']))); @@ -323,37 +339,39 @@ private static function buildLocalBusinessMappings($branchConfig) $mappings['image'] = count($images) === 1 ? $images[0] : $images; } } - + if (!empty($branchConfig['knowsLanguage']) && is_array($branchConfig['knowsLanguage'])) { $mappings['knowsLanguage'] = $branchConfig['knowsLanguage']; } - + if (!empty($branchConfig['paymentAccepted'])) { $mappings['paymentAccepted'] = $branchConfig['paymentAccepted']; } - + if (!empty($branchConfig['currenciesAccepted'])) { $mappings['currenciesAccepted'] = $branchConfig['currenciesAccepted']; } - + if (!empty($branchConfig['areaServed'])) { $mappings['areaServed'] = $branchConfig['areaServed']; } - + if (!empty($branchConfig['hasMap'])) { $mappings['hasMap'] = $branchConfig['hasMap']; } - + return $mappings; } - + /** * Standard WebPage Schema generieren * * @param rex_article $article REDAXO Artikel * @return array Schema-Konfiguration */ - private static function getDefaultWebPageSchema($article) + /** @return array */ + // @phpstan-ignore-next-line bewusst als interne Reserve-API vorhanden + private static function getDefaultWebPageSchema(rex_article $article): array { return [ 'type' => 'WebPage', @@ -370,7 +388,7 @@ private static function getDefaultWebPageSchema($article) 'priority' => 999 ]; } - + /** * Schema-Daten basierend auf Mapping generieren * @@ -379,39 +397,45 @@ private static function getDefaultWebPageSchema($article) * @param array $additionalData Zusätzliche Daten * @return array|null Generierte Schema-Daten */ - private static function generateSchemaData($schema, $article, $additionalData = []) + /** + * @param array $schema + * @param array $additionalData + * @return array|null + */ + // @phpstan-ignore-next-line bewusst als interne Reserve-API vorhanden + private static function generateSchemaData(array $schema, rex_article $article, array $additionalData = []): ?array { if (!isset($schema['config']['mappings'])) { return null; } - + $mappings = $schema['config']['mappings']; $data = []; - + foreach ($mappings as $property => $mapping) { if (is_array($mapping)) { if (isset($mapping['source'])) { // Property-Mapping - $value = \FriendsOfRedaxo\JsonLdManager\Mapping\DataSourceExtended::getValue( + $value = DataSourceExtended::getValue( $mapping['source'], $article, $additionalData ); - + // Fallback if (empty($value) && isset($mapping['fallback'])) { - $value = \FriendsOfRedaxo\JsonLdManager\Mapping\DataSourceExtended::getValue( + $value = DataSourceExtended::getValue( $mapping['fallback'], $article, $additionalData ); } - + // Transform if (!empty($value) && isset($mapping['transform'])) { - $value = \FriendsOfRedaxo\JsonLdManager\Mapping\DataSourceExtended::transformValue($value, $mapping['transform']); + $value = DataSourceExtended::transformValue($value, $mapping['transform']); } - + if (!empty($value)) { $data[$property] = $value; } elseif (isset($mapping['required']) && $mapping['required']) { @@ -430,30 +454,30 @@ private static function generateSchemaData($schema, $article, $additionalData = $data[$property] = $mapping; } } - + return !empty($data) ? $data : null; } - + /** * Verschachtelte Mappings verarbeiten * - * @param array $mapping Mapping-Konfiguration + * @param array $mapping Mapping-Konfiguration * @param rex_article $article REDAXO Artikel - * @param array $additionalData Zusätzliche Daten - * @return array|null Verarbeitete Daten + * @param array $additionalData Zusätzliche Daten + * @return array|null Verarbeitete Daten */ - private static function processNestedMapping($mapping, $article, $additionalData = []) + private static function processNestedMapping(array $mapping, rex_article $article, array $additionalData = []): ?array { $data = []; - + foreach ($mapping as $key => $value) { if (is_array($value) && isset($value['source'])) { - $resolvedValue = \FriendsOfRedaxo\JsonLdManager\Mapping\DataSourceExtended::getValue($value['source'], $article, $additionalData); - + $resolvedValue = DataSourceExtended::getValue($value['source'], $article, $additionalData); + if (empty($resolvedValue) && isset($value['fallback'])) { - $resolvedValue = \FriendsOfRedaxo\JsonLdManager\Mapping\DataSourceExtended::getValue($value['fallback'], $article, $additionalData); + $resolvedValue = DataSourceExtended::getValue($value['fallback'], $article, $additionalData); } - + if (!empty($resolvedValue)) { $data[$key] = $resolvedValue; } @@ -461,38 +485,38 @@ private static function processNestedMapping($mapping, $article, $additionalData $data[$key] = $value; } } - + return !empty($data) ? $data : null; } - + /** * Cache löschen * * @param int|null $articleId Spezifische Artikel-ID (optional) */ - public static function clearCache($articleId = null) + public static function clearCache(?int $articleId = null): void { if ($articleId) { if (class_exists('\rex_cache')) { - \rex_cache::delete('jsonld_manager', 'article_'.$articleId); + rex_cache::delete('jsonld_manager', 'article_'.$articleId); } } else { if (class_exists('\rex_cache')) { - \rex_cache::deleteNamespace('jsonld_manager'); + rex_cache::deleteNamespace('jsonld_manager'); } } - + self::$cache = []; } - + /** * Debug-Information hinzufügen * * @param string $type Debug-Typ * @param string $message Debug-Nachricht - * @param array $data Zusätzliche Daten + * @param array $data Zusätzliche Daten */ - private static function addDebugInfo($type, $message, $data = []) + private static function addDebugInfo(string $type, string $message, array $data = []): void { self::$debugInfo[] = [ 'timestamp' => microtime(true), @@ -501,30 +525,35 @@ private static function addDebugInfo($type, $message, $data = []) 'data' => $data ]; } - + /** * Debug-Info zur Browser-Console ausgeben * * @param string $message Debug-Nachricht - * @param array $data Zusätzliche Daten + * @param array $data Zusätzliche Daten */ - private static function outputConsoleDebug($message, $data = []) + private static function outputConsoleDebug(string $message, array $data = []): void { - // Console-Debug-Ausgabe deaktiviert - return; + if (!rex::isDebugMode()) { + return; + } + + \rex_logger::factory()->log(\Psr\Log\LogLevel::DEBUG, $message, [ + 'jsonld_renderer_data' => json_encode($data, JSON_UNESCAPED_UNICODE), + ]); } - + /** * Debug-Modal ausgeben (sollte vor eingebunden werden) * * @return string Debug-Modal HTML */ - public static function renderDebugModal() + public static function renderDebugModal(): string { - if (empty(self::$debugInfo) || !self::getSetting(\rex_addon::get('jsonld_manager'), 'debug_mode', false)) { + if (empty(self::$debugInfo) || !self::getSetting(rex_addon::get('jsonld_manager'), 'debug_mode', false)) { return ''; } - + $html = '
'; - + foreach (self::$debugInfo as $info) { - $time = date('H:i:s', $info['timestamp']) . substr($info['timestamp'], strpos($info['timestamp'], '.'), 4); + $timestamp = (float) $info['timestamp']; + $time = date('H:i:s', (int) $timestamp); + $timeString = (string) $timestamp; + $dotPos = strpos($timeString, '.'); + if (false !== $dotPos) { + $time .= substr($timeString, $dotPos, 4); + } $typeColor = self::getDebugTypeColor($info['type']); - + $html .= '
@@ -575,34 +610,35 @@ public static function renderDebugModal() ' . $time . '
' . htmlspecialchars($info['message']) . '
'; - + if (!empty($info['data'])) { + $encodedData = json_encode($info['data'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); $html .= '
Details
' . 
-                    htmlspecialchars(json_encode($info['data'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) . 
+                    htmlspecialchars(is_string($encodedData) ? $encodedData : '', ENT_QUOTES) . 
                     '
'; } - + $html .= '
'; } - + $html .= '
'; - + return $html; } - + /** * Farbe für Debug-Typ ermitteln * * @param string $type Debug-Typ * @return string Hex-Farbe */ - private static function getDebugTypeColor($type) + private static function getDebugTypeColor(string $type): string { $colors = [ 'render_start' => '#4CAF50', @@ -612,19 +648,19 @@ private static function getDebugTypeColor($type) 'schema_load' => '#FF9800', 'data_mapping' => '#9C27B0' ]; - + return $colors[$type] ?? '#FFC107'; } /** * Addon-Setting robust laden (unterstützt alte und neue Config-Struktur). * - * @param \rex_addon $addon + * @param \rex_addon_interface $addon * @param string $key * @param mixed $default * @return mixed */ - private static function getSetting(\rex_addon $addon, $key, $default = null) + private static function getSetting(\rex_addon_interface $addon, string $key, $default = null) { $global = $addon->getConfig('global_settings', []); if (isset($global['settings']) && array_key_exists($key, $global['settings'])) { @@ -638,22 +674,24 @@ private static function getSetting(\rex_addon $addon, $key, $default = null) * Effektive Branch-ID für Artikel ermitteln. * * @param int $articleId - * @return int|null + * @return array */ - private static function resolveBranchIdsForArticle($articleId, $clangId = 1) + // @phpstan-ignore-next-line bewusst als interne Reserve-API vorhanden + private static function resolveBranchIdsForArticle(int $articleId, int $clangId = 1): array { - return \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::resolveBranchIdsForArticle((int) $articleId, (int) $clangId); + return JsonLdGenerator::resolveBranchIdsForArticle($articleId, $clangId); } /** * Einheitliches JSON-LD Payload bauen. * - * @param array $jsonLdItems - * @return array + * @param array> $jsonLdItems + * @return array */ - private static function normalizeOutputPayload(array $jsonLdItems) + // @phpstan-ignore-next-line bewusst als interne Reserve-API vorhanden + private static function normalizeOutputPayload(array $jsonLdItems): array { - return \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::buildPayload($jsonLdItems); + return JsonLdGenerator::buildPayload($jsonLdItems); } } diff --git a/lib/JsonLdGenerator.php b/lib/JsonLdGenerator.php index 04c74ff..1e3747f 100644 --- a/lib/JsonLdGenerator.php +++ b/lib/JsonLdGenerator.php @@ -2,6 +2,16 @@ namespace FriendsOfRedaxo\JsonLdManager; +use rex_article; +use Exception; +use rex; +use RuntimeException; +use rex_config; +use rex_sql; +use rex_clang; +use rex_addon; +use rex_url; +use rex_yform_manager_dataset; use Url\Url; /** @@ -19,13 +29,13 @@ class JsonLdGenerator * Baut die komplette JSON-LD-Ausgabe fuer einen Artikel. * Diese Methode ist die gemeinsame Quelle fuer Backend-Vorschau, AJAX und Frontend. * - * @param int $articleId - * @param int|array|null $branchIds Override fuer LocalBusiness-Branch-IDs; null nutzt gespeicherte Auswahl/Fallback. + * @param int $articleId + * @param int|array|string|null $branchIds Override fuer LocalBusiness-Branch-IDs; null nutzt gespeicherte Auswahl/Fallback. * @param bool $isDebugMode * @param int|null $clangId - * @return array{disabled: bool, custom: bool, items: array, payload: ?array, json: string, branch_ids: array, meta: array, error: ?string} + * @return array{disabled: bool, custom: bool, items: array>, payload: array|null, json: string, branch_ids: array, meta: array, error: string|null} */ - public static function getArticleOutput($articleId, $branchIds = null, bool $isDebugMode = false, $clangId = null): array + public static function getArticleOutput(int $articleId, int|array|string|null $branchIds = null, bool $isDebugMode = false, mixed $clangId = null): array { $articleId = (int) $articleId; $effectiveClangId = self::normalizeClangId($clangId); @@ -45,7 +55,7 @@ public static function getArticleOutput($articleId, $branchIds = null, bool $isD 'error' => null, ]; - if ($articleId <= 0 || !\rex_article::get($articleId, $effectiveClangId)) { + if ($articleId <= 0 || !rex_article::get($articleId, $effectiveClangId)) { return $output; } @@ -69,7 +79,7 @@ public static function getArticleOutput($articleId, $branchIds = null, bool $isD $output['payload'] = $payload; $output['json'] = self::encodePayload($payload); $output['meta'] = self::buildDebugMeta($articleId, $effectiveClangId, $resolvedBranchIds, [], $payload, 'custom'); - } catch (\Exception $e) { + } catch (Exception $e) { $output['custom'] = true; $output['error'] = $e->getMessage(); } @@ -100,7 +110,7 @@ public static function getArticleOutput($articleId, $branchIds = null, bool $isD $meta['branch_names'] = $branchNames; } $output['meta'] = $meta; - } catch (\Exception $e) { + } catch (Exception $e) { $output['error'] = $e->getMessage(); } @@ -111,17 +121,17 @@ public static function getArticleOutput($articleId, $branchIds = null, bool $isD * Rendert die Artikel-Ausgabe als application/ld+json Script-Tag. * * @param int $articleId - * @param int|array|null $branchIds + * @param int|array|string|null $branchIds * @param bool $includeDebugOverlay * @param int|null $clangId */ - public static function renderArticleScript($articleId, $branchIds = null, bool $includeDebugOverlay = false, $clangId = null): string + public static function renderArticleScript(int $articleId, int|array|string|null $branchIds = null, bool $includeDebugOverlay = false, mixed $clangId = null): string { $output = self::getArticleOutput($articleId, $branchIds, $includeDebugOverlay, $clangId); if ($output['disabled'] || $output['json'] === '') { - if ($output['error'] && \rex::isDebugMode()) { + if ($output['error'] && rex::isDebugMode()) { return '' . "\n"; } return ''; @@ -130,7 +140,8 @@ public static function renderArticleScript($articleId, $branchIds = null, bool $ $html = '' . "\n"; if ($includeDebugOverlay && function_exists('jsonld_render_debug_overlay_script')) { - $html .= jsonld_render_debug_overlay_script($output['payload'], $output['meta']); + $payload = is_array($output['payload']) ? $output['payload'] : []; + $html .= jsonld_render_debug_overlay_script($payload, $output['meta']); } return $html; @@ -138,6 +149,9 @@ public static function renderArticleScript($articleId, $branchIds = null, bool $ /** * Einheitliches JSON-LD Payload bauen. + * + * @param array> $jsonLdItems + * @return array */ public static function buildPayload(array $jsonLdItems): array { @@ -153,6 +167,8 @@ public static function buildPayload(array $jsonLdItems): array /** * JSON-LD Payload einheitlich formatieren. + * + * @param array $payload */ public static function encodePayload(array $payload): string { @@ -167,7 +183,7 @@ public static function encodePayload(array $payload): string | JSON_HEX_QUOT ); if ($json === false || json_last_error() !== JSON_ERROR_NONE) { - throw new \RuntimeException('JSON-LD Encoding Error: ' . json_last_error_msg()); + throw new RuntimeException('JSON-LD Encoding Error: ' . json_last_error_msg()); } return $json; @@ -203,7 +219,7 @@ public static function pruneEmptyValues($value) return $value; } - public static function getArticleBranchKey($articleId, $clangId = null): string + public static function getArticleBranchKey(int $articleId, mixed $clangId = null): string { $clangId = self::normalizeClangId($clangId); if (class_exists(__NAMESPACE__ . '\\DomainConfig') && DomainConfig::isMultiDomain()) { @@ -213,7 +229,7 @@ public static function getArticleBranchKey($articleId, $clangId = null): string return 'article_branch_' . (int) $articleId . '_clang_' . $clangId; } - public static function getDisableJsonKey($articleId, $clangId = null): string + public static function getDisableJsonKey(int $articleId, mixed $clangId = null): string { $clangId = self::normalizeClangId($clangId); if (class_exists(__NAMESPACE__ . '\\DomainConfig') && DomainConfig::isMultiDomain()) { @@ -223,7 +239,7 @@ public static function getDisableJsonKey($articleId, $clangId = null): string return 'disable_json_' . (int) $articleId . '_clang_' . $clangId; } - public static function getCustomJsonKey($articleId, $clangId = null): string + public static function getCustomJsonKey(int $articleId, mixed $clangId = null): string { $clangId = self::normalizeClangId($clangId); if (class_exists(__NAMESPACE__ . '\\DomainConfig') && DomainConfig::isMultiDomain()) { @@ -233,41 +249,42 @@ public static function getCustomJsonKey($articleId, $clangId = null): string return 'custom_json_' . (int) $articleId . '_clang_' . $clangId; } - public static function isArticleJsonDisabled($articleId, $clangId = null): bool + public static function isArticleJsonDisabled(int $articleId, mixed $clangId = null): bool { - return (bool) \rex_config::get('jsonld_manager', self::getDisableJsonKey($articleId, $clangId), false); + return (bool) rex_config::get('jsonld_manager', self::getDisableJsonKey($articleId, $clangId), false); } - public static function getCustomJson($articleId, $clangId = null): string + public static function getCustomJson(int $articleId, mixed $clangId = null): string { - return (string) \rex_config::get('jsonld_manager', self::getCustomJsonKey($articleId, $clangId), ''); + return (string) rex_config::get('jsonld_manager', self::getCustomJsonKey($articleId, $clangId), ''); } - public static function hasCustomJson($articleId, $clangId = null): bool + public static function hasCustomJson(int $articleId, mixed $clangId = null): bool { return trim(self::getCustomJson($articleId, $clangId)) !== ''; } - public static function getArticleBranchIds($articleId, $clangId = null): array + /** @return array */ + public static function getArticleBranchIds(int $articleId, mixed $clangId = null): array { $clangId = self::normalizeClangId($clangId); - $branchIds = self::normalizeBranchIds(\rex_config::get('jsonld_manager', self::getArticleBranchKey($articleId, $clangId), [])); + $branchIds = self::normalizeBranchIds(rex_config::get('jsonld_manager', self::getArticleBranchKey($articleId, $clangId), [])); if (!empty($branchIds)) { return $branchIds; } try { - $sql = \rex_sql::factory(); + $sql = rex_sql::factory(); $clangCandidates = array_values(array_unique(array_filter([ (int) $clangId, - (int) \rex_clang::getStartId(), - ], static function ($id) { + (int) rex_clang::getStartId(), + ], static function (int $id): bool { return $id > 0; }))); foreach ($clangCandidates as $candidateClangId) { $sql->setQuery( - 'SELECT config FROM ' . \rex::getTable('jsonld_schemas') . ' WHERE article_id = ? AND clang_id = ? AND schema_type = "WebPage" AND active = 1 LIMIT 1', + 'SELECT config FROM ' . rex::getTable('jsonld_schemas') . ' WHERE article_id = ? AND clang_id = ? AND schema_type = "WebPage" AND active = 1 LIMIT 1', [(int) $articleId, (int) $candidateClangId] ); @@ -286,7 +303,7 @@ public static function getArticleBranchIds($articleId, $clangId = null): array return $schemaBranchIds; } } - } catch (\Exception $e) { + } catch (Exception $e) { // Fallback unten greift } @@ -294,11 +311,10 @@ public static function getArticleBranchIds($articleId, $clangId = null): array } /** - * Ermittelt die effektiven Branch-IDs fuer Backend und Frontend identisch. - * - * @param int|array|null $branchIds + * @param int|array|string|null $branchIds + * @return array */ - public static function resolveBranchIdsForArticle($articleId, $clangId = null, $branchIds = null): array + public static function resolveBranchIdsForArticle(int $articleId, mixed $clangId = null, int|array|string|null $branchIds = null): array { $clangId = self::normalizeClangId($clangId); if ($branchIds !== null) { @@ -321,21 +337,25 @@ public static function resolveBranchIdsForArticle($articleId, $clangId = null, $ * @param bool $isDebugMode Debug-Ausgabe aktiviert * @return array JSON-LD Array nach Schema.org Best Practice Reihenfolge */ - public static function generateForArticle($articleId, $branchId = null, $isDebugMode = false, $clangId = null) + /** + * @param int|array|string|null $branchId + * @return array> + */ + public static function generateForArticle(int $articleId, int|array|string|null $branchId = null, bool $isDebugMode = false, mixed $clangId = null): array { if (!$articleId) return []; $jsonLdItems = []; $effectiveClangId = self::normalizeClangId($clangId); - $currentArticle = \rex_article::get($articleId, $effectiveClangId); + $currentArticle = rex_article::get($articleId, $effectiveClangId); if (!$currentArticle) return []; - $addon = \rex_addon::get('jsonld_manager'); + $addon = rex_addon::get('jsonld_manager'); // Konfigurationen laden $websiteConfig = self::getGlobalSchemaConfig($addon, 'website_schema', $effectiveClangId, []); $organizationConfig = self::getGlobalSchemaConfig($addon, 'organization_schema', $effectiveClangId, []); - $localBusinessConfig = \FriendsOfRedaxo\JsonLdManager\LanguageConfig::getLocalizedConfig($addon, 'localbusiness_schema', $effectiveClangId, []); + $localBusinessConfig = LanguageConfig::getLocalizedConfig($addon, 'localbusiness_schema', $effectiveClangId, []); $branchIds = self::normalizeBranchIds($branchId); $branchConfigs = self::loadBranchConfigs($branchIds, $effectiveClangId, $localBusinessConfig, $isDebugMode); @@ -375,7 +395,7 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug $organizationSchema = [ '@context' => 'https://schema.org', '@type' => 'Organization', - '@id' => rtrim(\rex::getServer(), '/') . '/#organization', + '@id' => rtrim(rex::getServer(), '/') . '/#organization', 'name' => $organizationConfig['name'] ]; @@ -431,13 +451,14 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug // 2. WebSite Schema (Website der Organisation) if (!empty($websiteConfig['name'])) { + $websiteClang = rex_clang::get($effectiveClangId); $websiteSchema = [ '@context' => 'https://schema.org', '@type' => 'WebSite', '@id' => self::getWebsiteUrl() . '/#website', 'name' => $websiteConfig['name'], 'url' => self::getWebsiteUrl() . '/', - 'inLanguage' => \rex_clang::get($effectiveClangId)->getCode() + 'inLanguage' => $websiteClang ? $websiteClang->getCode() : 'de' ]; if (!empty($websiteConfig['description'])) { @@ -462,7 +483,7 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug // Verbindung zur Organization if (!empty($organizationConfig['name'])) { $websiteSchema['publisher'] = [ - '@id' => rtrim(\rex::getServer(), '/') . '/#organization' + '@id' => rtrim(rex::getServer(), '/') . '/#organization' ]; } @@ -486,7 +507,7 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug self::debugLog('DYNAMIC URL CHECK', [ 'dynamic_mapping_found' => $dynamicUrlMapping !== null, 'current_url' => $_SERVER['REQUEST_URI'] ?? 'unknown', - 'url_addon_available' => \rex_addon::get('url')->isAvailable() + 'url_addon_available' => rex_addon::get('url')->isAvailable() ]); } @@ -510,11 +531,12 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug } } else { // Standard WebPage Schema + $webpageClang = rex_clang::get($effectiveClangId); $webPageSchema = [ '@context' => 'https://schema.org', '@type' => 'WebPage', 'url' => self::getArticleUrl($articleId), - 'inLanguage' => \rex_clang::get($effectiveClangId)->getCode() + 'inLanguage' => $webpageClang ? $webpageClang->getCode() : 'de' ]; // Name aus Konfiguration @@ -523,16 +545,16 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug $webPageName = ''; if ($nameField === 'yrewrite_title') { - $webPageName = trim($currentArticle->getValue('yrewrite_title') ?: ''); + $webPageName = trim((string) ($currentArticle->getValue('yrewrite_title') ?: '')); } else { - $webPageName = trim($currentArticle->getValue($nameField) ?: ''); + $webPageName = trim((string) ($currentArticle->getValue($nameField) ?: '')); } if (empty($webPageName)) { if ($fallbackNameField === 'name') { $webPageName = $currentArticle->getName(); } else { - $webPageName = $currentArticle->getValue($fallbackNameField); + $webPageName = (string) $currentArticle->getValue($fallbackNameField); } } $webPageSchema['name'] = $webPageName ?: 'Artikel'; @@ -543,13 +565,13 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug $webPageDesc = ''; if ($descField === 'yrewrite_description') { - $webPageDesc = trim($currentArticle->getValue('yrewrite_description') ?: ''); + $webPageDesc = trim((string) ($currentArticle->getValue('yrewrite_description') ?: '')); } else { - $webPageDesc = trim($currentArticle->getValue($descField) ?: ''); + $webPageDesc = trim((string) ($currentArticle->getValue($descField) ?: '')); } if (empty($webPageDesc) && $fallbackDescField) { - $webPageDesc = trim($currentArticle->getValue($fallbackDescField) ?: ''); + $webPageDesc = trim((string) ($currentArticle->getValue($fallbackDescField) ?: '')); } if (!empty($webPageDesc)) { @@ -560,14 +582,14 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug if ($articleConfig['include_ispartof'] && !empty($websiteConfig['name'])) { $webPageSchema['isPartOf'] = [ '@type' => 'WebSite', - '@id' => rtrim(\rex::getServer(), '/') . '/#website' + '@id' => rtrim(rex::getServer(), '/') . '/#website' ]; } // about (Verbindung zur Organization) if ($articleConfig['include_about'] && !empty($organizationConfig['name'])) { $webPageSchema['about'] = [ - '@id' => rtrim(\rex::getServer(), '/') . '/#organization' + '@id' => rtrim(rex::getServer(), '/') . '/#organization' ]; } @@ -578,10 +600,11 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug if (!empty($imageValue)) { $imageUrl = ''; - if (strpos($imageValue, 'http') === 0) { - $imageUrl = $imageValue; + $imageValueString = (string) $imageValue; + if (strpos($imageValueString, 'http') === 0) { + $imageUrl = $imageValueString; } else { - $imageUrl = self::getWebsiteUrl() . '/media/' . $imageValue; + $imageUrl = self::getWebsiteUrl() . '/media/' . $imageValueString; } $webPageSchema['primaryImageOfPage'] = [ @@ -598,9 +621,12 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug if (!empty($dateValue) && $dateValue !== '0000-00-00 00:00:00') { if (is_numeric($dateValue)) { - $webPageSchema['dateModified'] = date('Y-m-d', $dateValue); + $webPageSchema['dateModified'] = date('Y-m-d', (int) $dateValue); } else { - $webPageSchema['dateModified'] = date('Y-m-d', strtotime($dateValue)); + $timestamp = strtotime((string) $dateValue); + if (false !== $timestamp) { + $webPageSchema['dateModified'] = date('Y-m-d', $timestamp); + } } } } @@ -658,7 +684,7 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug // 5. BreadcrumbList Schema (Navigation) $breadcrumbs = []; $position = 1; - $startArticleId = \rex_article::getSiteStartArticleId(); + $startArticleId = rex_article::getSiteStartArticleId(); // Startseite immer als erstes Element $breadcrumbs[] = [ @@ -715,7 +741,7 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug self::debugLog('BreadcrumbList Schema hinzugefügt', ['items' => count($breadcrumbs)]); self::debugLog('JSON-LD Generation abgeschlossen', [ 'total_schemas' => count($jsonLdItems), - 'schemas' => array_map(function($item) { return $item['@type']; }, $jsonLdItems), + 'schemas' => array_map(function(array $item) { return $item['@type']; }, $jsonLdItems), 'branch_id_used' => $primaryBranchId, 'branch_ids_used' => $branchIds ]); @@ -730,12 +756,20 @@ public static function generateForArticle($articleId, $branchId = null, $isDebug * @param string $message * @param array $data */ - private static function debugLog($message, $data = []) + /** @param array $data */ + private static function debugLog(string $message, array $data = []): void { - return; + if (!rex::isDebugMode()) { + return; + } + + \rex_logger::factory()->log(\Psr\Log\LogLevel::DEBUG, $message, [ + 'jsonld_data' => json_encode($data, JSON_UNESCAPED_UNICODE), + ]); } - public static function normalizeBranchIds($branchId): array + /** @return array */ + public static function normalizeBranchIds(mixed $branchId): array { if (is_array($branchId)) { $branchIds = $branchId; @@ -753,12 +787,13 @@ public static function normalizeBranchIds($branchId): array } $branchIds = array_map('intval', $branchIds); - return array_values(array_unique(array_filter($branchIds, static function ($id) { + return array_values(array_unique(array_filter($branchIds, static function (int $id): bool { return $id > 0; }))); } - private static function normalizeStringList($value): array + /** @return array */ + private static function normalizeStringList(mixed $value): array { if (is_array($value)) { $items = $value; @@ -768,29 +803,30 @@ private static function normalizeStringList($value): array $items = []; } - $items = array_map(static function ($item) { + $items = array_map(static function ($item): string { return trim((string) $item); }, $items); - return array_values(array_unique(array_filter($items, static function ($item) { + return array_values(array_unique(array_filter($items, static function (string $item): bool { return $item !== ''; }))); } - private static function normalizeClangId($clangId = null): int + private static function normalizeClangId(mixed $clangId = null): int { $clangId = $clangId !== null ? (int) $clangId : 0; - return $clangId > 0 ? $clangId : (int) \rex_clang::getCurrentId(); + return $clangId > 0 ? $clangId : (int) rex_clang::getCurrentId(); } + /** @return array */ private static function getDefaultBranchIds(int $clangId): array { try { [$domainWhere, $domainParams] = self::getBranchDomainCondition(); - $sql = \rex_sql::factory(); + $sql = rex_sql::factory(); $sql->setQuery( - 'SELECT id FROM ' . \rex::getTable('jsonld_localbusiness_branches') . ' + 'SELECT id FROM ' . rex::getTable('jsonld_localbusiness_branches') . ' WHERE clang_id = ?' . $domainWhere . ' ORDER BY is_main_branch DESC, sort_order ASC, id ASC LIMIT 1', @@ -800,14 +836,18 @@ private static function getDefaultBranchIds(int $clangId): array if ($sql->getRows() > 0) { return [(int) $sql->getValue('id')]; } - } catch (\Exception $e) { + } catch (Exception $e) { // Keine Branch verwenden } return []; } - private static function getGlobalSchemaConfig(\rex_addon $addon, string $baseKey, int $clangId, array $default = []): array + /** + * @param array $default + * @return array + */ + private static function getGlobalSchemaConfig(\rex_addon_interface $addon, string $baseKey, int $clangId, array $default = []): array { if (class_exists(__NAMESPACE__ . '\\DomainConfig') && DomainConfig::isMultiDomain()) { $configKey = $baseKey . '_domain_' . DomainConfig::getActiveDomainId() . '_clang_' . $clangId; @@ -817,15 +857,18 @@ private static function getGlobalSchemaConfig(\rex_addon $addon, string $baseKey } } - return \FriendsOfRedaxo\JsonLdManager\LanguageConfig::getLocalizedConfig($addon, $baseKey, $clangId, $default); + return LanguageConfig::getLocalizedConfig($addon, $baseKey, $clangId, $default); } + /** + * @return array{0: string, 1: array} + */ private static function getBranchDomainCondition(): array { if ( class_exists(__NAMESPACE__ . '\\DomainConfig') && DomainConfig::isMultiDomain() - && self::tableHasColumn(\rex::getTable('jsonld_localbusiness_branches'), 'domain_id') + && self::tableHasColumn(rex::getTable('jsonld_localbusiness_branches'), 'domain_id') ) { return [' AND (domain_id = ? OR domain_id IS NULL)', [(int) DomainConfig::getActiveDomainId()]]; } @@ -842,16 +885,22 @@ private static function tableHasColumn(string $table, string $column): bool } try { - $sql = \rex_sql::factory(); + $sql = rex_sql::factory(); $sql->setQuery('SHOW COLUMNS FROM ' . $table . ' LIKE ?', [$column]); $cache[$cacheKey] = $sql->getRows() > 0; - } catch (\Exception $e) { + } catch (Exception $e) { $cache[$cacheKey] = false; } return $cache[$cacheKey]; } + /** + * @param array $branchIds + * @param array> $items + * @param array|null $payload + * @return array + */ private static function buildDebugMeta(int $articleId, int $clangId, array $branchIds, array $items, ?array $payload, string $source): array { $types = self::extractTypes($items, $payload); @@ -875,11 +924,16 @@ private static function buildDebugMeta(int $articleId, int $clangId, array $bran return $meta; } + /** + * @param array> $items + * @param array|null $payload + * @return array + */ private static function extractTypes(array $items, ?array $payload): array { $types = []; foreach ($items as $item) { - if (is_array($item) && isset($item['@type'])) { + if (isset($item['@type'])) { $types[] = (string) $item['@type']; } } @@ -904,16 +958,22 @@ private static function extractTypes(array $items, ?array $payload): array return $types; } + /** + * @param array $branchIds + * @param array $baseConfig + * @return array}> + */ private static function loadBranchConfigs(array $branchIds, int $clangId, array $baseConfig, bool $isDebugMode): array { $branchConfigs = []; foreach ($branchIds as $singleBranchId) { try { - $sql = \rex_sql::factory(); - $sql->setQuery('SELECT branch_name, config FROM ' . \rex::getTable('jsonld_localbusiness_branches') . ' WHERE id = ? AND clang_id = ?', [$singleBranchId, $clangId]); + $sql = rex_sql::factory(); + $sql->setQuery('SELECT branch_name, config FROM ' . rex::getTable('jsonld_localbusiness_branches') . ' WHERE id = ? AND clang_id = ?', [$singleBranchId, $clangId]); if ($sql->hasNext()) { - $branchConfig = json_decode($sql->getValue('config'), true) ?: []; + $configRaw = $sql->getValue('config'); + $branchConfig = is_string($configRaw) ? (json_decode($configRaw, true) ?: []) : []; $mergedConfig = array_merge($baseConfig, $branchConfig); if (empty($mergedConfig['name'])) { $mergedConfig['name'] = $sql->getValue('branch_name'); @@ -931,7 +991,7 @@ private static function loadBranchConfigs(array $branchIds, int $clangId, array ]); } } - } catch (\Exception $e) { + } catch (Exception $e) { if ($isDebugMode) { self::debugLog('Branch-Config Fehler', [ 'branch_id' => $singleBranchId, @@ -945,7 +1005,11 @@ private static function loadBranchConfigs(array $branchIds, int $clangId, array } - private static function buildLocalBusinessSchema(array $localBusinessConfig, $branchId = null): ?array + /** + * @param array $localBusinessConfig + * @return array|null + */ + private static function buildLocalBusinessSchema(array $localBusinessConfig, ?int $branchId = null): ?array { if (empty($localBusinessConfig['name'])) { @@ -957,7 +1021,7 @@ private static function buildLocalBusinessSchema(array $localBusinessConfig, $br $localBusinessSchema = [ '@context' => 'https://schema.org', '@type' => $type, - '@id' => rtrim(\rex::getServer(), '/') . '/#localbusiness' . ($branchId ? '_' . $branchId : ''), + '@id' => rtrim(rex::getServer(), '/') . '/#localbusiness' . ($branchId ? '_' . $branchId : ''), 'name' => $localBusinessConfig['name'] ]; @@ -982,7 +1046,7 @@ private static function buildLocalBusinessSchema(array $localBusinessConfig, $br if (!empty($localBusinessConfig['description'])) { $localBusinessSchema['description'] = $localBusinessConfig['description']; } - if (!empty($localBusinessConfig['telephone']) && trim($localBusinessConfig['telephone']) !== '') { + if (!empty($localBusinessConfig['telephone'])) { $localBusinessSchema['telephone'] = $localBusinessConfig['telephone']; } if (!empty($localBusinessConfig['paymentAccepted'])) { @@ -1027,7 +1091,7 @@ private static function buildLocalBusinessSchema(array $localBusinessConfig, $br if (!empty($localBusinessConfig['openingHoursSpecification'])) { $openingHours = $localBusinessConfig['openingHoursSpecification']; - if (is_array($openingHours) && count($openingHours) > 0) { + if (is_array($openingHours)) { $hasValidHours = false; foreach ($openingHours as $hours) { if (is_array($hours) && !empty($hours['dayOfWeek']) && (!empty($hours['opens']) || !empty($hours['closes']))) { @@ -1064,6 +1128,10 @@ private static function buildLocalBusinessSchema(array $localBusinessConfig, $br return $localBusinessSchema; } + /** + * @param array $localBusinessConfig + * @return array + */ private static function normalizeLocalBusinessImageUrls(array $localBusinessConfig): array { $rawImages = $localBusinessConfig['images'] ?? $localBusinessConfig['image'] ?? []; @@ -1089,10 +1157,7 @@ private static function normalizeLocalBusinessImageUrls(array $localBusinessConf continue; } - $mediaPath = \rex_url::media($file); - if ($mediaPath === '') { - continue; - } + $mediaPath = rex_url::media($file); $imageUrls[] = str_starts_with($mediaPath, 'http') ? $mediaPath : $baseUrl . $mediaPath; } @@ -1111,44 +1176,51 @@ private static function getWebsiteUrl(): string } /** - * Prüft ob für die aktuelle URL ein dynamisches URL-Profil existiert - * @param int $articleId - * @param int $clangId - * @return array|null Mapping-Konfiguration oder null + * Prüft ob für die aktuelle URL ein dynamisches URL-Profil existiert. + * + * @return array{profile: array, schema_type: mixed, field_mappings: array}|null Mapping-Konfiguration oder null */ - private static function getDynamicUrlMapping($articleId, $clangId) + private static function getDynamicUrlMapping(int $articleId, int $clangId): ?array { // URL AddOn verfügbar prüfen - if (!\rex_addon::get('url')->isAvailable()) { + if (!rex_addon::get('url')->isAvailable()) { return null; } try { // Direkt über URL AddOn prüfen statt über REQUEST_URI - $urlObject = \Url\Url::resolveCurrent(); + $urlObject = Url::resolveCurrent(); if ($urlObject) { - $profileNamespace = $urlObject->getProfile()->getNamespace(); + $profileObject = $urlObject->getProfile(); + if (null === $profileObject) { + return null; + } + + $profileNamespace = $profileObject->getNamespace(); // URL-Profile mit aktiven Mappings laden - $sql = \rex_sql::factory(); + $sql = rex_sql::factory(); $profiles = $sql->getArray(' SELECT p.*, m.schema_type, m.field_mappings, m.active - FROM ' . \rex::getTable('url_generator_profile') . ' p - INNER JOIN ' . \rex::getTable('jsonld_url_profile_mappings') . ' m ON p.id = m.url_profile_id + FROM ' . rex::getTable('url_generator_profile') . ' p + INNER JOIN ' . rex::getTable('jsonld_url_profile_mappings') . ' m ON p.id = m.url_profile_id WHERE m.active = 1 AND p.namespace = ? ', [$profileNamespace]); if (!empty($profiles)) { $profile = $profiles[0]; // Erstes Match verwenden + $fieldMappingsRaw = $profile['field_mappings'] ?? null; + $fieldMappings = is_string($fieldMappingsRaw) ? (json_decode($fieldMappingsRaw, true) ?: []) : []; + return [ 'profile' => $profile, 'schema_type' => $profile['schema_type'], - 'field_mappings' => json_decode($profile['field_mappings'], true) ?: [] + 'field_mappings' => $fieldMappings, ]; } } - } catch (\Exception $e) { + } catch (Exception $e) { // Fehler ignorieren, fallback zu Standard-Schema } @@ -1161,7 +1233,9 @@ private static function getDynamicUrlMapping($articleId, $clangId) * @param string $currentUrl * @return bool */ - private static function matchesUrlProfile($profile, $currentUrl) + /** @param array $profile */ + // @phpstan-ignore-next-line bewusst als interne Reserve-API vorhanden + private static function matchesUrlProfile(array $profile, string $currentUrl): bool { $namespace = $profile['namespace'] ?? ''; @@ -1171,9 +1245,14 @@ private static function matchesUrlProfile($profile, $currentUrl) // Zusätzlich: URL AddOn Methode testen try { - $urlObject = \Url\Url::resolveCurrent(); + $urlObject = Url::resolveCurrent(); if ($urlObject) { - $urlNamespace = $urlObject->getProfile()->getNamespace(); + $profileObject = $urlObject->getProfile(); + if (null === $profileObject) { + return false; + } + + $urlNamespace = $profileObject->getNamespace(); $directMatch = ($urlNamespace === $namespace); @@ -1181,7 +1260,7 @@ private static function matchesUrlProfile($profile, $currentUrl) return true; } } - } catch (\Exception $e) { + } catch (Exception $e) { // Fehler ignorieren } @@ -1196,7 +1275,11 @@ private static function matchesUrlProfile($profile, $currentUrl) * @param bool $isDebugMode * @return array|null */ - private static function generateDynamicSchema($dynamicMapping, $articleId, $clangId, $isDebugMode) + /** + * @param array $dynamicMapping + * @return array|null + */ + private static function generateDynamicSchema(array $dynamicMapping, int $articleId, int $clangId, bool $isDebugMode): ?array { try { $schemaType = $dynamicMapping['schema_type'] ?? 'WebPage'; @@ -1216,7 +1299,11 @@ private static function generateDynamicSchema($dynamicMapping, $articleId, $clan '@context' => 'https://schema.org', '@type' => $schemaType, 'url' => self::getDynamicArticleUrl($profile, $articleId), - 'inLanguage' => \rex_clang::get($clangId)->getCode() + 'inLanguage' => (static function () use ($clangId): string { + $clang = rex_clang::get($clangId); + + return $clang ? $clang->getCode() : 'de'; + })(), ]; // YForm-Dataset über URL AddOn ermitteln @@ -1248,7 +1335,7 @@ private static function generateDynamicSchema($dynamicMapping, $articleId, $clan return $schema; - } catch (\Exception $e) { + } catch (Exception $e) { if ($isDebugMode) { self::debugLog('Fehler beim Generieren des dynamischen Schemas', [ 'error' => $e->getMessage(), @@ -1264,11 +1351,15 @@ private static function generateDynamicSchema($dynamicMapping, $articleId, $clan * @param array $profile URL-Profil-Daten * @return array|null Dataset-Array oder null */ - private static function getCurrentDataset($profile) + /** + * @param array $profile + * @return array|null + */ + private static function getCurrentDataset(array $profile): ?array { try { // Dataset-ID über URL AddOn ermitteln - GENAU WIE IM NEWS-MODUL - $urlObject = \Url\Url::resolveCurrent(); + $urlObject = Url::resolveCurrent(); if ($urlObject) { $datasetId = $urlObject->getDatasetId(); @@ -1279,7 +1370,7 @@ private static function getCurrentDataset($profile) if ($tableName) { // WICHTIG: Wie im News-Modul - YForm Manager Dataset verwenden - $dataset = \rex_yform_manager_dataset::get($datasetId, $tableName); + $dataset = rex_yform_manager_dataset::get($datasetId, $tableName); if ($dataset) { // Dataset zu Array konvertieren für Kompatibilität @@ -1289,7 +1380,7 @@ private static function getCurrentDataset($profile) } } } - } catch (\Exception $e) { + } catch (Exception $e) { // Fehler ignorieren } @@ -1306,9 +1397,13 @@ private static function getCurrentDataset($profile) * @param bool $isDebugMode * @return mixed */ - private static function resolveMappingValue($mapping, $dataset, $yformTableName, $articleId, $clangId, $isDebugMode) + /** + * @param array $mapping + * @param array|null $dataset + */ + private static function resolveMappingValue(array $mapping, ?array $dataset, ?string $yformTableName, int $articleId, int $clangId, bool $isDebugMode): mixed { - if (!is_array($mapping) || !isset($mapping['type'])) { + if (!isset($mapping['type'])) { return null; } @@ -1331,8 +1426,9 @@ private static function resolveMappingValue($mapping, $dataset, $yformTableName, case 'field': // Priorität 1: Dataset-Array (direkte SQL-Abfrage wie im Backend) - if ($dataset && is_array($dataset) && isset($dataset[$value])) { - $fieldValue = $dataset[$value]; + $valueKey = (string) $value; + if (null !== $dataset && isset($dataset[$valueKey])) { + $fieldValue = $dataset[$valueKey]; if ($isDebugMode) { self::debugLog('Feld-Wert über Dataset-Array aufgelöst', [ @@ -1346,14 +1442,14 @@ private static function resolveMappingValue($mapping, $dataset, $yformTableName, } // Fallback: GET-Parameter - if (self::isWhitelistedGetParam($value)) { - return \rex_request($value, 'string'); + if (self::isWhitelistedGetParam($valueKey)) { + return \rex_request($valueKey, 'string'); } // Fallback: Artikel-Feld - $article = \rex_article::get($articleId, $clangId); + $article = rex_article::get($articleId, $clangId); if ($article) { - return $article->getValue($value); + return $article->getValue($valueKey); } break; @@ -1367,19 +1463,26 @@ private static function resolveMappingValue($mapping, $dataset, $yformTableName, * @param array $profile * @return string|null */ - private static function getYFormTableName($profile) + /** @param array $profile */ + private static function getYFormTableName(array $profile): ?string { // Aus table_parameters if (!empty($profile['table_parameters'])) { - $tableParams = json_decode($profile['table_parameters'], true); + $tableParamsRaw = $profile['table_parameters']; + $tableParams = is_string($tableParamsRaw) ? json_decode($tableParamsRaw, true) : null; if ($tableParams && !empty($tableParams['table_name'])) { - return self::isValidTableName($tableParams['table_name']) ? $tableParams['table_name'] : null; + $tableName = $tableParams['table_name']; + if (is_string($tableName) && self::isValidTableName($tableName)) { + return $tableName; + } + + return null; } } // Fallback: table_name bereinigen if (!empty($profile['table_name'])) { - $tableName = str_replace('1_xxx_', '', $profile['table_name']); + $tableName = str_replace('1_xxx_', '', (string) $profile['table_name']); return self::isValidTableName($tableName) ? $tableName : null; } @@ -1392,7 +1495,8 @@ private static function getYFormTableName($profile) * @param int $articleId * @return string */ - private static function getDynamicArticleUrl($profile, $articleId) + /** @param array $profile */ + private static function getDynamicArticleUrl(array $profile, int $articleId): string { try { // Aktuelle URL aus URL AddOn verwenden @@ -1403,7 +1507,7 @@ private static function getDynamicArticleUrl($profile, $articleId) $requestPath = '/' . ltrim(parse_url($requestPath, PHP_URL_PATH) ?: '', '/'); return self::getWebsiteUrl() . $requestPath; } - } catch (\Exception $e) { + } catch (Exception $e) { // Fallback zu Standard-URL } @@ -1429,7 +1533,7 @@ private static function getArticleUrl(int $articleId): string private static function isWhitelistedGetParam(string $paramName): bool { - $whitelist = (array) \rex_addon::get('jsonld_manager')->getConfig('whitelist.get_params', []); + $whitelist = (array) rex_addon::get('jsonld_manager')->getConfig('whitelist.get_params', []); return in_array($paramName, $whitelist, true); } diff --git a/lib/LanguageConfig.php b/lib/LanguageConfig.php index 54930cb..ade710e 100644 --- a/lib/LanguageConfig.php +++ b/lib/LanguageConfig.php @@ -2,13 +2,18 @@ namespace FriendsOfRedaxo\JsonLdManager; +use rex_clang; +use rex_addon_interface; +use rex_url; + class LanguageConfig { private const SESSION_KEY = 'jsonld_manager_active_clang_id'; + /** @return array */ public static function getClangs(): array { - return \rex_clang::getAll(true); + return rex_clang::getAll(true); } public static function isMultilingual(): bool @@ -22,8 +27,8 @@ public static function getActiveClangId(): int if ($requested <= 0) { $requested = \rex_request('clang_id', 'int', 0); } - if ($requested > 0 && \rex_clang::exists($requested)) { - $clang = \rex_clang::get($requested); + if ($requested > 0 && rex_clang::exists($requested)) { + $clang = rex_clang::get($requested); if ($clang && $clang->isOnline()) { \rex_set_session(self::SESSION_KEY, $requested); return $requested; @@ -31,19 +36,23 @@ public static function getActiveClangId(): int } $sessionClangId = (int) \rex_session(self::SESSION_KEY, 'int', 0); - if ($sessionClangId > 0 && \rex_clang::exists($sessionClangId)) { - $sessionClang = \rex_clang::get($sessionClangId); + if ($sessionClangId > 0 && rex_clang::exists($sessionClangId)) { + $sessionClang = rex_clang::get($sessionClangId); if ($sessionClang && $sessionClang->isOnline()) { return $sessionClangId; } } - $fallbackClangId = \rex_clang::getCurrentId(); + $fallbackClangId = rex_clang::getCurrentId(); \rex_set_session(self::SESSION_KEY, $fallbackClangId); return $fallbackClangId; } - public static function getLocalizedConfig(\rex_addon $addon, string $baseKey, int $clangId, array $default = []): array + /** + * @param array $default + * @return array + */ + public static function getLocalizedConfig(rex_addon_interface $addon, string $baseKey, int $clangId, array $default = []): array { $localized = $addon->getConfig(self::localizedKey($baseKey, $clangId), null); if (is_array($localized)) { @@ -58,7 +67,8 @@ public static function getLocalizedConfig(\rex_addon $addon, string $baseKey, in return $default; } - public static function setLocalizedConfig(\rex_addon $addon, string $baseKey, int $clangId, array $config): void + /** @param array $config */ + public static function setLocalizedConfig(rex_addon_interface $addon, string $baseKey, int $clangId, array $config): void { $addon->setConfig(self::localizedKey($baseKey, $clangId), $config); } @@ -108,7 +118,7 @@ public static function renderClangTabs(int $activeClangId): string foreach (self::getClangs() as $clang) { $clangId = (int) $clang->getId(); $isActive = $clangId === $activeClangId; - $url = \rex_url::currentBackendPage(['clang' => $clangId]); + $url = rex_url::currentBackendPage(['clang' => $clangId]); $label = htmlspecialchars((string) $clang->getName()); $html .= '' . $label . ''; } diff --git a/lib/Mapping/DataSource.php b/lib/Mapping/DataSource.php index 99781d8..e0818f6 100644 --- a/lib/Mapping/DataSource.php +++ b/lib/Mapping/DataSource.php @@ -2,6 +2,17 @@ namespace FriendsOfRedaxo\JsonLdManager\Mapping; +use rex_article; +use rex_addon; +use rex_clang; +use rex; +use rex_category; +use rex_media; +use rex_media_manager; +use rex_url; +use rex_sql; +use rex_article_slice; + /** * DataSource - Datenquellen für JSON-LD Mapping * @@ -14,29 +25,22 @@ */ class DataSource { - /** - * @var array Cache für aufgelöste Werte - */ + /** @var array Cache für aufgelöste Werte */ private static $cache = []; - + /** - * Wert aus Datenquelle ermitteln - * - * @param string $source Datenquelle (z.B. 'article_name', 'get_param:marke') - * @param \rex_article $article REDAXO Artikel - * @param array $additionalData Zusätzliche Daten - * @return mixed|null Aufgelöster Wert + * @param array $additionalData */ - public static function getValue($source, $article, $additionalData = []) + public static function getValue(string $source, rex_article $article, array $additionalData = []): mixed { $cacheKey = md5($source . serialize($additionalData) . $article->getId()); - + if (isset(self::$cache[$cacheKey])) { return self::$cache[$cacheKey]; } - + $value = null; - + // Source-Type ermitteln if (strpos($source, ':') !== false) { list($type, $parameter) = explode(':', $source, 2); @@ -44,206 +48,199 @@ public static function getValue($source, $article, $additionalData = []) $type = $source; $parameter = null; } - + switch ($type) { - + // === ARTIKEL-DATEN === case 'article_name': $value = $article->getName(); break; - + case 'article_teaser': $value = $article->getValue('teaser'); break; - + case 'article_description': $value = $article->getValue('description'); break; - + case 'article_id': $value = $article->getId(); break; - + case 'category_name': $category = $article->getCategory(); $value = $category ? $category->getName() : null; break; - + // === SEO / YREWRITE === case 'seo_title': case 'meta_title': - if (\rex_addon::get('yrewrite')->isAvailable()) { + if (rex_addon::get('yrewrite')->isAvailable()) { $value = $article->getValue('yrewrite_title') ?: $article->getName(); } break; - + case 'meta_description': - if (\rex_addon::get('yrewrite')->isAvailable()) { + if (rex_addon::get('yrewrite')->isAvailable()) { $value = $article->getValue('yrewrite_description'); } break; - + case 'canonical_url': $value = self::getCanonicalUrl($article); break; - + case 'absolute_url': $value = self::getAbsoluteUrl($article); break; - + // === SPRACHE === case 'clang_code': - $clang = \rex_clang::get($article->getClangId()); + $clang = rex_clang::get($article->getClangId()); $value = $clang ? $clang->getCode() : 'de'; break; - + case 'clang_name': - $clang = \rex_clang::get($article->getClangId()); + $clang = rex_clang::get($article->getClangId()); $value = $clang ? $clang->getName() : 'Deutsch'; break; - + // === WEBSITE-DATEN === case 'sitename': - $value = \rex::getServerName(); + $value = rex::getServerName(); break; - + case 'base_url': - $value = \rex::getServer(); + $value = rex::getServer(); break; - + case 'server_name': - $value = \rex::getServerName(); + $value = rex::getServerName(); break; - + // === GET-PARAMETER === case 'get_param': if ($parameter) { $value = self::getWhitelistedGetParam($parameter); } break; - + // === MEDIEN === case 'media_field': if ($parameter) { $mediaFile = $article->getValue($parameter); - if ($mediaFile) { + if (is_string($mediaFile) && '' !== $mediaFile) { $value = self::getMediaUrl($mediaFile); } } break; - + case 'media_url': - if ($parameter) { + if (null !== $parameter && '' !== $parameter) { $value = self::getMediaUrl($parameter); } break; - + // === CUSTOM FIELDS === case 'custom_field': if ($parameter) { $value = $article->getValue($parameter); } break; - + // === STATISCHE WERTE === case 'static': $value = $parameter; break; - + // === ZUSÄTZLICHE DATEN === case 'additional': if ($parameter && isset($additionalData[$parameter])) { $value = $additionalData[$parameter]; } break; - + // === BERECHNET === case 'computed': $value = self::getComputedValue($parameter, $article, $additionalData); break; - + // === TEMPLATE-SPEZIFISCH === case 'template_name': - $template = \rex_template::get($article->getTemplateId()); - $value = $template ? $template->getName() : null; + $value = self::getTemplateName($article->getTemplateId()); break; - + // === DATUM/ZEIT === case 'create_date': $value = date('Y-m-d', $article->getCreateDate()); break; - + case 'update_date': $value = date('Y-m-d', $article->getUpdateDate()); break; - + case 'iso_date': $value = date('c', $article->getUpdateDate()); break; - + // === FALLBACK === default: // Als Custom Field versuchen $value = $article->getValue($source); } - + // Wert bereinigen if (is_string($value)) { $value = trim(strip_tags($value)); $value = $value !== '' ? $value : null; } - + // Cache speichern self::$cache[$cacheKey] = $value; - + return $value; } - + /** * GET-Parameter mit Whitelist-Prüfung * * @param string $paramName Parameter-Name * @return string|null Parameter-Wert oder null */ - private static function getWhitelistedGetParam($paramName) + private static function getWhitelistedGetParam(string $paramName): ?string { - $addon = \rex_addon::get('jsonld_manager'); + $addon = rex_addon::get('jsonld_manager'); $whitelist = $addon->getConfig('whitelist.get_params', []); - + if (!in_array($paramName, $whitelist)) { return null; } - + return \rex_request($paramName, 'string'); } - - /** - * Kanonische URL ermitteln - * - * @param \rex_article $article - * @return string URL - */ - private static function getCanonicalUrl($article) + + private static function getCanonicalUrl(rex_article|rex_category $article): string { - if (\rex_addon::get('yrewrite')->isAvailable()) { + if (rex_addon::get('yrewrite')->isAvailable()) { // YRewrite Full URL über REDAXO URL-System - return \rex::getServer() . \rex_getUrl($article->getId(), $article->getClangId(), [], '&'); + return rex::getServer() . \rex_getUrl($article->getId(), $article->getClangId(), [], '&'); } - - return \rex::getServer() . \rex_getUrl($article->getId(), $article->getClangId()); + + return rex::getServer() . \rex_getUrl($article->getId(), $article->getClangId()); } - + /** * Absolute URL ermitteln - * - * @param \rex_article $article + * + * @param rex_article $article * @return string URL */ - private static function getAbsoluteUrl($article) + private static function getAbsoluteUrl(rex_article $article): string { - return \rex::getServer() . \rex_getUrl($article->getId(), $article->getClangId()); + return rex::getServer() . \rex_getUrl($article->getId(), $article->getClangId()); } - + /** * Media-URL generieren * @@ -251,67 +248,69 @@ private static function getAbsoluteUrl($article) * @param string|null $type Media-Manager Type * @return string|null URL oder null */ - private static function getMediaUrl($filename, $type = null) + private static function getMediaUrl(string $filename, ?string $type = null): ?string { - if (!$filename) return null; - - $media = \rex_media::get($filename); + if ('' === $filename) { + return null; + } + + $media = rex_media::get($filename); if (!$media) return null; - - if ($type && \rex_addon::get('media_manager')->isAvailable()) { - return \rex::getServer() . \rex_url::media($filename, $type); + + if (null !== $type && '' !== $type && rex_addon::get('media_manager')->isAvailable()) { + return rex::getServer() . rex_media_manager::getUrl($type, $filename); } - - return \rex::getServer() . \rex_url::media($filename); + + return rex::getServer() . rex_url::media($filename); } - + /** - * Berechnete Werte - * - * @param string $function Funktion - * @param \rex_article $article - * @param array $additionalData - * @return mixed + * @param array $additionalData + * + * @return array|float|int|string|null */ - private static function getComputedValue($function, $article, $additionalData = []) + private static function getComputedValue(?string $function, rex_article $article, array $additionalData = []): array|string|int|float|null { switch ($function) { - + case 'breadcrumb_list': return self::generateBreadcrumbList($article); - + case 'organization': return self::getOrganizationData(); - + case 'current_datetime': return date('c'); - + case 'word_count': - $content = $article->getSlice(1)->getValue(1) ?: ''; + $content = self::getArticleTextContent($article); return str_word_count(strip_tags($content)); - + case 'reading_time': - $content = $article->getSlice(1)->getValue(1) ?: ''; + $content = self::getArticleTextContent($article); $wordCount = str_word_count(strip_tags($content)); return max(1, round($wordCount / 200)); // 200 Wörter/Minute - + default: return null; } } - + /** * Breadcrumb-Liste generieren * * @param \rex_article $article * @return array */ - private static function generateBreadcrumbList($article) + /** + * @return array + */ + private static function generateBreadcrumbList(rex_article $article): array { $breadcrumbs = []; $path = $article->getParentTree(); $path[] = $article; // Aktuelle Seite hinzufügen - + $position = 1; foreach ($path as $item) { $breadcrumbs[] = [ @@ -321,32 +320,38 @@ private static function generateBreadcrumbList($article) 'item' => self::getCanonicalUrl($item) ]; } - + return [ '@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => $breadcrumbs ]; } - + /** * Standard-Organisationsdaten * * @return array */ - private static function getOrganizationData() + /** + * @return array + */ + private static function getOrganizationData(): array { - $addon = \rex_addon::get('jsonld_manager'); + $addon = rex_addon::get('jsonld_manager'); $config = $addon->getConfig('organization', []); - + + $organizationName = $config['name'] ?? ''; + $logoFile = $config['logo'] ?? ''; + return [ '@type' => 'Organization', - 'name' => $config['name'] ?: \rex::getServerName(), - 'url' => \rex::getServer(), - 'logo' => $config['logo'] ? \rex::getServer() . \rex_url::media($config['logo']) : null + 'name' => is_string($organizationName) && '' !== trim($organizationName) ? trim($organizationName) : rex::getServerName(), + 'url' => rex::getServer(), + 'logo' => is_string($logoFile) && '' !== $logoFile ? rex::getServer() . rex_url::media($logoFile) : null, ]; } - + /** * Wert transformieren * @@ -354,49 +359,81 @@ private static function getOrganizationData() * @param string $transform Transform-Funktion * @return mixed Transformierter Wert */ - public static function transformValue($value, $transform) + public static function transformValue($value, string $transform) { switch ($transform) { - + case 'uppercase': return strtoupper($value); - + case 'lowercase': return strtolower($value); - + case 'ucfirst': return ucfirst($value); - + case 'truncate_100': return mb_substr($value, 0, 100) . (mb_strlen($value) > 100 ? '...' : ''); - + case 'truncate_160': return mb_substr($value, 0, 160) . (mb_strlen($value) > 160 ? '...' : ''); - + case 'strip_html': return strip_tags($value); - + case 'clean_text': - return trim(preg_replace('/\s+/', ' ', strip_tags($value))); - + $cleanValue = preg_replace('/\s+/', ' ', strip_tags((string) $value)); + return trim(is_string($cleanValue) ? $cleanValue : ''); + case 'price_format': return number_format((float)$value, 2, ',', '.') . ' EUR'; - + case 'url_absolute': - return \rex::getServer() . ltrim($value, '/'); - + return rex::getServer() . ltrim((string) $value, '/'); + default: return $value; } } - + /** * Cache zurücksetzen */ - public static function clearCache() + public static function clearCache(): void { self::$cache = []; } + + private static function getTemplateName(int $templateId): ?string + { + $sql = rex_sql::factory(); + $sql->setQuery('SELECT name FROM ' . rex::getTable('template') . ' WHERE id = ? LIMIT 1', [$templateId]); + + if (0 === $sql->getRows()) { + return null; + } + + $name = $sql->getValue('name'); + + return is_string($name) && '' !== trim($name) ? trim($name) : null; + } + + private static function getArticleTextContent(rex_article $article): string + { + $slices = rex_article_slice::getSlicesForArticle($article->getId(), $article->getClangId()); + $parts = []; + + foreach ($slices as $slice) { + for ($i = 1; $i <= 20; ++$i) { + $value = $slice->getValue('value' . $i); + if (is_string($value) && '' !== trim($value)) { + $parts[] = $value; + } + } + } + + return implode(' ', $parts); + } } \class_alias(__NAMESPACE__ . '\\DataSource', 'JsonldManager\\Mapping\\DataSource'); diff --git a/lib/Mapping/DataSourceExtended.php b/lib/Mapping/DataSourceExtended.php index 4a31023..f9fa583 100644 --- a/lib/Mapping/DataSourceExtended.php +++ b/lib/Mapping/DataSourceExtended.php @@ -2,6 +2,14 @@ namespace FriendsOfRedaxo\JsonLdManager\Mapping; +use rex_article; +use rex_user; +use rex_media; +use rex_addon; +use rex; +use rex_media_manager; +use rex_url; + /** * DataSource Extensions - Erweiterte Datenquellen für JSON-LD Mapping * @@ -22,7 +30,10 @@ class DataSourceExtended extends DataSource * @param array $additionalData Zusätzliche Daten * @return mixed|null Aufgelöster Wert */ - public static function getValue($source, $article, $additionalData = []) + /** + * @param array $additionalData + */ + public static function getValue(string $source, rex_article $article, array $additionalData = []): mixed { // Neue Cases für fehlende Properties if (strpos($source, ':') !== false) { @@ -31,32 +42,32 @@ public static function getValue($source, $article, $additionalData = []) $type = $source; $parameter = null; } - + switch ($type) { - + // === ERWEITERTE DATUM/ZEIT === case 'date_published': return date('Y-m-d', $article->getCreateDate()); - + case 'date_modified': return date('Y-m-d', $article->getUpdateDate()); - + case 'iso_date_published': return date('c', $article->getCreateDate()); - + case 'iso_date_modified': return date('c', $article->getUpdateDate()); - + // === AUTHOR & PUBLISHER === case 'author_name': $authorField = $article->getValue('author'); if ($authorField) { return $authorField; } else { - $createUser = rex_user::get($article->getCreateUser()); + $createUser = rex_user::get((int) $article->getCreateUser()); return $createUser ? $createUser->getValue('name') : null; } - + // === PRIMÄRES BILD === case 'primary_image': case 'featured_image': @@ -64,11 +75,11 @@ public static function getValue($source, $article, $additionalData = []) // Primäres Bild aus verschiedenen Quellen $imageField = $parameter ?: 'image'; // Standard: 'image' Feld $imageFile = $article->getValue($imageField); - if (!$imageFile) { + if (!is_string($imageFile) || '' === $imageFile) { // Fallback: Erstes Bild im Content oder Metainfo $imageFile = $article->getValue('meta_image') ?: $article->getValue('bild'); } - if ($imageFile) { + if (is_string($imageFile) && '' !== $imageFile) { $value = [ '@type' => 'ImageObject', 'url' => self::getMediaUrl($imageFile) @@ -85,13 +96,15 @@ public static function getValue($source, $article, $additionalData = []) return $value; } break; - + default: // Fallback zur parent getValue Methode return parent::getValue($source, $article, $additionalData); } + + return null; } - + /** * Media-URL generieren (vereinfacht) * @@ -99,18 +112,20 @@ public static function getValue($source, $article, $additionalData = []) * @param string|null $type Media-Manager Type * @return string|null URL oder null */ - protected static function getMediaUrl($filename, $type = null) + protected static function getMediaUrl(string $filename, ?string $type = null): ?string { - if (!$filename) return null; - - $media = \rex_media::get($filename); + if ('' === $filename) { + return null; + } + + $media = rex_media::get($filename); if (!$media) return null; - - if ($type && \rex_addon::get('media_manager')->isAvailable()) { - return \rex::getServer() . \rex_url::media($filename, $type); + + if (null !== $type && '' !== $type && rex_addon::get('media_manager')->isAvailable()) { + return rex::getServer() . rex_media_manager::getUrl($type, $filename); } - - return \rex::getServer() . \rex_url::media($filename); + + return rex::getServer() . rex_url::media($filename); } } diff --git a/lib/Url/RuleEngine.php b/lib/Url/RuleEngine.php index 55a48c2..2146c39 100644 --- a/lib/Url/RuleEngine.php +++ b/lib/Url/RuleEngine.php @@ -2,6 +2,14 @@ namespace FriendsOfRedaxo\JsonLdManager\Url; +use rex_article; +use rex_url; +use rex_sql; +use rex; +use InvalidArgumentException; +use Exception; +use rex_addon; + /** * URL Rule Engine - Dynamische URL-Filterung * @@ -18,28 +26,32 @@ class RuleEngine /** * @var array Cache für URL-Regeln */ - private static $rulesCache = null; - + /** @var array>>|null */ + private static ?array $rulesCache = null; + /** * Aktuelle URL gegen Regeln prüfen * * @return array|null Matching Rule Data oder null */ - public static function matchCurrentUrl() + /** + * @return array|null + */ + public static function matchCurrentUrl(): ?array { - $article = \rex_article::getCurrent(); + $article = rex_article::getCurrent(); if (!$article) { return null; } - + $rules = self::getUrlRules($article->getId()); if (empty($rules)) { return null; } - - $currentUrl = \rex_url::currentBackendPage(); + + $currentUrl = rex_url::currentBackendPage(); $getParams = $_GET; - + foreach ($rules as $rule) { if (self::matchRule($rule, $currentUrl, $getParams)) { return [ @@ -49,33 +61,39 @@ public static function matchCurrentUrl() ]; } } - + return null; } - + /** * URL-Regeln für Artikel laden * * @param int $articleId Artikel-ID * @return array Array von URL-Regeln */ - private static function getUrlRules($articleId) + /** + * @return list> + */ + private static function getUrlRules(int $articleId): array { $cacheKey = 'rules_' . $articleId; - + if (self::$rulesCache === null || !isset(self::$rulesCache[$cacheKey])) { - $sql = \rex_sql::factory(); + $sql = rex_sql::factory(); $sql->setQuery(' - SELECT * FROM '.\rex::getTable('jsonld_url_rules').' + SELECT * FROM '.rex::getTable('jsonld_url_rules').' WHERE article_id = ? AND active = 1 ORDER BY priority ASC ', [$articleId]); - + $rules = []; while ($sql->hasNext()) { - $config = json_decode($sql->getValue('schema_config'), true) ?: []; - $getParams = json_decode($sql->getValue('get_params'), true) ?: []; - + $configRaw = $sql->getValue('schema_config'); + $getParamsRaw = $sql->getValue('get_params'); + + $config = is_string($configRaw) ? (json_decode($configRaw, true) ?: []) : []; + $getParams = is_string($getParamsRaw) ? (json_decode($getParamsRaw, true) ?: []) : []; + $rules[] = [ 'id' => $sql->getValue('id'), 'name' => $sql->getValue('name'), @@ -87,13 +105,13 @@ private static function getUrlRules($articleId) ]; $sql->next(); } - + self::$rulesCache[$cacheKey] = $rules; } - + return self::$rulesCache[$cacheKey]; } - + /** * Regel gegen aktuelle URL/Parameter prüfen * @@ -102,7 +120,11 @@ private static function getUrlRules($articleId) * @param array $getParams GET-Parameter * @return bool Match gefunden */ - private static function matchRule($rule, $currentUrl, $getParams) + /** + * @param array $rule + * @param array $getParams + */ + private static function matchRule(array $rule, string $currentUrl, array $getParams): bool { // URL-Pattern prüfen (falls definiert) if (!empty($rule['url_pattern'])) { @@ -110,7 +132,7 @@ private static function matchRule($rule, $currentUrl, $getParams) return false; } } - + // Required GET-Parameter prüfen if (!empty($rule['get_params']['required'])) { foreach ($rule['get_params']['required'] as $param) { @@ -119,7 +141,7 @@ private static function matchRule($rule, $currentUrl, $getParams) } } } - + // Optional: GET-Parameter Werte validieren if (!empty($rule['get_params']['validation'])) { foreach ($rule['get_params']['validation'] as $param => $validation) { @@ -130,10 +152,10 @@ private static function matchRule($rule, $currentUrl, $getParams) } } } - + return true; } - + /** * Parameter-Wert validieren * @@ -141,18 +163,21 @@ private static function matchRule($rule, $currentUrl, $getParams) * @param array $validation Validierungs-Regeln * @return bool Validierung erfolgreich */ - private static function validateParamValue($value, $validation) + /** + * @param array $validation + */ + private static function validateParamValue(mixed $value, array $validation): bool { // Whitelist-Validierung if (!empty($validation['whitelist'])) { return in_array($value, $validation['whitelist']); } - + // Regex-Validierung if (!empty($validation['pattern'])) { - return preg_match($validation['pattern'], $value); + return 1 === preg_match((string) $validation['pattern'], (string) $value); } - + // Typ-Validierung if (!empty($validation['type'])) { switch ($validation['type']) { @@ -166,10 +191,10 @@ private static function validateParamValue($value, $validation) return true; } } - + return true; } - + /** * Daten aus Regel und GET-Parametern extrahieren * @@ -177,10 +202,15 @@ private static function validateParamValue($value, $validation) * @param array $getParams GET-Parameter * @return array Extrahierte Daten */ - private static function extractRuleData($rule, $getParams) + /** + * @param array $rule + * @param array $getParams + * @return array + */ + private static function extractRuleData(array $rule, array $getParams): array { $data = []; - + // GET-Parameter zu Daten mappen if (!empty($rule['get_params']['mapping'])) { foreach ($rule['get_params']['mapping'] as $source => $target) { @@ -189,15 +219,15 @@ private static function extractRuleData($rule, $getParams) } } } - + // Schema-spezifische Daten hinzufügen if (!empty($rule['schema_config']['additional_data'])) { $data = array_merge($data, $rule['schema_config']['additional_data']); } - + return $data; } - + /** * URL-Regel erstellen oder aktualisieren * @@ -205,15 +235,18 @@ private static function extractRuleData($rule, $getParams) * @param array $ruleData Regel-Daten * @return int Regel-ID */ - public static function createOrUpdateRule($articleId, $ruleData) + /** + * @param array $ruleData + */ + public static function createOrUpdateRule(int $articleId, array $ruleData): int { - $sql = \rex_sql::factory(); - + $sql = rex_sql::factory(); + // Validierung if (empty($ruleData['name'])) { - throw new \InvalidArgumentException('Regel-Name ist erforderlich'); + throw new InvalidArgumentException('Regel-Name ist erforderlich'); } - + $data = [ 'article_id' => $articleId, 'name' => $ruleData['name'], @@ -224,10 +257,10 @@ public static function createOrUpdateRule($articleId, $ruleData) 'priority' => $ruleData['priority'] ?? 100, 'modified' => date('Y-m-d H:i:s') ]; - + if (!empty($ruleData['id'])) { // Update - $sql->setTable(\rex::getTable('jsonld_url_rules')); + $sql->setTable(rex::getTable('jsonld_url_rules')); $sql->setWhere(['id' => $ruleData['id']]); $sql->setValues($data); $sql->update(); @@ -235,62 +268,65 @@ public static function createOrUpdateRule($articleId, $ruleData) } else { // Create $data['created'] = date('Y-m-d H:i:s'); - $sql->setTable(\rex::getTable('jsonld_url_rules')); + $sql->setTable(rex::getTable('jsonld_url_rules')); $sql->setValues($data); $sql->insert(); $ruleId = $sql->getLastId(); } - + // Cache leeren self::clearCache(); - + return $ruleId; } - + /** * URL-Regel löschen * * @param int $ruleId Regel-ID * @return bool Erfolgreich gelöscht */ - public static function deleteRule($ruleId) + public static function deleteRule(int $ruleId): bool { - $sql = \rex_sql::factory(); - $sql->setTable(\rex::getTable('jsonld_url_rules')); + $sql = rex_sql::factory(); + $sql->setTable(rex::getTable('jsonld_url_rules')); $sql->setWhere(['id' => $ruleId]); - + try { $sql->delete(); self::clearCache(); return true; - } catch (\Exception $e) { + } catch (Exception $e) { return false; } } - + /** * Alle URL-Regeln für Artikel abrufen * * @param int $articleId Artikel-ID * @return array Array von URL-Regeln */ - public static function getArticleRules($articleId) + /** + * @return list> + */ + public static function getArticleRules(int $articleId): array { - $sql = \rex_sql::factory(); + $sql = rex_sql::factory(); $sql->setQuery(' - SELECT * FROM '.\rex::getTable('jsonld_url_rules').' + SELECT * FROM '.rex::getTable('jsonld_url_rules').' WHERE article_id = ? ORDER BY priority ASC, name ASC ', [$articleId]); - + $rules = []; while ($sql->hasNext()) { $rules[] = [ 'id' => $sql->getValue('id'), 'name' => $sql->getValue('name'), 'url_pattern' => $sql->getValue('url_pattern'), - 'get_params' => json_decode($sql->getValue('get_params'), true), - 'schema_config' => json_decode($sql->getValue('schema_config'), true), + 'get_params' => is_string($sql->getValue('get_params')) ? (json_decode((string) $sql->getValue('get_params'), true) ?: []) : [], + 'schema_config' => is_string($sql->getValue('schema_config')) ? (json_decode((string) $sql->getValue('schema_config'), true) ?: []) : [], 'active' => (bool)$sql->getValue('active'), 'priority' => $sql->getValue('priority'), 'created' => $sql->getValue('created'), @@ -298,54 +334,58 @@ public static function getArticleRules($articleId) ]; $sql->next(); } - + return $rules; } - + /** * URL-Regel-Validierung * * @param array $ruleData Regel-Daten * @return array Validierungs-Fehler (leer = valide) */ - public static function validateRule($ruleData) + /** + * @param array $ruleData + * @return array + */ + public static function validateRule(array $ruleData): array { $errors = []; - + // Name erforderlich if (empty($ruleData['name'])) { $errors['name'] = 'Regel-Name ist erforderlich'; } - + // URL-Pattern Syntax prüfen if (!empty($ruleData['url_pattern'])) { if (@preg_match($ruleData['url_pattern'], '') === false) { $errors['url_pattern'] = 'Ungültiges Regex-Pattern'; } } - + // GET-Parameter Whitelist prüfen if (!empty($ruleData['get_params']['required'])) { - $addon = \rex_addon::get('jsonld_manager'); + $addon = rex_addon::get('jsonld_manager'); $whitelist = $addon->getConfig('whitelist.get_params', []); - + foreach ($ruleData['get_params']['required'] as $param) { if (!in_array($param, $whitelist)) { $errors['get_params'] = "Parameter '$param' ist nicht in der Whitelist"; } } } - + // Schema-Config validieren if (!empty($ruleData['schema_config'])) { if (empty($ruleData['schema_config']['schema_type'])) { $errors['schema_config'] = 'Schema-Type ist erforderlich'; } } - + return $errors; } - + /** * Test URL gegen Regel * @@ -354,14 +394,19 @@ public static function validateRule($ruleData) * @param array $testParams Test-Parameter * @return array Test-Ergebnis */ - public static function testRule($ruleData, $testUrl, $testParams = []) + /** + * @param array $ruleData + * @param array $testParams + * @return array + */ + public static function testRule(array $ruleData, string $testUrl, array $testParams = []): array { $result = [ 'match' => false, 'errors' => [], 'extracted_data' => [] ]; - + try { // Rule Mock erstellen $rule = [ @@ -369,27 +414,26 @@ public static function testRule($ruleData, $testUrl, $testParams = []) 'get_params' => $ruleData['get_params'] ?? [], 'schema_config' => $ruleData['schema_config'] ?? [] ]; - + // Test durchführen if (self::matchRule($rule, $testUrl, $testParams)) { $result['match'] = true; $result['extracted_data'] = self::extractRuleData($rule, $testParams); } - - } catch (\Exception $e) { + + } catch (Exception $e) { $result['errors'][] = $e->getMessage(); } - + return $result; } - + /** * Cache zurücksetzen */ - public static function clearCache() + public static function clearCache(): void { self::$rulesCache = null; - \rex_cache::deleteNamespace('jsonld_manager'); } } diff --git a/lib/template_functions.php b/lib/template_functions.php index 1060908..fe24a55 100644 --- a/lib/template_functions.php +++ b/lib/template_functions.php @@ -1,32 +1,37 @@ */ + function jsonld_manager_normalize_branch_ids(mixed $value): array { + return JsonLdGenerator::normalizeBranchIds($value); } /** * Liefert den Config-Key zum Deaktivieren von JSON-LD pro Artikel * (inkl. Multi-Domain-Unterstützung) */ - function jsonld_manager_disable_json_key($articleId, $clangId = null) { - return \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::getDisableJsonKey($articleId, $clangId); + function jsonld_manager_disable_json_key(int $articleId, mixed $clangId = null): string { + return JsonLdGenerator::getDisableJsonKey($articleId, $clangId); } /** * Lädt die für einen Artikel gespeicherten LocalBusiness-Branch-IDs * und normalisiert das Ergebnis auf ein Integer-Array. */ - function jsonld_manager_get_article_branch_ids($articleId, $clangId = null) { - return \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::getArticleBranchIds($articleId, $clangId); + /** @return array */ + function jsonld_manager_get_article_branch_ids(int $articleId, mixed $clangId = null): array { + return JsonLdGenerator::getArticleBranchIds($articleId, $clangId); } @@ -48,7 +53,7 @@ function jsonld_manager_get_article_branch_ids($articleId, $clangId = null) { */ function jsonld_prune_empty_values($value) { - return \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::pruneEmptyValues($value); + return JsonLdGenerator::pruneEmptyValues($value); } } @@ -56,12 +61,12 @@ function jsonld_prune_empty_values($value) /** * Einheitliches JSON-LD Payload bauen. * - * @param array $jsonLdData - * @return array + * @param array> $jsonLdData + * @return array */ - function jsonld_normalize_output_payload(array $jsonLdData) + function jsonld_normalize_output_payload(array $jsonLdData): array { - return \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::buildPayload($jsonLdData); + return JsonLdGenerator::buildPayload($jsonLdData); } } @@ -72,9 +77,9 @@ function jsonld_normalize_output_payload(array $jsonLdData) * * @return bool */ - function jsonld_is_backend_user_logged_in() + function jsonld_is_backend_user_logged_in(): bool { - return class_exists('rex_backend_login') && \rex_backend_login::hasSession(); + return class_exists('rex_backend_login') && rex_backend_login::hasSession(); } } @@ -91,26 +96,26 @@ function jsonld_is_debug_enabled() } $addon = rex_addon::get('jsonld_manager'); - + // Domain-spezifische Konfiguration prüfen (wenn Multi-Domain) if (class_exists('\FriendsOfRedaxo\JsonLdManager\DomainConfig')) { - if (\FriendsOfRedaxo\JsonLdManager\DomainConfig::isMultiDomain()) { - $activeDomainId = \FriendsOfRedaxo\JsonLdManager\DomainConfig::getActiveDomainId(); + if (DomainConfig::isMultiDomain()) { + $activeDomainId = DomainConfig::getActiveDomainId(); $configKey = 'global_settings_domain_' . $activeDomainId; $domainConfig = $addon->getConfig($configKey, []); - + if (isset($domainConfig['settings']) && array_key_exists('debug_mode', $domainConfig['settings'])) { return (bool) $domainConfig['settings']['debug_mode']; } } } - + // Fallback zu globaler Konfiguration $global = $addon->getConfig('global_settings', []); if (isset($global['settings']) && array_key_exists('debug_mode', $global['settings'])) { return (bool) $global['settings']['debug_mode']; } - + return (bool) $addon->getConfig('settings.debug_mode', false); } } @@ -123,14 +128,14 @@ function jsonld_is_debug_enabled() * @param rex_article $article * @return bool */ - function jsonld_is_template_output_allowed(\rex_article $article): bool + function jsonld_is_template_output_allowed(rex_article $article): bool { $addon = rex_addon::get('jsonld_manager'); $activeDomainId = 1; if (class_exists('\FriendsOfRedaxo\JsonLdManager\DomainConfig')) { - $activeDomainId = (int) \FriendsOfRedaxo\JsonLdManager\DomainConfig::getActiveDomainId(); - $configKey = \FriendsOfRedaxo\JsonLdManager\DomainConfig::isMultiDomain() + $activeDomainId = (int) DomainConfig::getActiveDomainId(); + $configKey = DomainConfig::isMultiDomain() ? 'global_settings_domain_' . $activeDomainId : 'global_settings'; } else { @@ -138,14 +143,14 @@ function jsonld_is_template_output_allowed(\rex_article $article): bool } $globalConfig = (array) rex_config::get('jsonld_manager', $configKey, []); - if (empty($globalConfig) && class_exists('\FriendsOfRedaxo\JsonLdManager\DomainConfig') && \FriendsOfRedaxo\JsonLdManager\DomainConfig::isMultiDomain()) { + if (empty($globalConfig) && class_exists('\FriendsOfRedaxo\JsonLdManager\DomainConfig') && DomainConfig::isMultiDomain()) { $globalConfig = (array) rex_config::get('jsonld_manager', 'global_settings', []); } $settings = (array) ($globalConfig['settings'] ?? []); $templates = (array) ($globalConfig['templates'] ?? []); $autoOutput = (bool) ($settings['auto_output'] ?? true); - $enabledIds = array_values(array_filter(array_map('intval', (array) ($templates['enabled_ids'] ?? [])), static function ($id) { + $enabledIds = array_values(array_filter(array_map('intval', (array) ($templates['enabled_ids'] ?? [])), static function (int $id): bool { return $id > 0; })); @@ -161,11 +166,11 @@ function jsonld_is_template_output_allowed(\rex_article $article): bool /** * Rendert ein benutzerfreundliches Debug-Overlay als JS (für Ausgabe im geeignet). * - * @param array $payload - * @param array $meta + * @param mixed $payload + * @param mixed $meta * @return string */ - function jsonld_render_debug_overlay_script($payload, $meta = []) + function jsonld_render_debug_overlay_script(mixed $payload, mixed $meta = []): string { // Defensive: null oder nicht-Array abfangen if (!is_array($payload)) { @@ -181,7 +186,7 @@ function jsonld_render_debug_overlay_script($payload, $meta = []) 'name' => 'Keine JSON-LD Daten vorhanden', 'description' => 'Bitte Konfiguration im Backend prüfen.' ]; - if (empty($payload) || (is_array($payload) && count($payload) === 0)) { + if (empty($payload)) { $payload = $payloadFallback; } $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); @@ -267,11 +272,11 @@ function jsonld_render_debug_overlay_script($payload, $meta = []) /** * JSON-LD für aktuelle Seite ausgeben * - * @param string|null $schemaType Spezifischer Schema-Type - * @param array $additionalData Zusätzliche Daten + * @param string|null $schemaType Spezifischer Schema-Type + * @param array $additionalData Zusätzliche Daten * @return string JSON-LD Script Tag */ - function jsonld_render($schemaType = null, $additionalData = []) + function jsonld_render(?string $schemaType = null, array $additionalData = []): string { try { $article = rex_article::getCurrent(); @@ -279,18 +284,18 @@ function jsonld_render($schemaType = null, $additionalData = []) return ''; } - return \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::renderArticleScript( + return JsonLdGenerator::renderArticleScript( (int) $article->getId(), null, jsonld_is_debug_enabled(), (int) $article->getClangId() ); - } catch (\Exception $e) { + } catch (Exception $e) { if (rex::isDebugMode()) { return ''; } } - + return ''; } } diff --git a/package.yml b/package.yml index 2ce3d82..f18840c 100644 --- a/package.yml +++ b/package.yml @@ -1,5 +1,5 @@ package: jsonld_manager -version: '1.0.0.beta.18' +version: '1.0.0.beta.19' author: 'Friends Of REDAXO' supportpage: https://github.com/FriendsOfREDAXO/jsonld_manager diff --git a/pages/article.php b/pages/article.php index 6acc7d7..78fb100 100644 --- a/pages/article.php +++ b/pages/article.php @@ -37,9 +37,6 @@ if (rex_post('action') === 'save_website_config' && rex_post('article_id', 'int') && $csrfToken->isValid()) { $articleId = rex_post('article_id', 'int'); $localbusiness_branch_ids = rex_post('localbusiness_branch_ids', 'array', []); - if (!is_array($localbusiness_branch_ids)) { - $localbusiness_branch_ids = $localbusiness_branch_ids ? [$localbusiness_branch_ids] : []; - } $config = [ 'name_field' => rex_post('name_field', 'string', ''), 'description_field' => rex_post('description_field', 'string', ''), @@ -82,7 +79,8 @@ /** * Artikel-Daten für AJAX abrufen */ -function getArticleData($articleId) { +/** @return array|null */ +function getArticleData(int $articleId): ?array { $sql = rex_sql::factory(); $sql->setQuery('SELECT id, name, template_id, updatedate FROM ' . rex::getTable('article') . ' WHERE id = ? AND clang_id = ?', [$articleId, rex_clang::getStartId()]); @@ -105,12 +103,14 @@ function getArticleData($articleId) { /** * Website Schema Konfiguration abrufen */ -function getWebsiteSchemaConfig($articleId) { +/** @return array */ +function getWebsiteSchemaConfig(int $articleId): array { $sql = rex_sql::factory(); $sql->setQuery('SELECT config, active FROM ' . rex::getTable('jsonld_schemas') . ' WHERE article_id = ? AND clang_id = ? AND schema_type = "WebPage"', [$articleId, rex_clang::getStartId()]); if ($sql->hasNext()) { - $config = json_decode($sql->getValue('config'), true) ?? []; + $configRaw = $sql->getValue('config'); + $config = is_string($configRaw) ? (json_decode($configRaw, true) ?? []) : []; $config['active'] = (bool) $sql->getValue('active'); // Kompatibilität: Einzelwert zu Array if (isset($config['localbusiness_branch_id']) && !isset($config['localbusiness_branch_ids'])) { @@ -133,8 +133,9 @@ function getWebsiteSchemaConfig($articleId) { /** * Verfügbare Meta-Felder ermitteln + * @return string[] */ -function getAvailableMetaFields() { +function getAvailableMetaFields(): array { $fields = [ '' => 'Bitte wählen', 'name' => 'Artikelname', @@ -180,8 +181,9 @@ function getAvailableMetaFields() { /** * Verfügbare LocalBusiness Standorte abrufen + * @return array */ -function getAvailableLocalBusinessBranches() { +function getAvailableLocalBusinessBranches(): array { $branches = [0 => 'Keine LocalBusiness Zuordnung (für allgemeine Seiten)']; try { @@ -189,11 +191,11 @@ function getAvailableLocalBusinessBranches() { $sql->setQuery('SELECT id, branch_name, is_main_branch FROM ' . rex::getTable('jsonld_localbusiness_branches') . ' ORDER BY is_main_branch DESC, sort_order ASC, branch_name ASC'); while ($sql->hasNext()) { - $label = $sql->getValue('branch_name'); + $label = (string) $sql->getValue('branch_name'); if ($sql->getValue('is_main_branch')) { $label .= ' (Hauptstandort)'; } - $branches[$sql->getValue('id')] = $label; + $branches[(int) $sql->getValue('id')] = $label; $sql->next(); } } catch (Exception $e) { @@ -206,7 +208,8 @@ function getAvailableLocalBusinessBranches() { /** * WebPage JSON-LD generieren */ -function generateWebPageJsonLd($articleId) { +/** @return array|null */ +function generateWebPageJsonLd(int $articleId): ?array { // Basis Article-Daten $article = rex_article::get($articleId, rex_clang::getStartId()); if (!$article) return null; @@ -216,7 +219,7 @@ function generateWebPageJsonLd($articleId) { $jsonld = [ "@context" => "https://schema.org", "@type" => "WebPage", - "@id" => (\rex_addon::get('yrewrite')->isAvailable() ? rex_yrewrite::getFullUrlByArticleId($articleId) : rex_url::frontendController() . '?article_id=' . $articleId) . '#webpage' + "@id" => (rex_addon::get('yrewrite')->isAvailable() ? rex_yrewrite::getFullUrlByArticleId($articleId) : rex_url::frontendController() . '?article_id=' . $articleId) . '#webpage' ]; // LocalBusiness-IDs als Array berücksichtigen if (!empty($config['localbusiness_branch_ids'])) { @@ -238,7 +241,7 @@ function generateWebPageJsonLd($articleId) { } // URL - $jsonld['url'] = \rex_addon::get('yrewrite')->isAvailable() + $jsonld['url'] = rex_addon::get('yrewrite')->isAvailable() ? rex_yrewrite::getFullUrlByArticleId($articleId) : rex_url::frontendController() . '?article_id=' . $articleId; @@ -246,7 +249,7 @@ function generateWebPageJsonLd($articleId) { if (!empty($config['image_field'])) { $imageValue = getFieldValue($article, $config['image_field']); if ($imageValue) { - $jsonld['image'] = (\rex_addon::get('yrewrite')->isAvailable() + $jsonld['image'] = (rex_addon::get('yrewrite')->isAvailable() ? rex_yrewrite::getFullUrlByArticleId(1) : rex_url::frontendController()) . '/media/' . $imageValue; } @@ -258,14 +261,14 @@ function generateWebPageJsonLd($articleId) { /** * Feld-Wert aus Artikel extrahieren */ -function getFieldValue($article, $fieldName) { +function getFieldValue(rex_article $article, string $fieldName): mixed { switch ($fieldName) { case 'name': return $article->getName(); case 'yrewrite_title': - return \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_title') : ''; + return rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_title') : ''; case 'yrewrite_description': - return \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_description') : ''; + return rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_description') : ''; case 'art_description': return $article->getValue('art_description'); default: @@ -296,7 +299,7 @@ function getFieldValue($article, $fieldName) { $templateSql->setQuery('SELECT id, name FROM ' . rex::getTable('template') . ' ORDER BY name'); $templates = []; while ($templateSql->hasNext()) { - $templates[$templateSql->getValue('id')] = $templateSql->getValue('name'); + $templates[(int) $templateSql->getValue('id')] = (string) $templateSql->getValue('name'); $templateSql->next(); } @@ -540,15 +543,15 @@ class="form-control" placeholder="Artikel suchen" style="width: 250px;"> echo 'Keine Artikel gefunden.'; } else { while ($sql->hasNext()) { - $articleId = $sql->getValue('id'); - $articleName = $sql->getValue('name'); - $templateId = $sql->getValue('template_id'); + $articleId = (int) $sql->getValue('id'); + $articleName = (string) $sql->getValue('name'); + $templateId = (int) $sql->getValue('template_id'); $templateName = $templates[$templateId] ?? 'Unbekannt'; echo ' ' . $articleId . ' ' . htmlspecialchars($articleName) . ' - ' . htmlspecialchars($templateName) . ' + ' . htmlspecialchars((string) $templateName) . ' '; $sql->next(); @@ -633,7 +636,7 @@ class="form-control" placeholder="Artikel suchen" style="width: 250px;"> Mehrere Standorte auswählbar. "Keine" für allgemeine Seiten wie Impressum. diff --git a/pages/article_jsonld.php b/pages/article_jsonld.php index 7f5f9ac..97c91c7 100644 --- a/pages/article_jsonld.php +++ b/pages/article_jsonld.php @@ -4,22 +4,24 @@ * * Zeigt für jeden Artikel den generierten JSON-LD Output an */ - +use FriendsOfRedaxo\JsonLdManager\LanguageConfig; +use FriendsOfRedaxo\JsonLdManager\JsonLdGenerator; use FriendsOfRedaxo\JsonLdManager\DomainConfig; $addon = rex_addon::get('jsonld_manager'); -$activeClangId = \FriendsOfRedaxo\JsonLdManager\LanguageConfig::getActiveClangId(); +$activeClangId = LanguageConfig::getActiveClangId(); $activeDomainId = DomainConfig::getActiveDomainId(); $csrfToken = rex_csrf_token::factory('jsonld_manager'); $csrfTokenField = $csrfToken->getHiddenField(); function jsonld_manager_custom_json_key(int $articleId, int $clangId): string { - return \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::getCustomJsonKey($articleId, $clangId); + return JsonLdGenerator::getCustomJsonKey($articleId, $clangId); } +/** @return array */ function getLocalBusinessBranches(): array { - $activeClangId = \FriendsOfRedaxo\JsonLdManager\LanguageConfig::getActiveClangId(); + $activeClangId = LanguageConfig::getActiveClangId(); $activeDomainId = DomainConfig::getActiveDomainId(); try { @@ -46,8 +48,8 @@ function getLocalBusinessBranches(): array $branches = []; while ($sql->hasNext()) { $branches[] = [ - 'id' => $sql->getValue('id'), - 'name' => $sql->getValue('branch_name'), + 'id' => (int) $sql->getValue('id'), + 'name' => (string) $sql->getValue('branch_name'), 'is_main' => (bool) $sql->getValue('is_main_branch') ]; $sql->next(); @@ -72,14 +74,14 @@ function getLocalBusinessBranches(): array if ($articleId > 0) { // Branch-Auswahl für diesen Artikel speichern - rex_config::set('jsonld_manager', jsonld_manager_article_branch_key($articleId, \FriendsOfRedaxo\JsonLdManager\LanguageConfig::getActiveClangId()), $branchIdsAjax); + rex_config::set('jsonld_manager', jsonld_manager_article_branch_key($articleId, LanguageConfig::getActiveClangId()), $branchIdsAjax); // JSON-LD mit derselben Ausgabe-Pipeline wie Frontend/Backend-Vorschau generieren. - $jsonldOutput = \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::getArticleOutput( + $jsonldOutput = JsonLdGenerator::getArticleOutput( $articleId, null, true, - \FriendsOfRedaxo\JsonLdManager\LanguageConfig::getActiveClangId() + LanguageConfig::getActiveClangId() ); rex_response::sendJson([ @@ -108,20 +110,25 @@ function getLocalBusinessBranches(): array } // Funktion für hierarchische Struktur-Verarbeitung (mit Ebenen) -function addArticlesFromCategoryHierarchical($category, &$articles, &$addedIds = [], $level = 0, $parentCategory = null) { +/** + * @param rex_category|null $category + * @param array> $articles + * @param array $addedIds + */ +function addArticlesFromCategoryHierarchical(?rex_category $category, array &$articles, array &$addedIds = [], int $level = 0, ?rex_category $parentCategory = null): void { // Startartikel der Kategorie hinzufügen (nur wenn noch nicht vorhanden) if ($category && !in_array($category->getId(), $addedIds)) { $articles[] = [ 'id' => $category->getId(), 'name' => $category->getName(), - 'yrewrite_title' => \rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_title') : '', + 'yrewrite_title' => rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_title') : '', 'category' => $parentCategory ? $parentCategory->getName() : 'Hauptebene', 'parent_id' => $category->getParentId(), 'url' => $category->getUrl(), 'createdate' => $category->getCreateDate(), 'updatedate' => $category->getUpdateDate(), - 'yrewrite_description' => \rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_description') : '', - 'yrewrite_image' => \rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_image') : '', + 'yrewrite_description' => rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_description') : '', + 'yrewrite_image' => rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_image') : '', 'status' => $category->isOnline() ? 'online' : 'offline', 'level' => $level, 'priority' => $category->getPriority() @@ -136,14 +143,14 @@ function addArticlesFromCategoryHierarchical($category, &$articles, &$addedIds = $articles[] = [ 'id' => $article->getId(), 'name' => $article->getName(), - 'yrewrite_title' => \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_title') : '', - 'category' => $category ? $category->getName() : 'Keine Kategorie', + 'yrewrite_title' => rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_title') : '', + 'category' => $category->getName(), 'parent_id' => $article->getCategoryId(), 'url' => $article->getUrl(), 'createdate' => $article->getCreateDate(), 'updatedate' => $article->getUpdateDate(), - 'yrewrite_description' => \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_description') : '', - 'yrewrite_image' => \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_image') : '', + 'yrewrite_description' => rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_description') : '', + 'yrewrite_image' => rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_image') : '', 'status' => $article->isOnline() ? 'online' : 'offline', 'level' => $level + 1, // Artikel sind eine Ebene tiefer als ihre Kategorie 'priority' => $article->getPriority() @@ -154,8 +161,8 @@ function addArticlesFromCategoryHierarchical($category, &$articles, &$addedIds = // Rekursiv für Unterkategorien (sortiert nach Priorität) $subCategories = $category ? $category->getChildren() : []; - usort($subCategories, function($a, $b) { - return $a->getPriority() - $b->getPriority(); + usort($subCategories, function(rex_category $a, rex_category $b): int { + return $a->getPriority() <=> $b->getPriority(); }); foreach ($subCategories as $subCategory) { @@ -164,20 +171,25 @@ function addArticlesFromCategoryHierarchical($category, &$articles, &$addedIds = } // Funktion für rekursive Struktur-Verarbeitung -function addArticlesFromCategory($category, &$articles, &$addedIds = []) { +/** + * @param rex_category|null $category + * @param array> $articles + * @param array $addedIds + */ +function addArticlesFromCategory(?rex_category $category, array &$articles, array &$addedIds = []): void { // Startartikel der Kategorie hinzufügen (nur wenn noch nicht vorhanden) if ($category && !in_array($category->getId(), $addedIds)) { $articles[] = [ 'id' => $category->getId(), 'name' => $category->getName(), - 'yrewrite_title' => \rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_title') : '', + 'yrewrite_title' => rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_title') : '', 'category' => $category->getParent() ? $category->getParent()->getName() : 'Hauptebene', 'parent_id' => $category->getParentId(), 'url' => $category->getUrl(), 'createdate' => $category->getCreateDate(), 'updatedate' => $category->getUpdateDate(), - 'yrewrite_description' => \rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_description') : '', - 'yrewrite_image' => \rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_image') : '', + 'yrewrite_description' => rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_description') : '', + 'yrewrite_image' => rex_addon::get('yrewrite')->isAvailable() ? $category->getValue('yrewrite_image') : '', 'status' => $category->isOnline() ? 'online' : 'offline', 'level' => 0, // Level hinzufügen für Kompatibilität 'priority' => $category->getPriority() @@ -192,14 +204,14 @@ function addArticlesFromCategory($category, &$articles, &$addedIds = []) { $articles[] = [ 'id' => $article->getId(), 'name' => $article->getName(), - 'yrewrite_title' => \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_title') : '', - 'category' => $category ? $category->getName() : 'Keine Kategorie', + 'yrewrite_title' => rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_title') : '', + 'category' => $category->getName(), 'parent_id' => $article->getCategoryId(), 'url' => $article->getUrl(), 'createdate' => $article->getCreateDate(), 'updatedate' => $article->getUpdateDate(), - 'yrewrite_description' => \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_description') : '', - 'yrewrite_image' => \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_image') : '', + 'yrewrite_description' => rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_description') : '', + 'yrewrite_image' => rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_image') : '', 'status' => $article->isOnline() ? 'online' : 'offline', 'level' => 1, // Level hinzufügen für Kompatibilität 'priority' => $article->getPriority() @@ -231,14 +243,14 @@ function addArticlesFromCategory($category, &$articles, &$addedIds = []) { $articles[] = [ 'id' => $startArticle->getId(), 'name' => $startArticle->getName(), - 'yrewrite_title' => \rex_addon::get('yrewrite')->isAvailable() ? $startArticle->getValue('yrewrite_title') : '', + 'yrewrite_title' => rex_addon::get('yrewrite')->isAvailable() ? $startArticle->getValue('yrewrite_title') : '', 'category' => 'Domain-Startseite', 'parent_id' => 0, 'url' => $startArticle->getUrl(), 'createdate' => $startArticle->getCreateDate(), 'updatedate' => $startArticle->getUpdateDate(), - 'yrewrite_description' => \rex_addon::get('yrewrite')->isAvailable() ? $startArticle->getValue('yrewrite_description') : '', - 'yrewrite_image' => \rex_addon::get('yrewrite')->isAvailable() ? $startArticle->getValue('yrewrite_image') : '', + 'yrewrite_description' => rex_addon::get('yrewrite')->isAvailable() ? $startArticle->getValue('yrewrite_description') : '', + 'yrewrite_image' => rex_addon::get('yrewrite')->isAvailable() ? $startArticle->getValue('yrewrite_image') : '', 'status' => $startArticle->isOnline() ? 'online' : 'offline', 'level' => 0, // Domain-Root-Level 'priority' => $startArticle->getPriority() @@ -267,8 +279,8 @@ function addArticlesFromCategory($category, &$articles, &$addedIds = []) { $categories = rex_category::getRootCategories(); // Kategorien nach Priorität sortieren - usort($categories, function($a, $b) { - return $a->getPriority() - $b->getPriority(); + usort($categories, function(rex_category $a, rex_category $b): int { + return $a->getPriority() <=> $b->getPriority(); }); // Root-Artikel hinzufügen (ohne Kategorie) - auch offline @@ -278,14 +290,14 @@ function addArticlesFromCategory($category, &$articles, &$addedIds = []) { $articles[] = [ 'id' => $article->getId(), 'name' => $article->getName(), - 'yrewrite_title' => \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_title') : '', + 'yrewrite_title' => rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_title') : '', 'category' => 'Hauptebene', 'parent_id' => 0, 'url' => $article->getUrl(), 'createdate' => $article->getCreateDate(), 'updatedate' => $article->getUpdateDate(), - 'yrewrite_description' => \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_description') : '', - 'yrewrite_image' => \rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_image') : '', + 'yrewrite_description' => rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_description') : '', + 'yrewrite_image' => rex_addon::get('yrewrite')->isAvailable() ? $article->getValue('yrewrite_image') : '', 'status' => $article->isOnline() ? 'online' : 'offline', 'level' => 0, // Root-Level 'priority' => $article->getPriority() @@ -302,7 +314,7 @@ function addArticlesFromCategory($category, &$articles, &$addedIds = []) { // Wenn kein Artikel ausgewählt, ersten nehmen if ($selectedArticleId === 0 && !empty($articles)) { - $selectedArticleId = $articles[0]['id']; + $selectedArticleId = (int) $articles[0]['id']; } // Custom JSON Form-Verarbeitung @@ -387,11 +399,11 @@ function addArticlesFromCategory($category, &$articles, &$addedIds = []) { $levelClass = $level > 0 ? ' article-level-' . $level : ''; // Prüfen ob Custom JSON für diesen Artikel existiert - $hasCustomJson = \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::hasCustomJson((int) $article['id'], $activeClangId); - $isJsonDisabled = \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::isArticleJsonDisabled((int) $article['id'], $activeClangId); + $hasCustomJson = JsonLdGenerator::hasCustomJson((int) $article['id'], $activeClangId); + $isJsonDisabled = JsonLdGenerator::isArticleJsonDisabled((int) $article['id'], $activeClangId); // Anzahl zusätzlicher Nicht-Hauptstandortn ermitteln - $assignedBranchIds = \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::getArticleBranchIds((int) $article['id'], $activeClangId); + $assignedBranchIds = JsonLdGenerator::getArticleBranchIds((int) $article['id'], $activeClangId); $nonMainBranchCount = 0; if (!empty($assignedBranchIds)) { // Prüfen wie viele zugeordnete Standorte keine Hauptstandort sind @@ -449,7 +461,7 @@ function addArticlesFromCategory($category, &$articles, &$addedIds = []) { } // HTML in String sammeln für Fragment-System -$content = \FriendsOfRedaxo\JsonLdManager\LanguageConfig::renderClangTabs($activeClangId) . ''; +$content = LanguageConfig::renderClangTabs($activeClangId) . ''; $jsonLdOutput = ''; $isCustomJson = false; @@ -457,9 +469,9 @@ function addArticlesFromCategory($category, &$articles, &$addedIds = []) { $customJsonRaw = ''; if ($selectedArticle) { - $customJsonRaw = \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::getCustomJson((int) $selectedArticleId, $activeClangId); + $customJsonRaw = JsonLdGenerator::getCustomJson((int) $selectedArticleId, $activeClangId); $branchOverride = $branchId > 0 ? [$branchId] : null; - $articleOutput = \FriendsOfRedaxo\JsonLdManager\JsonLdGenerator::getArticleOutput( + $articleOutput = JsonLdGenerator::getArticleOutput( (int) $selectedArticleId, $branchOverride, true, @@ -490,7 +502,7 @@ function addArticlesFromCategory($category, &$articles, &$addedIds = []) { } } -$content = \FriendsOfRedaxo\JsonLdManager\LanguageConfig::renderClangTabs($activeClangId) . ' +$content = LanguageConfig::renderClangTabs($activeClangId) . ' ' : '') . ' - ' . ($selectedArticle ? ' + ' . ' - ' : '') . ' + + ' . '
' . $jsonPreviewContent . '
diff --git a/pages/dynamic_urls.php b/pages/dynamic_urls.php index fa9ddc6..6b2e018 100644 --- a/pages/dynamic_urls.php +++ b/pages/dynamic_urls.php @@ -30,7 +30,7 @@ foreach ($profiles as $profile) { // Korrigiere Tabellenname (entferne 1_xxx_ Prefix) - $realTableName = str_replace('1_xxx_', '', $profile['table_name']); + $realTableName = str_replace('1_xxx_', '', (string) ($profile['table_name'] ?? '')); // Prüfe ob Mapping existiert $mappingExists = rex_sql::factory()->getArray( diff --git a/pages/dynamic_urls_edit.php b/pages/dynamic_urls_edit.php index 3bd8068..ad1e445 100644 --- a/pages/dynamic_urls_edit.php +++ b/pages/dynamic_urls_edit.php @@ -19,19 +19,19 @@ // Echte YForm-Tabelle aus table_parameters extrahieren $yformTableName = null; -if ($profile['table_parameters']) { - $tableParams = json_decode($profile['table_parameters'], true); +if (!empty($profile['table_parameters'])) { + $tableParams = json_decode((string) $profile['table_parameters'], true); if ($tableParams && !empty($tableParams['table_name'])) { - $yformTableName = $tableParams['table_name']; + $yformTableName = (string) $tableParams['table_name']; } } // Fallback: Korrigiere Tabellenname (entferne 1_xxx_ Prefix) if (!$yformTableName) { - $yformTableName = str_replace('1_xxx_', '', $profile['table_name']); + $yformTableName = str_replace('1_xxx_', '', (string) ($profile['table_name'] ?? '')); } -if (!is_string($yformTableName) || preg_match('/^[A-Za-z0-9_]+$/', $yformTableName) !== 1) { +if (preg_match('/^[A-Za-z0-9_]+$/', $yformTableName) !== 1) { echo rex_view::error('Ungültiger oder unsicherer Tabellenname im URL-Profil.'); return; } @@ -66,7 +66,7 @@ if (count($mapping) > 0) { // Update $sql->setTable(rex::getTable('jsonld_url_profile_mappings')); - $sql->setWhere(['id' => $mapping[0]['id']]); + $sql->setWhere(['id' => (int) ($mapping[0]['id'] ?? 0)]); } else { // Insert $sql->setTable(rex::getTable('jsonld_url_profile_mappings')); @@ -106,7 +106,7 @@ $tableFields = rex_sql::factory()->getArray('DESCRIBE ' . $yformTableName); } catch (rex_sql_exception $e) { // Fallback: Aus table_parameters laden - if ($profile['table_parameters']) { + if (!empty($profile['table_parameters']) && is_string($profile['table_parameters'])) { $tableParams = json_decode($profile['table_parameters'], true); if ($tableParams) { @@ -278,12 +278,20 @@ ]; // Config laden falls vorhanden -$config = []; +$config = [ + 'schema_type' => '', + 'active' => 1, + 'field_mappings' => [], +]; if (!empty($mapping)) { + $fieldMappingsConfig = []; + if (isset($mapping[0]['field_mappings']) && is_string($mapping[0]['field_mappings'])) { + $fieldMappingsConfig = json_decode($mapping[0]['field_mappings'], true) ?: []; + } $config = [ - 'schema_type' => $mapping[0]['schema_type'] ?? '', - 'active' => $mapping[0]['active'] ?? 1, - 'field_mappings' => json_decode($mapping[0]['field_mappings'] ?? '{}', true) + 'schema_type' => (string) ($mapping[0]['schema_type'] ?? ''), + 'active' => (int) ($mapping[0]['active'] ?? 1), + 'field_mappings' => is_array($fieldMappingsConfig) ? $fieldMappingsConfig : [], ]; } @@ -394,13 +402,35 @@ $fragment->setVar('body', $content, false); echo $fragment->parse('core/page/section.php'); +$schemaPropertiesJson = json_encode($schemaProperties, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$tableFieldsJson = json_encode(array_column($tableFields, 'Field'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$sampleDataJson = json_encode($sampleData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$profileNamespaceJson = json_encode((string) ($profile['namespace'] ?? ''), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$savedMappingsJson = json_encode($config['field_mappings'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + +if (!is_string($schemaPropertiesJson)) { + $schemaPropertiesJson = '{}'; +} +if (!is_string($tableFieldsJson)) { + $tableFieldsJson = '[]'; +} +if (!is_string($sampleDataJson)) { + $sampleDataJson = '{}'; +} +if (!is_string($profileNamespaceJson)) { + $profileNamespaceJson = '""'; +} +if (!is_string($savedMappingsJson)) { + $savedMappingsJson = '{}'; +} + ?>