['class'=>true, 'viewbox'=>true, 'width'=>true, 'height'=>true, 'fill'=>true, 'stroke'=>true, 'stroke-width'=>true, 'stroke-linecap'=>true, 'stroke-linejoin'=>true, 'aria-hidden'=>true, 'focusable'=>true], 'path' => ['d'=>true], 'circle' => ['cx'=>true,'cy'=>true,'r'=>true], 'rect' => ['x'=>true,'y'=>true,'width'=>true,'height'=>true,'rx'=>true], 'div' => ['class'=>true, 'role'=>true], 'span' => ['class'=>true, 'aria-hidden'=>true], 'p' => ['class'=>true], ]; } /** A request's named scalar value, normalized as a WordPress key. Never cast arrays. */ function mkb_glossar_request_key($input, $key, $default = '') { if (!is_array($input) || !isset($input[$key]) || !is_scalar($input[$key])) return $default; return sanitize_key(wp_unslash((string)$input[$key])); } /** An integer request identifier; reject malformed arrays rather than casting to 1. */ function mkb_glossar_request_id($input, $key) { if (!is_array($input) || !isset($input[$key]) || !is_scalar($input[$key])) return 0; return absint(wp_unslash((string)$input[$key])); } /** Request fields passed to WordPress text sanitizers must never be arrays or objects. */ function mkb_glossar_request_optional_scalar_fields($input, $fields) { if (!is_array($input) || !is_array($fields)) return false; foreach ($fields as $field) { if (array_key_exists($field, $input) && !is_scalar($input[$field])) return false; } return true; } function mkb_glossar_defaults() { return ['label'=>'Glossar','singular'=>'Begriff','slug'=>'glossar','auto_link'=>0,'max_links'=>3,'repeat_per_term'=>1,'enabled_types'=>['post','page'],'excluded_ids'=>[],'excluded_terms'=>[], 'schema_enabled'=>1, 'overview_page_size'=>36, 'details_design'=>[], 'mark_external_links'=>0, 'external_icon_style'=>'default', 'external_icon_attachment_id'=>0]; } function mkb_glossar_opts() { $v=get_option('mkb_glossar_options',[]); return array_merge(mkb_glossar_defaults(), is_array($v)?$v:[]); } /** Optional Elementor widgets are enabled by default, including on upgraded sites. * These switches change only Elementor widget registration, not shortcodes, blocks * or the shared renderers. Never infer a disabled widget from a missing option. */ function mkb_glossar_elementor_widget_keys() { return ['overview', 'search', 'details']; } function mkb_glossar_elementor_widget_enabled($key) { if (!in_array($key, mkb_glossar_elementor_widget_keys(), true)) return false; $settings=mkb_glossar_opts()['elementor_widgets']??[]; return !is_array($settings) || !array_key_exists($key, $settings) || (string)$settings[$key]!=='0'; } /** Display names from General settings, resolved for the current interface locale. * Never translate user-provided names; never use them in technical keys or slugs. */ function mkb_glossar_display_names($options = null) { $o = is_array($options) ? array_merge(mkb_glossar_defaults(), $options) : mkb_glossar_opts(); $collection = isset($o['label']) && is_scalar($o['label']) ? trim(sanitize_text_field((string)$o['label'])) : ''; $item = isset($o['singular']) && is_scalar($o['singular']) ? trim(sanitize_text_field((string)$o['singular'])) : ''; if ($collection === '' || $collection === 'Glossar') $collection = __('Glossary', 'mkb-glossar'); if ($item === '' || $item === 'Begriff') $item = __('Term', 'mkb-glossar'); return ['collection' => $collection, 'item' => $item]; } function mkb_glossar_register() { $o=mkb_glossar_opts(); // Only translate the built-in defaults; never translate user-defined labels. $names=mkb_glossar_display_names($o); $plural=$names['collection']; $singular=$names['item']; register_post_type('mkb_glossar', [ /* translators: %s is the configured singular name of an item (for example, Artikel). */ 'labels'=>['name'=>$plural,'singular_name'=>$singular,'add_new_item'=>sprintf(__('Create %s','mkb-glossar'),$singular),'edit_item'=>sprintf(__('Edit %s','mkb-glossar'),$singular),'all_items'=>sprintf(__('Overview: %s','mkb-glossar'),$plural)], 'public'=>true,'show_in_rest'=>true,'show_in_menu'=>true,'menu_icon'=>'dashicons-book-alt','supports'=>['title','editor','excerpt','thumbnail','revisions'], 'rewrite'=>['slug'=>$o['slug'],'with_front'=>false],'has_archive'=>$o['slug'],'exclude_from_search'=>false, ]); register_taxonomy('mkb_glossar_topic','mkb_glossar',['label'=>__('Topics','mkb-glossar'),'hierarchical'=>true,'show_in_rest'=>true,'show_admin_column'=>true,'rewrite'=>['slug'=>$o['slug'].'-thema']]); } add_action('init','mkb_glossar_register'); /** Supported separate-post translation languages, independent of admin display language. */ function mkb_glossar_url_languages() { if (function_exists('pll_languages_list')) return array_values(array_filter((array)pll_languages_list(['fields'=>'slug']))); if (mkb_glossar_wpml_enabled()) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. $languages=apply_filters('wpml_active_languages',null,['skip_missing'=>0]); if (is_array($languages) && $languages) return array_values(array_filter(array_keys($languages),'is_string')); // WPML may return null from wpml_active_languages in wp-admin, where WordPress // generates and persists rewrite rules. The SitePress API works in that context. global $sitepress; if (is_object($sitepress) && method_exists($sitepress,'get_active_languages')) { $active=$sitepress->get_active_languages(); if (is_array($active) && $active) { $codes=[]; foreach ($active as $key=>$entry) { $code=is_array($entry)?($entry['code']??$key):$key; if (is_string($code) && $code!=='') $codes[]=$code; } if ($codes) return array_values(array_unique($codes)); } } // Stored WPML language settings are available even when the language filter // has no front-end query context. Do not assume a specific permalink mode. $settings=get_option('icl_sitepress_settings',[]); if (is_array($settings) && !empty($settings['active_languages']) && is_array($settings['active_languages'])) { $codes=[]; foreach ($settings['active_languages'] as $key=>$entry) { $code=is_array($entry)?($entry['code']??$key):(is_string($entry)?$entry:$key); if (is_string($code) && $code!=='') $codes[]=$code; } if ($codes) return array_values(array_unique($codes)); } } return []; } /** The default language belongs to the active translation provider, not to this plugin. */ function mkb_glossar_default_language() { if (mkb_glossar_polylang_enabled() && function_exists('pll_default_language')) return (string)pll_default_language('slug'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. if (mkb_glossar_wpml_enabled()) return (string)apply_filters('wpml_default_language',''); return ''; } /** Explicit language overrides only. An empty option retains the original WordPress permalink. */ function mkb_glossar_language_base($language) { $opts=mkb_glossar_opts(); $bases=(array)($opts['language_slugs']??[]); return isset($bases[$language]) ? sanitize_title((string)$bases[$language]) : ''; } /** Resolve WPML's language home through its public URL API, not guessed /en/ prefixes. */ function mkb_glossar_wpml_language_home($language) { if (!mkb_glossar_wpml_enabled() || !$language) return ''; // get_option('home') avoids the active document-language filter on home_url(). $origin=trailingslashit((string)get_option('home')); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. $localized=apply_filters('wpml_permalink',$origin,$language); if (!is_string($localized) || !wp_parse_url($localized,PHP_URL_HOST)) return ''; return trailingslashit($localized); } /** Rewrite paths are relative to the site, with language prefixes owned by the translation plugin. */ function mkb_glossar_language_rewrites() { if (!get_option('permalink_structure')) return; $opts=mkb_glossar_opts(); $legacy=$opts['slug']; foreach (mkb_glossar_url_languages() as $language) { $base=mkb_glossar_language_base($language); if (!$base || $base===$legacy) continue; $prefix=''; if (mkb_glossar_polylang_enabled()) { // Polylang's public home URL knows whether the default-language prefix is hidden. $home=function_exists('pll_home_url')?pll_home_url($language):''; if (!$home) continue; $relative=trim((string)wp_parse_url($home,PHP_URL_PATH),'/'); $sitepath=trim((string)wp_parse_url(home_url('/'),PHP_URL_PATH),'/'); if ($sitepath!=='' && str_starts_with($relative,$sitepath.'/')) $relative=substr($relative,strlen($sitepath)+1); elseif ($relative===$sitepath) $relative=''; $prefix=$relative!==''?$relative.'/':''; } elseif (mkb_glossar_wpml_enabled()) { $home=mkb_glossar_wpml_language_home($language); if (!$home) continue; // Only create our own rules in directory mode. WPML retains control of domains/parameters. $origin_host=wp_parse_url((string)get_option('home'),PHP_URL_HOST); if (wp_parse_url($home,PHP_URL_HOST)!==$origin_host) continue; $relative=trim((string)wp_parse_url($home,PHP_URL_PATH),'/'); $sitepath=trim((string)wp_parse_url((string)get_option('home'),PHP_URL_PATH),'/'); if ($sitepath!=='' && str_starts_with($relative,$sitepath.'/')) $relative=substr($relative,strlen($sitepath)+1); elseif ($relative===$sitepath) $relative=''; if ($relative!=='' && !str_contains($relative,'/')) $prefix=$relative.'/'; elseif ($relative==='') $prefix=''; else continue; } else continue; add_rewrite_rule('^'.preg_quote($prefix.$base,'#').'/([^/]+)/?$','index.php?mkb_glossar=$matches[1]&lang='.rawurlencode($language),'top'); if (mkb_glossar_wpml_enabled() && $prefix!=='') { // WPML strips directory-language prefixes before WP::parse_request matches // rewrite rules. Its normalized request is `glossary/term`, not // `en/glossary/term`. Register both paths to accommodate this behavior, // while leaving Polylang's established prefix-aware rules untouched. add_rewrite_rule('^'.preg_quote($base,'#').'/([^/]+)/?$','index.php?mkb_glossar=$matches[1]&lang='.rawurlencode($language),'top'); } } } add_action('init','mkb_glossar_language_rewrites',30); add_filter('post_type_link',static function($url,$post) { if ($post->post_type!=='mkb_glossar' || !mkb_glossar_polylang_enabled() || !function_exists('pll_get_post_language') || !function_exists('pll_home_url')) return $url; $language=pll_get_post_language($post->ID,'slug'); $base=$language?mkb_glossar_language_base($language):''; if (!$base) return $url; $slug=$post->post_name; if (!$slug) return $url; return trailingslashit(pll_home_url($language)).$base.'/'. $slug.'/'; },20,2); /** Permalink override for WPML custom bases; keep the established Polylang path untouched. */ add_filter('post_type_link',static function($url,$post) { if ($post->post_type!=='mkb_glossar' || !mkb_glossar_wpml_enabled() || !$post->post_name) return $url; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. $language=(string)apply_filters('wpml_element_language_code','',['element_id'=>(int)$post->ID,'element_type'=>'post_mkb_glossar']); $base=$language?mkb_glossar_language_base($language):''; if (!$base || $base===(string)mkb_glossar_opts()['slug']) return $url; $home=mkb_glossar_wpml_language_home($language); if (!$home) return $url; // Only override for directory URLs. Alternative-domain setups need separate integration QA. if (wp_parse_url($home,PHP_URL_HOST)!==wp_parse_url((string)get_option('home'),PHP_URL_HOST)) return $url; return $home.$base.'/'.rawurlencode($post->post_name).'/'; },999,2); /** Topic visibility is editorial, not a builder-local list. Existing topics are visible by default. */ function mkb_glossar_topic_visible($term_id) { // get_terms() callbacks receive WP_Term objects; editor controls may pass an ID. $term_id=$term_id instanceof WP_Term ? $term_id->term_id : $term_id; return get_term_meta((int)$term_id,'_mkb_glossar_filter_hidden',true)!=='1'; } add_action('mkb_glossar_topic_add_form_fields',static function() { echo '
'; }); add_action('mkb_glossar_topic_edit_form_fields',static function($term) { echo ''.esc_html__('Frontend-Kategorienfilter','mkb-glossar').''; }); function mkb_glossar_save_topic_visibility($term_id) { if (!current_user_can('manage_categories')) return; // Term callbacks are fired by WordPress admin; guard REST/programmatic saves. if (!isset($_POST['mkb_glossar_topic_visible']) && !isset($_POST['_wpnonce_add-tag']) && !isset($_POST['_wpnonce'])) return; $nonce=isset($_POST['_wpnonce_add-tag'])?sanitize_text_field(wp_unslash($_POST['_wpnonce_add-tag'])):(isset($_POST['_wpnonce'])?sanitize_text_field(wp_unslash($_POST['_wpnonce'])):''); if (!wp_verify_nonce($nonce,'add-tag') && !wp_verify_nonce($nonce,'update-tag_'.(int)$term_id)) return; update_term_meta((int)$term_id,'_mkb_glossar_filter_hidden',isset($_POST['mkb_glossar_topic_visible'])?'0':'1'); } add_action('created_mkb_glossar_topic','mkb_glossar_save_topic_visibility'); add_action('edited_mkb_glossar_topic','mkb_glossar_save_topic_visibility'); /** Translation-aware topic list: keep all language relationships in the provider. */ function mkb_glossar_topic_language($term_id) { if (mkb_glossar_polylang_enabled() && function_exists('pll_get_term_language')) return (string)pll_get_term_language((int)$term_id,'slug'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. if (mkb_glossar_wpml_enabled()) return (string)apply_filters('wpml_element_language_code','',['element_id'=>(int)$term_id,'element_type'=>'tax_mkb_glossar_topic']); return ''; } function mkb_glossar_topic_translations($term_id) { if (mkb_glossar_polylang_enabled() && function_exists('pll_get_term_translations')) return (array)pll_get_term_translations((int)$term_id); if (mkb_glossar_wpml_enabled()) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. $trid=apply_filters('wpml_element_trid',null,(int)$term_id,'tax_mkb_glossar_topic'); if (!$trid) return []; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. $translations=apply_filters('wpml_get_element_translations',[],$trid,'tax_mkb_glossar_topic'); $result=[]; foreach ((array)$translations as $lang=>$item) if (is_object($item) && isset($item->element_id)) $result[(string)$lang]=(int)$item->element_id; return $result; } return []; } add_filter('manage_edit-mkb_glossar_topic_columns',static function($columns) { if (!mkb_glossar_polylang_enabled() && !mkb_glossar_wpml_enabled()) return $columns; $result=[]; foreach ($columns as $key=>$value) { $result[$key]=$value; if ($key==='name') $result['mkb_glossar_translation_status']=__('Sprache & Übersetzungen','mkb-glossar'); } return $result; },30); add_filter('manage_mkb_glossar_topic_custom_column',static function($output,$column,$term_id) { if ($column!=='mkb_glossar_translation_status') return $output; $current=mkb_glossar_topic_language($term_id); $linked=mkb_glossar_topic_translations($term_id); $languages=mkb_glossar_url_languages(); if (!$languages) return esc_html($current?:'—'); $out=''; foreach ($languages as $lang) { $lang=sanitize_key($lang); $label=strtoupper($lang); // translators: %s: language code of the active taxonomy translation. if ($lang===$current) $out.=''.esc_html($label).' '; elseif (!empty($linked[$lang])) { $url=get_edit_term_link((int)$linked[$lang],'mkb_glossar_topic','mkb_glossar'); // translators: %s: language code of an existing taxonomy translation. if ($url) $out.=''.esc_html($label).' '.esc_html__('Bearbeiten','mkb-glossar').' '; } elseif (mkb_glossar_polylang_enabled()) { // Use Polylang's term-creation context; linkage must be verified in Local. $url=add_query_arg(['taxonomy'=>'mkb_glossar_topic','post_type'=>'mkb_glossar','lang'=>$lang,'from_tag'=>(int)$term_id],admin_url('edit-tags.php')); // translators: %s: language code for a new taxonomy translation. $out.=''.esc_html($label).' + '; } else { // translators: %s: language code of a missing taxonomy translation. $out.=''.esc_html($label).' — '; } } return $out.''; },10,3); add_action('restrict_manage_terms',static function($taxonomy) { if ($taxonomy!=='mkb_glossar_topic' || (!mkb_glossar_polylang_enabled() && !mkb_glossar_wpml_enabled())) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. $selected=isset($_GET['lang']) && is_scalar($_GET['lang'])?sanitize_key(wp_unslash($_GET['lang'])):''; echo ''; echo ''; },10,1); add_action('admin_enqueue_scripts',static function() { if (!function_exists('get_current_screen')) return; $screen=get_current_screen(); if (!$screen || $screen->id!=='edit-mkb_glossar_topic') return; wp_add_inline_script('jquery-core',"document.addEventListener('change',function(e){if(e.target && e.target.id==='mkb-glossar-topic-language-filter'){var f=e.target.closest('form');if(f){f.submit();}}});"); }); function mkb_glossar_activate() { $fresh = false === get_option('mkb_glossar_options', false); if ($fresh) { add_option('mkb_glossar_options', mkb_glossar_defaults()); // One welcome for an actual new installation, never for an update or reactivation. if (false === get_option('mkb_glossar_setup_state', false)) { add_option('mkb_glossar_setup_state', 'pending', '', false); add_option('mkb_glossar_setup_redirect', 1, '', false); } } mkb_glossar_register(); flush_rewrite_rules(); } function mkb_glossar_meta() { foreach (['mkb_glossar_summary','mkb_glossar_synonyms'] as $name) register_post_meta('mkb_glossar',$name,['single'=>true,'type'=>'string','show_in_rest'=>true,'sanitize_callback'=>'sanitize_textarea_field','auth_callback'=>function(){return current_user_can('edit_posts');}]); register_post_meta('','_mkb_glossar_exclude',['single'=>true,'type'=>'boolean','show_in_rest'=>true,'auth_callback'=>function(){return current_user_can('edit_posts');}]); } add_action('init','mkb_glossar_meta'); function mkb_glossar_meta_box() { add_meta_box('mkb_glossar_details','Glossar-Informationen','mkb_glossar_details_box','mkb_glossar','normal'); $types=get_post_types(['show_ui'=>true],'names'); foreach ($types as $type) { if ($type!=='mkb_glossar') add_meta_box('mkb_glossar_link_rules','Glossar-Verlinkung','mkb_glossar_exclusion_box',$type,'side'); } } add_action('add_meta_boxes','mkb_glossar_meta_box'); function mkb_glossar_details_box($post) { wp_nonce_field('mkb_glossar_save','mkb_glossar_nonce'); echo '

'; echo '

'; } function mkb_glossar_exclusion_box($post) { wp_nonce_field('mkb_glossar_save','mkb_glossar_nonce'); echo ''; } function mkb_glossar_save_meta($id) { // A meta-box save may receive malformed POST values; reject non-scalar inputs // before WordPress text sanitizers, and never write without nonce and capability. if (!isset($_POST['mkb_glossar_nonce']) || !is_scalar($_POST['mkb_glossar_nonce'])) return; $nonce=sanitize_text_field(wp_unslash((string)$_POST['mkb_glossar_nonce'])); if (!wp_verify_nonce($nonce,'mkb_glossar_save') || (defined('DOING_AUTOSAVE')&&DOING_AUTOSAVE) || !current_user_can('edit_post',$id)) return; if (get_post_type($id)==='mkb_glossar') { foreach (['mkb_glossar_summary','mkb_glossar_synonyms'] as $key) { if (isset($_POST[$key]) && is_scalar($_POST[$key])) update_post_meta($id,$key,sanitize_textarea_field(wp_unslash((string)$_POST[$key]))); } } else { update_post_meta($id,'_mkb_glossar_exclude',isset($_POST['mkb_glossar_exclude'])?'1':'0'); } } add_action('save_post','mkb_glossar_save_meta'); /** Translation-plugin detection is optional: no language plugin is required. */ function mkb_glossar_wpml_enabled() { return defined('ICL_SITEPRESS_VERSION') && function_exists('apply_filters'); } function mkb_glossar_polylang_enabled() { return function_exists('pll_current_language') && function_exists('pll_get_post_language'); } /** WPML and Polylang use separate translated posts; otherwise keep the site-default behavior. */ function mkb_glossar_current_language() { if (mkb_glossar_polylang_enabled()) { $code=pll_current_language('slug'); } elseif (mkb_glossar_wpml_enabled()) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. $code=apply_filters('wpml_current_language',null); } else return 'site-default'; return is_string($code) && $code!=='' && $code!=='all' ? $code : 'site-default'; } /** Language of an individual glossary post, or null when unknown in an active translation system. */ function mkb_glossar_post_language($id) { if (!$id) return null; if (mkb_glossar_polylang_enabled()) { $code=pll_get_post_language((int)$id,'slug'); } elseif (mkb_glossar_wpml_enabled()) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. $code=apply_filters('wpml_element_language_code',null,['element_id'=>(int)$id,'element_type'=>'post_mkb_glossar']); } else return 'site-default'; return is_string($code) && $code!=='' ? $code : null; } /** Never use a differently translated or unclassified candidate as an autolink target. */ function mkb_glossar_in_language($id,$language) { if (!mkb_glossar_polylang_enabled() && !mkb_glossar_wpml_enabled()) return true; if ($language==='site-default') return false; return mkb_glossar_post_language($id)===$language; } function mkb_glossar_identity_key($value) { $value=trim(preg_replace('/\s+/u',' ',wp_strip_all_tags((string)$value))); return function_exists('mb_strtolower') ? mb_strtolower($value,'UTF-8') : strtolower($value); } function mkb_glossar_identity_language($id=0) { return $id ? (mkb_glossar_post_language($id) ?? mkb_glossar_current_language()) : mkb_glossar_current_language(); } function mkb_glossar_identity_conflicts($title,$synonyms='',$self=0,$language=null) { $language=$language ?? mkb_glossar_identity_language($self); $input=['title'=>mkb_glossar_identity_key($title)]; $synonym_keys=[]; foreach (preg_split('/\r\n|\r|\n/u',(string)$synonyms) ?: [] as $synonym) { $key=mkb_glossar_identity_key($synonym); if ($key!=='' && !in_array($key,$synonym_keys,true)) $synonym_keys[]=$key; } $input['synonyms']=$synonym_keys; $conflicts=[]; $ids=get_posts(['post_type'=>'mkb_glossar','post_status'=>['publish','draft','pending','private','future'],'posts_per_page'=>-1,'fields'=>'ids','orderby'=>'ID','order'=>'ASC']); foreach ($ids as $id) { $id=(int)$id; if ($id===(int)$self || mkb_glossar_identity_language($id)!==$language) continue; $existing_title=mkb_glossar_identity_key(get_the_title($id)); $existing_synonyms=[]; foreach (preg_split('/\r\n|\r|\n/u',(string)get_post_meta($id,'mkb_glossar_synonyms',true)) ?: [] as $s) { $k=mkb_glossar_identity_key($s); if ($k!=='') $existing_synonyms[]=$k; } if ($input['title']!=='' && $input['title']===$existing_title) $conflicts[]=['id'=>$id,'kind'=>'title','label'=>get_the_title($id)]; elseif ($input['title']!=='' && in_array($input['title'],$existing_synonyms,true)) $conflicts[]=['id'=>$id,'kind'=>'synonym','label'=>get_the_title($id)]; elseif ($existing_title!=='' && in_array($existing_title,$synonym_keys,true)) $conflicts[]=['id'=>$id,'kind'=>'synonym','label'=>get_the_title($id)]; elseif (array_intersect($synonym_keys,$existing_synonyms)) $conflicts[]=['id'=>$id,'kind'=>'synonym','label'=>get_the_title($id)]; } return $conflicts; } function mkb_glossar_identity_message($conflicts) { foreach ($conflicts as $entry) if ($entry['kind']==='title') return 'Dieser Glossarbegriff existiert bereits: '.($entry['label']??'').'. Bitte den vorhandenen Eintrag bearbeiten.'; $first=$conflicts[0]??null; return $first ? 'Dieser Begriff oder ein Synonym wird bereits beim Glossareintrag „'.$first['label'].'“ verwendet.' : ''; } function mkb_glossar_identity_editor_synonyms($post_id=0) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Restricted WordPress admin-post/classic-editor code; caller verifies nonce, capability and request shape before accessing POST. if (isset($_POST['mkb_glossar_synonyms']) && is_scalar($_POST['mkb_glossar_synonyms'])) return sanitize_textarea_field(wp_unslash((string)$_POST['mkb_glossar_synonyms'])); return $post_id ? (string)get_post_meta($post_id,'mkb_glossar_synonyms',true) : ''; } /** * Only interpret classic-editor POST fields after verifying the matching core * save nonce, object identity and editor capability. WordPress post.php already * checks these; this gate prevents other admin wp_insert_post() calls from * accidentally triggering our duplicate-check based on ambient POST fields. */ function mkb_glossar_identity_classic_submission_valid($postarr, $request) { if (!is_array($postarr) || !is_array($request)) return false; $action=mkb_glossar_request_key($request,'action'); $id=mkb_glossar_request_id($postarr,'ID'); if ($action==='editpost' && !$id) return false; if (!in_array($action,['editpost','post'],true)) return false; if (!isset($request['_wpnonce']) || !is_scalar($request['_wpnonce'])) return false; $nonce=sanitize_text_field(wp_unslash((string)$request['_wpnonce'])); if (!$nonce || !wp_verify_nonce($nonce,$action==='editpost'?'update-post_'.$id:'add-mkb_glossar')) return false; if ($id && (get_post_type($id)!=='mkb_glossar' || mkb_glossar_request_id($request,'post_ID')!==$id)) return false; return current_user_can($id?'edit_post':'edit_posts',$id?:null); } // REST rejects the entire operation before WordPress persists any changes. add_filter('rest_pre_insert_mkb_glossar',function($prepared,$request){ if (is_wp_error($prepared)) return $prepared; $id=(int)$request->get_param('id'); $title=isset($prepared->post_title)?$prepared->post_title:($id?get_the_title($id):''); $meta=(array)$request->get_param('meta'); $synonyms=array_key_exists('mkb_glossar_synonyms',$meta)?$meta['mkb_glossar_synonyms']:($id?get_post_meta($id,'mkb_glossar_synonyms',true):''); $conflicts=mkb_glossar_identity_conflicts($title,$synonyms,$id); if (!$conflicts) return $prepared; $primary=$conflicts[0]; return new WP_Error('mkb_glossar_duplicate',mkb_glossar_identity_message($conflicts),['status'=>409,'existing_id'=>$primary['id'],'edit_url'=>get_edit_post_link($primary['id'],'raw'),'conflict_kind'=>$primary['kind'],'existing_title'=>$primary['label']]); },10,2); // Classic editor: stop the submitted save before changes are committed. add_filter('wp_insert_post_data',function($data,$postarr){ if (($data['post_type']??'')!=='mkb_glossar' || !is_admin() || wp_doing_ajax() || (defined('REST_REQUEST')&&REST_REQUEST) || (defined('DOING_AUTOSAVE')&&DOING_AUTOSAVE)) return $data; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Restricted WordPress admin-post/classic-editor code; caller verifies nonce, capability and request shape before accessing POST. if (!mkb_glossar_identity_classic_submission_valid($postarr,$_POST)) return $data; $id=mkb_glossar_request_id($postarr,'ID'); $conflicts=mkb_glossar_identity_conflicts($data['post_title']??'',mkb_glossar_identity_editor_synonyms($id),$id); if (!$conflicts) return $data; $url=get_edit_post_link($conflicts[0]['id'],'raw'); wp_die(esc_html(mkb_glossar_identity_message($conflicts)).' Vorhandenen Begriff öffnen','MKB Glossar: Begriff bereits vorhanden',['response'=>409,'back_link'=>true]); },10,2); add_action('wp_ajax_mkb_glossar_check_identity',function(){ check_ajax_referer('mkb_glossar_identity','nonce'); if (!current_user_can('edit_posts')) wp_send_json_error(['message'=>'Keine Berechtigung'],403); if (!mkb_glossar_request_optional_scalar_fields($_POST,['title','synonyms','post_id','language'])) wp_send_json_error(['message'=>'Invalid input'],400); $title=isset($_POST['title'])?sanitize_text_field(wp_unslash($_POST['title'])):''; $synonyms=isset($_POST['synonyms'])?sanitize_textarea_field(wp_unslash($_POST['synonyms'])):''; $id=isset($_POST['post_id'])?absint($_POST['post_id']):0; $language=isset($_POST['language'])?sanitize_key(wp_unslash($_POST['language'])):''; if ($language && function_exists('mkb_glossar_editor_language_valid') && !mkb_glossar_editor_language_valid($language)) wp_send_json_error(['message'=>'Ungültige Sprache'],400); $conflicts=mkb_glossar_identity_conflicts($title,$synonyms,$id,$language?:null); $results=[]; foreach ($conflicts as $entry) $results[]=['kind'=>$entry['kind'],'label'=>$entry['label'],'url'=>(function_exists('mkb_glossar_editor_url')?mkb_glossar_editor_url($entry['id']):get_edit_post_link($entry['id'],'raw'))]; wp_send_json_success(['conflicts'=>$results,'message'=>mkb_glossar_identity_message($conflicts)]); }); add_action('admin_enqueue_scripts',function($hook){ if (!in_array($hook,['post.php','post-new.php'],true) || !function_exists('get_current_screen')) return; $screen=get_current_screen(); if (!$screen || $screen->post_type!=='mkb_glossar') return; wp_enqueue_script('mkb-glossar-identity',MKB_GLOSSAR_URL.'assets/admin-identity.js',['wp-data','wp-dom-ready'],MKB_GLOSSAR_VERSION,true); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. wp_add_inline_script('mkb-glossar-identity','window.mkbGlossarIdentity='.wp_json_encode(['url'=>admin_url('admin-ajax.php'),'nonce'=>wp_create_nonce('mkb_glossar_identity'),'postId'=>mkb_glossar_request_id($_GET,'post')]).';','before'); wp_enqueue_style('mkb-glossar-identity',MKB_GLOSSAR_URL.'assets/admin-identity.css',[],MKB_GLOSSAR_VERSION); }); function mkb_glossar_register_frontend_assets() { wp_register_style('mkb-glossar',MKB_GLOSSAR_URL.'assets/glossar.css',[],MKB_GLOSSAR_VERSION); wp_register_script('mkb-glossar',MKB_GLOSSAR_URL.'assets/glossar.js',[],MKB_GLOSSAR_VERSION,true); } add_action('init','mkb_glossar_register_frontend_assets'); function mkb_glossar_prepare_tooltip_style() { static $prepared=false; if ($prepared) return; $prepared=true; $design=(array)(mkb_glossar_opts()['tooltip']??[]); // Resolve Elementor inheritance only on the frontend and only when the kit is available. // Stored MKB overrides always win. No settings are written to Elementor. if (did_action('elementor/loaded') && class_exists('\\Elementor\\Plugin')) { $adapter = MKB_GLOSSAR_PATH . 'includes/elementor-design.php'; if (is_readable($adapter)) { require_once $adapter; $kit = mkb_glossar_elementor_inherited_values(); foreach ((array)($kit['values'] ?? []) as $key => $value) { if (!isset($design[$key]) || $design[$key] === '') $design[$key] = $value; } } } $properties=['background'=>'background-color','color'=>'color','border_color'=>'border-color','font_size'=>'font-size','radius'=>'border-radius','padding'=>'padding','font_family'=>'font-family','font_weight'=>'font-weight','line_height'=>'line-height','border_width'=>'border-width','max_width'=>'max-width']; $declarations=[]; foreach($properties as $key=>$property) { if (!isset($design[$key]) || $design[$key]==='') continue; $value=$design[$key]; if (in_array($key,['background','color','border_color'],true)) $value=sanitize_hex_color($value); elseif ($key==='font_family') { $allowed=['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']; if(!in_array($value,$allowed,true)) continue; } elseif ($key==='font_weight') $value=absint($value); elseif ($key==='line_height') { $unit=$design['line_height_unit']??'percent'; if (is_string($value) && preg_match('/^(?:[0-9]+(?:\.[0-9]+)?)(?:em|px|%)$/', $value)) { /* trusted, sanitized kit token */ } else { $n=(float)$value; if ($unit==='factor') $value=(string)min(3.5,max(0.8,$n)); elseif ($unit==='px') $value=(string)min(96,max(8,$n)).'px'; else $value=(string)(min(350,max(80,$n))/100); } } else $value=absint($value).'px'; if ($value) $declarations[]=$property.':'.$value; } if ($declarations) { $css='.mkb-glossar-tooltip{'.implode(';',$declarations).'}'; // Widgets inside a late-rendered Elementor template/sidebar may appear after // WordPress has printed head styles. Preserve the old design with one safe // footer fallback rather than losing overrides on archive/header layouts. if (function_exists('wp_style_is') && wp_style_is('mkb-glossar','done')) { $GLOBALS['mkb_glossar_late_tooltip_css']=$css; } else wp_add_inline_style('mkb-glossar',$css); } } /** Cheap context gate: avoid resolving Elementor Kit styling on unrelated pages. * Preload early for known shortcodes/blocks/Elementor documents or eligible * automatic linking, before WordPress prints head styles. The renderer handles * other late/dynamic cases without changing the tooltip design contract. */ function mkb_glossar_tooltip_context_may_need_style() { if (is_admin() || !function_exists('is_singular') || !is_singular()) return false; $id=(int)get_queried_object_id(); if ($id<=0) return false; $content=(string)get_post_field('post_content', $id); if ($content!=='' && ((function_exists('has_shortcode') && has_shortcode($content,'mkb_glossar')) || (function_exists('has_block') && has_block('mkb/glossar',$content)))) return true; $elementor_data=get_post_meta($id,'_elementor_data',true); if (is_string($elementor_data) && (strpos($elementor_data, 'mkb_glossar')!==false || strpos($elementor_data,'mkb/glossar')!==false)) return true; $settings=mkb_glossar_opts(); if (!empty($settings['auto_link']) && get_post_type($id)!=='mkb_glossar' && !mkb_glossar_is_excluded($id)) return true; return false; } add_action('wp_enqueue_scripts', static function() { if (mkb_glossar_tooltip_context_may_need_style()) mkb_glossar_prepare_tooltip_style(); }, 20); add_action('wp_footer', static function() { if (empty($GLOBALS['mkb_glossar_late_tooltip_css'])) return; // The CSS contains only allowlisted properties and validated style tokens. // The main glossary style may have been printed in the head by this stage. // A separate late handle is printed through the WordPress Styles API so // Elementor templates rendered after wp_head keep their intended overrides. wp_register_style('mkb-glossar-late-tooltip', false, [], MKB_GLOSSAR_VERSION); wp_enqueue_style('mkb-glossar-late-tooltip'); wp_add_inline_style('mkb-glossar-late-tooltip', $GLOBALS['mkb_glossar_late_tooltip_css']); wp_print_styles(['mkb-glossar-late-tooltip']); unset($GLOBALS['mkb_glossar_late_tooltip_css']); }, 2); function mkb_glossar_enqueue($tooltip_style_needed=true) { if ($tooltip_style_needed) mkb_glossar_prepare_tooltip_style(); wp_enqueue_style('mkb-glossar'); wp_enqueue_script('mkb-glossar'); } /** Shared configuration contract. All builders feed this function. */ function mkb_glossar_overview_config($input=[]) { $input=(array)$input; $defaults=['limit'=>-1,'topic'=>'','show_search'=>true,'show_alphabet'=>true,'show_topics'=>false,'show_search_label'=>true,'show_topic_label'=>true,'show_tooltips'=>true,'search_label'=>'','search_placeholder'=>'','alphabet_all_label'=>'','empty_message'=>'','topic_label'=>'','reset_label'=>'','columns'=>2]; $settings=array_merge($defaults,array_intersect_key($input,$defaults)); $settings['limit']=max(-1,min(500,(int)$settings['limit'])); $settings['topic']=sanitize_title((string)$settings['topic']); foreach (['show_search','show_alphabet','show_topics','show_search_label','show_topic_label','show_tooltips'] as $key) $settings[$key]=filter_var($settings[$key],FILTER_VALIDATE_BOOLEAN); foreach (['search_label','search_placeholder','alphabet_all_label','empty_message','topic_label','reset_label'] as $key) $settings[$key]=sanitize_text_field((string)$settings[$key]); $settings['columns']=max(1,min(4,(int)$settings['columns'])); return $settings; } /** Data retrieval is independent from shortcode, block and Elementor. */ function mkb_glossar_overview_posts($config) { $remote = function_exists('mkb_glossar_remote_enabled') && mkb_glossar_remote_enabled($config); $per_page = $remote ? mkb_glossar_remote_page_size() : $config['limit']; $args=['post_type'=>'mkb_glossar','post_status'=>'publish','posts_per_page'=>$per_page,'orderby'=>'title','order'=>'ASC','no_found_rows'=>true,'suppress_filters'=>!empty($config['_mkb_block_editor_preview'])]; if ($config['topic']!=='') $args['tax_query']=[['taxonomy'=>'mkb_glossar_topic','field'=>'slug','terms'=>$config['topic']]]; $query=new WP_Query($args); $language=$config['_mkb_language']??mkb_glossar_current_language(); return array_values(array_filter($query->posts,static function($post)use($language) { return mkb_glossar_in_language($post->ID,$language); })); } /** * Frontend text follows the content language, never the logged-in user's admin locale. * English msgids can be localized by standard WordPress language catalogs in future. * The German branch also preserves the historical output without a translation plugin. */ function mkb_glossar_frontend_text_for_language($key,$lang) { // Explicit content-language defaults must not pass through the admin user's // gettext locale: a German editor can preview an English Elementor document. $german=$lang==='site-default' || strtolower(substr((string)$lang,0,2))==='de'; $texts=[ 'search_label'=>['Begriffe durchsuchen','Search glossary terms'], 'standalone_search_label'=>['Glossar durchsuchen','Search the glossary'], 'standalone_search_placeholder'=>['Welchen Begriff suchst du?','Which term are you looking for?'], 'standalone_search_button'=>['Suchen','Search'], 'suggest_loading'=>['Passende Begriffe werden gesucht …','Looking for matching terms …'], 'suggest_empty'=>['Keine Vorschläge gefunden. Suche im vollständigen Glossar weiter.','No quick suggestions found. Search the full glossary instead.'], 'suggest_error'=>['Vorschläge momentan nicht verfügbar. Du kannst das ganze Glossar durchsuchen.','Suggestions are temporarily unavailable. You can search the full glossary.'], 'suggest_more'=>['Alle Suchergebnisse im Glossar ansehen','See all search results in the glossary'], 'suggest_count_one'=>['1 Begriff vorgeschlagen.','1 glossary term suggested.'], 'suggest_count_many'=>['%d Begriffe vorgeschlagen.','%d glossary terms suggested.'], 'search_placeholder'=>['Suchbegriff eingeben','Enter a search term'], 'alphabet_all_label'=>['Alle','All'], 'topic_label'=>['Thema auswählen','Select topic'], 'all_topics'=>['Alle Themen','All topics'], 'empty_message'=>['Keine passenden Begriffe gefunden.','No matching glossary terms found.'], 'alphabet_aria'=>['Alphabetisch filtern','Filter alphabetically'], 'other_initial'=>['Andere Anfangszeichen','Other initial symbols'], 'reset_filters'=>['Filter zurücksetzen','Reset filters'], 'load_more'=>['Weitere Begriffe laden','Load more terms'], 'loading'=>['Begriffe werden geladen …','Loading terms …'], 'load_failed'=>['Die Begriffe konnten nicht geladen werden. Bitte versuche es erneut.','The terms could not be loaded. Please try again.'], 'browse_all'=>['Alle Begriffe im Glossararchiv ansehen','Browse all terms in the glossary archive'], 'topics_editor_empty'=>['Für diese Sprache sind noch keine Themen vorhanden. Lege zunächst ein Thema an oder übersetze ein bestehendes.','No topics are available for this language yet. Add a topic or translate an existing one.'], ]; return isset($texts[$key]) ? $texts[$key][$german?0:1] : ''; } function mkb_glossar_frontend_text($key) { return mkb_glossar_frontend_text_for_language($key,mkb_glossar_current_language()); } /** Allow provider string translations for explicitly customized builder labels. */ function mkb_glossar_frontend_custom_text($value,$key,$language=null) { $value=(string)$value; $language=$language??mkb_glossar_current_language(); if ($value==='') return mkb_glossar_frontend_text_for_language($key,$language); if ($language==='site-default') return $value; if (mkb_glossar_polylang_enabled() && function_exists('pll_translate_string')) { $translated=pll_translate_string($value,$language); if (is_string($translated) && $translated!=='') return $translated; } if (mkb_glossar_wpml_enabled()) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration. $translated=apply_filters('wpml_translate_single_string',$value,'MKB Glossar','frontend_'.$key,$language); if (is_string($translated) && $translated!=='') return $translated; } return $value; } /** Elementor only: explain why an enabled filter has no usable topics. */ function mkb_glossar_is_elementor_preview() { if (!class_exists('\\Elementor\\Plugin')) return false; $plugin=\Elementor\Plugin::instance(); if (isset($plugin->preview) && method_exists($plugin->preview,'is_preview_mode') && $plugin->preview->is_preview_mode()) return true; if (isset($plugin->editor) && method_exists($plugin->editor,'is_edit_mode') && $plugin->editor->is_edit_mode()) return true; return false; } /** Gutenberg server-side preview: use the edited page language, not the REST request locale. */ function mkb_glossar_block_preview_language($attributes, $content='', $block=null) { $attributes=(array)$attributes; if (defined('REST_REQUEST') && REST_REQUEST && is_object($block)) { $post_id=isset($block->context['postId']) ? absint($block->context['postId']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET/navigation parameter; no state is changed and input is validated separately. if (!$post_id) $post_id=mkb_glossar_request_id($_GET,'post_id'); if ($post_id && get_post_status($post_id)) { $language=mkb_glossar_post_language($post_id); if (is_string($language) && $language!=='') { $attributes['_mkb_language']=$language; $attributes['_mkb_block_editor_preview']=true; } } } return mkb_glossar_render_overview($attributes); } /** Determine a meaningful, language-safe first character for alphabet navigation. */ function mkb_glossar_alphabet_key($title) { $title=(string)$title; if (!preg_match('/[\p{L}\p{N}]/u',$title,$match)) return '#'; $first=$match[0]; if (preg_match('/^\p{N}$/u',$first)) return '0-9'; if (function_exists('mb_strtoupper')) return mb_strtoupper($first,'UTF-8'); // WordPress can run without mbstring; at least preserve German letter parity. return strtr(strtoupper($first),['ä'=>'Ä','ö'=>'Ö','ü'=>'Ü','ß'=>'ẞ']); } /** Frontend search considers the complete definition and short summary, not only titles. */ function mkb_glossar_search_text($post) { $id=isset($post->ID)?(int)$post->ID:0; $title=get_the_title($id); $synonyms=get_post_meta($id,'mkb_glossar_synonyms',true); $summary=get_post_meta($id,'mkb_glossar_summary',true); $body=isset($post->post_content)?(string)$post->post_content:''; if (function_exists('strip_shortcodes')) $body=strip_shortcodes($body); $values=[$title,is_array($synonyms)?implode(' ',$synonyms):(string)$synonyms, (string)$summary,wp_strip_all_tags($body)]; return trim(implode(' ',array_filter($values,static function($value){return $value!=='';}))); } /** Quick-search landing uses exactly the same term/synonym scope as live suggestions. * The glossary overview's own search retains its historical full-text behavior. */ function mkb_glossar_term_lookup_text($post) { $id=isset($post->ID)?(int)$post->ID:0; $synonyms=get_post_meta($id,'mkb_glossar_synonyms',true); return (string)get_the_title($post).' '.(is_array($synonyms)?implode(' ',$synonyms):(string)$synonyms); } function mkb_glossar_landing_normalize($text) { $text=(string)$text; if (class_exists('\Normalizer')) { $normalized=\Normalizer::normalize($text,\Normalizer::FORM_KD); if (is_string($normalized)) $text=preg_replace('/\p{Mn}/u','',$normalized); } elseif (function_exists('remove_accents')) $text=remove_accents($text); $text=str_replace(['ß','ẞ'],['ss','ss'],$text); return function_exists('mb_strtolower')?mb_strtolower($text,'UTF-8'):strtolower($text); } function mkb_glossar_landing_term_matches($text,$query) { $needle=mkb_glossar_landing_normalize($query); return $needle==='' || str_contains(mkb_glossar_landing_normalize($text),$needle); } /** Shared semantic markup and frontend assets. */ function mkb_glossar_render_overview($input=[]) { $language=isset($input['_mkb_language']) && is_string($input['_mkb_language']) ? sanitize_key($input['_mkb_language']) : mkb_glossar_current_language(); $config=mkb_glossar_overview_config($input); $initial_query=function_exists('mkb_glossar_search_landing_query') && !is_admin() ? mkb_glossar_search_landing_query() : ''; // A sidebar search must remain visible and editable, even if the original overview hid its search field. if ($initial_query!=='') $config['show_search']=true; $config['_mkb_language']=$language; if (!empty($input['_mkb_block_editor_preview'])) $config['_mkb_block_editor_preview']=true; $remote = function_exists('mkb_glossar_remote_enabled') && mkb_glossar_remote_enabled($config); // Server-render the first matching page for a quick-search landing. Visitors // must not briefly see unrelated terms or need JavaScript to filter results. $landing_page = null; if ($remote && $initial_query!=='' && function_exists('mkb_glossar_remote_query')) { $landing_page=mkb_glossar_remote_query(['q'=>$initial_query,'match'=>'terms','topic'=>$config['topic'],'lang'=>$language,'page'=>1]); $posts=$landing_page['posts']; } else $posts=mkb_glossar_overview_posts($config); $config['_mkb_remote'] = $remote; $config['_mkb_initial_query'] = $initial_query; mkb_glossar_enqueue($config['show_tooltips']); $id=wp_unique_id('mkb-glossar-search-'); $topic_id=wp_unique_id('mkb-glossar-topic-'); $remote_props = $remote ? ' data-remote="1" data-ajax="'.esc_url(admin_url('admin-ajax.php')).'" data-topic="'.esc_attr($config['topic']).'" data-tooltips="'.($config['show_tooltips']?'1':'0').'" data-lang="'.esc_attr($language).'" data-loading="'.esc_attr(mkb_glossar_frontend_text_for_language('loading',$language)).'" data-failed="'.esc_attr(mkb_glossar_frontend_text_for_language('load_failed',$language)).'"' : ''; $out='
'; $out.='
'; if ($config['show_search']) { $out.='
'; if ($config['show_search_label']) $out.=''; $out.=''; $out.='
'; } if ($config['show_topics']) { $topics=get_terms(['taxonomy'=>'mkb_glossar_topic','hide_empty'=>true]); if (!is_wp_error($topics)) { $topics=array_values(array_filter($topics,'mkb_glossar_topic_visible')); if ($config['topic']!=='') $topics=array_values(array_filter($topics,static function($t)use($config){return $t->slug===$config['topic'];})); if ($topics) { $label=mkb_glossar_frontend_custom_text($config['topic_label'],'topic_label',$language); $out.='
'; if ($config['show_topic_label']) $out.=''; $out.='
'; } elseif (mkb_glossar_is_elementor_preview()) { $out.='
'.esc_html(mkb_glossar_frontend_text_for_language('topics_editor_empty',$language)).'
'; } } } $out.='
'; if ($config['show_search'] || $config['show_topics'] || $config['show_alphabet']) { $out.='
'; } if ($config['show_alphabet']) { $initials=[]; if ($remote && function_exists('mkb_glossar_remote_initials')) $initials=mkb_glossar_remote_initials(); else foreach ($posts as $post) $initials[]=mkb_glossar_alphabet_key(get_the_title($post)); $out.='
'; foreach (range('A','Z') as $letter) { $disabled=!$remote && !in_array($letter,$initials,true); $out.=''; } $extra=array_values(array_diff(array_unique($initials),range('A','Z'))); sort($extra,SORT_STRING); foreach ($extra as $initial) { $caption=$initial==='0-9'?'0–9':$initial; $other=$initial==='#'?' aria-label="'.esc_attr(mkb_glossar_frontend_text_for_language('other_initial',$language)).'"':''; $out.=''; } $out.='
'; } $out.=''; if ($remote) { $has_more=$landing_page!==null?(bool)$landing_page['has_more']:count($posts)>=mkb_glossar_remote_page_size(); $out.=''; $out.=''; $archive_url=get_post_type_archive_link('mkb_glossar'); if ($archive_url) $out.=''; } $initial_empty=false; if ($initial_query!=='') { $initial_empty=true; foreach ($posts as $post) { if (mkb_glossar_landing_term_matches(mkb_glossar_term_lookup_text($post),$initial_query)) { $initial_empty=false;break; } } } return $out.'

'.esc_html(mkb_glossar_frontend_custom_text($config['empty_message'],'empty_message',$language)).'

'; } /** One shared item renderer across initial and paginated public responses. */ function mkb_glossar_overview_items_html($posts,$config) { $out=''; foreach ($posts as $post) { $title=get_the_title($post); $initial=mkb_glossar_alphabet_key($title); $synonyms=get_post_meta($post->ID,'mkb_glossar_synonyms',true); $synonym_text=is_array($synonyms)?implode(' ',$synonyms):(string)$synonyms; $assigned=wp_get_post_terms($post->ID,'mkb_glossar_topic',['fields'=>'ids']); if (is_wp_error($assigned)) $assigned=[]; $summary=trim((string)get_post_meta($post->ID,'mkb_glossar_summary',true)); $with_tip=$config['show_tooltips'] && $summary!==''; $landing_query=(string)($config['_mkb_initial_query']??''); $term_text=$title.' '.$synonym_text; $hide_initial=$landing_query!=='' && !mkb_glossar_landing_term_matches($term_text,$landing_query); $out.='
  • ID):'').($with_tip?' class="mkb-glossar-term" data-definition="'.esc_attr(wp_strip_all_tags($summary)).'"':'').'>'.esc_html($title).(function_exists('mkb_glossar_external_marker_markup')?mkb_glossar_external_marker_markup($post->ID):'').'
  • '; } return $out; } /** Backwards-compatible public shortcode callback. */ function mkb_glossar_list($atts=[]) { return mkb_glossar_render_overview(shortcode_atts(mkb_glossar_overview_config([]),(array)$atts,'mkb_glossar')); } add_shortcode('mkb_glossar','mkb_glossar_list'); function mkb_glossar_block() { wp_register_script('mkb-glossar-editor',MKB_GLOSSAR_URL.'assets/block.js',['wp-blocks','wp-element','wp-components','wp-server-side-render','wp-block-editor','wp-i18n','wp-data'],MKB_GLOSSAR_VERSION,true); register_block_type('mkb/glossar',[ 'api_version'=>2,'editor_script'=>'mkb-glossar-editor','editor_style'=>'mkb-glossar', 'uses_context'=>['postId','postType'], 'attributes'=>[ 'limit'=>['type'=>'number','default'=>-1], 'topic'=>['type'=>'string','default'=>''], 'show_search'=>['type'=>'boolean','default'=>true], 'show_alphabet'=>['type'=>'boolean','default'=>true], 'columns'=>['type'=>'number','default'=>2], 'show_topics'=>['type'=>'boolean','default'=>false], 'show_search_label'=>['type'=>'boolean','default'=>true], 'show_topic_label'=>['type'=>'boolean','default'=>true], 'show_tooltips'=>['type'=>'boolean','default'=>true], 'search_label'=>['type'=>'string','default'=>''], 'search_placeholder'=>['type'=>'string','default'=>''], 'alphabet_all_label'=>['type'=>'string','default'=>''], 'empty_message'=>['type'=>'string','default'=>''], 'topic_label'=>['type'=>'string','default'=>''], 'reset_label'=>['type'=>'string','default'=>''] ], 'render_callback'=>'mkb_glossar_block_preview_language' ]); } add_action('init','mkb_glossar_block'); function mkb_glossar_is_excluded($post_id) { $o=mkb_glossar_opts(); if (!$post_id || in_array((int)$post_id,array_map('intval',(array)$o['excluded_ids']),true) || get_post_meta($post_id,'_mkb_glossar_exclude',true)) return true; if (!in_array(get_post_type($post_id),(array)$o['enabled_types'],true)) return true; foreach ((array)$o['excluded_terms'] as $taxonomy=>$ids) { $excluded_term_ids=array_values(array_filter(array_map('absint',(array)$ids))); // With an empty term list has_term([], ...) means 'any assigned term'. // Empty exclusion settings must never exclude categorized posts. if (!$excluded_term_ids || !taxonomy_exists($taxonomy)) continue; if (has_term($excluded_term_ids,$taxonomy,$post_id)) return true; } return false; } /** Collect canonical terms deterministically; first published glossary entry wins collisions. */ function mkb_glossar_candidates() { // Reuse the candidate list within one request; never persist user content in a // cross-request cache. Invalidated by WordPress whenever glossary data changes. $language=mkb_glossar_current_language(); if (isset($GLOBALS['mkb_glossar_request_language']) && $GLOBALS['mkb_glossar_request_language']!==$language) mkb_glossar_reset_request_cache(); if (isset($GLOBALS['mkb_glossar_request_candidates'])) return $GLOBALS['mkb_glossar_request_candidates']; $GLOBALS['mkb_glossar_request_language']=$language; $ids=get_posts(['post_type'=>'mkb_glossar','post_status'=>'publish','numberposts'=>-1,'fields'=>'ids','orderby'=>'ID','order'=>'ASC','suppress_filters'=>false]); $terms=[]; foreach ($ids as $id) { if (!mkb_glossar_in_language($id,$language)) continue; $names=array_merge([get_the_title($id)],preg_split('/\r\n|\r|\n/u',(string)get_post_meta($id,'mkb_glossar_synonyms',true)) ?: []); foreach ($names as $name) { $name=trim(wp_strip_all_tags($name)); if ($name==='' || (function_exists('mb_strlen')?mb_strlen($name,'UTF-8'):strlen($name))<2) continue; $key=function_exists('mb_strtolower')?mb_strtolower($name,'UTF-8'):strtolower($name); if (!isset($terms[$key])) $terms[$key]=['name'=>$name,'url'=>(function_exists('mkb_glossar_editorial_target_url')?mkb_glossar_editorial_target_url($id):get_permalink($id)),'summary'=>get_post_meta($id,'mkb_glossar_summary',true),'id'=>(int)$id,'newtab'=>(function_exists('mkb_glossar_editorial_target_newtab') && mkb_glossar_editorial_target_newtab($id)), 'rel'=>(function_exists('mkb_glossar_editorial_target_rel') ? mkb_glossar_editorial_target_rel($id) : ''), 'case_sensitive'=>get_post_meta($id,'mkb_glossar_case_sensitive',true)==='1', 'external'=>(function_exists('mkb_glossar_has_external_target') && mkb_glossar_has_external_target($id))]; } } uasort($terms,static function($a,$b){return strlen($b['name'])<=>strlen($a['name']) ?: strcmp($a['name'],$b['name']);}); $GLOBALS['mkb_glossar_request_candidates']=$terms; return $terms; } /** Compile one Unicode-aware expression, longest terms first (preg_match offsets are bytes). */ function mkb_glossar_pattern($candidates) { if (!$candidates) return null; $alternatives=array_map(static function($candidate){return (!empty($candidate['case_sensitive']) ? '(?-i:' : '(?i:') . preg_quote($candidate['name'],'~') . ')';},array_values($candidates)); // Prevent oversized PCRE patterns from consuming excessive request resources. if (strlen(implode('|',$alternatives))>150000) return null; return '~(? $count) { $id = (int)$id; if ($id <= 0) continue; $prior_seen[$id] = max((int)($prior_seen[$id] ?? 0), max(0, (int)$count)); } return [ 'total' => max(0, (int)($previous['total'] ?? 0), (int)$existing_total), 'seen' => $prior_seen, ]; } /** Reset the per-request cache after writes made during the same request. */ function mkb_glossar_reset_request_cache() { unset($GLOBALS['mkb_glossar_request_candidates'],$GLOBALS['mkb_glossar_request_pattern'],$GLOBALS['mkb_glossar_request_patterns'],$GLOBALS['mkb_glossar_request_language'],$GLOBALS['mkb_glossar_link_budgets']); } add_action('save_post_mkb_glossar','mkb_glossar_reset_request_cache'); add_action('wpml_language_has_switched','mkb_glossar_reset_request_cache'); add_action('pll_language_defined','mkb_glossar_reset_request_cache'); add_action('deleted_post','mkb_glossar_reset_request_cache'); add_action('trashed_post','mkb_glossar_reset_request_cache'); add_action('untrashed_post','mkb_glossar_reset_request_cache'); add_action('added_post_meta',function($mid,$id,$key){if (get_post_type($id)==='mkb_glossar' && in_array($key,['mkb_glossar_synonyms','mkb_glossar_summary','mkb_glossar_case_sensitive','mkb_glossar_external_url','mkb_glossar_use_external','mkb_glossar_external_newtab','mkb_glossar_external_nofollow','mkb_glossar_external_sponsored','mkb_glossar_external_ugc'],true)) mkb_glossar_reset_request_cache();},10,3); add_action('updated_post_meta',function($mid,$id,$key){if (get_post_type($id)==='mkb_glossar' && in_array($key,['mkb_glossar_synonyms','mkb_glossar_summary','mkb_glossar_case_sensitive','mkb_glossar_external_url','mkb_glossar_use_external','mkb_glossar_external_newtab','mkb_glossar_external_nofollow','mkb_glossar_external_sponsored','mkb_glossar_external_ugc'],true)) mkb_glossar_reset_request_cache();},10,3); add_action('deleted_post_meta',function($mids,$id,$key){if (get_post_type($id)==='mkb_glossar' && in_array($key,['mkb_glossar_synonyms','mkb_glossar_summary','mkb_glossar_case_sensitive','mkb_glossar_external_url','mkb_glossar_use_external','mkb_glossar_external_newtab','mkb_glossar_external_nofollow','mkb_glossar_external_sponsored','mkb_glossar_external_ugc'],true)) mkb_glossar_reset_request_cache();},10,3); /** Replace text nodes only, never attributes, links, editorial headings or interactive elements. */ function mkb_glossar_autolink($content) { $o=mkb_glossar_opts(); if (!is_string($content) || trim($content)==='' || is_admin() || empty($o['auto_link']) || !is_singular() || !class_exists('DOMDocument')) return $content; $post_id=get_queried_object_id(); // Do not compare get_the_ID() with the queried object here: theme rendering and // nested post loops can temporarily change the global post, especially on posts. // We deliberately keep the singular queried post + type/exclusion checks below. if (get_post_type($post_id)==='mkb_glossar' || mkb_glossar_is_excluded($post_id)) return $content; $terms=mkb_glossar_candidates(); $patterns=function_exists('mkb_glossar_cached_patterns') ? mkb_glossar_cached_patterns($terms) : [mkb_glossar_cached_pattern($terms)]; if (!$patterns || !$patterns[0]) return $content; $doc=new DOMDocument('1.0','UTF-8'); $prior=libxml_use_internal_errors(true); try { $ok=$doc->loadHTML('
    '.$content.'
    ',LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD); } finally { libxml_clear_errors(); libxml_use_internal_errors($prior); } if (!$ok) return $content; $root=$doc->getElementById('mkb-glossar-root'); if (!$root) return $content; $xpath=new DOMXPath($doc); $nodes=[]; foreach ($xpath->query('.//text()[normalize-space(.) != ""]',$root) as $node) $nodes[]=$node; $limit=max(1,min(100,(int)$o['max_links'])); $term_limit=max(1,min(10,(int)($o['repeat_per_term']??1))); // A native Elementor Text Editor and the enclosing the_content filter can // both visit the same text. Existing generated anchors count toward the // limits; they must never grant a second allocation of max_links. $existing_total=0; $existing_seen=[]; $url_ids=[]; foreach ($terms as $term) { $url=(string)($term['url']??''); if ($url!=='' && !isset($url_ids[$url])) $url_ids[$url]=(int)$term['id']; } foreach ($xpath->query('.//a[contains(concat(" ", normalize-space(@class), " "), " mkb-glossar-term ")]',$root) as $anchor) { if (!($anchor instanceof DOMElement)) continue; $existing_total++; $id=(int)$anchor->getAttribute('data-mkb-glossar-term-id'); // Backwards-compatible fallback for anchors created before this version. if (!$id) $id=(int)($url_ids[$anchor->getAttribute('href')]??0); if ($id>0) $existing_seen[$id]=($existing_seen[$id]??0)+1; } $budget_key=$post_id.'|'.mkb_glossar_current_language(); if (!isset($GLOBALS['mkb_glossar_link_budgets']) || !is_array($GLOBALS['mkb_glossar_link_budgets'])) $GLOBALS['mkb_glossar_link_budgets']=[]; $budget=mkb_glossar_link_budget_merge($GLOBALS['mkb_glossar_link_budgets'][$budget_key]??[], $existing_total, $existing_seen); $seen=$budget['seen']; $total=$budget['total']; $has_tooltips=false; if ($total >= $limit) { $GLOBALS['mkb_glossar_link_budgets'][$budget_key]=$budget; return $content; } $forbidden=['a','script','style','code','pre','button','textarea','select','option','svg','math','noscript','h1','h2','h3','h4','h5','h6','label']; foreach ($nodes as $node) { if ($total>=$limit) break; $blocked=false; for ($parent=$node->parentNode;$parent && $parent!==$root;$parent=$parent->parentNode) { if (!$parent instanceof DOMElement) continue; if (in_array(strtolower($parent->tagName),$forbidden,true) || $parent->hasAttribute('contenteditable') || $parent->hasAttribute('data-mkb-glossar-no-link') || ($parent->hasAttribute('class') && preg_match('/(?:^|\\s)(?:mkb-glossar-no-link|is-style-mkb-without-glossary-links)(?:\\s|$)/',$parent->getAttribute('class')))) {$blocked=true;break;} } if ($blocked) continue; $value=$node->nodeValue; $hits=[]; foreach ($patterns as $pattern) { $matches=[]; $matched=@preg_match_all($pattern,$value,$matches,PREG_OFFSET_CAPTURE); if ($matched) foreach ($matches[0] as $hit) $hits[]=$hit; } if (!$hits) continue; // Earliest occurrence first; the longest competing name wins at one position. usort($hits, static function($a,$b) {return $a[1]<=>$b[1] ?: strlen($b[0])<=>strlen($a[0]);}); $fragment=$doc->createDocumentFragment(); $last=0; $replaced=0; foreach ($hits as $hit) { if ($total>=$limit) break; $word=$hit[0]; $start=$hit[1]; $len=strlen($word); if ($start<$last) continue; // A longer overlapping match was already linked. $key=function_exists('mb_strtolower')?mb_strtolower($word,'UTF-8'):strtolower($word); if (!isset($terms[$key])) continue; $term=$terms[$key]; $id=$term['id']; if (!empty($term['case_sensitive']) && $word !== $term['name']) continue; if (($seen[$id]??0)>=$term_limit || !$term['url']) continue; $fragment->appendChild($doc->createTextNode(substr($value,$last,$start-$last))); $link=$doc->createElement('a'); $link->setAttribute('href',$term['url']); $link->setAttribute('class','mkb-glossar-term'); $link->setAttribute('data-mkb-glossar-term-id', (string)$id); if (!empty($term['newtab'])) $link->setAttribute('target', '_blank'); if (!empty($term['rel'])) $link->setAttribute('rel', $term['rel']); $summary=wp_strip_all_tags((string)$term['summary']); if ($summary!=='') { $link->setAttribute('data-definition',$summary); $has_tooltips=true; } $link->appendChild($doc->createTextNode($word)); if (!empty($term['external']) && function_exists('mkb_glossar_append_external_marker_dom')) mkb_glossar_append_external_marker_dom($doc,$link,$id); $fragment->appendChild($link); $seen[$id]=($seen[$id]??0)+1; $last=$start+$len; $total++; $replaced++; } if ($replaced) { $fragment->appendChild($doc->createTextNode(substr($value,$last))); $node->parentNode->replaceChild($fragment,$node); } } // Save the consolidated allocation even if this call adds no new links. $GLOBALS['mkb_glossar_link_budgets'][$budget_key]=['total'=>$total,'seen'=>$seen]; if ($total===$budget['total']) return $content; $html=''; foreach ($root->childNodes as $child) $html.=$doc->saveHTML($child); mkb_glossar_enqueue($has_tooltips); return $html; } add_filter('the_content','mkb_glossar_autolink',25);