getMessage()); } } /** * @param array $payload API entity or directory meta wrapper */ public static function schema_document_script(array $payload): string { if (! self::is_publishable_contract($payload)) { return ''; } $document = $payload['schemaDocument'] ?? null; if (! is_array($document) || ! isset($document['@graph']) || ! is_array($document['@graph'])) { return ''; } if (! self::page_url_matches($document)) { return ''; } $json = wp_json_encode( $document, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); if (! is_string($json) || $json === '') { return ''; } return '' . "\n"; } /** * @deprecated Shortcodes must not emit schema. Kept as empty for call-site safety. * @param array $provider */ public static function provider_json_ld(array $provider, ?array $theme = null): string { unset($provider, $theme); return ''; } /** * @deprecated Shortcodes must not emit schema. * @param array $location */ public static function location_json_ld(array $location, ?array $theme = null): string { unset($location, $theme); return ''; } /** * @deprecated Shortcodes must not emit schema. * @param array $organization */ public static function organization_json_ld(array $organization): string { unset($organization); return ''; } /** * @deprecated Directory schema comes from API meta.schemaDocument via wp_head. * @param list> $items */ public static function directory_json_ld(array $items, string $kind = 'provider', ?array $theme = null): string { unset($items, $kind, $theme); return ''; } public static function sitemap_image_url(?string $url): string { $url = trim((string) $url); if ($url === '') { return ''; } $host = strtolower((string) (wp_parse_url($url, PHP_URL_HOST) ?? '')); if ($host === 'cdn.sondy.io' || str_ends_with($host, '.cdn.sondy.io')) { return $url; } return ''; } /** * @return array|null */ private static function resolve_page_payload(): ?array { $client = new Powr_Dir_Api_Client(); if (! $client->is_configured()) { return null; } $provider_page_id = (int) get_option('powr_detail_page_id', 0); $location_page_id = (int) get_option('powr_location_listing_page_id', 0); $provider_slug = sanitize_title((string) get_query_var(Powr_Dir_Rewrites::QUERY_VAR_PROVIDER)); if ($provider_page_id > 0 && is_page($provider_page_id) && $provider_slug !== '') { $raw = $client->get_provider($provider_slug); return is_wp_error($raw) || ! is_array($raw) ? null : $raw; } $location_slug = sanitize_title((string) get_query_var(Powr_Dir_Rewrites::QUERY_VAR_LOCATION)); if ($location_page_id > 0 && is_page($location_page_id) && $location_slug !== '') { $raw = $client->get_location($location_slug); return is_wp_error($raw) || ! is_array($raw) ? null : $raw; } if ($provider_page_id > 0 && is_page($provider_page_id) && $provider_slug === '') { $raw = $client->get_providers(); if (is_wp_error($raw) || ! is_array($raw)) { return null; } return is_array($raw['meta'] ?? null) ? $raw['meta'] : null; } if ($location_page_id > 0 && is_page($location_page_id) && $location_slug === '') { $raw = $client->get_locations(); if (is_wp_error($raw) || ! is_array($raw)) { return null; } // Location list historically returns a bare list; prefer wrapped meta when present. if (isset($raw['meta']) && is_array($raw['meta'])) { return $raw['meta']; } return null; } return null; } /** * @param array $payload */ private static function is_publishable_contract(array $payload): bool { $contract = (int) ($payload['schemaContract'] ?? 0); if ($contract < 2) { return false; } return (bool) ($payload['publishable'] ?? false); } /** * Validation only: emit nothing when the request URL disagrees with WebPage.url. * * @param array $document */ private static function page_url_matches(array $document): bool { $graph = $document['@graph'] ?? null; if (! is_array($graph)) { return false; } $pageUrl = null; foreach ($graph as $node) { if (! is_array($node)) { continue; } $type = $node['@type'] ?? null; $types = is_array($type) ? $type : [$type]; if (in_array('WebPage', $types, true) || in_array('CollectionPage', $types, true)) { $pageUrl = isset($node['url']) ? (string) $node['url'] : null; break; } } if ($pageUrl === null || $pageUrl === '') { return true; } $request = home_url(add_query_arg([])); $reqHost = strtolower((string) (wp_parse_url($request, PHP_URL_HOST) ?? '')); $reqPath = rtrim((string) (wp_parse_url($request, PHP_URL_PATH) ?? ''), '/'); $docHost = strtolower((string) (wp_parse_url($pageUrl, PHP_URL_HOST) ?? '')); $docPath = rtrim((string) (wp_parse_url($pageUrl, PHP_URL_PATH) ?? ''), '/'); if ($reqHost === '' || $docHost === '') { return true; } // Cannot reliably validate without a request path (e.g. CLI / incomplete WP bootstrap). if ($reqPath === '') { return true; } return $reqHost === $docHost && $reqPath === $docPath; } /** * Admin-only HTML comment diagnostics (?powr_schema_debug=1). * Never prints tokens or full schemaDocument. */ private static function schema_debug_enabled(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only diagnostic flag if (! isset($_GET['powr_schema_debug'])) { return false; } return function_exists('current_user_can') && current_user_can('manage_options'); } private static function maybe_debug(string $message): void { if (! self::schema_debug_enabled()) { return; } echo '\n"; } /** * @param array $payload */ private static function maybe_debug_payload_gates(array $payload): void { if (! self::schema_debug_enabled()) { return; } $contract = (int) ($payload['schemaContract'] ?? 0); $publishable = (bool) ($payload['publishable'] ?? false); $document = $payload['schemaDocument'] ?? null; $hasDoc = is_array($document); $hasGraph = $hasDoc && isset($document['@graph']) && is_array($document['@graph']); $urlOk = $hasDoc ? self::page_url_matches($document) : false; $errors = $payload['publishErrors'] ?? []; $errorList = is_array($errors) ? implode(',', array_map('strval', $errors)) : ''; $msg = sprintf( 'gates contract=%d publishable=%s document=%s graph=%s url_match=%s publishErrors=%s', $contract, $publishable ? 'true' : 'false', $hasDoc ? 'yes' : 'no', $hasGraph ? 'yes' : 'no', $urlOk ? 'true' : 'false', $errorList !== '' ? $errorList : '(none)' ); self::maybe_debug($msg); } }