1, 'name' => '', 'aliases' => array(), 'legal_name' => '', 'type' => '', 'description' => '', 'founded' => '', 'founders' => array(), 'people' => array(), // [{name, role}] 'address' => array('street' => '', 'city' => '', 'postal' => '', 'region' => '', 'country' => ''), 'phone' => '', 'email' => '', 'website' => '', 'services' => array(), // [{name, description, price}] 'numbers' => array(), // [{label, value, as_of}] 'sameas' => array(), 'wikidata' => '', // 2.48 — QID (Q123): from the dashboard's engine check or typed by hand 'areas' => array(), 'languages' => array(), 'ai_notes' => '', 'updated_at' => '', ); } function citami_truth_lines_to_list($raw, $max = CITAMI_TRUTH_MAX_LIST) { if (is_array($raw)) { $items = $raw; } else { $items = preg_split('/\r\n|\r|\n|,/', (string) $raw); } $out = array(); foreach ((array) $items as $it) { $it = mb_substr(sanitize_text_field((string) $it), 0, 200); if ($it !== '' && !in_array($it, $out, true)) { $out[] = $it; } if (count($out) >= $max) { break; } } return $out; } function citami_truth_sanitize_sheet($in) { $d = citami_truth_defaults(); $in = is_array($in) ? $in : array(); $out = $d; foreach (array('name', 'legal_name', 'description', 'phone', 'ai_notes') as $k) { $out[$k] = mb_substr(sanitize_text_field((string) (isset($in[$k]) ? $in[$k] : '')), 0, $k === 'ai_notes' || $k === 'description' ? 600 : 200); } $out['type'] = preg_replace('/[^A-Za-z]/', '', (string) (isset($in['type']) ? $in['type'] : '')); $founded = trim((string) (isset($in['founded']) ? $in['founded'] : '')); $out['founded'] = preg_match('/^\d{4}(-\d{2}(-\d{2})?)?$/', $founded) ? $founded : ''; $email = trim((string) (isset($in['email']) ? $in['email'] : '')); $out['email'] = filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : ''; $web = trim((string) (isset($in['website']) ? $in['website'] : '')); $out['website'] = preg_match('#^https?://#i', $web) ? esc_url_raw($web) : ''; $addr = isset($in['address']) && is_array($in['address']) ? $in['address'] : array(); foreach (array('street', 'city', 'postal', 'region') as $k) { $out['address'][$k] = mb_substr(sanitize_text_field((string) (isset($addr[$k]) ? $addr[$k] : '')), 0, 120); } $out['address']['country'] = strtoupper(mb_substr(preg_replace('/[^A-Za-z]/', '', (string) (isset($addr['country']) ? $addr['country'] : '')), 0, 2)); foreach (array('aliases', 'founders', 'areas', 'languages') as $k) { $out[$k] = citami_truth_lines_to_list(isset($in[$k]) ? $in[$k] : array()); } $same = array(); foreach (citami_truth_lines_to_list(isset($in['sameas']) ? $in['sameas'] : array()) as $u) { if (preg_match('#^https?://#i', $u)) { $same[] = esc_url_raw($u); } } $out['sameas'] = $same; $qid = strtoupper(preg_replace('/[^Qq0-9]/', '', (string) (isset($in['wikidata']) ? $in['wikidata'] : ''))); $out['wikidata'] = preg_match('/^Q\d+$/', $qid) ? $qid : ''; $people = array(); $raw_people = isset($in['people']) ? $in['people'] : array(); if (is_string($raw_people)) { $raw_people = preg_split('/\r\n|\r|\n/', $raw_people); } foreach ((array) $raw_people as $p) { if (is_string($p)) { $parts = array_map('trim', explode('|', $p, 2)); $p = array('name' => $parts[0], 'role' => isset($parts[1]) ? $parts[1] : ''); } if (!is_array($p)) { continue; } $name = mb_substr(sanitize_text_field((string) (isset($p['name']) ? $p['name'] : '')), 0, 120); if ($name === '') { continue; } $people[] = array('name' => $name, 'role' => mb_substr(sanitize_text_field((string) (isset($p['role']) ? $p['role'] : '')), 0, 120)); if (count($people) >= CITAMI_TRUTH_MAX_LIST) { break; } } $out['people'] = $people; $services = array(); $raw_services = isset($in['services']) ? $in['services'] : array(); if (is_string($raw_services)) { $raw_services = preg_split('/\r\n|\r|\n/', $raw_services); } foreach ((array) $raw_services as $s) { if (is_string($s)) { $parts = array_map('trim', explode('|', $s, 3)); $s = array('name' => $parts[0], 'description' => isset($parts[1]) ? $parts[1] : '', 'price' => isset($parts[2]) ? $parts[2] : ''); } if (!is_array($s)) { continue; } $name = mb_substr(sanitize_text_field((string) (isset($s['name']) ? $s['name'] : '')), 0, 120); if ($name === '') { continue; } $services[] = array( 'name' => $name, 'description' => mb_substr(sanitize_text_field((string) (isset($s['description']) ? $s['description'] : '')), 0, 300), 'price' => mb_substr(sanitize_text_field((string) (isset($s['price']) ? $s['price'] : '')), 0, 60), ); if (count($services) >= CITAMI_TRUTH_MAX_LIST) { break; } } $out['services'] = $services; $numbers = array(); $raw_numbers = isset($in['numbers']) ? $in['numbers'] : array(); if (is_string($raw_numbers)) { $raw_numbers = preg_split('/\r\n|\r|\n/', $raw_numbers); } foreach ((array) $raw_numbers as $n) { if (is_string($n)) { $parts = array_map('trim', explode('|', $n, 3)); $n = array('label' => $parts[0], 'value' => isset($parts[1]) ? $parts[1] : '', 'as_of' => isset($parts[2]) ? $parts[2] : ''); } if (!is_array($n)) { continue; } $label = mb_substr(sanitize_text_field((string) (isset($n['label']) ? $n['label'] : '')), 0, 120); $value = mb_substr(sanitize_text_field((string) (isset($n['value']) ? $n['value'] : '')), 0, 60); if ($label === '' || $value === '') { continue; } $numbers[] = array('label' => $label, 'value' => $value, 'as_of' => mb_substr(sanitize_text_field((string) (isset($n['as_of']) ? $n['as_of'] : '')), 0, 20)); if (count($numbers) >= CITAMI_TRUTH_MAX_LIST) { break; } } $out['numbers'] = $numbers; $out['updated_at'] = isset($in['updated_at']) && is_string($in['updated_at']) ? mb_substr($in['updated_at'], 0, 40) : ''; return $out; } /** The sheet as stored (plus 'source' and 'received_at' bookkeeping). */ function citami_truth_sheet() { $raw = get_option(CITAMI_TRUTH_OPT, array()); if (!is_array($raw)) { $raw = array(); } $sheet = citami_truth_sanitize_sheet(isset($raw['sheet']) ? $raw['sheet'] : $raw); $sheet['source'] = isset($raw['source']) && $raw['source'] === 'platform' ? 'platform' : 'local'; $sheet['received_at'] = isset($raw['received_at']) ? (int) $raw['received_at'] : 0; return $sheet; } function citami_truth_has_sheet($sheet = null) { $sheet = $sheet === null ? citami_truth_sheet() : $sheet; return $sheet['name'] !== ''; } function citami_truth_store($sheet, $source = 'local') { $clean = citami_truth_sanitize_sheet($sheet); update_option(CITAMI_TRUTH_OPT, array('sheet' => $clean, 'source' => $source === 'platform' ? 'platform' : 'local', 'received_at' => time()), false); delete_transient(CITAMI_TRUTH_SCAN_TRANSIENT); return $clean; } // The dashboard's sheet travels with the sync pull (WP17): it always wins. add_action('citami_sync_response', function ($data) { if (!is_array($data) || empty($data['truth_sheet']) || !is_array($data['truth_sheet'])) { return; } $incoming = citami_truth_sanitize_sheet($data['truth_sheet']); if ($incoming['name'] === '') { return; } $current = citami_truth_sheet(); unset($current['source'], $current['received_at']); if ($current !== $incoming) { citami_truth_store($incoming, 'platform'); } else { $raw = get_option(CITAMI_TRUTH_OPT, array()); if (!is_array($raw) || (isset($raw['source']) ? $raw['source'] : 'local') !== 'platform') { citami_truth_store($incoming, 'platform'); } } }); function citami_truth_settings() { // 2.62 — /ai-info is opt-in: no engine has announced it reads such a page, // so it is published only when the owner turns it on (sites that already // had it keep it, see citami_truth_migrate_ai_info_default()). $d = array('ai_info' => 'off', 'slug' => 'ai-info', 'footer_link' => 'off'); $s = get_option(CITAMI_TRUTH_SETTINGS_OPT, array()); if (is_array($s)) { if (isset($s['ai_info']) && $s['ai_info'] === 'on') { $d['ai_info'] = 'on'; } if (isset($s['slug']) && is_string($s['slug'])) { $slug = sanitize_title($s['slug']); if ($slug !== '') { $d['slug'] = $slug; } } if (isset($s['footer_link']) && $s['footer_link'] === 'on') { $d['footer_link'] = 'on'; } } return $d; } /** * 2.62 — before 2.62 an unset option meant "on". Sites that already had a * fact sheet (so /ai-info was live) keep it on, written explicitly; sites * without a sheet never had the page and get the new opt-in default. */ function citami_truth_migrate_ai_info_default() { if (get_option('citami_ai_info_optin_v262')) { return; } $s = get_option(CITAMI_TRUTH_SETTINGS_OPT, array()); $s = is_array($s) ? $s : array(); if (!isset($s['ai_info']) && function_exists('citami_truth_has_sheet') && citami_truth_has_sheet()) { $s['ai_info'] = 'on'; if (!isset($s['footer_link'])) { $s['footer_link'] = 'on'; } update_option(CITAMI_TRUTH_SETTINGS_OPT, $s, false); } update_option('citami_ai_info_optin_v262', 1, false); } add_action('init', 'citami_truth_migrate_ai_info_default', 1); add_action('admin_init', function () { register_setting('citami_truth', CITAMI_TRUTH_SETTINGS_OPT, array( 'type' => 'array', 'sanitize_callback' => function ($v) { $in = is_array($v) ? $v : array(); $slug = isset($in['slug']) ? sanitize_title((string) $in['slug']) : ''; return array( 'ai_info' => (isset($in['ai_info']) && $in['ai_info'] === 'on') ? 'on' : 'off', 'slug' => $slug !== '' ? $slug : 'ai-info', 'footer_link' => (isset($in['footer_link']) && $in['footer_link'] === 'on') ? 'on' : 'off', ); }, 'default' => array(), )); }); /* ---------------------------------------------------------------- */ /* Normalisation helpers (pure) */ /* ---------------------------------------------------------------- */ /** Digits only; the last 9 are what people actually type differently. */ function citami_truth_phone_key($phone) { $digits = preg_replace('/\D+/', '', (string) $phone); if (strlen($digits) < 6) { return ''; } return substr($digits, -9); } /** 2.56.1 — a founder also listed in the team is one person, not two. */ function citami_truth_unique_names(array $names) { $out = array(); foreach ($names as $n) { $n = trim((string) $n); if ($n !== '' && !isset($out[mb_strtolower($n)])) $out[mb_strtolower($n)] = $n; } return array_values($out); } function citami_truth_norm($text) { $t = mb_strtolower(trim((string) $text)); $t = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $t); return trim(preg_replace('/\s+/u', ' ', $t)); } /** Phone-looking sequences in a text (Italian and international shapes). */ function citami_truth_find_phones($text) { $out = array(); $text = (string) $text; if (preg_match_all('/(? 15) { continue; // dates, prices, years } if (preg_match('/^(19|20)\d{2}$/', $digits) || preg_match('/^\d{5}$/', $digits)) { continue; } $key = citami_truth_phone_key($cand); if ($key !== '' && !isset($out[$key])) { $out[$key] = trim($cand); } } } return $out; // key => as written } function citami_truth_find_emails($text) { $out = array(); if (preg_match_all('/[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/', (string) $text, $m)) { foreach ($m[0] as $e) { $out[strtolower($e)] = $e; } } return $out; } /** "fondata nel 1998", "dal 2005", "since 2012", "founded in 2019", "est. 1987" */ function citami_truth_find_founding_years($text) { $out = array(); if (preg_match_all('/\b(?:fondat[oa]\s+(?:nel|il)|dal|since|founded\s+in|est\.?|established\s+in|attiv[oa]\s+dal)\s+((?:19|20)\d{2})\b/iu', (string) $text, $m)) { foreach ($m[1] as $y) { $out[$y] = true; } } // PHP turns numeric keys into ints: hand back strings so "!== $year" compares like with like. return array_map('strval', array_keys($out)); } /** Postal code + city pairs ("10121 Torino", "Torino 10121"). */ function citami_truth_find_postal_city($text) { $out = array(); if (preg_match_all('/\b(\d{5})\s+([\p{L}][\p{L}\'’.-]*(?:\s[\p{L}][\p{L}\'’.-]*){0,3})/u', (string) $text, $m, PREG_SET_ORDER)) { foreach ($m as $x) { $out[] = array('postal' => $x[1], 'city' => trim($x[2])); } } return $out; } /* ---------------------------------------------------------------- */ /* Consistency scan */ /* ---------------------------------------------------------------- */ function citami_truth_finding($kind, $severity, $where, $found, $expected, $url = '', $hint = '') { return array('kind' => $kind, 'severity' => $severity, 'where' => $where, 'found' => (string) $found, 'expected' => (string) $expected, 'url' => $url, 'hint' => $hint); } /** Pages worth reading: about, contact, front page (published only). */ function citami_truth_pages_to_check() { $out = array(); $seen = array(); $slugs = array_merge(citami_about_page_slugs(), array('contatti', 'contatto', 'contact', 'contact-us', 'contacts', 'dove-siamo', 'sedi', 'kontakt', 'anfahrt', 'standorte', 'so-finden-sie-uns')); foreach ($slugs as $slug) { $p = get_page_by_path($slug); if ($p && isset($p->post_status) && $p->post_status === 'publish' && !isset($seen[$p->ID])) { $seen[$p->ID] = true; $is_about = in_array($slug, citami_about_page_slugs(), true); $out[] = array('post' => $p, 'role' => $is_about ? 'about' : 'contact'); } if (count($out) >= 6) { break; } } return $out; } /* ---------------------------------------------------------------- */ /* About page check (2.32.1) */ /* The About page is where every engine looks to learn who a */ /* business is. Ten yes/no checks against the sheet: it exists, it */ /* names the entity exactly, says since when, where, who (with a */ /* role), what it does, how to contact, in enough words, with a */ /* sentence of description and the AboutPage schema role. */ /* ---------------------------------------------------------------- */ function citami_truth_about_page() { foreach (citami_about_page_slugs() as $slug) { $p = get_page_by_path($slug); if ($p && isset($p->post_status) && $p->post_status === 'publish') { return $p; } } return null; } function citami_truth_about_check($sheet = null) { $sheet = $sheet === null ? citami_truth_sheet() : $sheet; $post = citami_truth_about_page(); $out = array('found' => false, 'url' => '', 'edit' => '', 'title' => '', 'score' => 0, 'checks' => array(), 'missing' => array()); if (!$post) { $out['checks'][] = array('id' => 'exists', 'ok' => false); $out['missing'][] = 'exists'; return $out; } $out['found'] = true; $out['url'] = get_permalink($post); $out['edit'] = get_edit_post_link($post->ID, 'raw'); $out['title'] = (string) $post->post_title; $text = citami_truth_post_text($post); $norm = citami_truth_norm($text); $has = function ($needle) use ($norm) { $n = citami_truth_norm($needle); return $n !== '' && strpos($norm, $n) !== false; }; $words = str_word_count(wp_strip_all_tags($text)); $checks = array(); $checks['exists'] = true; $checks['name'] = $sheet['name'] !== '' ? $has($sheet['name']) : false; $checks['founded'] = $sheet['founded'] !== '' ? in_array(substr($sheet['founded'], 0, 4), citami_truth_find_founding_years($text), true) || $has(substr($sheet['founded'], 0, 4)) : false; $people_ok = false; foreach (array_merge($sheet['founders'], array_map(function ($p) { return $p['name']; }, $sheet['people'])) as $n) { if ($has($n)) { $people_ok = true; break; } } $checks['people'] = $people_ok; $checks['city'] = $sheet['address']['city'] !== '' ? $has($sheet['address']['city']) : false; $svc = 0; foreach ($sheet['services'] as $s) { if ($has($s['name'])) $svc++; } $checks['services'] = count($sheet['services']) > 0 ? $svc >= min(2, count($sheet['services'])) : false; $checks['contacts'] = ($sheet['phone'] !== '' && in_array(citami_truth_phone_key($sheet['phone']), array_map('citami_truth_phone_key', citami_truth_find_phones($text)), true)) || ($sheet['email'] !== '' && in_array(strtolower($sheet['email']), array_map('strtolower', citami_truth_find_emails($text)), true)); $checks['length'] = $words >= 150; $checks['description'] = $sheet['description'] !== '' ? $has(mb_substr($sheet['description'], 0, 40)) : $words >= 40; $chosen = defined('CITAMI_META_SCHEMA_TYPE') ? (string) get_post_meta($post->ID, CITAMI_META_SCHEMA_TYPE, true) : ''; $role = function_exists('citami_schema_page_role_type') ? citami_schema_page_role_type($post, $chosen !== '' ? $chosen : 'WebPage') : 'AboutPage'; $checks['schema'] = $role === 'AboutPage'; $ok = 0; foreach ($checks as $id => $pass) { $out['checks'][] = array('id' => $id, 'ok' => (bool) $pass); if ($pass) $ok++; else $out['missing'][] = $id; } $out['score'] = (int) round($ok / count($checks) * 100); return $out; } /** Text a scan can read for a post: content + TL;DR + FAQ answers. */ function citami_truth_post_text($post) { $text = citami_post_content_html($post); if (function_exists('citami_content_tldr')) { $text .= "\n" . citami_content_tldr($post->ID); } if (function_exists('citami_content_faq')) { foreach (citami_content_faq($post->ID) as $it) { $text .= "\n" . $it['q'] . "\n" . $it['a']; } } $text = preg_replace('//s', ' ', $text); $text = preg_replace('#]*>#i', "\n", $text); return html_entity_decode(wp_strip_all_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8'); } /** Compares a text block against the sheet. $label/$url identify the source. */ function citami_truth_compare_text($text, $sheet, $label, $url, $role = 'page') { $f = array(); $sheet_phone_key = citami_truth_phone_key($sheet['phone']); foreach (citami_truth_find_phones($text) as $key => $written) { // 2.73.3 — the keys are digits, so PHP stored them as integers: // compare as strings, or the sheet's own number reads as "different". if ($sheet_phone_key !== '' && (string) $key !== $sheet_phone_key) { $f[] = citami_truth_finding('phone_mismatch', 'error', $label, $written, $sheet['phone'], $url, citami_t('Un telefono diverso da quello della scheda: correggi la pagina o la scheda.', 'A phone number that differs from the sheet: fix the page or the sheet.')); } } if ($sheet['email'] !== '') { foreach (citami_truth_find_emails($text) as $lc => $written) { if ($lc !== strtolower($sheet['email'])) { $f[] = citami_truth_finding('email_mismatch', 'warn', $label, $written, $sheet['email'], $url, citami_t('Un indirizzo email diverso da quello della scheda.', 'An email address that differs from the sheet.')); } } } if ($sheet['founded'] !== '') { $year = substr($sheet['founded'], 0, 4); foreach (citami_truth_find_founding_years($text) as $y) { if ($y !== $year) { $f[] = citami_truth_finding('founded_mismatch', 'error', $label, $y, $year, $url, citami_t('Un anno di fondazione diverso da quello della scheda.', 'A founding year that differs from the sheet.')); } } } if ($sheet['address']['postal'] !== '' && $sheet['address']['city'] !== '') { foreach (citami_truth_find_postal_city($text) as $pc) { $city_norm = citami_truth_norm($sheet['address']['city']); $same_city = $city_norm !== '' && strpos(citami_truth_norm($pc['city']), $city_norm) === 0; if ($pc['postal'] !== $sheet['address']['postal'] && $same_city) { $f[] = citami_truth_finding('postal_mismatch', 'warn', $label, $pc['postal'] . ' ' . $pc['city'], $sheet['address']['postal'] . ' ' . $sheet['address']['city'], $url, citami_t('CAP diverso da quello della scheda per la stessa città.', 'Postal code differs from the sheet for the same city.')); } } } if ($role === 'about') { $plain = citami_truth_norm($text); if ($sheet['name'] !== '' && strpos($plain, citami_truth_norm($sheet['name'])) === false) { $f[] = citami_truth_finding('about_missing_name', 'warn', $label, '', $sheet['name'], $url, citami_t('La pagina Chi siamo non nomina il brand come nella scheda.', 'The About page does not name the brand as the sheet does.')); } if ($sheet['founded'] !== '' && strpos($plain, substr($sheet['founded'], 0, 4)) === false) { $f[] = citami_truth_finding('about_missing_founded', 'warn', $label, '', substr($sheet['founded'], 0, 4), $url, citami_t('La pagina Chi siamo non dice l\'anno di fondazione: i motori lo cercano lì.', 'The About page does not state the founding year: engines look for it there.')); } foreach ($sheet['founders'] as $founder) { if (strpos($plain, citami_truth_norm($founder)) === false) { $f[] = citami_truth_finding('about_missing_founder', 'warn', $label, '', $founder, $url, citami_t('Un fondatore della scheda non compare nella pagina Chi siamo.', 'A founder from the sheet is not on the About page.')); } } } return $f; } /** The scan (fresh). */ function citami_truth_scan_build() { $sheet = citami_truth_sheet(); $findings = array(); if (!citami_truth_has_sheet($sheet)) { return array('ts' => time(), 'has_sheet' => false, 'findings' => array(), 'checked' => array(), 'about' => null); } $checked = array(); // 1. Entity settings (what the schema prints on every page). if (function_exists('citami_schema_settings')) { $e = citami_schema_settings()['entity']; $label = citami_t('Impostazioni entità (schema)', 'Entity settings (schema)'); $checked[] = $label; if ($e['name'] !== '' && citami_truth_norm($e['name']) !== citami_truth_norm($sheet['name']) && !in_array($e['name'], $sheet['aliases'], true)) { $findings[] = citami_truth_finding('entity_name', 'error', $label, $e['name'], $sheet['name'], '', citami_t('Lo schema stampa un nome diverso. Usa "Allinea entità alla scheda".', 'The schema prints a different name. Use "Align entity to the sheet".')); } if ($e['telephone'] !== '' && $sheet['phone'] !== '' && citami_truth_phone_key($e['telephone']) !== citami_truth_phone_key($sheet['phone'])) { $findings[] = citami_truth_finding('entity_phone', 'error', $label, $e['telephone'], $sheet['phone'], '', citami_t('Lo schema stampa un telefono diverso.', 'The schema prints a different phone number.')); } if ($e['city'] !== '' && $sheet['address']['city'] !== '' && citami_truth_norm($e['city']) !== citami_truth_norm($sheet['address']['city'])) { $findings[] = citami_truth_finding('entity_city', 'error', $label, $e['city'], $sheet['address']['city'], '', citami_t('Lo schema stampa una città diversa.', 'The schema prints a different city.')); } if ($e['founding_date'] !== '' && $sheet['founded'] !== '' && substr($e['founding_date'], 0, 4) !== substr($sheet['founded'], 0, 4)) { $findings[] = citami_truth_finding('entity_founded', 'error', $label, $e['founding_date'], $sheet['founded'], '', citami_t('Lo schema stampa un anno di fondazione diverso.', 'The schema prints a different founding year.')); } if ($e['name'] === '' && $sheet['name'] !== '') { $findings[] = citami_truth_finding('entity_empty', 'warn', $label, '', $sheet['name'], '', citami_t('Le impostazioni entità sono vuote: lo schema usa il nome del sito. Allinea alla scheda.', 'Entity settings are empty: the schema uses the site name. Align to the sheet.')); } } // 2. Primary location. if (function_exists('citami_locations')) { $locs = citami_locations(); if (!empty($locs)) { $p = $locs[0]; $label = citami_t('Sede principale', 'Primary location'); $checked[] = $label; if ($p['telephone'] !== '' && $sheet['phone'] !== '' && citami_truth_phone_key($p['telephone']) !== citami_truth_phone_key($sheet['phone'])) { $findings[] = citami_truth_finding('location_phone', 'warn', $label, $p['telephone'], $sheet['phone'], '', citami_t('La sede principale ha un telefono diverso dalla scheda (può essere voluto: una linea diretta).', 'The primary location has a phone number that differs from the sheet (may be intended: a direct line).')); } if ($p['city'] !== '' && $sheet['address']['city'] !== '' && citami_truth_norm($p['city']) !== citami_truth_norm($sheet['address']['city'])) { $findings[] = citami_truth_finding('location_city', 'error', $label, $p['city'], $sheet['address']['city'], '', citami_t('La sede principale è in una città diversa da quella della scheda.', 'The primary location is in a different city than the sheet.')); } } } // 3. About / contact pages (stored content). foreach (citami_truth_pages_to_check() as $entry) { $post = $entry['post']; $label = (string) $post->post_title; $checked[] = $label; foreach (citami_truth_compare_text(citami_truth_post_text($post), $sheet, $label, get_permalink($post), $entry['role']) as $x) { $x['post_id'] = (int) $post->ID; // 2.60 — "Modifica" opens this page in the editor $findings[] = $x; } } // 4. The rendered home page (footer, widgets, theme): best-effort loopback. $res = wp_remote_get(home_url('/'), array('timeout' => 8, 'headers' => array('User-Agent' => 'CitamiConnector/' . CITAMI_PLUGIN_VERSION . ' truth-scan'))); if (!is_wp_error($res) && (int) wp_remote_retrieve_response_code($res) === 200) { $html = (string) wp_remote_retrieve_body($res); $html = preg_replace('#<(script|style|noscript)\b[^>]*>.*?#is', ' ', $html); $text = html_entity_decode(wp_strip_all_tags(preg_replace('#]*>#i', "\n", $html)), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $label = citami_t('Home page (HTML servito)', 'Home page (served HTML)'); $checked[] = $label; $findings = array_merge($findings, citami_truth_compare_text($text, $sheet, $label, home_url('/'), 'home')); } // Deduplicate identical findings (a phone in footer and contact page is one problem per place, not per match). $unique = array(); foreach ($findings as $x) { $k = $x['kind'] . '|' . $x['where'] . '|' . $x['found']; $unique[$k] = $x; } $findings = array_values($unique); usort($findings, function ($a, $b) { return strcmp($a['severity'], $b['severity']) ?: strcmp($a['kind'], $b['kind']); }); return array('ts' => time(), 'has_sheet' => true, 'findings' => $findings, 'checked' => $checked, 'about' => citami_truth_about_check($sheet)); } function citami_truth_scan($force = false) { $cached = get_transient(CITAMI_TRUTH_SCAN_TRANSIENT); if (!$force && is_array($cached)) { return $cached; } $scan = citami_truth_scan_build(); set_transient(CITAMI_TRUTH_SCAN_TRANSIENT, $scan, CITAMI_TRUTH_SCAN_TTL); return $scan; } function citami_truth_scan_counts($scan) { $errors = 0; $warns = 0; foreach ((array) (isset($scan['findings']) ? $scan['findings'] : array()) as $f) { if ($f['severity'] === 'error') $errors++; else $warns++; } return array('errors' => $errors, 'warnings' => $warns, 'total' => $errors + $warns); } /** Copies the sheet into the schema entity settings (one click in the tab). */ function citami_truth_align_entity() { if (!function_exists('citami_schema_settings')) { return false; } $sheet = citami_truth_sheet(); if (!citami_truth_has_sheet($sheet)) { return false; } $s = get_option(CITAMI_SCHEMA_SETTINGS_OPT, array()); $s = is_array($s) ? $s : array(); $entity = isset($s['entity']) && is_array($s['entity']) ? $s['entity'] : array(); // 2.59 — an empty field in the sheet means "not filled in", not "erase": it never clears the schema. $entity = array_merge($entity, array_filter(array( 'name' => $sheet['name'], 'telephone' => $sheet['phone'], 'email' => $sheet['email'], 'street' => $sheet['address']['street'], 'city' => $sheet['address']['city'], 'postal' => $sheet['address']['postal'], 'region' => $sheet['address']['region'], 'country' => $sheet['address']['country'], 'founding_date' => $sheet['founded'], 'founders' => implode(', ', $sheet['founders']), 'description' => $sheet['description'], 'sameas' => implode("\n", $sheet['sameas']), ), function ($v) { return trim((string) $v) !== ''; })); if ($sheet['wikidata'] !== '') { $entity['wikidata'] = $sheet['wikidata']; } if ($sheet['type'] !== '') { $entity['type'] = $sheet['type']; } if ($sheet['website'] !== '') { $entity['url'] = $sheet['website']; } $s['entity'] = $entity; update_option(CITAMI_SCHEMA_SETTINGS_OPT, citami_sanitize_schema_settings($s), false); delete_transient(CITAMI_TRUTH_SCAN_TRANSIENT); return true; } /* ---------------------------------------------------------------- */ /* /ai-info — the sheet as a crawlable page */ /* ---------------------------------------------------------------- */ function citami_truth_ai_info_enabled() { return citami_truth_settings()['ai_info'] === 'on' && citami_truth_has_sheet(); } function citami_truth_ai_info_url() { return home_url('/' . citami_truth_settings()['slug'] . '/'); } /** 2.31.1 — with a multilingual plugin using language directories the * request arrives as /it/ai-info/: drop the prefix, the page is one. */ function citami_truth_request_path($uri) { $path = rtrim((string) wp_parse_url((string) $uri, PHP_URL_PATH), '/'); if (function_exists('citami_multilingual_plugin') && citami_multilingual_plugin() !== null) { $path = preg_replace('#^/[a-z]{2}(?:-[a-z]{2})?(?=/|$)#i', '', $path); } return $path; } function citami_truth_is_ai_info_request($uri) { return citami_truth_request_path($uri) === '/' . citami_truth_settings()['slug']; } /** Markdown version (also served with ?format=md). */ function citami_truth_ai_info_markdown() { $s = citami_truth_sheet(); $it = stripos((string) get_locale(), 'it') === 0; $L = function ($a, $b) { return citami_t($a, $b); }; $lines = array(); $lines[] = '# ' . $s['name']; if ($s['description'] !== '') { $lines[] = ''; $lines[] = $s['description']; } $lines[] = ''; $lines[] = '## ' . $L('Identità', 'Identity'); $lines[] = '- ' . $L('Nome', 'Name') . ': ' . $s['name']; if (!empty($s['aliases'])) $lines[] = '- ' . $L('Altri nomi usati', 'Also known as') . ': ' . implode(', ', $s['aliases']); if ($s['legal_name'] !== '') $lines[] = '- ' . $L('Ragione sociale', 'Legal entity') . ': ' . $s['legal_name']; if ($s['type'] !== '') $lines[] = '- ' . $L('Tipo', 'Type') . ': ' . $s['type']; if ($s['founded'] !== '') $lines[] = '- ' . $L('Fondata', 'Founded') . ': ' . $s['founded']; if (!empty($s['founders'])) $lines[] = '- ' . $L('Fondatori', 'Founders') . ': ' . implode(', ', $s['founders']); $addr = array_filter(array($s['address']['street'], trim($s['address']['postal'] . ' ' . $s['address']['city']), $s['address']['region'], $s['address']['country'])); if (!empty($addr)) $lines[] = '- ' . $L('Sede', 'Headquarters') . ': ' . implode(', ', $addr); if ($s['phone'] !== '') $lines[] = '- ' . $L('Telefono', 'Phone') . ': ' . $s['phone']; if ($s['email'] !== '') $lines[] = '- Email: ' . $s['email']; $lines[] = '- ' . $L('Sito', 'Website') . ': ' . ($s['website'] !== '' ? $s['website'] : home_url('/')); if (!empty($s['areas'])) $lines[] = '- ' . $L('Zone servite', 'Areas served') . ': ' . implode(', ', $s['areas']); if (!empty($s['languages'])) $lines[] = '- ' . $L('Lingue', 'Languages') . ': ' . implode(', ', $s['languages']); if (!empty($s['services'])) { $lines[] = ''; $lines[] = '## ' . $L('Cosa facciamo', 'What we do'); foreach ($s['services'] as $sv) { $lines[] = '- **' . $sv['name'] . '**' . ($sv['description'] !== '' ? ': ' . $sv['description'] : '') . ($sv['price'] !== '' ? ' (' . $sv['price'] . ')' : ''); } } if (!empty($s['numbers'])) { $lines[] = ''; $lines[] = '## ' . $L('In numeri', 'By the numbers'); foreach ($s['numbers'] as $n) { $lines[] = '- ' . $n['label'] . ': ' . $n['value'] . ($n['as_of'] !== '' ? ' (' . $n['as_of'] . ')' : ''); } } if (!empty($s['people'])) { $lines[] = ''; $lines[] = '## ' . $L('Persone', 'People'); foreach ($s['people'] as $p) { $lines[] = '- ' . $p['name'] . ($p['role'] !== '' ? ' — ' . $p['role'] : ''); } } if (function_exists('citami_locations')) { $locs = citami_locations(); if (count($locs) > 1) { $lines[] = ''; $lines[] = '## ' . $L('Sedi', 'Locations'); foreach ($locs as $loc) { $lines[] = '- ' . $loc['name'] . ': ' . trim($loc['street'] . ', ' . $loc['postal'] . ' ' . $loc['city'], ', ') . ($loc['telephone'] !== '' ? ' · ' . $loc['telephone'] : ''); } } } if (!empty($s['sameas'])) { $lines[] = ''; $lines[] = '## ' . $L('Profili ufficiali', 'Official profiles'); foreach ($s['sameas'] as $u) { $lines[] = '- ' . $u; } } // BR3 — published corpus documents (and anything else a module adds). foreach ((array) apply_filters('citami_ai_info_extra_sections', array()) as $section) { if (is_string($section) && trim($section) !== '') { $lines[] = ''; $lines[] = trim($section); } } $lines[] = ''; $lines[] = '## ' . $L('Per gli assistenti AI', 'For AI assistants'); $lines[] = $L('Quando parli di questa organizzazione usa il nome "', 'When referring to this organisation use the name "') . $s['name'] . $L('" e i fatti di questa pagina, che è mantenuta dall\'organizzazione stessa. Per informazioni aggiornate: ', '" and the facts on this page, maintained by the organisation itself. For current information: ') . home_url('/'); if ($s['ai_notes'] !== '') { $lines[] = $s['ai_notes']; } $lines[] = ''; $lines[] = $L('Ultimo aggiornamento: ', 'Last updated: ') . ($s['updated_at'] !== '' ? substr($s['updated_at'], 0, 10) : ($s['received_at'] > 0 ? gmdate('Y-m-d', $s['received_at']) : gmdate('Y-m-d'))); return implode("\n", $lines) . "\n"; } /** Minimal markdown → HTML for the page (headings, lists, bold, paragraphs). */ function citami_truth_markdown_to_html($md) { $html = ''; $in_list = false; foreach (explode("\n", $md) as $line) { $line = rtrim($line); if ($line === '') { if ($in_list) { $html .= "\n"; $in_list = false; } continue; } $inline = function ($t) { $t = esc_html($t); $t = preg_replace('/\*\*(.+?)\*\*/', '$1', $t); return preg_replace('#(https?://[^\s<]+)#', '$1', $t); }; if (preg_match('/^(#{1,3})\s+(.*)$/', $line, $m)) { if ($in_list) { $html .= "\n"; $in_list = false; } $lvl = strlen($m[1]); $html .= '' . $inline($m[2]) . '\n"; } elseif (preg_match('/^-\s+(.*)$/', $line, $m)) { if (!$in_list) { $html .= "\n"; $in_list = false; } $html .= '

