'mkb_glossar_sanitize']); // Rewrite-rule flushes and stored migration markers are privileged writes. // An editor visiting wp-admin must not trigger an upgrade/migration side effect. if (!current_user_can('manage_options')) return; // Refresh persisted rewrite rules once on upgrade. In WPML, admin-side // language discovery must be available when the rules are regenerated. $rewrite_version = '3.14.20'; if (get_option('mkb_glossar_rewrite_version') !== $rewrite_version || get_option('mkb_glossar_flush')) { mkb_glossar_register(); mkb_glossar_language_rewrites(); flush_rewrite_rules(); update_option('mkb_glossar_rewrite_version', $rewrite_version); delete_option('mkb_glossar_flush'); } }); /** Only families already declared in the active Elementor Kit are selectable. * No font library is fetched and no external font stylesheet is enqueued here. */ function mkb_glossar_kit_font_families() { if (!did_action('elementor/loaded') || !class_exists('\Elementor\Plugin')) return []; $kit = mkb_glossar_get_optional_kit(); if (empty($kit['available'])) return []; $families = []; foreach ((array) ($kit['tokens']['typography'] ?? []) as $token) { $family = trim((string) ($token['style']['font_family'] ?? '')); if ($family === '' || strlen($family) > 200 || !preg_match('/^[\p{L}\p{N} ._\-]+$/u', $family)) continue; $families[$family] = $family; } natcasesort($families); return array_values($families); } /** Load the currently selected kit font for the admin live preview through Elementor. * Elementor owns Google Fonts opt-in, font classification, caching and local/CDN policy. * The Glossar never builds Google Fonts URLs and never loads the entire font catalog. */ function mkb_glossar_enqueue_preview_font() { if (!did_action('elementor/loaded') || !class_exists('\Elementor\Fonts')) return; $google_font = '\Elementor\Core\Files\Fonts\Google_Font'; if (!class_exists($google_font) || !method_exists($google_font, 'enqueue')) return; $chosen = trim((string) (mkb_glossar_opts()['tooltip']['font_family'] ?? '')); if ($chosen === '' || $chosen === 'inherit') { $kit = mkb_glossar_get_optional_inheritance(); $chosen = trim((string) ($kit['values']['font_family'] ?? '')); } if ($chosen === '' || !in_array($chosen, mkb_glossar_kit_font_families(), true)) return; if (!\Elementor\Fonts::is_google_fonts_enabled()) return; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. if (!apply_filters('elementor/frontend/print_google_fonts', true)) return; if (\Elementor\Fonts::get_font_type($chosen) !== \Elementor\Fonts::GOOGLE) return; try { $google_font::enqueue($chosen, $google_font::TYPE_DEFAULT); } catch (\Throwable $error) { // Font loading must not interrupt the Glossar settings screen. } } /** Resolve exactly one active-kit font through Elementor for the admin preview. */ add_action('wp_ajax_mkb_glossar_preview_font', function () { if (!current_user_can('manage_options')) wp_send_json_error(['message' => 'Forbidden'], 403); check_ajax_referer('mkb_glossar_preview_font', 'nonce'); if (!did_action('elementor/loaded') || !class_exists('\Elementor\Fonts')) wp_send_json_error(['message' => 'Elementor unavailable'], 400); $family = isset($_POST['family']) && is_string($_POST['family']) ? trim(sanitize_text_field(wp_unslash($_POST['family']))) : ''; if (!in_array($family, mkb_glossar_kit_font_families(), true)) wp_send_json_error(['message' => 'Font not in active kit'], 400); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. if (!\Elementor\Fonts::is_google_fonts_enabled() || !apply_filters('elementor/frontend/print_google_fonts', true)) { wp_send_json_success(['url' => '', 'blocked' => true]); } if (\Elementor\Fonts::get_font_type($family) !== \Elementor\Fonts::GOOGLE) { wp_send_json_success(['url' => '', 'blocked' => false]); } $loader = '\Elementor\Core\Files\Fonts\Google_Font'; if (!class_exists($loader) || !method_exists($loader, 'enqueue')) wp_send_json_error(['message' => 'Font loader unavailable'], 400); try { $loader::enqueue($family, $loader::TYPE_DEFAULT); $handle = 'elementor-gf-local-' . sanitize_key($family); if (!wp_style_is($handle, 'enqueued')) $handle = 'elementor-gf-' . sanitize_key($family); $style = wp_styles()->registered[$handle] ?? null; if (!$style || empty($style->src)) wp_send_json_error(['message' => 'Font stylesheet unavailable'], 500); $url = $style->src; if (strpos($url, '//') === 0) $url = set_url_scheme($url); if (!preg_match('~^https?://~i', $url)) $url = site_url($url); // Elementor owns this URL; reject non-HTTP schemes and unexpected hosts. $host = wp_parse_url($url, PHP_URL_HOST); $local_host = wp_parse_url(site_url(), PHP_URL_HOST); if (!in_array($host, [$local_host, 'fonts.googleapis.com'], true)) wp_send_json_error(['message' => 'Unsupported font host'], 400); wp_send_json_success(['url' => esc_url_raw($url), 'blocked' => false]); } catch (\Throwable $error) { wp_send_json_error(['message' => 'Font stylesheet unavailable'], 500); } }); function mkb_glossar_sanitize($raw) { $old = mkb_glossar_opts(); if (!is_array($raw)) return $old; $out = $old; $section = mkb_glossar_request_key($raw,'_section'); // Setup owns only labels and one URL base, never language rewrites or other options. // Validate the complete request before mutating a single persisted setting. if ($section === 'setup_names') { $existing = get_option('mkb_glossar_options', []); if (!is_array($existing)) $existing = []; if (!current_user_can('manage_options')) return $existing; $values = []; foreach (['label', 'singular'] as $key) { if (!isset($raw[$key]) || !is_string($raw[$key])) return $existing; $value = trim(sanitize_text_field($raw[$key])); if ($value === '' || preg_match_all('/./us', $value) > 80) return $existing; $values[$key] = $value; } $base = mkb_glossar_url_base_candidate($raw['slug'] ?? null); if ($base === '' || (mkb_glossar_setup_state() === 'pending' || $base !== (string)($old['slug'] ?? '')) && mkb_glossar_url_base_problem($base) !== null) return $existing; foreach ($values as $key => $value) $existing[$key] = $value; if ($base !== (string)($old['slug'] ?? '')) { $existing['slug'] = $base; update_option('mkb_glossar_flush', 1); } return $existing; } // A settings tab must only save its own fields. Other tabs retain their values. if ($section === 'general') { $out['label'] = isset($raw['label']) && is_scalar($raw['label']) ? sanitize_text_field((string)$raw['label']) : $old['label']; $out['singular'] = isset($raw['singular']) && is_scalar($raw['singular']) ? sanitize_text_field((string)$raw['singular']) : $old['singular']; $requested_base = mkb_glossar_url_base_candidate($raw['slug'] ?? null); if ($requested_base === '') { add_settings_error('mkb_glossar_options', 'invalid_base', __('Enter a valid single-segment URL base. The previous address was kept.', 'mkb-glossar'), 'error'); return $old; // Reject the entire General form instead of a partial save. } elseif ($requested_base !== (string)$old['slug'] && ($problem = mkb_glossar_url_base_problem($requested_base)) !== null) { add_settings_error('mkb_glossar_options', 'url_base_conflict', $problem, 'error'); return $old; // No label or translated-URL changes on collision. } else { $out['slug'] = $requested_base; } // Language-specific bases are opt-in. Existing installations retain their current URLs. if (function_exists('mkb_glossar_url_languages')) { $submitted = isset($raw['language_slugs']) && is_array($raw['language_slugs']) ? $raw['language_slugs'] : []; $default_language = mkb_glossar_default_language(); // Migrate a former explicit default-language override only when it differs // from the old global base. This prevents silently changing existing URLs. $existing = (array)($old['language_slugs'] ?? []); if ($default_language !== '' && isset($existing[$default_language])) { $legacy_default = sanitize_title((string)$existing[$default_language]); if ($legacy_default !== '' && $legacy_default !== sanitize_title((string)$old['slug']) && $out['slug'] === $old['slug']) { $migration_problem = mkb_glossar_url_base_problem($legacy_default); if ($migration_problem === null) { $out['slug'] = $legacy_default; add_settings_error('mkb_glossar_options','default_slug_migrated',__('The former default-language URL base was transferred to the main URL base. Review existing permalinks.','mkb-glossar'),'warning'); } else { add_settings_error('mkb_glossar_options','default_slug_conflict', $migration_problem, 'error'); } } } $active_languages = mkb_glossar_url_languages(); $translated = $existing; // Never lose translated URL bases when provider is unavailable. foreach ($active_languages as $code) { if ($code === $default_language) continue; if (!array_key_exists($code, $submitted) || !is_string($submitted[$code])) continue; $raw_value = trim($submitted[$code]); if ($raw_value === '') { unset($translated[$code]); continue; } $value = mkb_glossar_url_base_candidate($raw_value); if ($value === '') { add_settings_error('mkb_glossar_options','invalid_language_base', __('An invalid translated URL base was ignored. Existing URLs were kept.', 'mkb-glossar'), 'error'); continue; } // These URLs are prefixed/owned by the language plugin. Static root // conflicts are not evidence of a conflict within that language. unset($translated[$code]); if ($value !== $out['slug']) $translated[$code] = $value; } // The default language uses the global base; secondary language bases // may coincide with it (the language prefix keeps URLs distinct). $out['language_slugs'] = $translated; if ($translated !== $existing || $out['slug'] !== $old['slug']) update_option('mkb_glossar_flush', 1); } // Elementor widgets are optional. Existing installations default to all-on. // A complete validated map is required: malformed/nested values preserve the // previous configuration, and the fields are absent when Elementor is inactive. if (mkb_glossar_elementor_available() && array_key_exists('elementor_widgets', $raw)) { $submitted = $raw['elementor_widgets']; $keys = mkb_glossar_elementor_widget_keys(); $valid = is_array($submitted) && count($submitted) === count($keys) && !array_diff(array_keys($submitted), $keys); if ($valid) { foreach ($keys as $key) { if (!isset($submitted[$key]) || !is_scalar($submitted[$key]) || !in_array((string)$submitted[$key], ['0', '1'], true)) { $valid = false; break; } } } if ($valid) { $out['elementor_widgets'] = array_fill_keys($keys, 1); foreach ($keys as $key) $out['elementor_widgets'][$key] = (int)$submitted[$key]; } else { add_settings_error('mkb_glossar_options', 'invalid_widget_settings', __('The Elementor widget selection was not saved because the submitted values were invalid.', 'mkb-glossar'), 'error'); } } // One centrally chosen Elementor Free template; never trust an arbitrary ID. if (mkb_glossar_elementor_available() && array_key_exists('elementor_details_template', $raw)) { $candidate = is_scalar($raw['elementor_details_template']) ? absint($raw['elementor_details_template']) : -1; if ($candidate === 0 || ($candidate > 0 && function_exists('mkb_glossar_shared_template_is_valid') && mkb_glossar_shared_template_is_valid($candidate))) { $out['elementor_details_template'] = $candidate; } else { add_settings_error('mkb_glossar_options', 'invalid_shared_template', __('The selected Elementor template is unavailable. The previous setting was kept.', 'mkb-glossar'), 'error'); } } if ($out['slug'] !== $old['slug']) { add_settings_error('mkb_glossar_options', 'slug_updated', __('URL base changed. Review existing URLs and redirects.', 'mkb-glossar'), 'warning'); update_option('mkb_glossar_flush', 1); } } elseif ($section === 'presentation') { // Save only this tab, including an explicit unchecked schema switch. $out['schema_enabled'] = isset($raw['schema_enabled']) && is_scalar($raw['schema_enabled']) && (string)$raw['schema_enabled'] === '1' ? 1 : 0; $out['overview_page_size'] = isset($raw['overview_page_size']) && is_scalar($raw['overview_page_size']) ? max(1, min(200, absint($raw['overview_page_size']))) : (int)$old['overview_page_size']; $out['details_design'] = mkb_glossar_details_design_settings($raw['details_design'] ?? []); } elseif ($section === 'linking') { $out['mark_external_links'] = isset($raw['mark_external_links']) && is_scalar($raw['mark_external_links']) && (string)$raw['mark_external_links']==='1' ? 1 : 0; // Keep the global icon choice even when external markers are turned off. $choice = $raw['external_icon_style'] ?? 'default'; $out['external_icon_style'] = is_string($choice) && in_array($choice, mkb_glossar_external_icon_styles(), true) ? $choice : 'default'; $candidate = $raw['external_icon_attachment_id'] ?? 0; if (is_scalar($candidate) && ctype_digit((string)$candidate)) { $candidate = absint($candidate); if ($candidate === 0) { $out['external_icon_attachment_id'] = 0; } elseif (mkb_glossar_external_icon_attachment_url($candidate) !== '' && current_user_can('edit_post', $candidate)) { $out['external_icon_attachment_id'] = $candidate; } else { add_settings_error('mkb_glossar_options', 'invalid_external_icon', __('The selected image cannot be used as a glossary icon. The previous image was kept.', 'mkb-glossar'), 'error'); } } $out['auto_link'] = !empty($raw['auto_link']) && is_scalar($raw['auto_link']) ? 1 : 0; $out['max_links'] = isset($raw['max_links']) && is_scalar($raw['max_links']) ? max(1, min(100, absint($raw['max_links']))) : $old['max_links']; $out['repeat_per_term'] = isset($raw['repeat_per_term']) && is_scalar($raw['repeat_per_term']) ? max(1, min(10, absint($raw['repeat_per_term']))) : $old['repeat_per_term']; $allowed = array_keys(get_post_types(['public' => true])); $selected = isset($raw['enabled_types']) && is_array($raw['enabled_types']) ? array_filter($raw['enabled_types'], 'is_scalar') : []; $out['enabled_types'] = array_values(array_intersect($allowed, array_map('sanitize_key', $selected))); } elseif ($section === 'exclusions') { // Per-post exclusions are managed by authenticated AJAX and post editor. $out['excluded_terms'] = []; foreach ((array) ($raw['excluded_terms'] ?? []) as $tax => $ids) { $tax = is_scalar($tax) ? sanitize_key((string)$tax) : ''; if (!$tax || !taxonomy_exists($tax) || !is_scalar($ids)) continue; $out['excluded_terms'][$tax] = array_values(array_unique(array_filter(array_map('absint', preg_split('/[\s,;]+/', (string) $ids))))); } } elseif ($section === 'tooltips') { $allowed_keys = ['background','color','border_color','font_family','font_size','font_weight','line_height','radius','padding','border_width','max_width']; $input = isset($raw['tooltip']) && is_array($raw['tooltip']) ? $raw['tooltip'] : []; $value = (array) ($old['tooltip'] ?? []); foreach ($allowed_keys as $key) { if (isset($input[$key]) && !is_scalar($input[$key])) continue; $entry = isset($input[$key]) ? trim((string) $input[$key]) : ''; if ($entry === '') { unset($value[$key]); if ($key === 'line_height') unset($value['line_height_unit']); continue; } // Empty = inherit. if (in_array($key, ['background','color','border_color'], true)) { $entry = sanitize_hex_color($entry); if (!$entry) { unset($value[$key]); continue; } } elseif ($key === 'font_family') { $entry = sanitize_text_field($entry); if (!in_array($entry, array_merge(['inherit','Arial, sans-serif','Georgia, serif','Verdana, sans-serif','Tahoma, sans-serif','Trebuchet MS, sans-serif','Times New Roman, serif','Courier New, monospace','system-ui, sans-serif'], mkb_glossar_kit_font_families(), [(string) ($old['tooltip']['font_family'] ?? '')]), true)) { unset($value[$key]); continue; } } else { $limits = ['font_size' => [10, 48], 'font_weight' => [100, 900], 'line_height' => [0, 500], 'radius' => [0, 80], 'padding' => [0, 60], 'border_width' => [0, 10], 'max_width' => [180, 700]]; if ($key === 'line_height') { $raw_unit = $input['line_height_unit'] ?? ($old['tooltip']['line_height_unit'] ?? 'percent'); $unit = is_scalar($raw_unit) ? sanitize_key((string)$raw_unit) : 'percent'; if (!in_array($unit, ['factor','px','percent'], true)) $unit = 'factor'; $number = is_numeric($entry) ? (float) $entry : 0; $entry = $unit === 'factor' ? min(3.5, max(0.8, $number)) : ($unit === 'px' ? min(96, max(8, $number)) : min(350, max(80, $number))); $value['line_height_unit'] = $unit; } else { $entry = max($limits[$key][0], min($limits[$key][1], absint($entry))); if ($key === 'font_weight') $entry = (int) (round($entry / 100) * 100); } } $value[$key] = $entry; } $out['tooltip'] = $value; } return $out; } function mkb_glossar_settings_page() { if (!current_user_can('manage_options')) return; $tabs = ['general' => __('General','mkb-glossar'), 'linking' => __('Linking','mkb-glossar'), 'exclusions' => __('Exclusions','mkb-glossar'), 'presentation' => __('Appearance & SEO','mkb-glossar'), 'tooltips' => __('Tooltips','mkb-glossar')]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. $tab = mkb_glossar_request_key($_GET, 'tab', 'general'); if (!isset($tabs[$tab])) $tab = 'general'; $o = mkb_glossar_opts(); // Each tab opens at the instruction that explains its main controls. $help_topic = ['general' => 'start-step-1', 'linking' => 'linking-step-2', 'exclusions' => 'linking-step-4', 'presentation' => 'display-step-5', 'tooltips' => 'tooltips-step-4'][$tab]; echo '

