['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 '
';
});
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.='
';
}
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);