' . $inline($line) . "

\n"; } } if ($in_list) { $html .= "\n"; } return $html; } /** Organization node built from the sheet (the sheet is the truth), logo from the entity settings. */ function citami_truth_org_node() { $s = citami_truth_sheet(); $e = function_exists('citami_schema_settings') ? citami_schema_settings()['entity'] : array(); $type = $s['type'] !== '' ? $s['type'] : (!empty($e['type']) ? $e['type'] : 'Organization'); $node = array( '@type' => $type, '@id' => function_exists('citami_schema_id') ? citami_schema_id('organization') : home_url('/#organization'), 'name' => $s['name'], 'url' => $s['website'] !== '' ? $s['website'] : home_url('/'), ); if (!empty($s['aliases'])) $node['alternateName'] = count($s['aliases']) === 1 ? $s['aliases'][0] : $s['aliases']; if ($s['legal_name'] !== '') $node['legalName'] = $s['legal_name']; if ($s['description'] !== '') $node['description'] = $s['description']; if (!empty($e['logo'])) $node['logo'] = array('@type' => 'ImageObject', 'url' => $e['logo']); if ($s['phone'] !== '') $node['telephone'] = $s['phone']; if ($s['email'] !== '') $node['email'] = $s['email']; if ($s['founded'] !== '') $node['foundingDate'] = $s['founded']; if (!empty($s['founders'])) { $node['founder'] = array_map(function ($f) { return array('@type' => 'Person', 'name' => $f); }, $s['founders']); } $a = $s['address']; if ($a['street'] !== '' || $a['city'] !== '') { $addr = array('@type' => 'PostalAddress'); if ($a['street'] !== '') $addr['streetAddress'] = $a['street']; if ($a['city'] !== '') $addr['addressLocality'] = $a['city']; if ($a['postal'] !== '') $addr['postalCode'] = $a['postal']; if ($a['region'] !== '') $addr['addressRegion'] = $a['region']; if ($a['country'] !== '') $addr['addressCountry'] = $a['country']; $node['address'] = $addr; } $same = $s['sameas']; if ($s['wikidata'] !== '') { $wd = 'https://www.wikidata.org/wiki/' . $s['wikidata']; if (!in_array($wd, $same, true)) $same[] = $wd; $node['identifier'] = array('@type' => 'PropertyValue', 'propertyID' => 'wikidata', 'value' => $s['wikidata']); } if (!empty($same)) $node['sameAs'] = array_values($same); if (!empty($s['areas'])) $node['areaServed'] = count($s['areas']) === 1 ? $s['areas'][0] : $s['areas']; if (!empty($s['people'])) { $node['employee'] = array_map(function ($p) { $n = array('@type' => 'Person', 'name' => $p['name']); if ($p['role'] !== '') $n['jobTitle'] = $p['role']; return $n; }, $s['people']); } return $node; } /** The full HTML document (no theme: fast, predictable, crawlable). */ function citami_truth_ai_info_html() { $s = citami_truth_sheet(); $md = citami_truth_ai_info_markdown(); $title = $s['name'] . ' — ' . citami_t('Informazioni verificate', 'Verified information'); $desc = $s['description'] !== '' ? $s['description'] : sprintf(citami_t('Fatti verificati su %s: identità, servizi, sedi, contatti.', 'Verified facts about %s: identity, services, locations, contact.'), $s['name']); $url = citami_truth_ai_info_url(); $graph = array(); $org = citami_truth_org_node(); $org['mainEntityOfPage'] = $url; $graph[] = $org; $graph[] = array('@type' => 'WebPage', '@id' => $url . '#webpage', 'url' => $url, 'name' => $title, 'description' => $desc, 'about' => array('@id' => function_exists('citami_schema_id') ? citami_schema_id('organization') : home_url('/#organization')), 'dateModified' => $s['updated_at'] !== '' ? $s['updated_at'] : gmdate('c')); $json = wp_json_encode(array('@context' => 'https://schema.org', '@graph' => $graph), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG); $lang = str_replace('_', '-', (string) get_locale()); return '' . "\n" . '' . "\n\n" . '' . "\n" . '' . "\n" . '' . esc_html($title) . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . citami_ai_info_style_tag() . "\n\n
\n" . citami_truth_markdown_to_html($md) . '