'.esc_html__('MKB Glossar Settings','mkb-glossar').'

'; echo '
'; // Help is a normal link alongside, but not inside, the settings-tab navigation. echo ''.esc_html__('Handbook','mkb-glossar').'
'; settings_errors(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. if ($tab === 'general' && (isset($_GET['setup_skipped']) || isset($_GET['setup_completed']))) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. echo '

' . esc_html(isset($_GET['setup_skipped']) ? __('Setup was skipped. All settings remain available; you can restart the assistant below.', 'mkb-glossar') : __('Setup completed. You can restart the assistant later under General settings.', 'mkb-glossar')) . '

'; } if ($tab !== 'exclusions') { echo '
'; settings_fields('mkb_glossar_settings'); } if ($tab !== 'exclusions') { $panel_class = in_array($tab, ['general', 'linking', 'presentation'], true) ? ' mkb-glossar-ui-form-card' : ($tab === 'tooltips' ? ' mkb-glossar-ui-tooltip-card' : ''); echo '
'; if ($tab === 'general') { echo '

'.esc_html__('Basic settings', 'mkb-glossar').'

'.esc_html__('Manage glossary labels and the public URL base.', 'mkb-glossar').'

'; } elseif ($tab === 'presentation') { echo '

'.esc_html__('Appearance and structured glossary data','mkb-glossar').'

'.esc_html__('Choose how your glossary appears: how many terms visitors see first, how sources and related terms look, and what search engines learn about your explanations.','mkb-glossar').'

'; } elseif ($tab === 'linking') { echo '

'.esc_html__('Automatic glossary linking', 'mkb-glossar').'

'.esc_html__('Configure automatic links and choose the content types where linking is allowed.', 'mkb-glossar').'

'; } } if ($tab === 'tooltips') echo '

'.esc_html__('Tooltip design', 'mkb-glossar').'

'.esc_html__('Customize individual tooltip properties or keep inherited values.', 'mkb-glossar').'

'; if ($tab !== 'exclusions') { echo ''; } if ($tab === 'presentation') { echo ''; echo ''; echo ''; } if ($tab === 'general') { $default_language = function_exists('mkb_glossar_default_language') ? mkb_glossar_default_language() : ''; $languages = function_exists('mkb_glossar_url_languages') ? mkb_glossar_url_languages() : []; foreach (['label'=>__('Collection name','mkb-glossar'), 'singular'=>__('Name of one item','mkb-glossar'), 'slug'=>__('URL base','mkb-glossar')] as $key => $label) { // translators: %s: ISO language code for the default language of the glossary URL. if ($key === 'slug' && $default_language !== '') $label = sprintf(__('URL base · %s (default)','mkb-glossar'), strtoupper($default_language)); echo ''; } $language_slugs = (array)($o['language_slugs'] ?? []); $other_languages = array_values(array_filter($languages, static fn($code) => is_string($code) && $code !== $default_language)); if ($other_languages) { echo ''; } } elseif ($tab === 'linking') { echo ''; echo ''; // One radio group; WordPress stores exactly the same option keys as in DEV.3.16.10. $icon_choices = [ 'default' => ['external-link.svg', __('Standard', 'mkb-glossar')], 'arrow' => ['arrow-up-right.svg', __('Arrow', 'mkb-glossar')], 'globe' => ['globe.svg', __('Globe', 'mkb-glossar')], 'link' => ['link.svg', __('Link', 'mkb-glossar')], ]; $selected_icon = (string)($o['external_icon_style'] ?? 'default'); if (!in_array($selected_icon, mkb_glossar_external_icon_styles(), true)) $selected_icon = 'default'; $image_id = absint($o['external_icon_attachment_id'] ?? 0); $image_url = mkb_glossar_external_icon_attachment_url($image_id); echo ''; foreach (['max_links'=>__('Maximum links per content','mkb-glossar'), 'repeat_per_term'=>__('Links per glossary term and page','mkb-glossar')] as $key => $label) { echo ''; } echo ''; } elseif ($tab === 'exclusions') { // This section is not submitted as a settings table: individual rules are managed with AJAX. // No empty form/table above this pane: it caused the persistent tab-to-card gap. echo '
'; echo '
'; echo '