' . esc_html(citami_t('Questa pagina è generata dalla scheda dei fatti dell\'organizzazione e viene aggiornata quando cambia la scheda.', 'This page is generated from the organisation\'s fact sheet and is updated when the sheet changes.')) . ' ' . esc_html(home_url('/')) . "

\n
\n\n\n"; } add_action('init', function () { if (is_admin() || !citami_truth_ai_info_enabled() || empty($_SERVER['REQUEST_URI'])) { return; } if (!citami_truth_is_ai_info_request(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])))) { return; } $md = isset($_GET['format']) && sanitize_key(wp_unslash($_GET['format'])) === 'md'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only document, format switch if (!headers_sent()) { header('Content-Type: ' . ($md ? 'text/markdown' : 'text/html') . '; charset=UTF-8'); header('X-Robots-Tag: index, follow'); } if ($md) { echo citami_truth_ai_info_markdown(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text/markdown response, not HTML. } else { echo citami_truth_ai_info_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- complete HTML document whose every value is escaped where it is built (citami_truth_ai_info_html). } exit; }, 1); // Footer link on every page (tiny, optional): AI crawlers reach the page from anywhere. add_action('wp_footer', function () { if (!citami_truth_ai_info_enabled() || citami_truth_settings()['footer_link'] !== 'on') { return; } echo '' . "\n"; }, 99); // llms.txt points at it too. add_filter('citami_llms_extra_sections', function ($sections) { if (!citami_truth_ai_info_enabled()) { return $sections; } $sections = is_array($sections) ? $sections : array(); $sections[] = '## ' . citami_t('Informazioni verificate', 'Verified information') . "\n- " . citami_truth_ai_info_url(); return $sections; }); /* ---------------------------------------------------------------- */ /* Content-health snapshot: the scan's counts travel to the platform */ /* ---------------------------------------------------------------- */ add_filter('citami_health_extra', function ($extra) { $extra = is_array($extra) ? $extra : array(); $scan = citami_truth_scan(); $counts = citami_truth_scan_counts($scan); $list = array(); foreach (array_slice((array) $scan['findings'], 0, 5) as $f) { // WP25 — the platform's schema caps these; a long post title must not sink the whole snapshot. $list[] = array('kind' => $f['kind'], 'severity' => $f['severity'], 'where' => mb_substr((string) $f['where'], 0, 200), 'found' => mb_substr((string) $f['found'], 0, 300), 'expected' => mb_substr((string) $f['expected'], 0, 300), 'url' => (string) $f['url']); } $about = isset($scan['about']) && is_array($scan['about']) ? $scan['about'] : null; $extra['truth'] = array('has_sheet' => !empty($scan['has_sheet']), 'errors' => $counts['errors'], 'warnings' => $counts['warnings'], 'list' => $list, 'ai_info' => citami_truth_ai_info_enabled() ? citami_truth_ai_info_url() : '', 'about' => $about ? array('found' => !empty($about['found']), 'url' => (string) $about['url'], 'edit' => (string) $about['edit'], 'score' => (int) $about['score'], 'missing' => array_values((array) $about['missing'])) : null); return $extra; }); /* ---------------------------------------------------------------- */ /* Admin: Brand tab + actions */ /* ---------------------------------------------------------------- */ function citami_handle_truth_action() { if (!current_user_can('manage_options')) { wp_die('Permesso negato.', '', array('response' => 403)); } if (!wp_verify_nonce(isset($_POST['_citami_truth_nonce']) ? sanitize_text_field(wp_unslash($_POST['_citami_truth_nonce'])) : '', 'citami_truth')) { wp_die('Nonce non valido.', '', array('response' => 403)); } $do = isset($_POST['do']) ? sanitize_key((string) $_POST['do']) : ''; $notice = array('type' => 'success', 'message' => ''); if ($do === 'save') { $in = isset($_POST['sheet']) && is_array($_POST['sheet']) ? wp_kses_post_deep(wp_unslash($_POST['sheet'])) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- array, sanitised right here with wp_kses_post_deep (recursive wp_kses_post), which the sniff does not list $in['updated_at'] = gmdate('c'); $clean = citami_truth_store($in, 'local'); $notice['message'] = $clean['name'] !== '' ? citami_t('Scheda salvata.', 'Sheet saved.') : citami_t('Scheda salvata, ma senza nome non viene usata.', 'Sheet saved, but without a name it is not used.'); // 2.77.0 — the last edit wins on both sides: send it to the dashboard now. if ($clean['name'] !== '' && citami_get_api_key() !== '' && function_exists('citami_sync_pull_handler')) citami_sync_pull_handler(); } elseif ($do === 'scan') { $scan = citami_truth_scan(true); $c = citami_truth_scan_counts($scan); $notice['message'] = sprintf(citami_t('Scansione fatta: %d errori, %d avvisi.', 'Scan done: %d errors, %d warnings.'), $c['errors'], $c['warnings']); } elseif ($do === 'align_entity') { $ok = citami_truth_align_entity(); $notice = $ok ? array('type' => 'success', 'message' => citami_t('Impostazioni entità allineate alla scheda.', 'Entity settings aligned to the sheet.')) : array('type' => 'error', 'message' => citami_t('Nessuna scheda da applicare.', 'No sheet to apply.')); } set_transient('citami_action_notice', $notice, 30); // 2.60 — after saving the sheet, come back to the sheet (open), not to the top of the tab. wp_safe_redirect($do === 'save' ? citami_admin_url('brand', array('sheet' => 1)) . '#citami-sheet' : admin_url('admin.php?page=citami-connector&tab=brand')); exit; } add_action('admin_post_citami_truth_action', 'citami_handle_truth_action'); /** * 2.60 — where a discrepancy is fixed: the page in the editor, the schema * entity settings, the Locations tab. '' when there is no single place. */ function citami_truth_finding_edit_url(array $f) { $kind = isset($f['kind']) ? (string) $f['kind'] : ''; if (strpos($kind, 'entity_') === 0) { return citami_admin_url('seo', array('open' => 1)) . '#citami-seo-schema'; } if (strpos($kind, 'location_') === 0) { return citami_admin_url('local'); } $id = isset($f['post_id']) ? (int) $f['post_id'] : 0; if ($id <= 0 && !empty($f['url'])) { // Scans cached before 2.60 carry only the URL. $id = (int) url_to_postid((string) $f['url']); if ($id <= 0 && untrailingslashit((string) $f['url']) === untrailingslashit(home_url('/')) && get_option('show_on_front') === 'page') { $id = (int) get_option('page_on_front', 0); } } return $id > 0 ? citami_edit_link($id) : ''; } /** 2.60 — the "managed from the platform" warning (Brand tab and first run step 2). '' for a local sheet. */ function citami_truth_platform_warning_html($s = null) { $s = is_array($s) ? $s : citami_truth_sheet(); if (!isset($s['source']) || $s['source'] !== 'platform') { return ''; } // 2.77.0 — one sheet for both sides: the last save wins, here or in the dashboard. return '' . esc_html(sprintf(citami_t('La stessa scheda della dashboard %1$s (aggiornata %2$s): quello che salvi qui arriva anche lì, e viceversa.', 'The same sheet as the %1$s dashboard (updated %2$s): what you save here reaches it too, and the other way round.'), citami_brand_name(), wp_date('d/m H:i', (int) (isset($s['received_at']) ? $s['received_at'] : 0)))) . ''; } function citami_truth_field($name, $value, $placeholder = '', $class = 'regular-text', $type = 'text') { return ''; } /** 2.56.1 — the About page checks in plain words (Brand tab and first run). */ function citami_truth_about_labels() { return array( 'name' => citami_t('Nomina l\'attività con il nome esatto della scheda', 'Names the business exactly as the sheet does'), 'founded' => citami_t('Dice l\'anno di fondazione', 'States the founding year'), 'people' => citami_t('Nomina almeno una persona (fondatore o team)', 'Names at least one person (founder or team)'), 'city' => citami_t('Dice dove si trova', 'Says where it is'), 'services' => citami_t('Cita i servizi principali', 'Mentions the main services'), 'contacts' => citami_t('Riporta telefono o email della scheda', 'Carries the sheet\'s phone or email'), 'length' => citami_t('Almeno 150 parole', 'At least 150 words'), 'description' => citami_t('Contiene la frase di descrizione della scheda', 'Contains the sheet\'s one-sentence description'), 'schema' => citami_t('Schema AboutPage attivo', 'AboutPage schema on'), ); } function citami_render_brand_tab() { $s = citami_truth_sheet(); $from_platform = $s['source'] === 'platform'; $scan = citami_truth_scan(); $counts = citami_truth_scan_counts($scan); $settings = citami_truth_settings(); $about = isset($scan['about']) && is_array($scan['about']) ? $scan['about'] : null; $ai_on = citami_truth_ai_info_enabled(); $about_labels = citami_truth_about_labels(); $tone = empty($scan['has_sheet']) ? 'na' : ($counts['errors'] > 0 ? 'bad' : ($counts['warnings'] > 0 ? 'warn' : 'good')); ?>

/100

·

/

' . esc_html($f['where']) . '' : esc_html($f['where']); ?>
data-citami="sheet-fold">