'.esc_html__('Individual content exclusions','mkb-glossar').'

'; echo '

'.esc_html__('Search and exclude individual pages, posts or custom post types.','mkb-glossar').'

'; echo ''; echo '
'; echo '
'; echo '

'.esc_html__('Currently excluded','mkb-glossar').'

0
'; echo '
'.esc_html__('Content','mkb-glossar').''.esc_html__('Type','mkb-glossar').''.esc_html__('Exclusion reason','mkb-glossar').''.esc_html__('Actions','mkb-glossar').'
'; echo '

'.esc_html__('You can also edit individual exclusions in the content editor. Changes take effect immediately.','mkb-glossar').'

'; echo '

'.esc_html__('Taxonomy rules','mkb-glossar').'

'; echo '

'.esc_html__('Exclude all content assigned to selected categories or terms. These rules are separate from individual exclusions.','mkb-glossar').'

'; echo ''; settings_fields('mkb_glossar_settings'); echo ''; $found = false; foreach (get_taxonomies(['public'=>true], 'objects') as $tax) { if ($tax->name === 'mkb_glossar_topic') continue; $found = true; $taxonomy_label = $tax->name === 'post_format' ? __('Post formats (WordPress)', 'mkb-glossar') : $tax->label; echo '
'; echo ''; echo '
'; } if (!$found) echo '

'.esc_html__('No public taxonomies available.','mkb-glossar').'

'; echo '

'.esc_html__('Enter numeric taxonomy term IDs (not glossary entry IDs), separated by commas. Leave unused fields empty.','mkb-glossar').' '.esc_html__('How to find IDs and use exclusions','mkb-glossar').'

'; echo '

'; echo '
'; return; } elseif ($tab === 'tooltips') { echo ''.esc_html__('Design inheritance','mkb-glossar').'

'.esc_html__('Each field can inherit the global design or override it. Clear an override to restore inheritance.','mkb-glossar').'

'; $fields = [ 'background'=>__('Background color','mkb-glossar'), 'color'=>__('Text color','mkb-glossar'), 'border_color'=>__('Border color','mkb-glossar'), 'font_family'=>__('Font family','mkb-glossar'), 'font_size'=>__('Font size (px)','mkb-glossar'), 'font_weight'=>__('Font weight','mkb-glossar'), 'line_height'=>__('Line height','mkb-glossar'), 'radius'=>__('Corner radius (px)','mkb-glossar'), 'padding'=>__('Padding (px)','mkb-glossar'), 'border_width'=>__('Border width (px)','mkb-glossar'), 'max_width'=>__('Maximum width (px)','mkb-glossar') ]; $fonts = [''=>'— '.__('Inherit','mkb-glossar').' —','system-ui, sans-serif'=>'System UI','Arial, sans-serif'=>'Arial','Georgia, serif'=>'Georgia','Verdana, sans-serif'=>'Verdana','Tahoma, sans-serif'=>'Tahoma','Trebuchet MS, sans-serif'=>'Trebuchet MS','Times New Roman, serif'=>'Times New Roman','Courier New, monospace'=>'Courier New']; $kit_fonts = mkb_glossar_kit_font_families(); foreach ($kit_fonts as $family) { if (!isset($fonts[$family])) $fonts[$family] = $family; } // A previously saved family remains visible even if its Kit token was removed. // Saving it again is refused by validation unless Elementor still supplies it. $saved_family = (string) ($o['tooltip']['font_family'] ?? ''); if ($saved_family !== '' && !isset($fonts[$saved_family])) $fonts[$saved_family] = $saved_family; foreach ($fields as $key => $label) { $value = $o['tooltip'][$key] ?? ''; echo '
'; if (in_array($key,['background','color','border_color'],true)) { echo ''; echo ''; echo ''; echo ''; } elseif ($key==='font_family') { echo ''; } elseif ($key === 'font_weight') { $weights = [100=>'Thin',200=>'Extra Light',300=>'Light',400=>'Regular',500=>'Medium',600=>'Semi Bold',700=>'Bold',800=>'Extra Bold',900=>'Black']; echo ''; } elseif ($key === 'line_height') { $unit = $o['tooltip']['line_height_unit'] ?? 'percent'; // Legacy data used percentages. echo ''; echo ''; } else { echo ''; } echo '
'; echo ''; } } echo ''; if ($tab === 'general' && mkb_glossar_elementor_available()) { $selected = absint($o['elementor_details_template'] ?? 0); echo '
'; echo '

' . esc_html__('One design for sources and related terms on every glossary page', 'mkb-glossar') . '

'; echo '

' . esc_html__('This setting controls only the two sections “Sources and references” and “Related terms” below each glossary explanation—not the term title, the explanation or the whole page. With Elementor Free, save a container holding the MKB Glossary Details widget. In that widget, select an example glossary term for the editor preview and design the sections once. Choose the saved container below to apply that design automatically to every published glossary term.', 'mkb-glossar') . '

'; echo ''; $choices = function_exists('mkb_glossar_shared_template_choices') ? mkb_glossar_shared_template_choices() : []; echo ''; echo '

' . esc_html__('The term selected for the Elementor editor preview is only an example. On the actual site each glossary page displays its own sources and related terms. Without a usable template, MKB Glossar keeps the standard appearance. You do not have to edit each term.', 'mkb-glossar') . '

'; if (did_action('elementor/loaded') && current_user_can('edit_posts')) { echo '' . esc_html__('Edit saved Elementor templates', 'mkb-glossar') . ''; } echo '
'; // Keep all three widgets enabled on upgrades until an administrator opts out. $widget_labels = [ 'overview' => __('Glossary overview', 'mkb-glossar'), 'search' => __('Glossary search field', 'mkb-glossar'), 'details' => __('Sources and related terms', 'mkb-glossar'), ]; echo '

'.esc_html__('Elementor widgets', 'mkb-glossar').'

'; echo '

'.esc_html__('Choose which MKB Glossar widgets appear in Elementor. All widgets are enabled by default. Shortcodes and Gutenberg blocks work independently of these switches.', 'mkb-glossar').'

'; echo '
'; foreach ($widget_labels as $key => $label) { $id = 'mkb-glossar-elementor-widget-' . $key; echo '
'; echo '
'; } echo '

'.esc_html__('Disable only widgets you do not use. A widget removed from Elementor may no longer render on existing pages that contain it. Re-enable it here to restore those pages.', 'mkb-glossar').'

'; } echo '

'; if ($tab === 'tooltips') { echo ''; // Close MKB settings form before the independent Elementor global form. echo ''; } if ($tab !== 'exclusions') echo ''; if ($tab !== 'tooltips') echo ''; if ($tab === 'general') { echo '
'; echo '

' . esc_html__('Setup assistant', 'mkb-glossar') . '

' . esc_html__('Choose your names first, then review the other setup steps. You can reopen the assistant without resetting content or settings.', 'mkb-glossar') . '

'; echo '
'; echo ''; wp_nonce_field('mkb_glossar_setup_restart'); echo '
'; echo '
'; echo '
'; echo '

'.esc_html__('Integration','mkb-glossar').'

'.esc_html(mkb_glossar_elementor_available() ? __('Build the overview with Elementor, Gutenberg or a shortcode.', 'mkb-glossar') : __('Place the overview on a page with Gutenberg or a shortcode.', 'mkb-glossar')).'

'; mkb_glossar_render_shortcode_copy_control(); mkb_glossar_render_shortcode_reference('settings'); echo '

' . esc_html__('Using Gutenberg? Add the “MKB Glossar” block instead.', 'mkb-glossar') . '

'; echo '
'; } echo ''; } add_action('admin_enqueue_scripts',function($hook){ if ($hook !== 'mkb_glossar_page_mkb-glossar') return; // RC4.5: URL cleanup for consumed one-time notices; never emit raw script tags. wp_enqueue_script( 'mkb-glossar-admin-notice-cleanup', MKB_GLOSSAR_URL . 'assets/admin-notice-cleanup.js', [], MKB_GLOSSAR_VERSION, true ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. $tab = mkb_glossar_request_key($_GET, 'tab', 'general'); if (!in_array($tab,['general','linking','exclusions','presentation','tooltips'],true)) return; $settings_css_path = MKB_GLOSSAR_PATH . 'assets/admin-settings.css'; $settings_css_version = is_file($settings_css_path) ? (string) filemtime($settings_css_path) : MKB_GLOSSAR_VERSION; wp_enqueue_style('mkb-glossar-admin', MKB_GLOSSAR_URL.'assets/admin-settings.css', [], $settings_css_version); if ($tab === 'linking') { wp_enqueue_media(); // WordPress native, permission-aware Media Library picker. $icon_js = MKB_GLOSSAR_PATH . 'assets/admin-external-icon.js'; wp_enqueue_script('mkb-glossar-external-icon-admin', MKB_GLOSSAR_URL . 'assets/admin-external-icon.js', ['media-views'], is_file($icon_js) ? (string)filemtime($icon_js) : MKB_GLOSSAR_VERSION, true); wp_add_inline_script('mkb-glossar-external-icon-admin', 'window.mkbGlossarIconLabels=' . wp_json_encode([ 'title' => __('Choose an icon image', 'mkb-glossar'), 'button' => __('Use this icon', 'mkb-glossar'), 'empty' => __('No image selected; the standard icon will be used.', 'mkb-glossar'), 'preview' => __('Selected icon preview', 'mkb-glossar'), ]) . ';', 'before'); } if ($tab === 'presentation') { $appearance_js = MKB_GLOSSAR_PATH . 'assets/admin-maturity.js'; wp_enqueue_script('mkb-glossar-admin-maturity', MKB_GLOSSAR_URL . 'assets/admin-maturity.js', [], is_file($appearance_js) ? (string)filemtime($appearance_js) : MKB_GLOSSAR_VERSION, true); } if ($tab === 'general') { // Reuse the already-tested Help copy behavior; load only on the embedding screen. $copy_script = MKB_GLOSSAR_PATH . 'assets/admin-help.js'; wp_enqueue_script('mkb-glossar-admin-shortcode-copy', MKB_GLOSSAR_URL . 'assets/admin-help.js', [], is_file($copy_script) ? (string) filemtime($copy_script) : MKB_GLOSSAR_VERSION, true); } if ($tab === 'tooltips' && did_action('elementor/loaded') && class_exists('\Elementor\Plugin')) { // Keep Elementor global color text fields and native swatches in sync without submitting the form. $global_color_sync = <<<'JS' document.addEventListener('input', function (event) { var target = event.target; var item = target.closest('.mkb-glossar-global-color-item'); if (!item || !item.closest('form.mkb-glossar-global-form')) return; var picker = item.querySelector('input[data-mkb-global-picker]'); var hex = item.querySelector('input[type="text"][name^="colors["]'); if (!picker || !hex) return; if (target === picker) { hex.value = picker.value; } else if (target === hex) { var value = hex.value.trim(); if (/^#[0-9a-fA-F]{6}$/.test(value)) picker.value = value; } }); document.addEventListener('keydown', function (event) { if (event.key !== 'Enter' || event.isComposing) return; var target = event.target; if (target.matches('form.mkb-glossar-global-form input[type="text"][name^="colors["]')) { event.preventDefault(); } }); JS; wp_enqueue_script('jquery-core'); wp_add_inline_script('jquery-core', $global_color_sync, 'after'); } if ($tab === 'tooltips') mkb_glossar_enqueue_preview_font(); if ($tab === 'tooltips') { wp_enqueue_script('mkb-glossar-admin-design',MKB_GLOSSAR_URL.'assets/admin-design.js',[],MKB_GLOSSAR_VERSION,true); wp_add_inline_script('mkb-glossar-admin-design', 'window.mkbGlossarElementorKit = '.wp_json_encode(mkb_glossar_get_optional_inheritance()).';', 'before'); wp_add_inline_script('mkb-glossar-admin-design', 'window.mkbGlossarPreviewFonts = '.wp_json_encode(['url'=>admin_url('admin-ajax.php'),'nonce'=>wp_create_nonce('mkb_glossar_preview_font'),'families'=>mkb_glossar_kit_font_families()]).';', 'before'); wp_add_inline_script('mkb-glossar-admin-design', 'window.mkbGlossarDesignLabels = '.wp_json_encode(['custom'=>__('Individual','mkb-glossar'),'inherited'=>__('Inherited','mkb-glossar'),'source'=>__('Source','mkb-glossar'),'fallback'=>__('MKB default','mkb-glossar')]).';', 'before'); return; } wp_enqueue_script('mkb-glossar-admin-exclusions', MKB_GLOSSAR_URL.'assets/admin-exclusions.js',[],MKB_GLOSSAR_VERSION,true); wp_add_inline_script('mkb-glossar-admin-exclusions','window.mkbGlossarExclusions='.wp_json_encode([ 'url'=>admin_url('admin-ajax.php'), 'nonce'=>wp_create_nonce('mkb_glossar_exclusions'), 'remove'=>__('Remove','mkb-glossar'), 'add'=>__('Exclude','mkb-glossar'), 'none'=>__('No matches','mkb-glossar'), 'empty'=>__('No individually excluded content.','mkb-glossar'), 'type'=>__('Content type','mkb-glossar'), 'reason'=>__('Reason','mkb-glossar'), 'individual'=>__('Individual exclusion','mkb-glossar'), 'legacy'=>__('Legacy ID rule','mkb-glossar'), 'edit'=>__('Edit','mkb-glossar'), 'searchHint'=>__('Choose a result to exclude it','mkb-glossar'), 'loading'=>__('Loading…','mkb-glossar'), 'error'=>__('Could not update exclusions. Please try again.','mkb-glossar'), 'content'=>__('Content','mkb-glossar') ]).';','before'); }); add_action('wp_ajax_mkb_glossar_exclusions',function(){ if (!current_user_can('manage_options')) wp_send_json_error(['message'=>'Forbidden'],403); check_ajax_referer('mkb_glossar_exclusions','nonce'); // The nonce and capability are checked above; reject malformed request shapes before sanitization. if (!mkb_glossar_request_optional_scalar_fields($_POST,['mode','id','query'])) wp_send_json_error(['message'=>'Invalid input'],400); $mode = mkb_glossar_request_key($_POST,'mode','list'); if (!in_array($mode,['list','search','add','remove'],true)) wp_send_json_error(['message'=>'Invalid mode'],400); if (in_array($mode,['add','remove'],true)) { $raw_id = isset($_POST['id']) && is_scalar($_POST['id']) ? sanitize_text_field(wp_unslash((string) $_POST['id'])) : null; if (!is_scalar($raw_id) || !ctype_digit((string)$raw_id)) wp_send_json_error(['message'=>'Invalid content'],400); $id=absint($raw_id); $p=get_post($id); if (!$p || $p->post_type==='mkb_glossar' || !current_user_can('edit_post',$id)) wp_send_json_error(['message'=>'Invalid content'],400); update_post_meta($id,'_mkb_glossar_exclude',$mode==='add'?'1':'0'); // Legacy global ID exclusions are preserved and continue to be effective. } $o=mkb_glossar_opts(); $ids=array_map('absint',(array)($o['excluded_ids'] ?? [])); $marked=get_posts(['post_type'=>'any','post_status'=>'any','posts_per_page'=>500,'fields'=>'ids', 'meta_query'=>[['key'=>'_mkb_glossar_exclude','value'=>'1']]]); $ids=array_values(array_unique(array_merge($ids,$marked))); $excluded=[]; foreach ($ids as $id) { $p=get_post($id); if (!$p || $p->post_type==='mkb_glossar') continue; $excluded[]=['id'=>$id,'title'=>get_the_title($id),'type'=>$p->post_type,'type_label'=>get_post_type_object($p->post_type)->labels->singular_name ?? $p->post_type,'locked'=>in_array($id,(array)$o['excluded_ids']),'edit'=>get_edit_post_link($id,'raw') ?: '']; } $results=[]; if ($mode==='search') { $query=sanitize_text_field(wp_unslash((string)($_POST['query'] ?? ''))); if ((function_exists('mb_strlen') ? mb_strlen($query) : strlen($query)) > 150) wp_send_json_error(['message'=>'Search too long'],400); if (function_exists('mb_strlen') ? mb_strlen($query)>=2 : strlen($query)>=2) { $posts=get_posts(['post_type'=>array_values(array_diff(get_post_types(['show_ui'=>true]),['mkb_glossar','attachment'])),'post_status'=>['publish','draft','private','pending','future'],'posts_per_page'=>15,'s'=>$query]); foreach ($posts as $p) $results[]=['id'=>$p->ID,'title'=>get_the_title($p),'type'=>$p->post_type,'type_label'=>get_post_type_object($p->post_type)->labels->singular_name ?? $p->post_type,'excluded'=>in_array($p->ID,$ids)]; } } wp_send_json_success(['excluded'=>$excluded,'results'=>$results]); }); /** Lazily load the integration only when Elementor is active and ready. */ function mkb_glossar_get_optional_kit() { if (!did_action('elementor/loaded') || !class_exists('\Elementor\Plugin')) return ['available'=>false]; require_once MKB_GLOSSAR_PATH . 'includes/elementor-design.php'; return mkb_glossar_elementor_design_tokens(); } function mkb_glossar_get_optional_inheritance() { if (!did_action('elementor/loaded') || !class_exists('\Elementor\Plugin')) return ['available'=>false, 'values'=>[], 'sources'=>[]]; require_once MKB_GLOSSAR_PATH . 'includes/elementor-design.php'; return mkb_glossar_elementor_inherited_values(); } // Register the guarded global color save handler only when Elementor is loaded. add_action('admin_init', function () { if (!did_action('elementor/loaded') || !class_exists('\Elementor\Plugin')) return; require_once MKB_GLOSSAR_PATH . 'includes/elementor-global-editor.php'; require_once MKB_GLOSSAR_PATH . 'includes/elementor-typography-editor.php'; require_once MKB_GLOSSAR_PATH . 'includes/elementor-backups.php'; }); // DEV.3.9.0: shared, screen-scoped asset routing for non-settings admin pages. // Preserve separate asset handles and only load assets on their original screens. add_action('admin_enqueue_scripts', static function () { $screen = get_current_screen(); $is_terms = $screen && $screen->base === 'edit-tags' && $screen->taxonomy === 'mkb_glossar_topic'; $is_posts = $screen && $screen->base === 'edit' && $screen->post_type === 'mkb_glossar'; if ($is_terms || $is_posts) { $management_css = MKB_GLOSSAR_PATH . 'assets/admin-management.css'; wp_enqueue_style('mkb-glossar-management-ui', MKB_GLOSSAR_URL . 'assets/admin-management.css', [], is_file($management_css) ? (string)filemtime($management_css) : MKB_GLOSSAR_VERSION); if ($is_posts) { wp_enqueue_script('mkb-glossar-list-filters', MKB_GLOSSAR_URL . 'assets/admin-list-filters.js', [], MKB_GLOSSAR_VERSION, true); } } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. $page = isset($_GET['page']) && is_scalar($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. $tab = isset($_GET['tab']) && is_scalar($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : ''; if ($page === 'mkb-glossar' && in_array($tab, ['', 'general', 'linking', 'exclusions', 'presentation', 'tooltips'], true)) { // Filemtime also refreshes this scoped stylesheet during UI revisions at the same DEV version. $design_css = MKB_GLOSSAR_PATH . 'assets/admin-design-system.css'; $design_version = is_file($design_css) ? (string) filemtime($design_css) : MKB_GLOSSAR_VERSION; wp_enqueue_style('mkb-glossar-backup-ui', MKB_GLOSSAR_URL . 'assets/admin-design-system.css', [], $design_version); wp_enqueue_script('mkb-glossar-backup-ui', MKB_GLOSSAR_URL . 'assets/admin-backup-ui.js', [], MKB_GLOSSAR_VERSION, true); } }); /** A language selector in the glossary list, backed by the translation provider's query. */ add_action('restrict_manage_posts',function($post_type,$which='top') { if ($post_type !== 'mkb_glossar' || $which !== 'top') return; if (!mkb_glossar_polylang_enabled() || !function_exists('pll_languages_list')) return; $languages = (array)pll_languages_list(['fields'=>'slug']); if (!$languages) return; // Reuse Polylang's canonical `lang` query variable and never add a second // request filter. If Polylang has already emitted its own selector, skip ours. if (did_action('pll_admin_language_filter_rendered')) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. $chosen = mkb_glossar_request_key($_GET,'lang','all'); echo ''; echo ''; },40,2); /** DEV.3.14.7: Native taxonomy-backed topic selector on the glossary post list. */ add_action('restrict_manage_posts', static function ($post_type, $which = 'top') { if ($post_type !== 'mkb_glossar' || $which !== 'top') return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. $language = isset($_GET['lang']) && is_scalar($_GET['lang']) ? sanitize_key(wp_unslash($_GET['lang'])) : 'all'; $args = ['taxonomy' => 'mkb_glossar_topic', 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']; // Explicit language for Polylang. Empty language means no language constraint. if (mkb_glossar_polylang_enabled()) $args['lang'] = $language === 'all' ? '' : $language; $terms = get_terms($args); if (is_wp_error($terms)) $terms = []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. $chosen = isset($_GET['mkb_topic_filter']) && is_scalar($_GET['mkb_topic_filter']) ? sanitize_title(wp_unslash($_GET['mkb_topic_filter'])) : ''; echo ''; echo ''; }, 45, 2); /** Filter glossary posts with the registered WordPress topic taxonomy, without custom SQL. */ add_action('pre_get_posts', static function ($query) { if (!is_admin() || !$query->is_main_query()) return; if ($query->get('post_type') !== 'mkb_glossar') return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. if (empty($_GET['mkb_topic_filter']) || !is_scalar($_GET['mkb_topic_filter'])) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. $slug = sanitize_title(wp_unslash($_GET['mkb_topic_filter'])); if ($slug === '') return; $tax_query = (array)$query->get('tax_query'); $tax_query[] = ['taxonomy' => 'mkb_glossar_topic', 'field' => 'slug', 'terms' => [$slug], 'include_children' => true]; $query->set('tax_query', $tax_query); }); /** Glossary table: show the last editorial update separately from publication date. */ add_filter('manage_edit-mkb_glossar_columns', static function($columns) { $out = []; foreach ($columns as $key => $label) { if ($key === 'date') $out['mkb_glossar_modified'] = __('Last edited', 'mkb-glossar'); $out[$key] = $label; } if (!isset($out['mkb_glossar_modified'])) $out['mkb_glossar_modified'] = __('Last edited', 'mkb-glossar'); return $out; }, 20); add_action('manage_mkb_glossar_posts_custom_column', static function($column, $post_id) { if ($column !== 'mkb_glossar_modified') return; echo ''; }, 10, 2); add_filter('manage_edit-mkb_glossar_sortable_columns', static function($columns) { $columns['mkb_glossar_modified'] = 'modified'; $columns['taxonomy-mkb_glossar_topic'] = 'mkb_glossar_first_topic'; return $columns; }); /** Sort the native topics column by the first alphabetical topic name. * Multi-topic items sort by their alphabetically first topic. Empty topics come last. */ add_filter('posts_clauses', static function($clauses, $query) { if (!is_admin() || !$query->is_main_query() || $query->get('post_type') !== 'mkb_glossar' || $query->get('orderby') !== 'mkb_glossar_first_topic') return $clauses; global $wpdb; $topic = $wpdb->prepare( "(SELECT MIN(t.name) FROM {$wpdb->term_relationships} AS tr JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id WHERE tr.object_id = {$wpdb->posts}.ID AND tt.taxonomy = %s)", 'mkb_glossar_topic' ); $order = strtoupper((string)$query->get('order')) === 'DESC' ? 'DESC' : 'ASC'; $clauses['orderby'] = "CASE WHEN {$topic} IS NULL THEN 1 ELSE 0 END ASC, {$topic} {$order}, {$wpdb->posts}.post_title ASC"; return $clauses; }, 20, 2);